From 513d0d4f5852c1e3ec47455fee6d9a94666199c2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Jan 2025 15:54:19 -0800 Subject: [PATCH 001/115] don't explicitly disable on load it will already be disabled and it results in error messages in non-fort modes --- plugins/infinite-sky.cpp | 8 +++++--- plugins/timestream.cpp | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 3a2aefacb5..9b41c2e2ca 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -31,9 +31,9 @@ REQUIRE_GLOBAL(world); namespace DFHack { // for configuration-related logging - DBG_DECLARE(infiniteSky, control, DebugCategory::LINFO); + DBG_DECLARE(infinitesky, control, DebugCategory::LINFO); // for logging during creation of z-levels - DBG_DECLARE(infiniteSky, cycle, DebugCategory::LINFO); + DBG_DECLARE(infinitesky, cycle, DebugCategory::LINFO); } static const string CONFIG_KEY = string(plugin_name) + "/config"; @@ -98,7 +98,9 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { } // Call plugin_enable to set value to ensure the event handler is properly registered - plugin_enable(out, config.get_bool(CONFIG_IS_ENABLED)); + if (config.get_bool(CONFIG_IS_ENABLED)) { + plugin_enable(out, true); + } DEBUG(control, out) .print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index 3cb8f7a462..a472c2efec 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -216,7 +216,9 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { migrate_old_config(out); } - plugin_enable(out, config.get_bool(CONFIG_IS_ENABLED)); + if (config.get_bool(CONFIG_IS_ENABLED)) { + plugin_enable(out, true); + } DEBUG(control,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); From 8e276c708c7586b60cbfbf6d54833f3f6c577069 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Jan 2025 17:15:54 -0800 Subject: [PATCH 002/115] expose Units::setAutomaticProfessions to Lua --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 4 ++++ library/LuaApi.cpp | 1 + 3 files changed, 6 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index a0c3d88f87..344fa13ef0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: ## API ## Lua +- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index bfadfc6899..000840c76b 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1817,6 +1817,10 @@ Units module in the in-game labor management screens (including DFHack's `labor manipulator screen `). +* ``dfhack.units.setAutomaticProfessions(unit)`` + + Set appropriate labors on a unit based on current work detail settings. + * ``dfhack.units.computeMovementSpeed(unit)`` Computes number of frames * 100 it takes the unit to move in its current diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index b92e0eddbb..4b82dddc17 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2094,6 +2094,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getExperience), WRAPM(Units, isValidLabor), WRAPM(Units, setLaborValidity), + WRAPM(Units, setAutomaticProfessions), WRAPM(Units, computeMovementSpeed), WRAPM(Units, computeSlowdownFactor), WRAPM(Units, getProfession), From bcfdaa1441b4eebcf139541365ab645619207815 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 25 Jan 2025 07:14:31 +0000 Subject: [PATCH 003/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index abe2e0b9b6..4436a582a2 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit abe2e0b9b6cad8804824267b5683d36eaf2b7aa4 +Subproject commit 4436a582a2e12c388145f86c2bbd0e6e30d8b382 diff --git a/scripts b/scripts index 34012d635d..1a58e456cd 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 34012d635d47da217e42e99aba1fba1c3690f665 +Subproject commit 1a58e456cd02b5640c4a076acabcfb035a6ee307 From 80f840b944217d650920c8cf8164825a48660b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 25 Jan 2025 09:16:34 +0100 Subject: [PATCH 004/115] Make EditFiedl grab focus instead of proxying it to children --- library/lua/gui/widgets/edit_field.lua | 28 ++++++++++--------- library/lua/gui/widgets/text_area.lua | 6 +++- .../widgets/text_area/text_area_content.lua | 3 +- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 203a2aa78a..f2a4b9dc2b 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -57,6 +57,13 @@ function TextFieldArea:getPreferredFocusState() return false end +function TextFieldArea:hasFocus() + return self.parent_view.focus +end + +function TextFieldArea:setFocus(focus) + return self.parent_view:setFocus(focus) +end ---------------- -- Edit field -- ---------------- @@ -128,8 +135,6 @@ function EditField:init() key = self.key, on_submit = self.on_submit, on_submit2 = self.on_submit2, - on_focus = self:callback('onFocus'), - on_unfocus = self.on_unfocus, ignore_keys={ table.unpack(self.ignore_keys) }, @@ -142,12 +147,10 @@ function EditField:init() self.text_area.frame.l = self.label:getTextWidth() end -function EditField:onFocus() +function EditField:setFocus(focus) self.saved_text = self.text - if self.on_focus then - self:on_focus() - end + return EditField.super.setFocus(self, focus) end function EditField:getPreferredFocusState() @@ -187,10 +190,6 @@ function EditField:onTextAreaTextChange(text) end end -function EditField:setFocus(focus) - self.text_area:setFocus(focus) -end - function EditField:insert(text) local old = self.text self:setText( @@ -200,12 +199,15 @@ function EditField:insert(text) end function EditField:onInput(keys) - if not self.text_area.focus then - return self:inputToSubviews(keys) + if keys._MOUSE_L and self:getMousePos() then + self:setFocus(true) + end + + if not self.focus then + return self.label:onInput(keys) end if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then - self:setText(self.saved_text) self:setFocus(false) return true end diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index 75d83fef43..c906bf9223 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -84,6 +84,10 @@ function TextArea:clearHistory() return self.text_area.history:clear() end +function TextArea:hasFocus() + return self.focus +end + function TextArea:onCursorChange(cursor, old_cursor) local x, y = self.text_area.wrapped_text:indexToCoords( self.text_area.cursor @@ -197,7 +201,7 @@ function TextArea:onInput(keys) self:setFocus(true) end - if not self.focus then + if not self:hasFocus() then return false end diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index 97e6fe1c6e..cf3698f086 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -238,7 +238,7 @@ function TextAreaContent:onRenderBody(dc) local show_focus = not self.enable_cursor_blink or ( not self:hasSelection() - and self.parent_view.focus + and self.parent_view:hasFocus() and gui.blink_visible(530) ) @@ -486,7 +486,6 @@ function TextAreaContent:onMouseInput(keys) end elseif keys._MOUSE_L_DOWN then - local mouse_x, mouse_y = self:getMousePos() if mouse_x and mouse_y then From 34d8cce237a59cf807a550c735a007cd11f9b869 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Jan 2025 10:41:36 -0800 Subject: [PATCH 005/115] document fix for gui/gm-editor --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 344fa13ef0..b807b01c8a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/gm-editor`: fix Enter key not being recognized for opening the selected object ## Misc Improvements From 3ff7bfb981d2c24c80b629990a96833173f125c4 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Sat, 25 Jan 2025 18:47:34 +0000 Subject: [PATCH 006/115] Auto-update structures ref for 51.03 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 9934e63447..b015c41635 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9934e634472c43fa5153cebf0ff5e44213b991a8 +Subproject commit b015c41635cd3a310ab75025cb509a3b43832d2a From da50931ad2ccd8bf3ffcaab10c78ad0cedb95b82 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Jan 2025 10:52:49 -0800 Subject: [PATCH 007/115] bump version to 51.03-r1 revert scripts and stonesense to 51.02-r1 tags to exclude new features that aren't ready yet --- CMakeLists.txt | 2 +- docs/changelog.txt | 6 +++++- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f09bc5f33c..197b2af86d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.02") +set(DF_VERSION "51.03") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index b807b01c8a..757c81e96c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,6 @@ Template for new versions: ## New Features ## Fixes -- `gui/gm-editor`: fix Enter key not being recognized for opening the selected object ## Misc Improvements @@ -69,6 +68,11 @@ Template for new versions: ## Removed +# 51.03-r1 + +## Fixes +- `gui/gm-editor`: fix Enter key not being recognized for opening the selected object + # 51.02-r1 ## Misc Improvements diff --git a/plugins/stonesense b/plugins/stonesense index 4436a582a2..abe2e0b9b6 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 4436a582a2e12c388145f86c2bbd0e6e30d8b382 +Subproject commit abe2e0b9b6cad8804824267b5683d36eaf2b7aa4 diff --git a/scripts b/scripts index 1a58e456cd..34012d635d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1a58e456cd02b5640c4a076acabcfb035a6ee307 +Subproject commit 34012d635d47da217e42e99aba1fba1c3690f665 From 761217a8662897e9a19ad05f0e05eb0647d5af8d Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Sat, 25 Jan 2025 20:51:20 +0000 Subject: [PATCH 008/115] Auto-update structures ref for 51.03 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index b015c41635..23a61d55bf 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b015c41635cd3a310ab75025cb509a3b43832d2a +Subproject commit 23a61d55bf67c6202f1fcc38390fb32dbdfa6e73 From 3f417ebca0d7cf3b6d9c790a6a6dd97855743461 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Jan 2025 13:31:44 -0800 Subject: [PATCH 009/115] bump release to 51.03-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 197b2af86d..fc11d87633 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.03") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 757c81e96c..1fa9a7eb7a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,11 @@ Template for new versions: ## Removed +# 51.03-r1.1 + +## Misc Improvements +- Compatibility with Itch release of DF 51.03 + # 51.03-r1 ## Fixes From 36ec788544b0dd0625493685a9a9975844e63a59 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 26 Jan 2025 07:14:59 +0000 Subject: [PATCH 010/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index abe2e0b9b6..3fb137a225 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit abe2e0b9b6cad8804824267b5683d36eaf2b7aa4 +Subproject commit 3fb137a22539817523471e424ab1004c4984604c diff --git a/scripts b/scripts index 34012d635d..ce8efb4d4e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 34012d635d47da217e42e99aba1fba1c3690f665 +Subproject commit ce8efb4d4e4761759e4ca9119b868fae5f3fb19b From 0171478a4936251fef24f4ea45253da0bd099721 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 20:13:38 -0800 Subject: [PATCH 011/115] special case version 51.03 so ci works --- ci/download-df.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci/download-df.sh b/ci/download-df.sh index 399b75714b..7c403d4a92 100755 --- a/ci/download-df.sh +++ b/ci/download-df.sh @@ -8,6 +8,9 @@ set -e minor=$(echo "$DF_VERSION" | cut -d. -f1) patch=$(echo "$DF_VERSION" | cut -d. -f2) +if [ "$DF_VERSION" = "51.03" ]; then + patch=02 +fi df_url="https://www.bay12games.com/dwarves/df_${minor}_${patch}" if test "$OS_TARGET" = "windows"; then WGET="C:/msys64/usr/bin/wget.exe" From e303e5b7e8679406e2b19716eb33f8da2c17d4b6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 20:30:09 -0800 Subject: [PATCH 012/115] add PR template --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..e68a754c63 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1 @@ +If this PR makes an externally-visible change in behavior or API, please add an appropriate line to `docs/changelog.txt`. From e6964e41b4995f8a6fc31896f7bc7a07351e1874 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 22:07:43 -0800 Subject: [PATCH 013/115] take advantage of steamcmd cache for generate symbols --- .github/workflows/generate-symbols.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 7c81b4844e..727f53c7be 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -99,6 +99,13 @@ jobs: run: Xvfb :0 -screen 0 1600x1200x24 & # Steam + - name: Preload steamcmd + if: inputs.channel == 'all' || inputs.channel == 'steam' + uses: actions/cache/restore@v4 + with: + path: ${{ env.HOME }}/Steam + key: steamcmd-${{ github.sha }} + restore-keys: steamcmd - name: Setup steamcmd if: inputs.channel == 'all' || inputs.channel == 'steam' id: steamcmd @@ -226,6 +233,13 @@ jobs: path: metasm # Steam + - name: Preload steamcmd + if: inputs.channel == 'all' || inputs.channel == 'steam' + uses: actions/cache/restore@v4 + with: + path: ${{ env.HOME }}/Steam + key: steamcmd-${{ github.sha }} + restore-keys: steamcmd - name: Setup steamcmd if: inputs.channel == 'all' || inputs.channel == 'steam' id: steamcmd From 6da4bd9a5fceeadf464928ebcd0f24c6acd88422 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 27 Jan 2025 06:13:26 +0000 Subject: [PATCH 014/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 3fb137a225..004bafefe7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 3fb137a22539817523471e424ab1004c4984604c +Subproject commit 004bafefe75ccc2c291bff73c72fc7081f2842cb diff --git a/scripts b/scripts index ce8efb4d4e..33c1e52528 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ce8efb4d4e4761759e4ca9119b868fae5f3fb19b +Subproject commit 33c1e52528dd0ef06b540fb6da1ed53d8f982ee1 From ca6f3349ec880ef0e06ba784fdb423c393c776c9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 23:15:44 -0800 Subject: [PATCH 015/115] fix restore key for cached steamcmd --- .github/workflows/generate-symbols.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 727f53c7be..c3c7063e0e 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -104,8 +104,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ${{ env.HOME }}/Steam - key: steamcmd-${{ github.sha }} - restore-keys: steamcmd + key: steamcmd - name: Setup steamcmd if: inputs.channel == 'all' || inputs.channel == 'steam' id: steamcmd @@ -238,8 +237,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ${{ env.HOME }}/Steam - key: steamcmd-${{ github.sha }} - restore-keys: steamcmd + key: steamcmd - name: Setup steamcmd if: inputs.channel == 'all' || inputs.channel == 'steam' id: steamcmd From 3dab606e16e8bd0e78e669b45c9327f430a5f5fa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 23:16:16 -0800 Subject: [PATCH 016/115] watch Itch and Classic for releases too also fix steam monitoring so it doesn't notify unless there is a new build id. this will prevent spurious notifications when there is just a metadata update --- .github/workflows/watch-df-release.yml | 153 +++++++++++++++++++++++++ .github/workflows/watch-df-steam.yml | 84 -------------- 2 files changed, 153 insertions(+), 84 deletions(-) create mode 100644 .github/workflows/watch-df-release.yml delete mode 100644 .github/workflows/watch-df-steam.yml diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml new file mode 100644 index 0000000000..bfecad897b --- /dev/null +++ b/.github/workflows/watch-df-release.yml @@ -0,0 +1,153 @@ +name: Watch DF Releases + +on: + schedule: + - cron: '8/10 * * * *' + workflow_dispatch: + +jobs: + check-steam: + if: github.repository == 'DFHack/dfhack' + name: Check Steam for new DF releases (${{ matrix.df_steam_branch }} branch) + runs-on: ubuntu-latest + concurrency: watch-release-steam-${{ matrix.df_steam_branch }} + strategy: + fail-fast: false + matrix: + # df_steam_branch: which DF Steam branch to watch + # platform: leave blank to default to all + # structures_ref: leave blank to default to master + # dfhack_ref: leave blank if no structures update is desired + # steam_branch: leave blank if no DFHack steam push is desired + include: + - df_steam_branch: public + - df_steam_branch: beta + steps: + - name: Fetch state + uses: actions/cache/restore@v4 + with: + path: state + key: watch-release-steam-${{ matrix.df_steam_branch }} + - name: Compare branch metadata + uses: nick-fields/retry@v3 + with: + timeout_minutes: 2 + command: | + blob=$(wget 'https://api.steamcmd.net/v1/info/975370?pretty=1' -O- | \ + awk '/^ *"branches"/,0' | \ + awk '/^ *"${{ matrix.df_steam_branch }}"/,0') + buildid=$(echo "$blob" | \ + fgrep buildid | \ + head -n1 | \ + cut -d'"' -f4) + timestamp=$(echo "$blob" | \ + fgrep timeupdated | \ + head -n1 | \ + cut -d'"' -f4) + test -z "$buildid" && echo "no buildid result" && exit 1 + test -z "$timestamp" && echo "no timestamp result" && exit 1 + test "$buildid" -gt 0 || exit 1 + test "$timestamp" -gt 0 || exit 1 + echo "buildid and timestamp of last branch update: $buildid, $timestamp" + mkdir -p state + touch state/buildid state/timestamp + last_buildid=$(cat state/buildid) + last_timestamp=$(cat state/timestamp) + if [ -z "$last_timestamp" ]; then + echo "no stored timestamp" + buildid=0 + last_timestamp=0 + else + echo "stored buildid and timestamp of last branch update: $last_buildid, $last_timestamp" + fi + if [ "$buildid" -ne "$last_buildid" -a "$timestamp" -gt "$last_timestamp" ]; then + echo "branch updated" + echo "$buildid" >state/buildid + echo "$timestamp" >state/timestamp + echo BUILDID=$timestamp >> $GITHUB_ENV + fi + - name: Discord Webhook Action + uses: tsickert/discord-webhook@v5.3.0 + if: env.BUILDID + with: + webhook-url: ${{ secrets.DISCORD_TEAM_PRIVATE_WEBHOOK_URL }} + content: "<@&${{ secrets.DISCORD_TEAM_ROLE_ID }}> DF Steam branch updated: ${{ matrix.df_steam_branch }} (build id: ${{ env.BUILDID }})" + - name: Launch symbol generation workflow + if: env.BUILDID && matrix.dfhack_ref + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh workflow run generate-symbols.yml \ + -R DFHack/dfhack \ + -r ${{ matrix.dfhack_ref }} \ + -f structures_ref=${{ matrix.structures_ref }} \ + -f version=auto \ + -f platform=${{ matrix.platform }} \ + -f channel=steam \ + -f df_steam_branch=${{ matrix.df_steam_branch }} \ + -f steam_branch=${{ matrix.steam_branch }} + - name: Save state + uses: actions/cache/save@v4 + if: env.BUILDID + with: + path: state + key: watch-release-steam-${{ matrix.df_steam_branch }}-${{ env.BUILDID }} + + check-non-steam: + if: github.repository == 'DFHack/dfhack' + name: Check ${{ matrix.channel }} for new DF releases + runs-on: ubuntu-latest + concurrency: watch-release-${{ matrix.channel }} + strategy: + fail-fast: false + matrix: + include: + - channel: itch + url: 'https://kitfoxgames.itch.io/dwarf-fortress' + prefix: 'dwarf_fortress' + - channel: classic + url: 'https://www.bay12games.com/dwarves/' + prefix: 'df' + steps: + - name: Fetch state + uses: actions/cache/restore@v4 + with: + path: state + key: watch-release-${{ matrix.channel }} + - name: Compare versions + uses: nick-fields/retry@v3 + with: + timeout_minutes: 2 + command: | + version=$(wget "${{ matrix.url }}" -qO- | tr '"' '\n' | fgrep 'tar.bz2' | head -n1 | sed -r 's/${{ matrix.prefix }}_([0-9]{2})_([0-9]{2})_linux.tar.bz2/\1.\2/') + echo "latest ${{ matrix.channel }} version: $version" + if ! grep -qE '^[0-9]+\.[0-9]+$' <<<"$version"; then + echo "invalid version" + exit 1 + fi + mkdir -p state + touch state/last_version + last_version=$(cat state/last_version) + if [ -z "$last_version" ]; then + echo "no stored version" + last_version=0 + else + echo "stored version: $last_version" + fi + if [ "$(tr -d '.' <<<"$version")" -gt "$(tr -d '.' <<<"$last_version")" ]; then + echo "${{ matrix.channel }} has been updated" + echo "$version" >state/last_version + echo NEW_VERSION=$version >> $GITHUB_ENV + fi + - name: Discord Webhook Action + uses: tsickert/discord-webhook@v5.3.0 + if: env.NEW_VERSION + with: + webhook-url: ${{ secrets.DISCORD_TEAM_PRIVATE_WEBHOOK_URL }} + content: "<@&${{ secrets.DISCORD_TEAM_ROLE_ID }}> DF ${{ matrix.channel }} updated to ${{ env.NEW_VERSION }}" + - name: Save state + uses: actions/cache/save@v4 + if: env.NEW_VERSION + with: + path: state + key: watch-release-${{ matrix.channel }}-${{ env.NEW_VERSION }} diff --git a/.github/workflows/watch-df-steam.yml b/.github/workflows/watch-df-steam.yml deleted file mode 100644 index d61edcd236..0000000000 --- a/.github/workflows/watch-df-steam.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Watch DF Steam Releases - -on: - schedule: - - cron: '8/10 * * * *' - workflow_dispatch: - -jobs: - check-steam: - if: github.repository == 'DFHack/dfhack' - name: Check DF ${{ matrix.df_steam_branch }} branch - runs-on: ubuntu-latest - concurrency: steampoll-${{ matrix.df_steam_branch }} - strategy: - fail-fast: false - matrix: - # df_steam_branch: which DF Steam branch to watch - # platform: leave blank to default to all - # structures_ref: leave blank to default to master - # dfhack_ref: leave blank if no structures update is desired - # steam_branch: leave blank if no DFHack steam push is desired - include: - - df_steam_branch: public - - df_steam_branch: beta - steps: - - name: Fetch state - uses: actions/cache/restore@v4 - with: - path: state - key: watchstate-${{ matrix.df_steam_branch }} - - name: Compare branch metadata - uses: nick-fields/retry@v3 - with: - timeout_minutes: 2 - command: | - timestamp=$(curl -X GET 'https://api.steamcmd.net/v1/info/975370?pretty=1' | \ - awk '/^ *"branches"/,0' | \ - awk '/^ *"${{ matrix.df_steam_branch }}"/,0' | \ - fgrep timeupdated | \ - head -n1 | \ - cut -d'"' -f4) - test -z "$timestamp" && echo "no timestamp result" && exit 1 - test "$timestamp" -gt 0 || exit 1 - echo "timestamp of last branch update: $timestamp" - mkdir -p state - touch state/timestamp - last_timestamp=$(cat state/timestamp) - if [ -z "$last_timestamp" ]; then - echo "no stored timestamp" - last_timestamp=0 - else - echo "stored timestamp of last branch update: $last_timestamp" - fi - if [ "$timestamp" -gt "$last_timestamp" ]; then - echo "branch updated" - echo "$timestamp" >state/timestamp - echo TIMESTAMP=$timestamp >> $GITHUB_ENV - fi - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v5.3.0 - if: env.TIMESTAMP - with: - webhook-url: ${{ secrets.DISCORD_TEAM_PRIVATE_WEBHOOK_URL }} - content: "<@&${{ secrets.DISCORD_TEAM_ROLE_ID }}> DF Steam branch updated: ${{ matrix.df_steam_branch }}" - - name: Launch symbol generation workflow - if: env.TIMESTAMP && matrix.dfhack_ref - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh workflow run generate-symbols.yml \ - -R DFHack/dfhack \ - -r ${{ matrix.dfhack_ref }} \ - -f structures_ref=${{ matrix.structures_ref }} \ - -f version=auto \ - -f platform=${{ matrix.platform }} \ - -f channel=steam \ - -f df_steam_branch=${{ matrix.df_steam_branch }} \ - -f steam_branch=${{ matrix.steam_branch }} - - name: Save state - uses: actions/cache/save@v4 - if: env.TIMESTAMP - with: - path: state - key: watchstate-${{ matrix.df_steam_branch }}-${{ env.TIMESTAMP }} From 62b869f7020262c1ece8437d1916a3554ca943d5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 23:20:02 -0800 Subject: [PATCH 017/115] fix typo --- .github/workflows/watch-df-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index bfecad897b..e5b026bf0d 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -55,7 +55,7 @@ jobs: last_timestamp=$(cat state/timestamp) if [ -z "$last_timestamp" ]; then echo "no stored timestamp" - buildid=0 + last_buildid=0 last_timestamp=0 else echo "stored buildid and timestamp of last branch update: $last_buildid, $last_timestamp" From a626f124df4635612eefdcc5ae277cb0a73e0cb7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 23:23:47 -0800 Subject: [PATCH 018/115] tidy up --- .github/workflows/generate-symbols.yml | 12 ------------ .github/workflows/watch-df-release.yml | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index c3c7063e0e..7c81b4844e 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -99,12 +99,6 @@ jobs: run: Xvfb :0 -screen 0 1600x1200x24 & # Steam - - name: Preload steamcmd - if: inputs.channel == 'all' || inputs.channel == 'steam' - uses: actions/cache/restore@v4 - with: - path: ${{ env.HOME }}/Steam - key: steamcmd - name: Setup steamcmd if: inputs.channel == 'all' || inputs.channel == 'steam' id: steamcmd @@ -232,12 +226,6 @@ jobs: path: metasm # Steam - - name: Preload steamcmd - if: inputs.channel == 'all' || inputs.channel == 'steam' - uses: actions/cache/restore@v4 - with: - path: ${{ env.HOME }}/Steam - key: steamcmd - name: Setup steamcmd if: inputs.channel == 'all' || inputs.channel == 'steam' id: steamcmd diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index e5b026bf0d..752ffafdd9 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -8,7 +8,7 @@ on: jobs: check-steam: if: github.repository == 'DFHack/dfhack' - name: Check Steam for new DF releases (${{ matrix.df_steam_branch }} branch) + name: Check Steam (${{ matrix.df_steam_branch }}) for new DF releases runs-on: ubuntu-latest concurrency: watch-release-steam-${{ matrix.df_steam_branch }} strategy: From 2a17dd55b6ccd7ddc7f65856e409df7fe840207c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 23:25:24 -0800 Subject: [PATCH 019/115] add channel name to debug output --- .github/workflows/watch-df-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index 752ffafdd9..883e0526a6 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -132,7 +132,7 @@ jobs: echo "no stored version" last_version=0 else - echo "stored version: $last_version" + echo "stored ${{ matrix.channel }} version: $last_version" fi if [ "$(tr -d '.' <<<"$version")" -gt "$(tr -d '.' <<<"$last_version")" ]; then echo "${{ matrix.channel }} has been updated" From d07898ddea2aa07c19c4f7253a365f66324e1c5f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 23:32:45 -0800 Subject: [PATCH 020/115] improve wording of notification messages --- .github/workflows/watch-df-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index 883e0526a6..b2daf22fe2 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -71,7 +71,7 @@ jobs: if: env.BUILDID with: webhook-url: ${{ secrets.DISCORD_TEAM_PRIVATE_WEBHOOK_URL }} - content: "<@&${{ secrets.DISCORD_TEAM_ROLE_ID }}> DF Steam branch updated: ${{ matrix.df_steam_branch }} (build id: ${{ env.BUILDID }})" + content: "<@&${{ secrets.DISCORD_TEAM_ROLE_ID }}> Steam ${{ matrix.df_steam_branch }} branch updated (build id: ${{ env.BUILDID }})" - name: Launch symbol generation workflow if: env.BUILDID && matrix.dfhack_ref env: @@ -144,7 +144,7 @@ jobs: if: env.NEW_VERSION with: webhook-url: ${{ secrets.DISCORD_TEAM_PRIVATE_WEBHOOK_URL }} - content: "<@&${{ secrets.DISCORD_TEAM_ROLE_ID }}> DF ${{ matrix.channel }} updated to ${{ env.NEW_VERSION }}" + content: "<@&${{ secrets.DISCORD_TEAM_ROLE_ID }}> ${{ matrix.channel }} updated to ${{ env.NEW_VERSION }}" - name: Save state uses: actions/cache/save@v4 if: env.NEW_VERSION From efaf9925c306dc60057d0fdaa6784587afcc460b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Jan 2025 23:35:41 -0800 Subject: [PATCH 021/115] fix cache key value --- .github/workflows/watch-df-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index b2daf22fe2..77e77ffcc9 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -64,7 +64,7 @@ jobs: echo "branch updated" echo "$buildid" >state/buildid echo "$timestamp" >state/timestamp - echo BUILDID=$timestamp >> $GITHUB_ENV + echo BUILDID=$buildid >> $GITHUB_ENV fi - name: Discord Webhook Action uses: tsickert/discord-webhook@v5.3.0 From 5c54642da79a9b6403b3fee4c2ae31bde8a594ed Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Jan 2025 07:47:15 -0800 Subject: [PATCH 022/115] prep for 51.04 release --- CMakeLists.txt | 4 ++-- ci/download-df.sh | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fc11d87633..c85c190971 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.03") -set(DFHACK_RELEASE "r1.1") +set(DF_VERSION "51.04") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/ci/download-df.sh b/ci/download-df.sh index 7c403d4a92..ed8b373a25 100755 --- a/ci/download-df.sh +++ b/ci/download-df.sh @@ -8,7 +8,7 @@ set -e minor=$(echo "$DF_VERSION" | cut -d. -f1) patch=$(echo "$DF_VERSION" | cut -d. -f2) -if [ "$DF_VERSION" = "51.03" ]; then +if [ "$DF_VERSION" = "51.03" -o "$DF_VERSION" = "51.04" ]; then patch=02 fi df_url="https://www.bay12games.com/dwarves/df_${minor}_${patch}" diff --git a/plugins/stonesense b/plugins/stonesense index 004bafefe7..abe2e0b9b6 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 004bafefe75ccc2c291bff73c72fc7081f2842cb +Subproject commit abe2e0b9b6cad8804824267b5683d36eaf2b7aa4 diff --git a/scripts b/scripts index 33c1e52528..34012d635d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 33c1e52528dd0ef06b540fb6da1ed53d8f982ee1 +Subproject commit 34012d635d47da217e42e99aba1fba1c3690f665 From 5245183a143ed86215a85f4fb8fbdd623d665b6d Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:51:38 +0000 Subject: [PATCH 023/115] Auto-update structures ref for 51.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 23a61d55bf..300862ae16 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 23a61d55bf67c6202f1fcc38390fb32dbdfa6e73 +Subproject commit 300862ae1624b39d73f43c6dfd2ab00e3454434d From 8690bbd7fc68e2df9b194b458d1a4f8c0b2f8ae5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Jan 2025 07:52:31 -0800 Subject: [PATCH 024/115] update changelog --- docs/changelog.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1fa9a7eb7a..e747e8aacc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,11 @@ Template for new versions: ## Removed +# 51.04-r1 + +## Misc Improvements +- Compatibility with Steam release of DF 51.04 + # 51.03-r1.1 ## Misc Improvements From 6ef2ee6c55a55764de74b620aac89b63f2ca02e7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Jan 2025 21:13:22 -0800 Subject: [PATCH 025/115] use new itch-dl filename filter --- .github/workflows/generate-symbols.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 7c81b4844e..0310e7f6c3 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -163,8 +163,9 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY - tar xjf "kitfoxgames/dwarf-fortress/files/dwarf_fortress_${minor}_${patch}_linux.tar.bz2" -C DF_itch + fname="kitfoxgames/dwarf-fortress/files/dwarf_fortress_${minor}_${patch}_linux.tar.bz2" + itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" + tar xjf "${fname}" -C DF_itch tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_itch xml/symbols_gen_linux.sh ${{ inputs.version }} ITCH DF_itch @@ -270,8 +271,9 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY - unzip -d DF_itch "kitfoxgames/dwarf-fortress/files/dwarf_fortress_${minor}_${patch}_windows.zip" + fname="kitfoxgames/dwarf-fortress/files/dwarf_fortress_${minor}_${patch}_windows.zip" + itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" + unzip -d DF_itch "${fname}" xml/symbols_gen_windows.sh ${{ inputs.version }} ITCH DF_itch # Classic From a731dfa2ca526b229e79bea00cf495cd4d10acb4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Jan 2025 21:16:42 -0800 Subject: [PATCH 026/115] glob match take 2 --- .github/workflows/generate-symbols.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 0310e7f6c3..85a0067c85 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -163,9 +163,9 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="kitfoxgames/dwarf-fortress/files/dwarf_fortress_${minor}_${patch}_linux.tar.bz2" + fname="dwarf_fortress_${minor}_${patch}_linux.tar.bz2" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" - tar xjf "${fname}" -C DF_itch + tar xjf "kitfoxgames/dwarf-fortress/files/${fname}" -C DF_itch tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_itch xml/symbols_gen_linux.sh ${{ inputs.version }} ITCH DF_itch @@ -271,9 +271,9 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="kitfoxgames/dwarf-fortress/files/dwarf_fortress_${minor}_${patch}_windows.zip" + fname="dwarf_fortress_${minor}_${patch}_windows.zip" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" - unzip -d DF_itch "${fname}" + unzip -d DF_itch "kitfoxgames/dwarf-fortress/files/${fname}" xml/symbols_gen_windows.sh ${{ inputs.version }} ITCH DF_itch # Classic From 1730865725c76e96866b1e80c63176dd4386bdd9 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 28 Jan 2025 06:02:30 +0000 Subject: [PATCH 027/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index abe2e0b9b6..c71abf6d9c 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit abe2e0b9b6cad8804824267b5683d36eaf2b7aa4 +Subproject commit c71abf6d9c2c377883f858b0aa97308d57ee9ca2 diff --git a/scripts b/scripts index 34012d635d..33c1e52528 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 34012d635d47da217e42e99aba1fba1c3690f665 +Subproject commit 33c1e52528dd0ef06b540fb6da1ed53d8f982ee1 From 582c1e750c1a171c44b82923357f72cb1274941f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 29 Jan 2025 07:16:24 +0000 Subject: [PATCH 028/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index c71abf6d9c..f1abee357a 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit c71abf6d9c2c377883f858b0aa97308d57ee9ca2 +Subproject commit f1abee357a06fa02be16a5bbf08c838bd251f492 diff --git a/scripts b/scripts index 33c1e52528..d68e0cebc9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 33c1e52528dd0ef06b540fb6da1ed53d8f982ee1 +Subproject commit d68e0cebc9da3fad475d40baf4016c2c5c8323fb From 3a708558ff2217e45644a4ceb02d3c3415268fab Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 30 Jan 2025 07:16:09 +0000 Subject: [PATCH 029/115] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index f1abee357a..d3f735c0b0 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f1abee357a06fa02be16a5bbf08c838bd251f492 +Subproject commit d3f735c0b0773520da38b75acc072dd8614e8e29 From 3071db5eb4f45e0d7949f80b117921721a7651c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 30 Jan 2025 07:51:42 +0100 Subject: [PATCH 030/115] Fix Edit Field long lines changing text cursor behaviour --- library/lua/gui/widgets/edit_field.lua | 1 - library/lua/gui/widgets/text_area.lua | 10 +++++++++- test/library/gui/widgets.TextArea.lua | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index f2a4b9dc2b..7e7ff46a3d 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -114,7 +114,6 @@ function EditField:init() self:setFocus(true) end - self.start_pos = 1 self.cursor = #self.text + 1 self.ignore_keys = self.ignore_keys or {} diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index c906bf9223..2dd722ba55 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -69,7 +69,15 @@ function TextArea:setText(text) self:getCursor() ) - return self.text_area:setText(text) + self.text_area:setText(text) + + if self.one_line_mode then + self.render_start_x = 1 + local cursor = self:getCursor() + if cursor then + self:setCursor(math.min(self:getCursor(), #text + 1)) + end + end end function TextArea:getCursor() diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 8212763237..a5748afd5f 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -3391,3 +3391,24 @@ function test.should_scroll_horizontally_in_one_line_mode() screen:dismiss() end + +function test.should_reset_horizontal_in_one_line_mode() + local text_area, screen, window, widget = arrange_textarea({ + w=40, + one_line_mode=true + }) + + local text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque dignissim volutpat orci, sed' + + widget:setText(text) + + widget:setCursor(#text + 1) + + expect.eq(read_rendered_text(text_area), text:sub(-39) .. '_') + + widget:setText('Lorem ipsum') + + expect.eq(read_rendered_text(text_area), 'Lorem ipsum_') + + screen:dismiss() +end From 555f1b4e5cc7b2f742c862976fb9f7ded87c5f78 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 30 Jan 2025 02:38:06 -0800 Subject: [PATCH 031/115] update to current upstream json library --- library/lua/json/internal.lua | 1109 ++++++++++++++++++++++++++++----- 1 file changed, 962 insertions(+), 147 deletions(-) diff --git a/library/lua/json/internal.lua b/library/lua/json/internal.lua index 35f50e35ff..92502193f1 100644 --- a/library/lua/json/internal.lua +++ b/library/lua/json/internal.lua @@ -2,20 +2,20 @@ -- -- Simple JSON encoding and decoding in pure Lua. -- --- Copyright 2010-2014 Jeffrey Friedl +-- Copyright 2010-2017 Jeffrey Friedl -- http://regex.info/blog/ --- -- Latest version: http://regex.info/blog/lua/json -- -- This code is released under a Creative Commons CC-BY "Attribution" License: -- http://creativecommons.org/licenses/by/3.0/deed.en_US -- --- It can be used for any purpose so long as the copyright notice above, --- the web-page links above, and the 'AUTHOR_NOTE' string below are --- maintained. Enjoy. +-- It can be used for any purpose so long as: +-- 1) the copyright notice above is maintained +-- 2) the web-page links above are maintained +-- 3) the 'AUTHOR_NOTE' string below is maintained -- -local VERSION = 20141223.14 -- version history at end of file -local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20141223.14 ]-" +local VERSION = '20211016.28' -- version history at end of file +local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20211016.28 ]-" -- -- The 'AUTHOR_NOTE' variable exists so that information about the source @@ -30,7 +30,7 @@ local OBJDEF = { -- -- Simple JSON encoding and decoding in pure Lua. --- http://www.json.org/ +-- JSON definition: http://www.json.org/ -- -- -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines @@ -66,7 +66,7 @@ local OBJDEF = { -- -- -- --- ERROR HANDLING +-- ERROR HANDLING DURING DECODE -- -- With most errors during decoding, this code calls -- @@ -77,10 +77,10 @@ local OBJDEF = { -- replace the default JSON:onDecodeError() with your own function. -- -- The default onDecodeError() merely augments the message with data --- about the text and the location if known (and if a second 'etc' --- argument had been provided to decode(), its value is tacked onto the --- message as well), and then calls JSON.assert(), which itself defaults --- to Lua's built-in assert(), and can also be overridden. +-- about the text and the location (and, an 'etc' argument had been +-- provided to decode(), its value is tacked onto the message as well), +-- and then calls JSON.assert(), which itself defaults to Lua's built-in +-- assert(), and can also be overridden. -- -- For example, in an Adobe Lightroom plugin, you might use something like -- @@ -102,9 +102,9 @@ local OBJDEF = { -- -- JSON:onDecodeOfHTMLError(message, text, nil, etc) -- --- The use of the fourth 'etc' argument allows stronger coordination --- between decoding and error reporting, especially when you provide your --- own error-handling routines. Continuing with the the Adobe Lightroom +-- The use of the 'etc' argument allows stronger coordination between +-- decoding and error reporting, especially when you provide your own +-- error-handling routines. Continuing with the the Adobe Lightroom -- plugin example: -- -- function JSON:onDecodeError(message, text, location, etc) @@ -128,6 +128,29 @@ local OBJDEF = { -- -- -- +-- If the JSON text passed to decode() has trailing garbage (e.g. as with the JSON "[123]xyzzy"), +-- the method +-- +-- JSON:onTrailingGarbage(json_text, location, parsed_value, etc) +-- +-- is invoked, where: +-- +-- 'json_text' is the original JSON text being parsed, +-- 'location' is the count of bytes into 'json_text' where the garbage starts (6 in the example), +-- 'parsed_value' is the Lua result of what was successfully parsed ({123} in the example), +-- 'etc' is as above. +-- +-- If JSON:onTrailingGarbage() does not abort, it should return the value decode() should return, +-- or nil + an error message. +-- +-- local new_value, error_message = JSON:onTrailingGarbage() +-- +-- The default JSON:onTrailingGarbage() simply invokes JSON:onDecodeError("trailing garbage"...), +-- but you can have this package ignore trailing garbage via +-- +-- function JSON:onTrailingGarbage(json_text, location, parsed_value, etc) +-- return parsed_value +-- end -- -- -- DECODING AND STRICT TYPES @@ -150,6 +173,28 @@ local OBJDEF = { -- tables that have a metatable set, for example, Lightroom API calls.) -- -- +-- DECODING AND STRICT PARSING +-- +-- If strictParsing is true in your JSON object, or if you set strictParsing as a decode option, +-- some kinds of technically-invalid JSON that would normally be accepted are rejected with an error. +-- +-- For example, passing in an empty string +-- +-- JSON:decode("") +-- +-- normally succeeds with a return value of nil, but +-- +-- JSON:decode("", nil, { strictParsing = true }) +-- +-- results in an error being raised (onDecodeError is called). +-- +-- JSON.strictParsing = true +-- JSON:decode("") +-- +-- achieves the same thing. +-- +-- +-- -- ENCODING (from a lua table to a JSON string) -- -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines @@ -162,27 +207,55 @@ local OBJDEF = { -- -- JSON:onEncodeError(message, etc) -- --- which you can override in your local JSON object. +-- which you can override in your local JSON object. Also see "HANDLING UNSUPPORTED VALUE TYPES" below. +-- +-- The 'etc' in the error call is the second argument to encode() and encode_pretty(), or nil if it wasn't provided. +-- +-- +-- +-- +-- ENCODING OPTIONS +-- +-- An optional third argument, a table of options, can be provided to encode(). +-- +-- encode_options = { +-- -- options for making "pretty" human-readable JSON (see "PRETTY-PRINTING" below) +-- pretty = true, -- turn pretty formatting on +-- indent = " ", -- use this indent for each level of an array/object +-- align_keys = false, -- if true, align the keys in a way that sounds like it should be nice, but is actually ugly +-- array_newline = false, -- if true, array elements become one to a line rather than inline +-- +-- -- other output-related options +-- null = "\0", -- see "ENCODING JSON NULL VALUES" below +-- stringsAreUtf8 = false, -- see "HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA" below +-- } +-- +-- json_string = JSON:encode(mytable, etc, encode_options) +-- +-- +-- +-- For reference, the defaults are: +-- +-- pretty = false +-- null = nil, +-- stringsAreUtf8 = false, -- --- The 'etc' in the error call is the second argument to encode() --- and encode_pretty(), or nil if it wasn't provided. -- -- -- PRETTY-PRINTING -- --- An optional third argument, a table of options, allows a bit of --- configuration about how the encoding takes place: +-- Enabling the 'pretty' encode option helps generate human-readable JSON. -- -- pretty = JSON:encode(val, etc, { --- pretty = true, -- if false, no other options matter --- indent = " ", -- this provides for a three-space indent per nesting level --- align_keys = false, -- see below +-- pretty = true, +-- indent = " ", +-- align_keys = false, -- }) -- --- encode() and encode_pretty() are identical except that encode_pretty() --- provides a default options table if none given in the call: +-- encode_pretty() is also provided: it's identical to encode() except +-- that encode_pretty() provides a default options table if none given in the call: -- --- { pretty = true, align_keys = false, indent = " " } +-- { pretty = true, indent = " ", align_keys = false, array_newline = false } -- -- For example, if -- @@ -209,10 +282,11 @@ local OBJDEF = { -- "wards": 11 -- } -- --- The following three lines return identical results: +-- The following lines all return identical strings: -- JSON:encode_pretty(data) --- JSON:encode_pretty(data, nil, { pretty = true, align_keys = false, indent = " " }) --- JSON:encode (data, nil, { pretty = true, align_keys = false, indent = " " }) +-- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = false, array_newline = false}) +-- JSON:encode_pretty(data, nil, { pretty = true, indent = " " }) +-- JSON:encode (data, nil, { pretty = true, indent = " " }) -- -- An example of setting your own indent string: -- @@ -252,6 +326,16 @@ local OBJDEF = { -- encode_pretty() prior to version 20141223.14. -- -- +-- HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA +-- +-- If the 'stringsAreUtf8' encode option is set to true, consider Lua strings not as a sequence of bytes, +-- but as a sequence of UTF-8 characters. +-- +-- Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH +-- separators, if found in a string, are encoded with a JSON escape instead of being dumped as is. +-- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON +-- to also be valid Java. +-- -- AMBIGUOUS SITUATIONS DURING THE ENCODING -- -- During the encode, if a Lua table being encoded contains both string @@ -268,10 +352,255 @@ local OBJDEF = { -- To prohibit this conversion and instead make it an error condition, set -- JSON.noKeyConversion = true -- +-- +-- ENCODING JSON NULL VALUES +-- +-- Lua tables completely omit keys whose value is nil, so without special handling there's +-- no way to represent JSON object's null value in a Lua table. For example +-- JSON:encode({ username = "admin", password = nil }) +-- +-- produces: +-- +-- {"username":"admin"} +-- +-- In order to actually produce +-- +-- {"username":"admin", "password":null} +-- - - - +-- one can include a string value for a "null" field in the options table passed to encode().... +-- any Lua table entry with that value becomes null in the JSON output: +-- +-- JSON:encode({ username = "admin", password = "xyzzy" }, -- First arg is the Lua table to encode as JSON. +-- nil, -- Second arg is the 'etc' value, ignored here +-- { null = "xyzzy" }) -- Third arg is th options table +-- +-- produces: +-- +-- {"username":"admin", "password":null} +-- +-- Just be sure to use a string that is otherwise unlikely to appear in your data. +-- The string "\0" (a string with one null byte) may well be appropriate for many applications. +-- +-- The "null" options also applies to Lua tables that become JSON arrays. +-- JSON:encode({ "one", "two", nil, nil }) +-- +-- produces +-- +-- ["one","two"] +-- +-- while +-- +-- NullPlaceholder = "\0" +-- encode_options = { null = NullPlaceholder } +-- JSON:encode({ "one", "two", NullPlaceholder, NullPlaceholder}, nil, encode_options) +-- produces +-- +-- ["one","two",null,null] +-- +-- +-- +-- HANDLING LARGE AND/OR PRECISE NUMBERS +-- +-- +-- Without special handling, numbers in JSON can lose precision in Lua. +-- For example: +-- +-- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') +-- +-- print("small: ", type(T.small), T.small) +-- print("big: ", type(T.big), T.big) +-- print("precise: ", type(T.precise), T.precise) +-- +-- produces +-- +-- small: number 12345 +-- big: number 1.2345678901235e+28 +-- precise: number 9876.6789012346 +-- +-- Precision is lost with both 'big' and 'precise'. +-- +-- This package offers ways to try to handle this better (for some definitions of "better")... +-- +-- The most precise method is by setting the global: +-- +-- JSON.decodeNumbersAsObjects = true +-- +-- When this is set, numeric JSON data is encoded into Lua in a form that preserves the exact +-- JSON numeric presentation when re-encoded back out to JSON, or accessed in Lua as a string. +-- +-- This is done by encoding the numeric data with a Lua table/metatable that returns +-- the possibly-imprecise numeric form when accessed numerically, but the original precise +-- representation when accessed as a string. +-- +-- Consider the example above, with this option turned on: +-- +-- JSON.decodeNumbersAsObjects = true +-- +-- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') +-- +-- print("small: ", type(T.small), T.small) +-- print("big: ", type(T.big), T.big) +-- print("precise: ", type(T.precise), T.precise) +-- +-- This now produces: +-- +-- small: table 12345 +-- big: table 12345678901234567890123456789 +-- precise: table 9876.67890123456789012345 +-- +-- However, within Lua you can still use the values (e.g. T.precise in the example above) in numeric +-- contexts. In such cases you'll get the possibly-imprecise numeric version, but in string contexts +-- and when the data finds its way to this package's encode() function, the original full-precision +-- representation is used. +-- +-- You can force access to the string or numeric version via +-- JSON:forceString() +-- JSON:forceNumber() +-- For example, +-- local probably_okay = JSON:forceNumber(T.small) -- 'probably_okay' is a number +-- +-- Code the inspects the JSON-turned-Lua data using type() can run into troubles because what used to +-- be a number can now be a table (e.g. as the small/big/precise example above shows). Update these +-- situations to use JSON:isNumber(item), which returns nil if the item is neither a number nor one +-- of these number objects. If it is either, it returns the number itself. For completeness there's +-- also JSON:isString(item). +-- +-- If you want to try to avoid the hassles of this "number as an object" kludge for all but really +-- big numbers, you can set JSON.decodeNumbersAsObjects and then also set one or both of +-- JSON:decodeIntegerObjectificationLength +-- JSON:decodeDecimalObjectificationLength +-- They refer to the length of the part of the number before and after a decimal point. If they are +-- set and their part is at least that number of digits, objectification occurs. If both are set, +-- objectification occurs when either length is met. +-- +-- ----------------------- +-- +-- Even without using the JSON.decodeNumbersAsObjects option, you can encode numbers in your Lua +-- table that retain high precision upon encoding to JSON, by using the JSON:asNumber() function: +-- +-- T = { +-- imprecise = 123456789123456789.123456789123456789, +-- precise = JSON:asNumber("123456789123456789.123456789123456789") +-- } +-- +-- print(JSON:encode_pretty(T)) +-- +-- This produces: +-- +-- { +-- "precise": 123456789123456789.123456789123456789, +-- "imprecise": 1.2345678912346e+17 +-- } +-- +-- +-- ----------------------- +-- +-- A different way to handle big/precise JSON numbers is to have decode() merely return the exact +-- string representation of the number instead of the number itself. This approach might be useful +-- when the numbers are merely some kind of opaque object identifier and you want to work with them +-- in Lua as strings anyway. +-- +-- This approach is enabled by setting +-- +-- JSON.decodeIntegerStringificationLength = 10 +-- +-- The value is the number of digits (of the integer part of the number) at which to stringify numbers. +-- NOTE: this setting is ignored if JSON.decodeNumbersAsObjects is true, as that takes precedence. +-- +-- Consider our previous example with this option set to 10: +-- +-- JSON.decodeIntegerStringificationLength = 10 +-- +-- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') +-- +-- print("small: ", type(T.small), T.small) +-- print("big: ", type(T.big), T.big) +-- print("precise: ", type(T.precise), T.precise) +-- +-- This produces: +-- +-- small: number 12345 +-- big: string 12345678901234567890123456789 +-- precise: number 9876.6789012346 +-- +-- The long integer of the 'big' field is at least JSON.decodeIntegerStringificationLength digits +-- in length, so it's converted not to a Lua integer but to a Lua string. Using a value of 0 or 1 ensures +-- that all JSON numeric data becomes strings in Lua. +-- +-- Note that unlike +-- JSON.decodeNumbersAsObjects = true +-- this stringification is simple and unintelligent: the JSON number simply becomes a Lua string, and that's the end of it. +-- If the string is then converted back to JSON, it's still a string. After running the code above, adding +-- print(JSON:encode(T)) +-- produces +-- {"big":"12345678901234567890123456789","precise":9876.6789012346,"small":12345} +-- which is unlikely to be desired. +-- +-- There's a comparable option for the length of the decimal part of a number: +-- +-- JSON.decodeDecimalStringificationLength +-- +-- This can be used alone or in conjunction with +-- +-- JSON.decodeIntegerStringificationLength +-- +-- to trip stringification on precise numbers with at least JSON.decodeIntegerStringificationLength digits after +-- the decimal point. (Both are ignored if JSON.decodeNumbersAsObjects is true.) +-- +-- This example: +-- +-- JSON.decodeIntegerStringificationLength = 10 +-- JSON.decodeDecimalStringificationLength = 5 +-- +-- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') +-- +-- print("small: ", type(T.small), T.small) +-- print("big: ", type(T.big), T.big) +-- print("precise: ", type(T.precise), T.precise) +-- +-- produces: +-- +-- small: number 12345 +-- big: string 12345678901234567890123456789 +-- precise: string 9876.67890123456789012345 +-- +-- +-- HANDLING UNSUPPORTED VALUE TYPES +-- +-- Among the encoding errors that might be raised is an attempt to convert a table value that has a type +-- that this package hasn't accounted for: a function, userdata, or a thread. You can handle these types as table +-- values (but not as table keys) if you supply a JSON:unsupportedTypeEncoder() method along the lines of the +-- following example: +-- +-- function JSON:unsupportedTypeEncoder(value_of_unsupported_type) +-- if type(value_of_unsupported_type) == 'function' then +-- return "a function value" +-- else +-- return nil +-- end +-- end +-- +-- Your unsupportedTypeEncoder() method is actually called with a bunch of arguments: +-- +-- self:unsupportedTypeEncoder(value, parents, etc, options, indent, for_key) +-- +-- The 'value' is the function, thread, or userdata to be converted to JSON. +-- +-- The 'etc' and 'options' arguments are those passed to the original encode(). The other arguments are +-- probably of little interest; see the source code. (Note that 'for_key' is never true, as this function +-- is invoked only on table values; table keys of these types still trigger the onEncodeError method.) +-- +-- If your unsupportedTypeEncoder() method returns a string, it's inserted into the JSON as is. +-- If it returns nil plus an error message, that error message is passed through to an onEncodeError invocation. +-- If it returns only nil, processing falls through to a default onEncodeError invocation. +-- +-- If you want to handle everything in a simple way: +-- +-- function JSON:unsupportedTypeEncoder(value) +-- return tostring(value) +-- end +-- -- -- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT -- @@ -279,7 +608,9 @@ local OBJDEF = { -- onDecodeError -- onDecodeOfNilError -- onDecodeOfHTMLError +-- onTrailingGarbage -- onEncodeError +-- unsupportedTypeEncoder -- -- If you want to create a separate Lua JSON object with its own error handlers, -- you can reload JSON.lua or use the :new() method. @@ -287,11 +618,10 @@ local OBJDEF = { --------------------------------------------------------------------------- local default_pretty_indent = " " -local default_pretty_options = { pretty = true, align_keys = false, indent = default_pretty_indent } - -local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray -local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject +local default_pretty_options = { pretty = true, indent = default_pretty_indent, align_keys = false, array_newline = false } +local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray +local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject function OBJDEF:newArray(tbl) return setmetatable(tbl or {}, isArray) @@ -301,6 +631,129 @@ function OBJDEF:newObject(tbl) return setmetatable(tbl or {}, isObject) end + + + +local function getnum(op) + return type(op) == 'number' and op or op.N +end + +local isNumber = { + __tostring = function(T) return T.S end, + __unm = function(op) return getnum(op) end, + + __concat = function(op1, op2) return tostring(op1) .. tostring(op2) end, + __add = function(op1, op2) return getnum(op1) + getnum(op2) end, + __sub = function(op1, op2) return getnum(op1) - getnum(op2) end, + __mul = function(op1, op2) return getnum(op1) * getnum(op2) end, + __div = function(op1, op2) return getnum(op1) / getnum(op2) end, + __mod = function(op1, op2) return getnum(op1) % getnum(op2) end, + __pow = function(op1, op2) return getnum(op1) ^ getnum(op2) end, + __lt = function(op1, op2) return getnum(op1) < getnum(op2) end, + __eq = function(op1, op2) return getnum(op1) == getnum(op2) end, + __le = function(op1, op2) return getnum(op1) <= getnum(op2) end, +} +isNumber.__index = isNumber + +function OBJDEF:asNumber(item) + + if getmetatable(item) == isNumber then + -- it's already a JSON number object. + return item + elseif type(item) == 'table' and type(item.S) == 'string' and type(item.N) == 'number' then + -- it's a number-object table that lost its metatable, so give it one + return setmetatable(item, isNumber) + else + -- the normal situation... given a number or a string representation of a number.... + local holder = { + S = tostring(item), -- S is the representation of the number as a string, which remains precise + N = tonumber(item), -- N is the number as a Lua number. + } + return setmetatable(holder, isNumber) + end +end + +-- +-- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above, +-- return the string version. This shouldn't be needed often because the 'isNumber' object should autoconvert +-- to a string in most cases, but it's here to allow it to be forced when needed. +-- +function OBJDEF:forceString(item) + if type(item) == 'table' and type(item.S) == 'string' then + return item.S + else + return tostring(item) + end +end + +-- +-- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above, +-- return the numeric version. +-- +function OBJDEF:forceNumber(item) + if type(item) == 'table' and type(item.N) == 'number' then + return item.N + else + return tonumber(item) + end +end + +-- +-- If the given item is a number, return it. Otherwise, return nil. +-- This, this can be used both in a conditional and to access the number when you're not sure its form. +-- +function OBJDEF:isNumber(item) + if type(item) == 'number' then + return item + elseif type(item) == 'table' and type(item.N) == 'number' then + return item.N + else + return nil + end +end + +function OBJDEF:isString(item) + if type(item) == 'string' then + return item + elseif type(item) == 'table' and type(item.S) == 'string' then + return item.S + else + return nil + end +end + + + + +-- +-- Some utf8 routines to deal with the fact that Lua handles only bytes +-- +local function top_three_bits(val) + return math.floor(val / 0x20) +end + +local function top_four_bits(val) + return math.floor(val / 0x10) +end + +local function unicode_character_bytecount_based_on_first_byte(first_byte) + local W = string.byte(first_byte) + if W < 0x80 then + return 1 + elseif (W == 0xC0) or (W == 0xC1) or (W >= 0x80 and W <= 0xBF) or (W >= 0xF5) then + -- this is an error -- W can't be the start of a utf8 character + return 0 + elseif top_three_bits(W) == 0x06 then + return 2 + elseif top_four_bits(W) == 0x0E then + return 3 + else + return 4 + end +end + + + local function unicode_codepoint_as_utf8(codepoint) -- -- codepoint is a number @@ -367,7 +820,7 @@ end function OBJDEF:onDecodeError(message, text, location, etc) if text then if location then - message = string.format("%s at char %d of: %s", message, location, text) + message = string.format("%s at byte %d of: %s", message, location, text) else message = string.format("%s: %s", message, text) end @@ -384,6 +837,10 @@ function OBJDEF:onDecodeError(message, text, location, etc) end end +function OBJDEF:onTrailingGarbage(json_text, location, parsed_value, etc) + return self:onDecodeError("trailing garbage", json_text, location, etc) +end + OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError @@ -399,7 +856,7 @@ function OBJDEF:onEncodeError(message, etc) end end -local function grok_number(self, text, start, etc) +local function grok_number(self, text, start, options) -- -- Grab the integer part -- @@ -407,7 +864,8 @@ local function grok_number(self, text, start, etc) or text:match("^-?0", start) if not integer_part then - self:onDecodeError("expected number", text, start, etc) + self:onDecodeError("expected number", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end local i = start + integer_part:len() @@ -427,20 +885,86 @@ local function grok_number(self, text, start, etc) i = i + exponent_part:len() local full_number_text = integer_part .. decimal_part .. exponent_part + + if options.decodeNumbersAsObjects then + + local objectify = false + + if not options.decodeIntegerObjectificationLength and not options.decodeDecimalObjectificationLength then + -- no options, so objectify + objectify = true + + elseif (options.decodeIntegerObjectificationLength + and + (integer_part:len() >= options.decodeIntegerObjectificationLength or exponent_part:len() > 0)) + + or + (options.decodeDecimalObjectificationLength + and + (decimal_part:len() >= options.decodeDecimalObjectificationLength or exponent_part:len() > 0)) + then + -- have options and they are triggered, so objectify + objectify = true + end + + if objectify then + return OBJDEF:asNumber(full_number_text), i + end + -- else, fall through to try to return as a straight-up number + + else + + -- Not always decoding numbers as objects, so perhaps encode as strings? + + -- + -- If we're told to stringify only under certain conditions, so do. + -- We punt a bit when there's an exponent by just stringifying no matter what. + -- I suppose we should really look to see whether the exponent is actually big enough one + -- way or the other to trip stringification, but I'll be lazy about it until someone asks. + -- + if (options.decodeIntegerStringificationLength + and + (integer_part:len() >= options.decodeIntegerStringificationLength or exponent_part:len() > 0)) + + or + + (options.decodeDecimalStringificationLength + and + (decimal_part:len() >= options.decodeDecimalStringificationLength or exponent_part:len() > 0)) + then + return full_number_text, i -- this returns the exact string representation seen in the original JSON + end + + end + + local as_number = tonumber(full_number_text) if not as_number then - self:onDecodeError("bad number", text, start, etc) + self:onDecodeError("bad number", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end return as_number, i end -local function grok_string(self, text, start, etc) +local backslash_escape_conversion = { + ['"'] = '"', + ['/'] = "/", + ['\\'] = "\\", + ['b'] = "\b", + ['f'] = "\f", + ['n'] = "\n", + ['r'] = "\r", + ['t'] = "\t", +} + +local function grok_string(self, text, start, options) if text:sub(start,start) ~= '"' then - self:onDecodeError("expected string's opening quote", text, start, etc) + self:onDecodeError("expected string's opening quote", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end local i = start + 1 -- +1 to bypass the initial quote @@ -452,58 +976,119 @@ local function grok_string(self, text, start, etc) return VALUE, i + 1 end if c ~= '\\' then - VALUE = VALUE .. c - i = i + 1 - elseif text:match('^\\b', i) then - VALUE = VALUE .. "\b" - i = i + 2 - elseif text:match('^\\f', i) then - VALUE = VALUE .. "\f" - i = i + 2 - elseif text:match('^\\n', i) then - VALUE = VALUE .. "\n" - i = i + 2 - elseif text:match('^\\r', i) then - VALUE = VALUE .. "\r" - i = i + 2 - elseif text:match('^\\t', i) then - VALUE = VALUE .. "\t" - i = i + 2 - else - local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) - if hex then - i = i + 6 -- bypass what we just read - - -- We have a Unicode codepoint. It could be standalone, or if in the proper range and - -- followed by another in a specific range, it'll be a two-code surrogate pair. - local codepoint = tonumber(hex, 16) - if codepoint >= 0xD800 and codepoint <= 0xDBFF then - -- it's a hi surrogate... see whether we have a following low - local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) - if lo_surrogate then - i = i + 6 -- bypass the low surrogate we just read - codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) - else - -- not a proper low, so we'll just leave the first codepoint as is and spit it out. - end + + -- should grab the next bytes as per the number of bytes for this utf8 character + local byte_count = unicode_character_bytecount_based_on_first_byte(c) + + local next_character + if byte_count == 0 then + self:onDecodeError("non-utf8 sequence", text, i, options.etc) + elseif byte_count == 1 then + if options.strictParsing and string.byte(c) < 0x20 then + self:onDecodeError("Unescaped control character", text, i+1, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end - VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) + next_character = c + elseif byte_count == 2 then + next_character = text:match('^(.[\128-\191])', i) + elseif byte_count == 3 then + next_character = text:match('^(.[\128-\191][\128-\191])', i) + elseif byte_count == 4 then + next_character = text:match('^(.[\128-\191][\128-\191][\128-\191])', i) + end + + if not next_character then + self:onDecodeError("incomplete utf8 sequence", text, i, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible + end + + + VALUE = VALUE .. next_character + i = i + byte_count + + else + -- + -- We have a backslash escape + -- + i = i + 1 + + local next_byte = text:match('^(.)', i) + if next_byte == nil then + -- string ended after the \ + self:onDecodeError("unfinished \\ escape", text, i, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end + + if backslash_escape_conversion[next_byte] then + VALUE = VALUE .. backslash_escape_conversion[next_byte] + i = i + 1 else + -- + -- The only other valid use of \ that remains is in the form of \u#### + -- + + local hex = text:match('^u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) + if hex then + i = i + 5 -- bypass what we just read + + -- We have a Unicode codepoint. It could be standalone, or if in the proper range and + -- followed by another in a specific range, it'll be a two-code surrogate pair. + local codepoint = tonumber(hex, 16) + if codepoint >= 0xD800 and codepoint <= 0xDBFF then + -- it's a hi surrogate... see whether we have a following low + local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) + if lo_surrogate then + i = i + 6 -- bypass the low surrogate we just read + codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) + else + -- not a proper low, so we'll just leave the first codepoint as is and spit it out. + end + end + VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) + + elseif options.strictParsing then + --local next_byte = text:match('^\\(.)', i) printf("NEXT[%s]", next_byte); + self:onDecodeError("illegal use of backslash escape", text, i, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + else + local byte_count = unicode_character_bytecount_based_on_first_byte(next_byte) + if byte_count == 0 then + self:onDecodeError("non-utf8 sequence after backslash escape", text, i, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end + + local next_character + if byte_count == 1 then + next_character = next_byte + elseif byte_count == 2 then + next_character = text:match('^(.[\128-\191])', i) + elseif byte_count == 3 then + next_character = text:match('^(.[\128-\191][\128-\191])', i) + elseif byte_count == 3 then + next_character = text:match('^(.[\128-\191][\128-\191][\128-\191])', i) + end + + if next_character == nil then + -- incomplete utf8 character after escape + self:onDecodeError("incomplete utf8 sequence after backslash escape", text, i, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end - -- just pass through what's escaped - VALUE = VALUE .. text:match('^\\(.)', i) - i = i + 2 + VALUE = VALUE .. next_character + i = i + byte_count + end end end end - self:onDecodeError("unclosed string", text, start, etc) + self:onDecodeError("unclosed string", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end local function skip_whitespace(text, start) - local _, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 + local _, match_end = text:find("^[ \n\r\t]+", start) -- [ https://datatracker.ietf.org/doc/html/rfc7158#section-2 ] if match_end then return match_end + 1 else @@ -513,9 +1098,11 @@ end local grok_one -- assigned later -local function grok_object(self, text, start, etc) +local function grok_object(self, text, start, options) + if text:sub(start,start) ~= '{' then - self:onDecodeError("expected '{'", text, start, etc) + self:onDecodeError("expected '{'", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' @@ -527,17 +1114,18 @@ local function grok_object(self, text, start, etc) end local text_len = text:len() while i <= text_len do - local key, new_i = grok_string(self, text, i, etc) + local key, new_i = grok_string(self, text, i, options) i = skip_whitespace(text, new_i) if text:sub(i, i) ~= ':' then - self:onDecodeError("expected colon", text, i, etc) + self:onDecodeError("expected colon", text, i, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible end i = skip_whitespace(text, i + 1) - local new_val, new_i = grok_one(self, text, i) + local new_val, new_i = grok_one(self, text, i, options) VALUE[key] = new_val @@ -553,18 +1141,21 @@ local function grok_object(self, text, start, etc) end if text:sub(i, i) ~= ',' then - self:onDecodeError("expected comma or '}'", text, i, etc) + self:onDecodeError("expected comma or '}'", text, i, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible end i = skip_whitespace(text, i + 1) end - self:onDecodeError("unclosed '{'", text, start, etc) + self:onDecodeError("unclosed '{'", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end -local function grok_array(self, text, start, etc) +local function grok_array(self, text, start, options) if text:sub(start,start) ~= '[' then - self:onDecodeError("expected '['", text, start, etc) + self:onDecodeError("expected '['", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' @@ -577,7 +1168,7 @@ local function grok_array(self, text, start, etc) local text_len = text:len() while i <= text_len do - local val, new_i = grok_one(self, text, i) + local val, new_i = grok_one(self, text, i, options) -- can't table.insert(VALUE, val) here because it's a no-op if val is nil VALUE[VALUE_INDEX] = val @@ -593,33 +1184,36 @@ local function grok_array(self, text, start, etc) return VALUE, i + 1 end if text:sub(i, i) ~= ',' then - self:onDecodeError("expected comma or '['", text, i, etc) + self:onDecodeError("expected comma or ']'", text, i, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible end i = skip_whitespace(text, i + 1) end - self:onDecodeError("unclosed '['", text, start, etc) + self:onDecodeError("unclosed '['", text, start, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible end -grok_one = function(self, text, start, etc) +grok_one = function(self, text, start, options) -- Skip any whitespace start = skip_whitespace(text, start) if start > text:len() then - self:onDecodeError("unexpected end of string", text, nil, etc) + self:onDecodeError("unexpected end of string", text, nil, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible end if text:find('^"', start) then - return grok_string(self, text, start, etc) + return grok_string(self, text, start, options) elseif text:find('^[-0123456789 ]', start) then - return grok_number(self, text, start, etc) + return grok_number(self, text, start, options) elseif text:find('^%{', start) then - return grok_object(self, text, start, etc) + return grok_object(self, text, start, options) elseif text:find('^%[', start) then - return grok_array(self, text, start, etc) + return grok_array(self, text, start, options) elseif text:find('^true', start) then return true, start + 4 @@ -628,31 +1222,88 @@ grok_one = function(self, text, start, etc) return false, start + 5 elseif text:find('^null', start) then - return nil, start + 4 + return options.null, start + 4 else - self:onDecodeError("can't parse JSON", text, start, etc) + self:onDecodeError("can't parse JSON", text, start, options.etc) + return nil, 1 -- in case the error method doesn't abort, return something sensible end end -function OBJDEF:decode(text, etc) +function OBJDEF:decode(text, etc, options) + -- + -- If the user didn't pass in a table of decode options, make an empty one. + -- + if type(options) ~= 'table' then + options = {} + end + + -- + -- If they passed in an 'etc' argument, stuff it into the options. + -- (If not, any 'etc' field in the options they passed in remains to be used) + -- + if etc ~= nil then + options.etc = etc + end + + + -- + -- apply global options + -- + if options.decodeNumbersAsObjects == nil then + options.decodeNumbersAsObjects = self.decodeNumbersAsObjects + end + if options.decodeIntegerObjectificationLength == nil then + options.decodeIntegerObjectificationLength = self.decodeIntegerObjectificationLength + end + if options.decodeDecimalObjectificationLength == nil then + options.decodeDecimalObjectificationLength = self.decodeDecimalObjectificationLength + end + if options.decodeIntegerStringificationLength == nil then + options.decodeIntegerStringificationLength = self.decodeIntegerStringificationLength + end + if options.decodeDecimalStringificationLength == nil then + options.decodeDecimalStringificationLength = self.decodeDecimalStringificationLength + end + if options.strictParsing == nil then + options.strictParsing = self.strictParsing + end + + if type(self) ~= 'table' or self.__index ~= OBJDEF then - OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc) + local error_message = "JSON:decode must be called in method format" + OBJDEF:onDecodeError(error_message, nil, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible end if text == nil then - self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc) + local error_message = "nil passed to JSON:decode()" + self:onDecodeOfNilError(error_message, nil, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible + elseif type(text) ~= 'string' then - self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc) + local error_message = "expected string argument to JSON:decode()" + self:onDecodeError(string.format("%s, got %s", error_message, type(text)), nil, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible end + -- If passed an empty string.... if text:match('^%s*$') then - return nil + if options.strictParsing then + local error_message = "empty string passed to JSON:decode()" + self:onDecodeOfNilError(error_message, nil, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible + else + -- we'll consider it nothing, but not an error + return nil + end end if text:match('^%s*<') then -- Can't be JSON... we'll assume it's HTML - self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc) + local error_message = "HTML passed to JSON:decode()" + self:onDecodeOfHTMLError(error_message, text, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible end -- @@ -661,40 +1312,57 @@ function OBJDEF:decode(text, etc) -- but this package can't handle them. -- if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then - self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc) + local error_message = "JSON package groks only UTF-8, sorry" + self:onDecodeError(error_message, text, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible end - local success, value = pcall(grok_one, self, text, 1, etc) + + -- + -- Finally, go parse it + -- + local success, value, next_i = pcall(grok_one, self, text, 1, options) if success then - return value + + local error_message = nil + if next_i ~= #text + 1 then + -- something's left over after we parsed the first thing.... whitespace is allowed. + next_i = skip_whitespace(text, next_i) + + -- if we have something left over now, it's trailing garbage + if next_i ~= #text + 1 then + value, error_message = self:onTrailingGarbage(text, next_i, value, options.etc) + end + end + return value, error_message + else - -- if JSON:onDecodeError() didn't abort out of the pcall, we'll have received the error message here as "value", so pass it along as an assert. + + -- If JSON:onDecodeError() didn't abort out of the pcall, we'll have received + -- the error message here as "value", so pass it along as an assert. + local error_message = value if self.assert then - self.assert(false, value) + self.assert(false, error_message) else - assert(false, value) + assert(false, error_message) end - -- and if we're still here, return a nil and throw the error message on as a second arg - return nil, value + -- ...and if we're still here (because the assert didn't throw an error), + -- return a nil and throw the error message on as a second arg + return nil, error_message + end end local function backslash_replacement_function(c) - if c == "\n" then - return "\\n" - elseif c == "\r" then - return "\\r" - elseif c == "\t" then - return "\\t" - elseif c == "\b" then - return "\\b" - elseif c == "\f" then - return "\\f" - elseif c == '"' then - return '\\"' - elseif c == '\\' then - return '\\\\' + if c == "\n" then return "\\n" + elseif c == "\r" then return "\\r" + elseif c == "\t" then return "\\t" + elseif c == "\b" then return "\\b" + elseif c == "\f" then return "\\f" + elseif c == '"' then return '\\"' + elseif c == '\\' then return '\\\\' + elseif c == '/' then return '/' else return string.format("\\u%04x", c:byte()) end @@ -704,12 +1372,25 @@ local chars_to_be_escaped_in_JSON_string = '[' .. '"' -- class sub-pattern to match a double quote .. '%\\' -- class sub-pattern to match a backslash + .. '/' -- class sub-pattern to match a forwardslash .. '%z' -- class sub-pattern to match a null .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters .. ']' -local function json_string_literal(value) + +local LINE_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2028) +local PARAGRAPH_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2029) +local function json_string_literal(value, options) local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) + if options.stringsAreUtf8 then + -- + -- This feels really ugly to just look into a string for the sequence of bytes that we know to be a particular utf8 character, + -- but utf8 was designed purposefully to make this kind of thing possible. Still, feels dirty. + -- I'd rather decode the byte stream into a character stream, but it's not technically needed so + -- not technically worth it. + -- + newval = newval:gsub(LINE_SEPARATOR_as_utf8, '\\u2028'):gsub(PARAGRAPH_SEPARATOR_as_utf8,'\\u2029') + end return '"' .. newval .. '"' end @@ -736,6 +1417,8 @@ local function object_or_array(self, T, etc) elseif not maximum_number_key or key > maximum_number_key then maximum_number_key = key end + elseif type(key) == 'boolean' then + table.insert(string_keys, tostring(key)) else self:onEncodeError("can't encode table with a key of type " .. type(key), etc) end @@ -801,18 +1484,36 @@ end -- Encode -- -- 'options' is nil, or a table with possible keys: --- pretty -- if true, return a pretty-printed version --- indent -- a string (usually of spaces) used to indent each nested level --- align_keys -- if true, align all the keys when formatting a table -- -local encode_value -- must predeclare because it calls itself -function encode_value(self, value, parents, etc, options, indent) +-- pretty -- If true, return a pretty-printed version. +-- +-- indent -- A string (usually of spaces) used to indent each nested level. +-- +-- align_keys -- If true, align all the keys when formatting a table. The result is uglier than one might at first imagine. +-- Results are undefined if 'align_keys' is true but 'pretty' is not. +-- +-- array_newline -- If true, array elements are formatted each to their own line. The default is to all fall inline. +-- Results are undefined if 'array_newline' is true but 'pretty' is not. +-- +-- null -- If this exists with a string value, table elements with this value are output as JSON null. +-- +-- stringsAreUtf8 -- If true, consider Lua strings not as a sequence of bytes, but as a sequence of UTF-8 characters. +-- (Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH +-- separators, if found in a string, are encoded with a JSON escape instead of as raw UTF-8. +-- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON +-- to also be valid Java.) +-- +-- +local function encode_value(self, value, parents, etc, options, indent, for_key) - if value == nil then + -- + -- keys in a JSON object can never be null, so we don't even consider options.null when converting a key value + -- + if value == nil or (not for_key and options and options.null and value == options.null) then return 'null' elseif type(value) == 'string' then - return json_string_literal(value) + return json_string_literal(value, options) elseif type(value) == 'number' then if value ~= value then @@ -844,8 +1545,24 @@ function encode_value(self, value, parents, etc, options, indent) return tostring(value) elseif type(value) ~= 'table' then + + if self.unsupportedTypeEncoder then + local user_value, user_error = self:unsupportedTypeEncoder(value, parents, etc, options, indent, for_key) + -- If the user's handler returns a string, use that. If it returns nil plus an error message, bail with that. + -- If only nil returned, fall through to the default error handler. + if type(user_value) == 'string' then + return user_value + elseif user_value ~= nil then + self:onEncodeError("unsupportedTypeEncoder method returned a " .. type(user_value), etc) + elseif user_error then + self:onEncodeError(tostring(user_error), etc) + end + end + self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) + elseif getmetatable(value) == isNumber then + return tostring(value) else -- -- A table to be converted to either a JSON object or array. @@ -872,14 +1589,22 @@ function encode_value(self, value, parents, etc, options, indent) -- -- An array... -- + local key_indent + if options.array_newline then + key_indent = indent .. tostring(options.indent or "") + else + key_indent = indent + end + local ITEMS = { } for i = 1, maximum_number_key do - table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, indent)) + table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, key_indent)) end - if options.pretty then - local array_indent = indent .. tostring(options.indent or "") - result_value = "[\n" .. array_indent .. table.concat(ITEMS, ",\n" .. array_indent) .. "\n" .. indent .. "]" + if options.array_newline then + result_value = "[\n" .. key_indent .. table.concat(ITEMS, ",\n" .. key_indent) .. "\n" .. indent .. "]" + elseif options.pretty then + result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" else result_value = "[" .. table.concat(ITEMS, ",") .. "]" end @@ -895,7 +1620,7 @@ function encode_value(self, value, parents, etc, options, indent) local KEYS = { } local max_key_length = 0 for _, key in ipairs(object_keys) do - local encoded = encode_value(self, tostring(key), parents, etc, options, indent) + local encoded = encode_value(self, tostring(key), parents, etc, options, indent, true) if options.align_keys then max_key_length = math.max(max_key_length, #encoded) end @@ -917,7 +1642,7 @@ function encode_value(self, value, parents, etc, options, indent) local PARTS = { } for _, key in ipairs(object_keys) do local encoded_val = encode_value(self, TT[key], parents, etc, options, indent) - local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent) + local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent, true) table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) end result_value = "{" .. table.concat(PARTS, ",") .. "}" @@ -935,19 +1660,44 @@ function encode_value(self, value, parents, etc, options, indent) end end +local function top_level_encode(self, value, etc, options) + local val = encode_value(self, value, {}, etc, options) + if val == nil then + --PRIVATE("may need to revert to the previous public verison if I can't figure out what the guy wanted") + return val + else + return val + end +end function OBJDEF:encode(value, etc, options) if type(self) ~= 'table' or self.__index ~= OBJDEF then OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) end - return encode_value(self, value, {}, etc, options or nil) + + -- + -- If the user didn't pass in a table of decode options, make an empty one. + -- + if type(options) ~= 'table' then + options = {} + end + + return top_level_encode(self, value, etc, options) end function OBJDEF:encode_pretty(value, etc, options) if type(self) ~= 'table' or self.__index ~= OBJDEF then OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc) end - return encode_value(self, value, {}, etc, options or default_pretty_options) + + -- + -- If the user didn't pass in a table of decode options, use the default pretty ones + -- + if type(options) ~= 'table' then + options = default_pretty_options + end + + return top_level_encode(self, value, etc, options) end function OBJDEF.__tostring() @@ -973,6 +1723,71 @@ return OBJDEF:new() -- -- Version history: -- +-- 20211016.28 Had forgotten to document the strictParsing option. +-- +-- 20211015.27 Better handle some edge-case errors [ thank you http://seriot.ch/projects/parsing_json.html ; all tests are now successful ] +-- +-- Added some semblance of proper UTF8 parsing, and now aborts with an error on ilformatted UTF8. +-- +-- Added the strictParsing option: +-- Aborts with an error on unknown backslash-escape in strings +-- Aborts on naked control characters in strings +-- Aborts when decode is passed a whitespace-only string +-- +-- For completeness, when encoding a Lua string into a JSON string, escape a forward slash. +-- +-- String decoding should be a bit more efficient now. +-- +-- 20170927.26 Use option.null in decoding as well. Thanks to Max Sindwani for the bump, and sorry to Oliver Hitz +-- whose first mention of it four years ago was completely missed by me. +-- +-- 20170823.25 Added support for JSON:unsupportedTypeEncoder(). +-- Thanks to Chronos Phaenon Eosphoros (https://github.com/cpeosphoros) for the idea. +-- +-- 20170819.24 Added support for boolean keys in tables. +-- +-- 20170416.23 Added the "array_newline" formatting option suggested by yurenchen (http://www.yurenchen.com/) +-- +-- 20161128.22 Added: +-- JSON:isString() +-- JSON:isNumber() +-- JSON:decodeIntegerObjectificationLength +-- JSON:decodeDecimalObjectificationLength +-- +-- 20161109.21 Oops, had a small boo-boo in the previous update. +-- +-- 20161103.20 Used to silently ignore trailing garbage when decoding. Now fails via JSON:onTrailingGarbage() +-- http://seriot.ch/parsing_json.php +-- +-- Built-in error message about "expected comma or ']'" had mistakenly referred to '[' +-- +-- Updated the built-in error reporting to refer to bytes rather than characters. +-- +-- The decode() method no longer assumes that error handlers abort. +-- +-- Made the VERSION string a string instead of a number +-- + +-- 20160916.19 Fixed the isNumber.__index assignment (thanks to Jack Taylor) +-- +-- 20160730.18 Added JSON:forceString() and JSON:forceNumber() +-- +-- 20160728.17 Added concatenation to the metatable for JSON:asNumber() +-- +-- 20160709.16 Could crash if not passed an options table (thanks jarno heikkinen ). +-- +-- Made JSON:asNumber() a bit more resilient to being passed the results of itself. +-- +-- 20160526.15 Added the ability to easily encode null values in JSON, via the new "null" encoding option. +-- (Thanks to Adam B for bringing up the issue.) +-- +-- Added some support for very large numbers and precise floats via +-- JSON.decodeNumbersAsObjects +-- JSON.decodeIntegerStringificationLength +-- JSON.decodeDecimalStringificationLength +-- +-- Added the "stringsAreUtf8" encoding option. (Hat tip to http://lua-users.org/wiki/JsonModules ) +-- -- 20141223.14 The encode_pretty() routine produced fine results for small datasets, but isn't really -- appropriate for anything large, so with help from Alex Aulbach I've made the encode routines -- more flexible, and changed the default encode_pretty() to be more generally useful. From ff0c2b6a4148d52141f67b9df72ab76c1e8699c5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 30 Jan 2025 02:38:19 -0800 Subject: [PATCH 032/115] reading empty files should return an empty table, not nil --- library/lua/json.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/lua/json.lua b/library/lua/json.lua index 6ba8483dd0..dc3902d009 100644 --- a/library/lua/json.lua +++ b/library/lua/json.lua @@ -1,5 +1,6 @@ local _ENV = mkmodule('json') -local internal = require 'json.internal' + +local internal = require('json.internal') local fs = dfhack.filesystem encode_defaults = { @@ -39,7 +40,7 @@ function decode_file(path, ...) end local contents = f:read('*all') f:close() - return decode(contents, ...) + return decode(contents, ...) or {} end local _file = defclass() From 03dcdd6e7117dfdaf798f43701a41090f3fcc3ba Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 30 Jan 2025 02:49:27 -0800 Subject: [PATCH 033/115] update changelog for #5227 --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index e747e8aacc..b8d28decc8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command ## Misc Improvements From f90152c6e844bb9e96fcc47b51ba3cf0ec5ef43f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:20:30 +0000 Subject: [PATCH 034/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index d3f735c0b0..408cef2943 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit d3f735c0b0773520da38b75acc072dd8614e8e29 +Subproject commit 408cef2943b880f2e78bac52ac3e04a124394db6 diff --git a/scripts b/scripts index d68e0cebc9..5cbfb58f61 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d68e0cebc9da3fad475d40baf4016c2c5c8323fb +Subproject commit 5cbfb58f616a6603c146606585e7c277a6dc52be From 91529fa5fcb6cd1da426e8e5a24169f25dd0edb7 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Fri, 31 Jan 2025 15:59:07 +0800 Subject: [PATCH 035/115] Edit military module --- library/include/modules/Military.h | 1 + library/modules/Military.cpp | 101 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 013f804415..dc69dac52b 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,6 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); +DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 0477c366b0..23fc15a871 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -4,6 +4,7 @@ #include "MiscUtils.h" #include "modules/Military.h" #include "modules/Translation.h" +#include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" #include "df/historical_figure.h" @@ -289,3 +290,103 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s } } } + +static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) +{ + int32_t start_year = -1; + for (size_t i = 0; i < hf->entity_links.size(); i++) + { + auto link = strict_virtual_cast(hf->entity_links[i]); + if (link == nullptr) continue; + if (link->squad_id != squad->id) continue; + + hf->entity_links.erase(hf->entity_links.begin() + i); + delete link; + + start_year = link->start_year; + break; + } + if (start_year == -1) return false; + + auto former_squad = new df::histfig_entity_link_former_squadst(); + former_squad->squad_id = squad->id; + former_squad->entity_id = squad->entity_id; + former_squad->start_year = start_year; + former_squad->end_year = *df::global::cur_year; + former_squad->link_strength = 100; + + hf->entity_links.push_back(former_pos); + return true; +} + +static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squad) +{ + std::vector nps; + if (! Units::getNoblePositions(&nps, hf)) return false; + + int32_t assignment_id = -1; + for (auto& np : nps) + { + if (np.entity.id != squad->entity_id) continue; + if (np.assignment.squad_id != squad.id) continue; + + np.assignment->histfig = -1; + np.assignment->histfig2 = -1; + + assignment_id = np.assignment->id; + break; + } + if (assignment_id == -1) return false; + + int32_t start_year = -1; + for (size_t i = 0; i < hf->entity_links.size(); i++) + { + auto link = strict_virtual_cast(hf->entity_links[i]); + if (link == nullptr) continue; + if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; + + hf->entity_links.erase(hf->entity_links.begin() + i); + delete link; + + start_year = link->start_year; + break; + } + if (start_year == -1) return false; + + auto former_pos = new df::histfig_entity_link_former_positionst(); + former_pos->assignment_id = assignment_id; + former_pos->entity_id = squad->entity_id; + former_pos->start_year = start_year; + former_pos->end_year = *df::global::cur_year; + former_pos->link_strength = 100; + + hf->entity_links.push_back(former_pos); + return true; +} + +bool Military::removeFromSquad(int32_t unit_id) +{ + df::unit* unit = df::unit::find(unit_id); + if (unit == nullptr) return false; + if (unit->military.squad_id == -1 || unit->military.squad_position == -1) return false; + + int32_t squad_id = unit->military.squad_id; + df::squad* squad = df::squad::find(squad_id); + if (squad == nullptr) return false; + + // remove from squad information + int32_t squad_pos = unit->military.squad_position; + df::squad_position* pos = squad->positions.at(squad_pos); + pos->occupant = -1; + + // remove from unit information + unit->military.squad_id = -1; + unit->military.squad_position = -1; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) return false; + + return squad_pos == 0 // is unit a commander? + ? remove_captain_entity_link(hf, squad) + : remove_soldier_entity_link(hf, squad); +} From d88277fa3fe6f4ef7ca7991859ac0478126e9b94 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 31 Jan 2025 07:57:35 -0800 Subject: [PATCH 036/115] Update changelog.txt --- docs/changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d28decc8..1bb7b42bcc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -37,7 +37,6 @@ Template for new versions: ## API ## Lua -- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings ## Removed @@ -58,6 +57,7 @@ Template for new versions: ## Fixes - `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command +- `createitem`: output items will now end up at look cursor if active ## Misc Improvements @@ -66,6 +66,7 @@ Template for new versions: ## API ## Lua +- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings ## Removed From e6e1972fb9bd1b97c27c255351f314170d7a8689 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 31 Jan 2025 07:59:53 -0800 Subject: [PATCH 037/115] Support adv look cursor * Update dwarfmode.lua - Make cursor fns support adv mode look * Update Gui.cpp - Make cursor fns support adv mode look * Update changevein.cpp - Use getCursorPos * Update createitem.cpp - Use getCursorPos * Update cursecheck.cpp - Doesn't use cursor * Update stripcaged.cpp - Doesn't use cursor * Update zone.cpp - Use getCursorPos --- library/lua/gui/dwarfmode.lua | 25 +++++++++-- library/modules/Gui.cpp | 80 +++++++++++++++++++++++++++-------- plugins/changevein.cpp | 13 +++--- plugins/createitem.cpp | 10 +++-- plugins/cursecheck.cpp | 7 ++- plugins/devel/stripcaged.cpp | 1 - plugins/zone.cpp | 15 ++++--- 7 files changed, 109 insertions(+), 42 deletions(-) diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 793acbd520..80d30282cf 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -7,6 +7,7 @@ local utils = require('utils') local dscreen = dfhack.screen +local a_look = df.global.game.main_interface.adventure.look local g_cursor = df.global.cursor local g_sel_rect = df.global.selection_rect local world_map = df.global.world.map @@ -38,17 +39,35 @@ end ---@return df.coord|nil function getCursorPos() - if g_cursor.x >= 0 then + if dfhack.world.isAdventureMode() then + if a_look.open then + return copyall(a_look.cursor) + end + elseif g_cursor.x >= 0 then return copyall(g_cursor) end end function setCursorPos(cursor) - df.global.cursor = copyall(cursor) + if dfhack.world.isAdventureMode() then + a_look.cursor = copyall(cursor) + else + df.global.cursor = copyall(cursor) + end end function clearCursorPos() - df.global.cursor = xyz2pos(nil) + if dfhack.world.isAdventureMode() then + if not a_look.open then + return + end + local u = dfhack.world.getAdventurer() + if u and u.pos:isValid() then + a_look.cursor = copyall(u.pos) + end + else + df.global.cursor = xyz2pos(nil) + end end function getSelection() diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3e9dd0dda9..ed6ddcba7c 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2744,9 +2744,18 @@ df::coord Gui::getViewportPos() df::coord Gui::getCursorPos() { using df::global::cursor; + if (World::isAdventureMode()) + { + if (!game) + return df::coord(); + auto &look = game->main_interface.adventure.look; + if (!look.open) + return df::coord(); + return look.cursor; + } + if (!cursor) return df::coord(); - return df::coord(cursor->x, cursor->y, cursor->z); } @@ -2911,7 +2920,7 @@ bool Gui::inRenameBuilding() return false; } -bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getViewCoords(int32_t &x, int32_t &y, int32_t &z) { x = *df::global::window_x; y = *df::global::window_y; @@ -2919,7 +2928,7 @@ bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z) return true; } -bool Gui::setViewCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setViewCoords(const int32_t x, const int32_t y, const int32_t z) { (*df::global::window_x) = x; (*df::global::window_y) = y; @@ -2927,32 +2936,67 @@ bool Gui::setViewCoords (const int32_t x, const int32_t y, const int32_t z) return true; } -bool Gui::getCursorCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getCursorCoords(int32_t &x, int32_t &y, int32_t &z) { - x = df::global::cursor->x; - y = df::global::cursor->y; - z = df::global::cursor->z; + using df::global::cursor; + bool is_adv = World::isAdventureMode(); + if (is_adv || !cursor) + { + df::coord p; + if (is_adv && game) + { + auto &look = game->main_interface.adventure.look; + if (look.open) + p = look.cursor; + } + x = p.x; y = p.y; z = p.z; + return p.isValid(); + } + + x = cursor->x; y = cursor->y; z = cursor->z; return has_cursor(); } -bool Gui::getCursorCoords (df::coord &pos) +bool Gui::getCursorCoords(df::coord &pos) { - pos.x = df::global::cursor->x; - pos.y = df::global::cursor->y; - pos.z = df::global::cursor->z; - return has_cursor(); + using df::global::cursor; + df::coord p; + if (World::isAdventureMode()) + { + if (game) + { + auto &look = game->main_interface.adventure.look; + if (look.open) + p = look.cursor; + } + } + else if (cursor) + p = df::coord(cursor->x, cursor->y, cursor->z); + + pos = p; + return p.isValid(); } //FIXME: confine writing of coords to map bounds? -bool Gui::setCursorCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setCursorCoords(const int32_t x, const int32_t y, const int32_t z) { - df::global::cursor->x = x; - df::global::cursor->y = y; - df::global::cursor->z = z; + using df::global::cursor; + if (World::isAdventureMode()) + { + if (!game) + return false; + auto &look = game->main_interface.adventure.look; + look.cursor = df::coord(x, y, z); + return true; + } + if (!cursor) + return false; + + cursor->x = x; cursor->y = y; cursor->z = z; return true; } -bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getDesignationCoords(int32_t &x, int32_t &y, int32_t &z) { x = selection_rect->start_x; y = selection_rect->start_y; @@ -2960,7 +3004,7 @@ bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) return (x >= 0) ? false : true; } -bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setDesignationCoords(const int32_t x, const int32_t y, const int32_t z) { selection_rect->start_x = x; selection_rect->start_y = y; diff --git a/plugins/changevein.cpp b/plugins/changevein.cpp index a73d9584e9..f143a25bc1 100644 --- a/plugins/changevein.cpp +++ b/plugins/changevein.cpp @@ -1,13 +1,14 @@ // Allow changing the material of a mineral inclusion #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" +#include "TileTypes.h" -#include "DataDefs.h" +#include "modules/Gui.h" #include "modules/Maps.h" #include "modules/Materials.h" -#include "TileTypes.h" #include "df/block_square_event.h" #include "df/block_square_event_mineralst.h" @@ -21,7 +22,6 @@ using namespace df::enums; DFHACK_PLUGIN("changevein"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); constexpr uint8_t NORTH = 0; constexpr uint8_t EAST = 1; @@ -212,7 +212,8 @@ command_result df_changevein (color_ostream &out, vector & parameters) out.printerr("Map is not available!\n"); return CR_FAILURE; } - if (!cursor || cursor->x == -30000) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.printerr("No cursor detected - please place the cursor over a mineral vein.\n"); return CR_FAILURE; @@ -232,14 +233,14 @@ command_result df_changevein (color_ostream &out, vector & parameters) return CR_FAILURE; } - df::map_block *block = Maps::getTileBlock(cursor->x, cursor->y, cursor->z); + auto block = Maps::getTileBlock(pos); if (!block) { out.printerr("Invalid tile selected.\n"); return CR_FAILURE; } df::block_square_event_mineralst *mineral = NULL; - int tx = cursor->x % 16, ty = cursor->y % 16; + int tx = pos.x % 16, ty = pos.y % 16; for (auto evt : block->block_events) { if (evt->getType() != block_square_event_type::mineral) diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index 1a720c3f4c..1f4b582047 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -30,7 +30,6 @@ using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("createitem"); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(gametype); REQUIRE_GLOBAL(cur_year_tick); @@ -84,7 +83,10 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t out_items[i]->moveToGround(building->centerx, building->centery, building->z); } else if (move_to_cursor) - out_items[i]->moveToGround(cursor->x, cursor->y, cursor->z); + { + auto pos = Gui::getCursorPos(); + out_items[i]->moveToGround(pos.x, pos.y, pos.z); + } // else createItem() already put it on the floor at the unit's feet, so we're good // Special logic for creating proper gloves in pairs @@ -395,11 +397,13 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { auto unit = Gui::getSelectedUnit(out, true); if (!unit) { + auto pos = Gui::getCursorPos(); if (*gametype == game_type::ADVENTURE_ARENA || World::isAdventureMode()) { // Use the adventurer unit unit = World::getAdventurer(); + move_to_cursor = pos.isValid(); } - else if (cursor->x >= 0) + else if (pos.isValid()) { // Use the first possible citizen if possible, otherwise the first unit for (auto u : Units::citizensRange(world->units.active)) { unit = u; diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 9c41d73912..958e571271 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -1,8 +1,8 @@ // cursecheck plugin // -// check single tile or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set -// if a cursor is active only the selected tile will be observed -// without cursor the whole map will be checked +// check unit or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set +// if a unit is selected only the selected unit will be observed +// otherwise the whole map will be checked // by default cursed creatures will be only counted // // the tool was intended to help finding vampires but it will also list necromancers, werebeasts and zombies @@ -38,7 +38,6 @@ using namespace df::enums; DFHACK_PLUGIN("cursecheck"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); enum curses { None = 0, diff --git a/plugins/devel/stripcaged.cpp b/plugins/devel/stripcaged.cpp index 14189bdc30..25f764841f 100644 --- a/plugins/devel/stripcaged.cpp +++ b/plugins/devel/stripcaged.cpp @@ -35,7 +35,6 @@ using std::string; using namespace DFHack; using namespace df::enums; using df::global::world; -using df::global::cursor; using df::global::plotinfo; using namespace DFHack::Gui; diff --git a/plugins/zone.cpp b/plugins/zone.cpp index 612e042282..1e69b7e193 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -55,7 +55,6 @@ using std::vector; DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(ui_building_item_cursor); @@ -844,14 +843,15 @@ static void chainInfo(color_ostream & out, df::building* building, bool list_ref static df::building* getAssignableBuildingAtCursor(color_ostream& out) { // set building at cursor position to be new target building - if (cursor->x == -30000) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.printerr("No cursor; place cursor over activity zone, pen," " pasture, pit, pond, chain, or cage.\n"); return NULL; } - auto building_at_tile = Buildings::findAtTile(Gui::getCursorPos()); + auto building_at_tile = Buildings::findAtTile(pos); // cagezone wants a pen/pit as starting point if (isCage(building_at_tile)) @@ -861,7 +861,7 @@ static df::building* getAssignableBuildingAtCursor(color_ostream& out) } else { - auto zone_at_tile = Buildings::findPenPitAt(Gui::getCursorPos()); + auto zone_at_tile = Buildings::findPenPitAt(pos); if(!zone_at_tile) { out << "No pen/pasture, pit, or cage under cursor!" << endl; @@ -1069,7 +1069,8 @@ static command_result df_zone(color_ostream &out, vector & parameters) } else if(p0 == "zinfo") { - if (cursor->x == -30000) { + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.color(COLOR_RED); out << "No cursor; place cursor over activity zone, chain, or cage." << endl; out.reset_color(); @@ -1081,10 +1082,10 @@ static command_result df_zone(color_ostream &out, vector & parameters) // (doesn't use the findXyzAtCursor() methods because zones might // overlap and contain a cage or chain) vector zones; - Buildings::findCivzonesAt(&zones, Gui::getCursorPos()); + Buildings::findCivzonesAt(&zones, pos); for (auto zone = zones.begin(); zone != zones.end(); ++zone) zoneInfo(out, *zone, verbose); - df::building* building = Buildings::findAtTile(Gui::getCursorPos()); + df::building* building = Buildings::findAtTile(pos); chainInfo(out, building, verbose); cageInfo(out, building, verbose); return CR_OK; From 9504cd88bad86b45ef59b2029a96681dcc858da1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 1 Feb 2025 11:02:09 +0000 Subject: [PATCH 038/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 408cef2943..9117df9642 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 408cef2943b880f2e78bac52ac3e04a124394db6 +Subproject commit 9117df96422a86f9310d3d1d43e23fbb1d30533d diff --git a/scripts b/scripts index 5cbfb58f61..0420f9b957 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 5cbfb58f616a6603c146606585e7c277a6dc52be +Subproject commit 0420f9b957ddd41b0b10713f004f40fdc3659015 From 206d840dc5bac3802a1ffd7ff72c1cbfa64b358c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 2 Feb 2025 07:15:04 +0000 Subject: [PATCH 039/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 9117df9642..6213fe7ad0 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9117df96422a86f9310d3d1d43e23fbb1d30533d +Subproject commit 6213fe7ad08d8b52060f72f885986638d9deab3e diff --git a/scripts b/scripts index 0420f9b957..6936ce8de8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 0420f9b957ddd41b0b10713f004f40fdc3659015 +Subproject commit 6936ce8de8db5b00c490172159a1ab7ffbda28ca From 63a4baea11ad502b5be95389458974da29270a6b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 15:38:55 -0800 Subject: [PATCH 040/115] fix multiline copy and paste in one_line_mode --- docs/changelog.txt | 1 + .../lua/gui/widgets/text_area/text_area_content.lua | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d28decc8..2f8e286e13 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes - `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command +- `gui/launcher`: flatten text when pasting multi-line text from the clipboard ## Misc Improvements diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index cf3698f086..cfa790a355 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -127,7 +127,11 @@ function TextAreaContent:eraseSelection() end function TextAreaContent:setClipboard(text) - dfhack.internal.setClipboardTextCp437Multiline(text) + if self.one_line_mode then + dfhack.internal.setClipboardTextCp437(text) + else + dfhack.internal.setClipboardTextCp437Multiline(text) + end end function TextAreaContent:copy() @@ -151,7 +155,7 @@ function TextAreaContent:copy() self:lineStartOffset(), self:lineEndOffset() ) - if curr_line:sub(-1,-1) ~= NEWLINE then + if not self.one_line_mode and curr_line:sub(-1,-1) ~= NEWLINE then curr_line = curr_line .. NEWLINE end @@ -170,8 +174,9 @@ function TextAreaContent:cut() end function TextAreaContent:paste() - local clipboard_lines = dfhack.internal.getClipboardTextCp437Multiline() - local clipboard = table.concat(clipboard_lines, '\n') + local clipboard = self.one_line_mode and + dfhack.internal.getClipboardTextCp437() or + table.concat(dfhack.internal.getClipboardTextCp437Multiline(), '\n') if clipboard then if self.clipboard_mode == CLIPBOARD_MODE.LINE and not self:hasSelection() then local origin_offset = self.cursor From 38e91d5fadbaf08797a540fb8f9a62a2b39e61d5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 15:33:43 -0800 Subject: [PATCH 041/115] add API for getting unpaused ms --- docs/changelog.txt | 1 + library/Core.cpp | 14 +++++++++----- library/include/Core.h | 9 +++++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d28decc8..c3c307f313 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ Template for new versions: ## Documentation ## API +- ``Core::getUnpausedMs``: new API for getting unpaused ms since load in a fort-mode game ## Lua diff --git a/library/Core.cpp b/library/Core.cpp index 94a94d748c..9dc2d2e68a 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -145,20 +145,20 @@ bool PerfCounters::getIgnorePauseState() { return ignore_pause_state; } -void PerfCounters::registerTick(uint32_t baseline_ms) { +uint32_t PerfCounters::registerTick(uint32_t baseline_ms) { if (!World::isFortressMode() || World::ReadPauseState()) { last_tick_baseline_ms = 0; - return; + return 0; } // only update when the tick counter has advanced if (!world || last_frame_counter == world->frame_counter) - return; + return 0; last_frame_counter = world->frame_counter; if (last_tick_baseline_ms == 0) { last_tick_baseline_ms = baseline_ms; - return; + return 0; } uint32_t elapsed_ms = baseline_ms - last_tick_baseline_ms; @@ -173,6 +173,8 @@ void PerfCounters::registerTick(uint32_t baseline_ms) { recent_ticks.history[recent_ticks.head_idx] = elapsed_ms; recent_ticks.sum_ms += elapsed_ms; + + return elapsed_ms; } uint32_t PerfCounters::getUnpausedFps() { @@ -1705,6 +1707,7 @@ bool Core::InitMainThread() { } perf_counters.reset(); + unpaused_ms = 0; return true; } @@ -2141,7 +2144,7 @@ int Core::Update() } uint32_t start_ms = p->getTickCount(); - perf_counters.registerTick(start_ms); + unpaused_ms += perf_counters.registerTick(start_ms); doUpdate(out); perf_counters.incCounter(perf_counters.total_update_ms, start_ms); } @@ -2337,6 +2340,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) case SC_WORLD_LOADED: { perf_counters.reset(); + unpaused_ms = 0; Persistence::Internal::load(out); plug_mgr->doLoadWorldData(out); loadModScriptPaths(out); diff --git a/library/include/Core.h b/library/include/Core.h index 8725c1faeb..556e5e1b03 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -100,9 +100,11 @@ namespace DFHack bool getIgnorePauseState(); // noop if game is paused and getIgnorePauseState() returns false - void incCounter(uint32_t &perf_counter, uint32_t baseline_ms); + void incCounter(uint32_t &counter, uint32_t baseline_ms); + + // returns number of unpaused ms since last tick + uint32_t registerTick(uint32_t baseline_ms); - void registerTick(uint32_t baseline_ms); uint32_t getUnpausedFps(); private: @@ -219,6 +221,7 @@ namespace DFHack static void cheap_tokenise(std::string const& input, std::vector &output); PerfCounters perf_counters; + uint32_t getUnpausedMs() { return unpaused_ms; } lua_State* getLuaState(bool bypass_assertion = false) { assert(bypass_assertion || isSuspended()); @@ -334,6 +337,8 @@ namespace DFHack lua_State* State; + uint32_t unpaused_ms; // reset to 0 on map load + friend class CoreService; friend class ServerConnection; friend class CoreSuspender; From 7a8e2d8a64f65ff6a92c4894f2d30f4fd4db6841 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 16:34:57 -0800 Subject: [PATCH 042/115] de-conflict ctrl-a hotkeys --- docs/changelog.txt | 1 + plugins/lua/sort/unitselector.lua | 2 +- plugins/lua/zone.lua | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2f8e286e13..acc7e5c646 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## Fixes - `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command - `gui/launcher`: flatten text when pasting multi-line text from the clipboard +- Ctrl-a hotkeys have been changed to something else for tools that also have an editable text field, where Ctrl-a is interpreted as select all text ## Misc Improvements diff --git a/plugins/lua/sort/unitselector.lua b/plugins/lua/sort/unitselector.lua index 83e4623f78..41a28476b7 100644 --- a/plugins/lua/sort/unitselector.lua +++ b/plugins/lua/sort/unitselector.lua @@ -220,7 +220,7 @@ function BurrowAssignmentOverlay:init() widgets.HotkeyLabel{ frame={l=1, t=0, r=1}, label='Select all/none', - key='CUSTOM_CTRL_A', + key='CUSTOM_CTRL_N', on_activate=toggle_all, }, }, diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index d0f1b01779..92940ef48e 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -425,7 +425,7 @@ function AssignAnimal:init() widgets.HotkeyLabel{ frame={l=0, b=2+(can_assign_pets and 0 or 1)}, label='Assign all/none', - key='CUSTOM_CTRL_A', + key='CUSTOM_CTRL_N', on_activate=self:callback('toggle_visible'), visible=self.get_multi_select, auto_width=true, From 12865342f879098bfe6f915596395ca05626472f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Feb 2025 01:52:59 +0000 Subject: [PATCH 043/115] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 300862ae16..ee9a53f73b 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 300862ae1624b39d73f43c6dfd2ab00e3454434d +Subproject commit ee9a53f73b4d2bf918dca0fc9dcb450f53d6e7c3 diff --git a/scripts b/scripts index 6936ce8de8..4d17b7fea8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6936ce8de8db5b00c490172159a1ab7ffbda28ca +Subproject commit 4d17b7fea8cce0ab86cea1a6ca7e2029a56f2de2 From 4e3622847b4783ab8e63d7b4cd33bfdb056d45b8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 17:54:26 -0800 Subject: [PATCH 044/115] increase wait time between steam query retries --- .github/workflows/watch-df-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index 77e77ffcc9..d86f993430 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -117,7 +117,8 @@ jobs: - name: Compare versions uses: nick-fields/retry@v3 with: - timeout_minutes: 2 + timeout_minutes: 5 + retry_wait_seconds: 60 command: | version=$(wget "${{ matrix.url }}" -qO- | tr '"' '\n' | fgrep 'tar.bz2' | head -n1 | sed -r 's/${{ matrix.prefix }}_([0-9]{2})_([0-9]{2})_linux.tar.bz2/\1.\2/') echo "latest ${{ matrix.channel }} version: $version" From 62da868c2dc825c3a2f465af8ed3fe5ca813a0b4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 18:04:38 -0800 Subject: [PATCH 045/115] refine changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1c64894558..97dc4ee3df 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,7 +59,7 @@ Template for new versions: ## Fixes - `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command - `gui/launcher`: flatten text when pasting multi-line text from the clipboard -- Ctrl-a hotkeys have been changed to something else for tools that also have an editable text field, where Ctrl-a is interpreted as select all text +- Ctrl-a hotkeys have been changed to something else (Ctrl-n) for tools that also have an editable text field, where Ctrl-a is interpreted as select all text ## Misc Improvements From cfe391f5aabb178846d6cce7cad5324a57366033 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 18:04:51 -0800 Subject: [PATCH 046/115] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 4d17b7fea8..191cdbeae9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4d17b7fea8cce0ab86cea1a6ca7e2029a56f2de2 +Subproject commit 191cdbeae99203b1aa38342b93df1bfa95946c46 From 83ff842e0aed8078fe791c5d7cb606b5bd7b2347 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 18:36:29 -0800 Subject: [PATCH 047/115] bump version to 51.04-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 14 ++++++++++---- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c85c190971..eeb55544c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.04") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 97dc4ee3df..3668383d2b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,21 +57,27 @@ Template for new versions: ## New Features ## Fixes -- `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command -- `gui/launcher`: flatten text when pasting multi-line text from the clipboard -- Ctrl-a hotkeys have been changed to something else (Ctrl-n) for tools that also have an editable text field, where Ctrl-a is interpreted as select all text ## Misc Improvements ## Documentation ## API -- ``Core::getUnpausedMs``: new API for getting unpaused ms since load in a fort-mode game ## Lua ## Removed +# 51.04-r1.1 + +## Fixes +- `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command +- `gui/launcher`: flatten text when pasting multi-line text from the clipboard +- Ctrl-a hotkeys have been changed to something else (Ctrl-n) for tools that also have an editable text field, where Ctrl-a is interpreted as select all text + +## API +- ``Core::getUnpausedMs``: new API for getting unpaused ms since load in a fort-mode game + # 51.04-r1 ## Misc Improvements diff --git a/plugins/stonesense b/plugins/stonesense index 6213fe7ad0..9d851db080 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 6213fe7ad08d8b52060f72f885986638d9deab3e +Subproject commit 9d851db080012add38631af37ea5c5b60a893e79 diff --git a/scripts b/scripts index 191cdbeae9..b1aa3b365d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 191cdbeae99203b1aa38342b93df1bfa95946c46 +Subproject commit b1aa3b365d611432195b80d1d064c942f54a78c7 From f72118ed26ef9487b16faba11c64f2d366b1bc9a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Feb 2025 04:13:35 +0000 Subject: [PATCH 048/115] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index b1aa3b365d..6a952903e7 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b1aa3b365d611432195b80d1d064c942f54a78c7 +Subproject commit 6a952903e72199e469b44e0c74764c1c89f4b2e3 From bca91ec460503fdcf02ca4b0a6c60fff8932e521 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 20:47:11 -0800 Subject: [PATCH 049/115] increase the timeout and retry delay for steam --- .github/workflows/watch-df-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index d86f993430..6e72333034 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -31,7 +31,8 @@ jobs: - name: Compare branch metadata uses: nick-fields/retry@v3 with: - timeout_minutes: 2 + timeout_minutes: 5 + retry_wait_seconds: 60 command: | blob=$(wget 'https://api.steamcmd.net/v1/info/975370?pretty=1' -O- | \ awk '/^ *"branches"/,0' | \ From deea1b0ffdfd84f4e5517b4216a6aff3f6bf4560 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:43:19 -0800 Subject: [PATCH 050/115] add sand example to changelayer docs --- docs/plugins/changelayer.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/plugins/changelayer.rst b/docs/plugins/changelayer.rst index f986a1e3d1..44fa1a3279 100644 --- a/docs/plugins/changelayer.rst +++ b/docs/plugins/changelayer.rst @@ -22,8 +22,8 @@ Usage When run without options, ``changelayer`` will: -- only affect the geology layer at the current cursor position -- only affect the biome that covers the current cursor position +- only affect the geology layer at the current keyboard cursor position +- only affect the biome that covers the current keyboard cursor position - not allow changing stone to soil and vice versa You can use the `probe` command on various tiles around your map to find valid @@ -34,8 +34,9 @@ Examples ``changelayer GRANITE`` Convert the layer at the cursor position into granite. -``changelayer SILTY_CLAY force`` - Convert the layer at the cursor position into clay, even if it's stone. +``changelayer SAND_RED force`` + Convert the layer at the cursor position into red sand, even if it's + currently stone. ``changelayer MARBLE all_biomes all_layers`` Convert all layers of all biomes which are not soil into marble. From 95b56d96f8bcdac0aca1338b8cb11e2248e4de77 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:44:16 -0800 Subject: [PATCH 051/115] add more boolean options to parser --- library/lua/argparse.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index d9ee57d139..a76e9d3c6d 100644 --- a/library/lua/argparse.lua +++ b/library/lua/argparse.lua @@ -237,8 +237,8 @@ function coords(arg, arg_name, skip_validation) return pos end -local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true, - ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false} +local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true,["enable"]=true,["enabled"]=true, + ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false,["disable"]=false,["disabled"]=false} ---@nodiscard ---@param arg string ---@param arg_name? string From 149f5905d29a05a04d7677fb64cdf3b811123ac5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:44:42 -0800 Subject: [PATCH 052/115] don't add suffix to focus string for non-difficulty tabs --- library/modules/Gui.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3e9dd0dda9..951482d23f 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -817,11 +817,12 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.settings.current_mode); - if (game->main_interface.settings.doing_custom_settings) - newFocusString += "/CustomSettings"; - else - newFocusString += "/Default"; - + if (game->main_interface.settings.current_mode == df::settings_tab_type::DIFFICULTY) { + if (game->main_interface.settings.doing_custom_settings) + newFocusString += "/CustomSettings"; + else + newFocusString += "/Default"; + } focusStrings.push_back(newFocusString); } if (game->main_interface.adventure.aim_projectile.open) { From 974e5ddc1f3d696278b18fa73d689544504ba8c3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:01:35 -0800 Subject: [PATCH 053/115] combine into one file --- plugins/spectate/CMakeLists.txt | 3 +- plugins/spectate/pause.cpp | 187 ------------------------ plugins/spectate/pause.h | 76 ---------- plugins/spectate/spectate.cpp | 242 +++++++++++++++++++++++++++++++- 4 files changed, 241 insertions(+), 267 deletions(-) delete mode 100644 plugins/spectate/pause.cpp delete mode 100644 plugins/spectate/pause.h diff --git a/plugins/spectate/CMakeLists.txt b/plugins/spectate/CMakeLists.txt index d2de072d8b..3f205171dc 100644 --- a/plugins/spectate/CMakeLists.txt +++ b/plugins/spectate/CMakeLists.txt @@ -2,7 +2,6 @@ project(spectate) SET(SOURCES - spectate.cpp - pause.cpp) + spectate.cpp) dfhack_plugin(${PROJECT_NAME} ${SOURCES}) diff --git a/plugins/spectate/pause.cpp b/plugins/spectate/pause.cpp deleted file mode 100644 index f94b1e73a9..0000000000 --- a/plugins/spectate/pause.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "pause.h" -#include -#include -#include -#include -#include -#include - -#include - -using namespace DFHack; -using namespace Pausing; -using namespace df::enums; - -// marked by REQUIRE_GLOBAL in spectate.cpp -using df::global::plotinfo; -using df::global::d_init; - -std::unordered_set PlayerLock::locks; -std::unordered_set AnnouncementLock::locks; - -namespace pausing { - AnnouncementLock announcementLock("monitor"); - PlayerLock playerLock("monitor"); - - const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); - bool state_saved = false; // indicates whether a restore state is ok - bool saved_states[announcement_flag_arr_size]; // state to restore - bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) - bool allow_player_pause = true; // toggles player pause ability - - using namespace df::enums; - struct player_pause_hook : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { - if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { - input->erase(interface_key::D_PAUSE); - } - INTERPOSE_NEXT(feed)(input); - } - }; - - IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); -} -using namespace pausing; - -template -inline bool any_lock(Locks locks) { - return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); -} - -template -inline bool only_lock(Locks locks, LockT* this_lock) { - return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { - if (lock == this_lock) { - return lock->isLocked(); - } - return !lock->isLocked(); - }); -} - -template -inline bool only_or_none_locked(Locks locks, LockT* this_lock) { - for (auto &L: locks) { - if (L == this_lock) { - continue; - } - if (L->isLocked()) { - return false; - } - } - return true; -} - -template -inline bool reportLockedLocks(color_ostream &out, Locks locks) { - out.color(DFHack::COLOR_YELLOW); - for (auto &L: locks) { - if (L->isLocked()) { - out.print("Lock: '%s'\n", L->name.c_str()); - } - } - out.reset_color(); - return true; -} - -bool AnnouncementLock::captureState() { - if (only_or_none_locked(locks, this)) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - return true; - } - return false; -} - -void AnnouncementLock::lock() { - Lock::lock(); - captureState(); -} - -bool AnnouncementLock::isAnyLocked() const { - return any_lock(locks); -} - -bool AnnouncementLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void AnnouncementLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} - -bool PlayerLock::isAnyLocked() const { - return any_lock(locks); -} - -bool PlayerLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void PlayerLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} - -bool World::DisableAnnouncementPausing() { - if (!announcementLock.isAnyLocked()) { - for (auto& flag : d_init->announcements.flags) { - flag.bits.PAUSE = false; - //out.print("pause: %d\n", flag.bits.PAUSE); - } - return true; - } - return false; -} - -bool World::SaveAnnouncementSettings() { - if (!announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - state_saved = true; - return true; - } - return false; -} - -bool World::RestoreAnnouncementSettings() { - if (!announcementLock.isAnyLocked() && state_saved) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; - } - return true; - } - return false; -} - -bool World::EnablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = true; - } - return allow_player_pause; -} - -bool World::DisablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = false; - } - return !allow_player_pause; -} - -bool World::IsPlayerPausingEnabled() { - return allow_player_pause; -} - -void World::Update() { - static bool did_once = false; - if (!did_once) { - did_once = true; - INTERPOSE_HOOK(player_pause_hook, feed).apply(); - } - if (announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; - } - } -} diff --git a/plugins/spectate/pause.h b/plugins/spectate/pause.h deleted file mode 100644 index ab736ed531..0000000000 --- a/plugins/spectate/pause.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once -#include -#include -#include - -namespace DFHack { - //////////// - // Locking mechanisms for control over pausing - namespace Pausing - { - class Lock - { - bool locked = false; - public: - const std::string name; - explicit Lock(const char* name) : name(name){} - virtual ~Lock()= default; - virtual bool isAnyLocked() const = 0; - virtual bool isOnlyLocked() const = 0; - bool isLocked() const { return locked; } - virtual void lock() { locked = true; } //simply locks the lock - void unlock() { locked = false; } - virtual void reportLocks(color_ostream &out) = 0; - }; - - // non-blocking lock resource used in conjunction with the announcement functions in World - class AnnouncementLock : public Lock - { - static std::unordered_set locks; - public: - explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } - ~AnnouncementLock() override { locks.erase(this); } - bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) - void lock() override; // locks and attempts to capture state - bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the Player pause functions in World - class PlayerLock : public Lock - { - static std::unordered_set locks; - public: - explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } - ~PlayerLock() override { locks.erase(this); } - bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the pause set state function in World -// todo: integrate with World::SetPauseState -// class PauseStateLock : public Lock -// { -// static std::unordered_set locks; -// public: -// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } -// ~PauseStateLock() override { locks.erase(this); } -// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked -// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked -// void reportLocks(color_ostream &out) override; -// }; - } - namespace World { - bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open - bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open - bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) - - bool EnablePlayerPausing(); // enable player pausing if all locks are open - bool DisablePlayerPausing(); // disable player pausing if all locks are open - bool IsPlayerPausingEnabled(); // returns whether the player can pause or not - - void Update(); - } -} diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 36bd2d94b2..29ec27718f 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -1,5 +1,3 @@ -#include "pause.h" - #include "Debug.h" #include "Export.h" #include "PluginManager.h" @@ -40,6 +38,246 @@ using namespace DFHack; using namespace Pausing; using namespace df::enums; +//////////// +// Locking mechanisms for control over pausing +namespace Pausing +{ + class Lock + { + bool locked = false; + public: + const std::string name; + explicit Lock(const char* name) : name(name){} + virtual ~Lock()= default; + virtual bool isAnyLocked() const = 0; + virtual bool isOnlyLocked() const = 0; + bool isLocked() const { return locked; } + virtual void lock() { locked = true; } //simply locks the lock + void unlock() { locked = false; } + virtual void reportLocks(color_ostream &out) = 0; + }; + + // non-blocking lock resource used in conjunction with the announcement functions in World + class AnnouncementLock : public Lock + { + static std::unordered_set locks; + public: + explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } + ~AnnouncementLock() override { locks.erase(this); } + bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) + void lock() override; // locks and attempts to capture state + bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked + bool isOnlyLocked() const override; // returns true if locked and no other instance is locked + void reportLocks(color_ostream &out) override; + }; + + // non-blocking lock resource used in conjunction with the Player pause functions in World + class PlayerLock : public Lock + { + static std::unordered_set locks; + public: + explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } + ~PlayerLock() override { locks.erase(this); } + bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked + bool isOnlyLocked() const override; // returns true if locked and no other instance is locked + void reportLocks(color_ostream &out) override; + }; + + // non-blocking lock resource used in conjunction with the pause set state function in World +// todo: integrate with World::SetPauseState +// class PauseStateLock : public Lock +// { +// static std::unordered_set locks; +// public: +// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } +// ~PauseStateLock() override { locks.erase(this); } +// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked +// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked +// void reportLocks(color_ostream &out) override; +// }; +} +namespace World { + bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open + bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open + bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) + + bool EnablePlayerPausing(); // enable player pausing if all locks are open + bool DisablePlayerPausing(); // disable player pausing if all locks are open + bool IsPlayerPausingEnabled(); // returns whether the player can pause or not + + void Update(); +} + +std::unordered_set PlayerLock::locks; +std::unordered_set AnnouncementLock::locks; + +namespace pausing { + AnnouncementLock announcementLock("monitor"); + PlayerLock playerLock("monitor"); + + const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); + bool state_saved = false; // indicates whether a restore state is ok + bool saved_states[announcement_flag_arr_size]; // state to restore + bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) + bool allow_player_pause = true; // toggles player pause ability + + using namespace df::enums; + struct player_pause_hook : df::viewscreen_dwarfmodest { + typedef df::viewscreen_dwarfmodest interpose_base; + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { + if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { + input->erase(interface_key::D_PAUSE); + } + INTERPOSE_NEXT(feed)(input); + } + }; + + IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); +} +using namespace pausing; + +template +inline bool any_lock(Locks locks) { + return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +} + +template +inline bool only_lock(Locks locks, LockT* this_lock) { + return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { + if (lock == this_lock) { + return lock->isLocked(); + } + return !lock->isLocked(); + }); +} + +template +inline bool only_or_none_locked(Locks locks, LockT* this_lock) { + for (auto &L: locks) { + if (L == this_lock) { + continue; + } + if (L->isLocked()) { + return false; + } + } + return true; +} + +template +inline bool reportLockedLocks(color_ostream &out, Locks locks) { + out.color(DFHack::COLOR_YELLOW); + for (auto &L: locks) { + if (L->isLocked()) { + out.print("Lock: '%s'\n", L->name.c_str()); + } + } + out.reset_color(); + return true; +} + +bool AnnouncementLock::captureState() { + if (only_or_none_locked(locks, this)) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; + } + return true; + } + return false; +} + +void AnnouncementLock::lock() { + Lock::lock(); + captureState(); +} + +bool AnnouncementLock::isAnyLocked() const { + return any_lock(locks); +} + +bool AnnouncementLock::isOnlyLocked() const { + return only_lock(locks, this); +} + +void AnnouncementLock::reportLocks(color_ostream &out) { + reportLockedLocks(out, locks); +} + +bool PlayerLock::isAnyLocked() const { + return any_lock(locks); +} + +bool PlayerLock::isOnlyLocked() const { + return only_lock(locks, this); +} + +void PlayerLock::reportLocks(color_ostream &out) { + reportLockedLocks(out, locks); +} + +bool World::DisableAnnouncementPausing() { + if (!announcementLock.isAnyLocked()) { + for (auto& flag : d_init->announcements.flags) { + flag.bits.PAUSE = false; + //out.print("pause: %d\n", flag.bits.PAUSE); + } + return true; + } + return false; +} + +bool World::SaveAnnouncementSettings() { + if (!announcementLock.isAnyLocked()) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; + } + state_saved = true; + return true; + } + return false; +} + +bool World::RestoreAnnouncementSettings() { + if (!announcementLock.isAnyLocked() && state_saved) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; + } + return true; + } + return false; +} + +bool World::EnablePlayerPausing() { + if (!playerLock.isAnyLocked()) { + allow_player_pause = true; + } + return allow_player_pause; +} + +bool World::DisablePlayerPausing() { + if (!playerLock.isAnyLocked()) { + allow_player_pause = false; + } + return !allow_player_pause; +} + +bool World::IsPlayerPausingEnabled() { + return allow_player_pause; +} + +void World::Update() { + static bool did_once = false; + if (!did_once) { + did_once = true; + INTERPOSE_HOOK(player_pause_hook, feed).apply(); + } + if (announcementLock.isAnyLocked()) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; + } + } +} + struct Configuration { bool unpause = false; bool disengage = false; From 1fc6737c1ecb701d90ecdec7aecfabe2f727e7a7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:02:32 -0800 Subject: [PATCH 054/115] move spectate to main plugins dir --- plugins/CMakeLists.txt | 2 +- plugins/{spectate => }/spectate.cpp | 0 plugins/spectate/CMakeLists.txt | 7 ------- 3 files changed, 1 insertion(+), 8 deletions(-) rename plugins/{spectate => }/spectate.cpp (100%) delete mode 100644 plugins/spectate/CMakeLists.txt diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ff71159e5e..8f230ff583 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -111,7 +111,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) #dfhack_plugin(steam-engine steam-engine.cpp) - add_subdirectory(spectate) + dfhack_plugin(spectate spectate.cpp LINK_LIBRARIES lua) #dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) add_subdirectory(stockpiles) dfhack_plugin(stocks stocks.cpp LINK_LIBRARIES lua) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate.cpp similarity index 100% rename from plugins/spectate/spectate.cpp rename to plugins/spectate.cpp diff --git a/plugins/spectate/CMakeLists.txt b/plugins/spectate/CMakeLists.txt deleted file mode 100644 index 3f205171dc..0000000000 --- a/plugins/spectate/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ - -project(spectate) - -SET(SOURCES - spectate.cpp) - -dfhack_plugin(${PROJECT_NAME} ${SOURCES}) From 2c27dc6b48753a1cf599d829d4c629138de8b65c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:03:12 -0800 Subject: [PATCH 055/115] update spectate docs to reflect update plan --- docs/plugins/spectate.rst | 152 +++++++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 36 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 1e400a06e4..cd7a8b96c5 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -2,8 +2,31 @@ spectate ======== .. dfhack-tool:: - :summary: Automatically follow productive dwarves. - :tags: fort interface + :summary: Automated spectator mode. + :tags: fort inspection interface + +This tool is for those who like to watch their dwarves go about their business. + +When enabled, `spectate` will lock the camera to following the dwarves +scurrying around your fort. Every once in a while, it will automatically switch +to following a different dwarf. It can also switch to following animals, +hostiles, or visiting units. You can switch to the next target (or a previous +target) immediately with the left/right arrow keys. + +`spectate` will disengage and turn itself off when you move the map, just like +the vanilla follow mechanic. It will also disengage immediately if you open the +squads menu for military action. + +It can also annotate your dwarves on the map with their name, job, and other +information, either as floating tooltips or in a panel that comes up when you +hover the mouse over a target. + +Run `gui/spectate` to configure the plugin's settings. + +Settings are saved globally, so your preferences for `spectate` and its +overlays will apply to all forts, not just the currently loaded one. Follow +mode is automatically disabled when you load a fort so you can get your +bearings before re-enabling. Usage ----- @@ -11,52 +34,109 @@ Usage :: enable spectate - spectate + spectate [status] spectate set - spectate enable|disable - -When enabled, the plugin will lock the camera to following the dwarves -scurrying around your fort. Every once in a while, it will automatically switch -to following a different dwarf, preferring dwarves on z-levels with the highest -job activity. - -If you have the ``auto-disengage`` feature disabled, you can switch to a new -dwarf immediately by hitting one of the map movement keys (``wasd`` by -default). To stop following dwarves, bring up `gui/launcher` and run -``disable spectate``. - -Changes to settings will be saved with your fort, but if `spectate` is enabled -when you save the fort, it will disenable itself when you load so you can get -your bearings before re-enabling follow mode with ``enable spectate`` again. + spectate overlay enable|disable Examples -------- ``enable spectate`` - Starting following dwarves and observing life in your fort. + Start following dwarves and observing life in your fort. ``spectate`` The plugin reports its configured status. -``spectate enable auto-unpause`` - Enable the spectate plugin to automatically dismiss pause events caused - by the game. Siege events are one example of such a game event. +``spectate set auto-unpause true`` + Configure `spectate` to automatically dismiss popups and pause events, like + siege announcements. -``spectate set tick-threshold 1000`` - Set the tick interval between camera changes back to its default value. +``spectate set follow-seconds 30`` + Configure `spectate` to switch targets every 30 seconds when in follow mode. -Features --------- -:auto-unpause: Toggle auto-dismissal of game pause events. (default: disabled) -:auto-disengage: Toggle auto-disengagement of plugin through player - intervention while unpaused. (default: disabled) -:animals: Toggle whether to sometimes follow animals. (default: disabled) -:hostiles: Toggle whether to sometimes follow hostiles (eg. undead, - titans, invaders, etc.) (default: disabled) -:visiting: Toggle whether to sometimes follow visiting units (eg. - diplomats) +``spectate overlay follow enable`` + Show informative tooltips that follow each unit on the map. Settings -------- -:tick-threshold: Set the plugin's tick interval for changing the followed - dwarf. (default: 1000) + +``auto-disengage`` (default: enabled) + Toggle automatically disabling the plugin when the player moves the map or + opens the squad panel. If this is disabled, you will need to manually + disable the plugin to turn off follow mode. + +``auto-unpause`` (default: disabled) + Toggle auto-dismissal of announcements that pause the game, like sieges, + forgotten beasts, etc. + +``cinematic-action`` (default: enabled) + Toggle whether to switch targets more rapidly when there is conflict. + +``follow-seconds`` (default: 10) + Set the time interval for changing the followed unit. + +``include-animals`` (default: disabled) + Toggle whether to sometimes follow fort animals. + +``include-hostiles`` (default: disabled) + Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, + etc.) + +``include-visiting`` (default: disabled) + Toggle whether to sometimes follow visiting units, like diplomats. + +``include-wildlife`` (default: disabled) + Toggle whether to sometimes follow wildlife. + +``prefer-conflict`` (default: enabled) + Toggle whether to prefer following units in active conflict. + +``prefer-new-arrivals`` (default: enabled) + Toggle whether to prefer following (non-siege) units that have newly + arrived on the map. + +``tooltip-follow-job`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + job of the dwarf in the tooltip. + +``tooltip-follow-name`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + name of the dwarf in the tooltip. + +``tooltip-follow-stress`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + happiness level (stress) of the dwarf in the tooltip. + +``tooltip-hover-job`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + job of the dwarf in the hover panel. + +``tooltip-hover-name`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + name of the dwarf in the hover panel. + +``tooltip-hover-stress`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + happiness level (stress) of the dwarf in the hover panel. + +Overlays +-------- + +``spectate`` provides two overlays via the `overlay` framework to add +information and functionality to the main map. These overlays can be controlled +via the ``spectate overlay`` command or the ``Overlays`` tab in +`gui/control-panel`. + +The information displayed by these overlays can be configured via the +``spectate set`` command or the `gui/spectate` interface. + +``spectate.follow`` + Show informative tooltips that follow each unit on the map. You can enable + this overlay by running ``spectate overlay follow enable`` or, + equivalently, ``overlay enable spectate.follow``. + +``spectate.hover`` + Show a popup panel with selected information when your mouse cursor hovers + over a unit. You can enable this overlay by running + ``spectate overlay hover enable`` or, equivalently, + ``overlay enable spectate.hover``. From a065980a1fc65ba0096f00a43ce771d04704b40c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 15:32:09 -0800 Subject: [PATCH 056/115] initial implementation of most of the new behavior --- docs/plugins/spectate.rst | 10 +- plugins/lua/spectate.lua | 149 ++++++ plugins/spectate.cpp | 933 +++++++++++++------------------------- 3 files changed, 461 insertions(+), 631 deletions(-) create mode 100644 plugins/lua/spectate.lua diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index cd7a8b96c5..f49849c850 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -35,6 +35,7 @@ Usage enable spectate spectate [status] + spectate toggle spectate set spectate overlay enable|disable @@ -44,6 +45,10 @@ Examples ``enable spectate`` Start following dwarves and observing life in your fort. +``spectate toggle`` + Toggle the plugin on or off. Intended for use with a keybinding. The + default is Ctrl-Shift-S. + ``spectate`` The plugin reports its configured status. @@ -73,7 +78,8 @@ Settings Toggle whether to switch targets more rapidly when there is conflict. ``follow-seconds`` (default: 10) - Set the time interval for changing the followed unit. + Set the time interval for changing the followed unit. The interval does not + include time that the game is paused. ``include-animals`` (default: disabled) Toggle whether to sometimes follow fort animals. @@ -82,7 +88,7 @@ Settings Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, etc.) -``include-visiting`` (default: disabled) +``include-visitors`` (default: disabled) Toggle whether to sometimes follow visiting units, like diplomats. ``include-wildlife`` (default: disabled) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua new file mode 100644 index 0000000000..ba7e9ba7ba --- /dev/null +++ b/plugins/lua/spectate.lua @@ -0,0 +1,149 @@ +local _ENV = mkmodule('plugins.spectate') + +local argparse = require('argparse') +local json = require('json') +local overlay = require('plugins.overlay') +local utils = require('utils') + +-- settings starting with 'tooltip-' are not passed to the C++ plugin +local lua_only_settings_prefix = 'tooltip-' + +local function get_default_state() + return { + ['auto-disengage']=true, + ['auto-unpause']=false, + ['cinematic-action']=true, + ['follow-seconds']=10, + ['include-animals']=false, + ['include-hostiles']=false, + ['include-visitors']=false, + ['include-wildlife']=false, + ['prefer-conflict']=true, + ['prefer-new-arrivals']=true, + ['tooltip-follow-job']=true, + ['tooltip-follow-name']=true, + ['tooltip-follow-stress']=true, + ['tooltip-hover-job']=true, + ['tooltip-hover-name']=true, + ['tooltip-hover-stress']=true, + } +end + +local function load_state() + local state = get_default_state() + local config = json.open('dfhack-config/spectate.json') + for key in pairs(config.data) do + if state[key] == nil then + config.data[key] = nil + end + end + utils.assign(state, config.data) + config.data = state + return config +end + +local config = load_state() + +function refresh_cpp_config() + for name,value in pairs(config.data) do + if not name:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(name, value) + end + end +end + +----------------------------- +-- commandline interface + +local function print_status() + print('spectate is:', isEnabled() and 'enabled' or 'disabled') + print() + print('settings:') + for key, value in pairs(config.data) do + print(' ' .. key .. ': ' .. tostring(value)) + end +end + +local function do_toggle() + if isEnabled() then + dfhack.run_command('disable', 'spectate') + else + dfhack.run_command('enable', 'spectate') + end +end + +local function set_setting(key, value) + if config.data[key] == nil then + qerror('unknown setting: ' .. key) + end + if key == 'follow-seconds' then + value = argparse.positiveInt(value, 'follow-seconds') + else + value = argparse.boolean(value, key) + end + config.data[key] = value + config:write() + if not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(key, value) + end +end + +local function set_overlay(name, value) + if not name:startswith('spectate.') then + name = 'spectate.' .. name + end + if name ~= 'spectate.follow' and name ~= 'spectate.hover' then + qerror('unknown overlay: ' .. name) + end + value = argparse.boolean(value, name) + dfhack.run_command('overlay', value and 'enable' or 'disable', name) +end + +function parse_commandline(args) + local command = table.remove(args, 1) + if not command or command == 'status' then + print_status() + elseif command == 'toggle' then + do_toggle() + elseif command == 'set' then + set_setting(args[1], args[2]) + elseif command == 'overlay' then + set_overlay(args[1], args[2]) + else + return false + end + + return true +end + +----------------------------- +-- overlays + +FollowOverlay = defclass(FollowOverlay, overlay.OverlayWidget) +FollowOverlay.ATTRS{ + desc='Adds info tooltips that follow units on the map.', + default_pos={x=1,y=1}, + fullscreen=true, + viewscreens='dwarfmode/Default', +} + +HoverOverlay = defclass(HoverOverlay, overlay.OverlayWidget) +HoverOverlay.ATTRS{ + desc='Shows info popup when hovering the mouse over units on the map.', + default_pos={x=1,y=1}, + fullscreen=true, + viewscreens='dwarfmode/Default', +} + +OVERLAY_WIDGETS = { + follow=FollowOverlay, + hover=HoverOverlay, +} + +return _ENV diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 29ec27718f..ae33031b14 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -1,716 +1,391 @@ #include "Debug.h" -#include "Export.h" +#include "LuaTools.h" +#include "PluginLua.h" #include "PluginManager.h" -#include "modules/EventManager.h" -#include "modules/World.h" -#include "modules/Maps.h" #include "modules/Gui.h" -#include "modules/Job.h" #include "modules/Units.h" +#include "modules/World.h" -#include "df/job.h" -#include "df/unit.h" -#include "df/historical_figure.h" -#include "df/global_objects.h" +#include "df/announcements.h" +#include "df/d_init.h" #include "df/plotinfost.h" +#include "df/unit.h" #include "df/world.h" -#include "df/viewscreen.h" -#include "df/creature_raw.h" -#include #include -#include - -// Debugging -namespace DFHack { - DBG_DECLARE(log, plugin, DebugCategory::LINFO); -} - -DFHACK_PLUGIN("spectate"); -DFHACK_PLUGIN_IS_ENABLED(enabled); - -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(d_init); // used in pause.cpp using namespace DFHack; -using namespace Pausing; -using namespace df::enums; -//////////// -// Locking mechanisms for control over pausing -namespace Pausing -{ - class Lock - { - bool locked = false; - public: - const std::string name; - explicit Lock(const char* name) : name(name){} - virtual ~Lock()= default; - virtual bool isAnyLocked() const = 0; - virtual bool isOnlyLocked() const = 0; - bool isLocked() const { return locked; } - virtual void lock() { locked = true; } //simply locks the lock - void unlock() { locked = false; } - virtual void reportLocks(color_ostream &out) = 0; - }; - - // non-blocking lock resource used in conjunction with the announcement functions in World - class AnnouncementLock : public Lock - { - static std::unordered_set locks; - public: - explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } - ~AnnouncementLock() override { locks.erase(this); } - bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) - void lock() override; // locks and attempts to capture state - bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the Player pause functions in World - class PlayerLock : public Lock - { - static std::unordered_set locks; - public: - explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } - ~PlayerLock() override { locks.erase(this); } - bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; +using std::string; +using std::vector; - // non-blocking lock resource used in conjunction with the pause set state function in World -// todo: integrate with World::SetPauseState -// class PauseStateLock : public Lock -// { -// static std::unordered_set locks; -// public: -// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } -// ~PauseStateLock() override { locks.erase(this); } -// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked -// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked -// void reportLocks(color_ostream &out) override; -// }; -} -namespace World { - bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open - bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open - bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) +DFHACK_PLUGIN("spectate"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); - bool EnablePlayerPausing(); // enable player pausing if all locks are open - bool DisablePlayerPausing(); // disable player pausing if all locks are open - bool IsPlayerPausingEnabled(); // returns whether the player can pause or not +REQUIRE_GLOBAL(d_init); +REQUIRE_GLOBAL(plotinfo); +REQUIRE_GLOBAL(world); - void Update(); +namespace DFHack { + DBG_DECLARE(spectate, control, DebugCategory::LINFO); + DBG_DECLARE(spectate, cycle, DebugCategory::LINFO); } -std::unordered_set PlayerLock::locks; -std::unordered_set AnnouncementLock::locks; - -namespace pausing { - AnnouncementLock announcementLock("monitor"); - PlayerLock playerLock("monitor"); - - const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); - bool state_saved = false; // indicates whether a restore state is ok - bool saved_states[announcement_flag_arr_size]; // state to restore - bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) - bool allow_player_pause = true; // toggles player pause ability - - using namespace df::enums; - struct player_pause_hook : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { - if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { - input->erase(interface_key::D_PAUSE); - } - INTERPOSE_NEXT(feed)(input); - } - }; +static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle +static bool was_in_settings = false; // whether we were in the vanilla settings screen last update - IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); -} -using namespace pausing; +static const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); +static std::unique_ptr saved_announcement_settings; -template -inline bool any_lock(Locks locks) { - return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +static void save_announcement_settings(color_ostream &out) { + if (!saved_announcement_settings) + saved_announcement_settings = std::make_unique(new uint32_t[announcement_flag_arr_size]); + DEBUG(control,out).print("saving announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + (*saved_announcement_settings)[i] = d_init->announcements.flags[i].whole; } -template -inline bool only_lock(Locks locks, LockT* this_lock) { - return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { - if (lock == this_lock) { - return lock->isLocked(); - } - return !lock->isLocked(); - }); +static void restore_announcement_settings(color_ostream &out) { + if (!saved_announcement_settings) + return; + DEBUG(control,out).print("restoring saved announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + d_init->announcements.flags[i].whole = (*saved_announcement_settings)[i]; } -template -inline bool only_or_none_locked(Locks locks, LockT* this_lock) { - for (auto &L: locks) { - if (L == this_lock) { - continue; - } - if (L->isLocked()) { - return false; - } +static void scrub_announcements(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); + return; } - return true; -} -template -inline bool reportLockedLocks(color_ostream &out, Locks locks) { - out.color(DFHack::COLOR_YELLOW); - for (auto &L: locks) { - if (L->isLocked()) { - out.print("Lock: '%s'\n", L->name.c_str()); - } + DEBUG(control,out).print("removing PAUSE from announcement settings\n"); + for (auto& flag : d_init->announcements.flags) { + flag.bits.DO_MEGA = false; + flag.bits.PAUSE = false; + flag.bits.RECENTER = false; } - out.reset_color(); - return true; } -bool AnnouncementLock::captureState() { - if (only_or_none_locked(locks, this)) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - return true; +struct Configuration { + bool auto_disengage; + bool auto_unpause; + bool cinematic_action; + bool include_animals; + bool include_hostiles; + bool include_visitors; + bool include_wildlife; + bool prefer_conflict; + bool prefer_new_arrivals; + int32_t follow_ms; + + void reset() { + auto_disengage = true; + auto_unpause = false; + cinematic_action = true; + include_animals = false; + include_hostiles = false; + include_visitors = false; + include_wildlife = false; + prefer_conflict = true; + prefer_new_arrivals = true; + follow_ms = 10000; } - return false; -} +} config; -void AnnouncementLock::lock() { - Lock::lock(); - captureState(); -} +static command_result do_command(color_ostream &out, vector ¶meters); +static void follow_a_dwarf(color_ostream &out); -bool AnnouncementLock::isAnyLocked() const { - return any_lock(locks); -} +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(control,out).print("initializing %s\n", plugin_name); -bool AnnouncementLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void AnnouncementLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} + commands.push_back(PluginCommand( + plugin_name, + "Automated spectator mode.", + do_command)); -bool PlayerLock::isAnyLocked() const { - return any_lock(locks); + return CR_OK; } -bool PlayerLock::isOnlyLocked() const { - return only_lock(locks, this); +static void cleanup(color_ostream &out) { + if (saved_announcement_settings) { + restore_announcement_settings(out); + delete[] *saved_announcement_settings; + saved_announcement_settings.reset(); + } } -void PlayerLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } -bool World::DisableAnnouncementPausing() { - if (!announcementLock.isAnyLocked()) { - for (auto& flag : d_init->announcements.flags) { - flag.bits.PAUSE = false; - //out.print("pause: %d\n", flag.bits.PAUSE); + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(control,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + if (enable) { + INFO(control,out).print("Spectate mode enabled!\n"); + config.reset(); + if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "refresh_cpp_config")) { + WARN(control,out).print("Failed to refresh config\n"); + } + follow_a_dwarf(out); + } else { + INFO(control,out).print("Spectate mode disabled!\n"); + plotinfo->follow_unit = -1; + cleanup(out); } - return true; + } else { + DEBUG(control,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); } - return false; + return CR_OK; } -bool World::SaveAnnouncementSettings() { - if (!announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - state_saved = true; - return true; - } - return false; +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(control,out).print("shutting down %s\n", plugin_name); + cleanup(out); + return CR_OK; } -bool World::RestoreAnnouncementSettings() { - if (!announcementLock.isAnyLocked() && state_saved) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + switch (event) { + case SC_WORLD_LOADED: + next_cycle_unpaused_ms = 0; + break; + case SC_WORLD_UNLOADED: + if (is_enabled) { + DEBUG(control,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + cleanup(out); } - return true; + break; + default: + break; } - return false; + return CR_OK; } -bool World::EnablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = true; +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + if (!was_in_settings) { + DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); + restore_announcement_settings(out); + was_in_settings = true; + } + } else if (was_in_settings) { + was_in_settings = false; + if (config.auto_unpause) { + DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); + save_announcement_settings(out); + scrub_announcements(out); + } } - return allow_player_pause; -} -bool World::DisablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = false; + if (config.auto_disengage && plotinfo->follow_unit < 0) { + DEBUG(cycle,out).print("auto-disengage triggered\n"); + is_enabled = false; + cleanup(out); + return CR_OK; } - return !allow_player_pause; -} -bool World::IsPlayerPausingEnabled() { - return allow_player_pause; + if ((!config.auto_disengage && plotinfo->follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) + follow_a_dwarf(out); + return CR_OK; } -void World::Update() { - static bool did_once = false; - if (!did_once) { - did_once = true; - INTERPOSE_HOOK(player_pause_hook, feed).apply(); - } - if (announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; - } +static command_result do_command(color_ostream &out, vector ¶meters) { + bool show_help = false; + if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "parse_commandline", std::make_tuple(parameters), + 1, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } -} - -struct Configuration { - bool unpause = false; - bool disengage = false; - bool animals = false; - bool hostiles = true; - bool visitors = false; - int32_t tick_threshold = 1000; -} config; - -Pausing::AnnouncementLock* pause_lock = nullptr; -bool lock_collision = false; -bool announcements_disabled = false; - -#define base 0.99 -static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; -enum ConfigData { - UNPAUSE, - DISENGAGE, - TICK_THRESHOLD, - ANIMALS, - HOSTILES, - VISITORS -}; + return show_help ? CR_WRONG_USAGE : CR_OK; +} -static PersistentDataItem pconfig; - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); -command_result spectate (color_ostream &out, std::vector & parameters); -#define COORDARGS(id) id.x, id.y, id.z - -namespace SP { - bool following_dwarf = false; - df::unit* our_dorf = nullptr; - int32_t timestamp = -1; - std::default_random_engine RNG; - - void DebugUnitVector(std::vector units) { - if (debug_plugin.isEnabled(DFHack::DebugCategory::LDEBUG)) { - for (auto unit: units) { - DEBUG(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", - unit->id, - Units::isAnimal(unit), - Units::isDanger(unit), - Units::isVisiting(unit)); - } - } - } +///////////////////////////////////////////////////// +// cycle logic +// - void PrintStatus(color_ostream &out) { - out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); - out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off."); - out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off."); - out.print(" %-20s\t%s\n", "animals: ", config.animals ? "on." : "off."); - out.print(" %-20s\t%s\n", "hostiles: ", config.hostiles ? "on." : "off."); - out.print(" %-20s\t%s\n", "visiting: ", config.visitors ? "on." : "off."); - out.print(" SETTINGS:\n"); - out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); - if (following_dwarf) - out.print(" %-21s\t%s[id: %d]\n","FOLLOWING:", our_dorf ? our_dorf->name.first_name.c_str() : "nullptr", plotinfo->follow_unit); - } +static bool is_in_combat(df::unit *unit) { + return false; +} - void SetUnpauseState(bool state) { - // we don't need to do any of this yet if the plugin isn't enabled - if (enabled) { - // todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings] - // lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete - // The onupdate function above ensure the procedure properly completes, thus we only care about - // state reversal here ergo `enabled != state` - if (lock_collision && config.unpause != state) { - WARN(plugin).print("Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own.\n"); - // if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings, - // therefore nothing to revert and the lock won't even be engaged (nothing to unlock) - lock_collision = false; - config.unpause = state; - if (config.unpause) { - // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock - pause_lock->lock(); - } - return; - } - // update the announcement settings if we can - if (state) { - if (World::SaveAnnouncementSettings()) { - World::DisableAnnouncementPausing(); - announcements_disabled = true; - pause_lock->lock(); - } else { - WARN(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); - lock_collision = true; - } - } else { - pause_lock->unlock(); - if (announcements_disabled) { - if (!World::RestoreAnnouncementSettings()) { - // this in theory shouldn't happen, if others use the lock like we do in spectate - WARN(plugin).print("Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own.\n"); - lock_collision = true; - } else { - announcements_disabled = false; - } - } - } - if (lock_collision) { - ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); - WARN(plugin).print( - " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" - " The action you were attempting will complete when the following lock or locks lift.\n"); - pause_lock->reportLocks(Core::getInstance().getConsole()); - } - } - config.unpause = state; - } +static bool is_fleeing(df::unit *unit) { + return false; +} - void SaveSettings() { - if (pconfig.isValid()) { - pconfig.ival(UNPAUSE) = config.unpause; - pconfig.ival(DISENGAGE) = config.disengage; - pconfig.ival(TICK_THRESHOLD) = config.tick_threshold; - pconfig.ival(ANIMALS) = config.animals; - pconfig.ival(HOSTILES) = config.hostiles; - pconfig.ival(VISITORS) = config.visitors; - } - } +static void get_dwarf_buckets(color_ostream &out, + vector &active_combat_units, + vector &passive_combat_units, + vector &job_units, + vector &other_units) +{ + static const std::unordered_set boring_jobs = { + df::job_type::Eat, + df::job_type::Drink, + df::job_type::Sleep, + }; - void LoadSettings() { - pconfig = World::GetPersistentSiteData(CONFIG_KEY); + for (auto unit : world->units.active) { + if (Units::isDead(unit) || !Units::isActive(unit) || unit->flags1.bits.caged || unit->flags1.bits.chained || Units::isHidden(unit)) + continue; + if (!config.include_animals && Units::isAnimal(unit)) + continue; + if (!config.include_hostiles && Units::isDanger(unit)) + continue; + if (!config.include_visitors && Units::isVisitor(unit)) + continue; + if (!config.include_wildlife && Units::isWildlife(unit)) + continue; - if (!pconfig.isValid()) { - pconfig = World::AddPersistentSiteData(CONFIG_KEY); - SaveSettings(); + if (is_in_combat(unit)) { + if (is_fleeing(unit)) + passive_combat_units.push_back(unit); + else + active_combat_units.push_back(unit); + } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { + job_units.push_back(unit); } else { - config.unpause = pconfig.ival(UNPAUSE); - config.disengage = pconfig.ival(DISENGAGE); - config.tick_threshold = pconfig.ival(TICK_THRESHOLD); - config.animals = pconfig.ival(ANIMALS); - config.hostiles = pconfig.ival(HOSTILES); - config.visitors = pconfig.ival(VISITORS); - pause_lock->unlock(); - SetUnpauseState(config.unpause); + other_units.push_back(unit); } } +} - bool FollowADwarf() { - if (enabled && !World::ReadPauseState()) { - df::coord viewMin = Gui::getViewportPos(); - df::coord viewMax{viewMin}; - const auto &dims = Gui::getDwarfmodeViewDims().map().second; - viewMax.x += dims.x - 1; - viewMax.y += dims.y - 1; - viewMax.z = viewMin.z; - std::vector units; - static auto add_if = [&](std::function check) { - for (auto unit : world->units.active) { - if (check(unit)) { - units.push_back(unit); - } - } - }; - static auto valid = [](df::unit* unit) { - if (Units::isAnimal(unit)) { - return config.animals; - } - if (Units::isVisiting(unit)) { - return config.visitors; - } - if (Units::isDanger(unit)) { - return config.hostiles; - } - return true; - }; - static auto calc_extra_weight = [](size_t idx, double r1, double r2) { - switch(idx) { - case 0: - return r2; - case 1: - return (r2-r1)/1.3; - case 2: - return (r2-r1)/2; - default: - return 0.0; - } - }; - /// Collecting our choice pool - /////////////////////////////// - std::array ranges{}; - std::array range_exists{}; - static auto build_range = [&](size_t idx){ - size_t first = idx * 2; - size_t second = idx * 2 + 1; - size_t previous = first - 1; - // first we get the end of the range - ranges[second] = units.size() - 1; - // then we calculate whether the range exists, and set the first index appropriately - if (idx == 0) { - range_exists[idx] = ranges[second] >= 0; - ranges[first] = 0; - } else { - range_exists[idx] = ranges[second] > ranges[previous]; - ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); - } - }; - - /// RANGE 0 (in view + working) - // grab valid working units - add_if([&](df::unit* unit) { - return valid(unit) && - Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)) && - Units::isCitizen(unit, true) && - unit->job.current_job; - }); - build_range(0); - - /// RANGE 1 (in view) - add_if([&](df::unit* unit) { - return valid(unit) && Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)); - }); - build_range(1); - - /// RANGE 2 (working citizens) - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; - }); - build_range(2); - - /// RANGE 3 (citizens) - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true); - }); - build_range(3); - - /// RANGE 4 (any valid) - add_if(valid); - build_range(4); - - // selecting from our choice pool - if (!units.empty()) { - std::array bw{23,17,13,7,1}; // probability weights for each range - std::vector i; - std::vector w; - bool at_least_one = false; - // in one word, elegance - for(size_t idx = 0; idx < range_exists.size(); ++idx) { - if (range_exists[idx]) { - at_least_one = true; - const auto &r1 = ranges[idx*2]; - const auto &r2 = ranges[idx*2+1]; - double extra = calc_extra_weight(idx, r1, r2); - i.push_back(r1); - w.push_back(bw[idx] + extra); - if (r1 != r2) { - i.push_back(r2); - w.push_back(bw[idx] + extra); - } - } - } - if (!at_least_one) { - return false; - } - DebugUnitVector(units); - std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); - // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) - size_t idx = follow_any(RNG); - our_dorf = units[idx]; - plotinfo->follow_unit = our_dorf->id; - timestamp = world->frame_counter; - return true; - } else { - WARN(plugin).print("units vector is empty!\n"); - } - } - return false; - } - - void onUpdate(color_ostream &out) { - // keeps announcement pause settings locked - World::Update(); // from pause.h - - // Plugin Management - if (lock_collision) { - if (config.unpause) { - // player asked for auto-unpause enabled - World::SaveAnnouncementSettings(); - if (World::DisableAnnouncementPausing()) { - // now that we've got what we want, we can lock it down - lock_collision = false; - } - } else { - if (World::RestoreAnnouncementSettings()) { - lock_collision = false; - } - } - } - int failsafe = 0; - while (config.unpause && !world->status.popups.empty() && ++failsafe <= 10) { - // dismiss announcement popup(s) - Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT); - if (World::ReadPauseState()) { - // WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is - World::SetPauseState(false); - } - } - if (failsafe >= 10) { - out.printerr("spectate encountered a problem dismissing a popup!\n"); - } +static std::default_random_engine rng; - // plugin logic - static int32_t last_tick = -1; - int32_t tick = world->frame_counter; - if (!World::ReadPauseState() && tick - last_tick >= 1) { - last_tick = tick; - // validate follow state - if (!following_dwarf || !our_dorf || plotinfo->follow_unit < 0 || tick - timestamp >= config.tick_threshold) { - // we're not following anyone - following_dwarf = false; - if (!config.disengage) { - // try to - following_dwarf = FollowADwarf(); - } else if (!World::ReadPauseState()) { - plugin_enable(out, false); - } - } - } +static uint32_t get_next_cycle_unpaused_ms(bool has_active_combat) { + int32_t delay_ms = config.follow_ms; + if (has_active_combat) { + std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); + int32_t delay_ms = distribution(rng); + delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } -}; - -DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { - commands.push_back(PluginCommand("spectate", - "Automated spectator mode.", - spectate, - false)); - pause_lock = new AnnouncementLock("spectate"); - return CR_OK; + return Core::getInstance().getUnpausedMs() + delay_ms; } -DFhackCExport command_result plugin_shutdown (color_ostream &out) { - delete pause_lock; - return CR_OK; +static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { + if (bucket.empty()) + return; + intervals.push_back(units.size() + bucket.size()); + weights.push_back(weight); + units.insert(units.end(), bucket.begin(), bucket.end()); } -DFhackCExport command_result plugin_load_site_data (color_ostream &out) { - SP::LoadSettings(); - if (enabled) { - SP::following_dwarf = SP::FollowADwarf(); - SP::PrintStatus(out); +#define DUMP_BUCKET(name) \ + DEBUG(cycle,out).print("bucket: " #name ", size: %zd\n", name.size()); \ + if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ + for (auto u : name) { \ + DEBUG(cycle,out).print(" unit %d: %s\n", u->id, DF2CONSOLE(Units::getReadableName(u)).c_str()); \ + } \ } - return DFHack::CR_OK; -} -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; +#define DUMP_FLOAT_VECTOR(name) \ + DEBUG(cycle,out).print(#name ":\n"); \ + for (float f : name) { \ + DEBUG(cycle,out).print(" %d\n", (int)f); \ } - if (enable && !enabled) { - out.print("Spectate mode enabled!\n"); - enabled = true; // enable_auto_unpause won't do anything without this set now - SP::SetUnpauseState(config.unpause); - } else if (!enable && enabled) { - // warp 8, engage! - out.print("Spectate mode disabled!\n"); - // we need to retain whether auto-unpause is enabled, but we also need to disable its effect - bool temp = config.unpause; - SP::SetUnpauseState(false); - config.unpause = temp; +static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; +static const float JOB_WEIGHT = 3.0f; +static const float OTHER_WEIGHT = 1.0f; + +static void follow_a_dwarf(color_ostream &out) { + DEBUG(cycle,out).print("choosing a unit to follow\n"); + + vector active_combat_units; + vector passive_combat_units; + vector job_units; + vector other_units; + get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); + + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(!active_combat_units.empty()); + + // coalesce the buckets and add weights + vector units; + vector intervals; + vector weights; + intervals.push_back(0); + add_bucket(active_combat_units, units, intervals, weights, config.prefer_conflict ? ACTIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(passive_combat_units, units, intervals, weights, config.prefer_conflict ? PASSIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); + add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); + + if (units.empty()) { + DEBUG(cycle,out).print("no units to follow\n"); + return; } - enabled = enable; - return DFHack::CR_OK; -} -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (enabled) { - switch (event) { - case SC_WORLD_UNLOADED: - SP::our_dorf = nullptr; - SP::following_dwarf = false; - enabled = false; - default: - break; - } + std::piecewise_constant_distribution distribution(intervals.begin(), intervals.end(), weights.begin()); + int unit_idx = distribution(rng); + df::unit *unit = units[unit_idx]; + + if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { + DUMP_BUCKET(active_combat_units); + DUMP_BUCKET(passive_combat_units); + DUMP_BUCKET(job_units); + DUMP_BUCKET(other_units); + DUMP_FLOAT_VECTOR(intervals); + DUMP_FLOAT_VECTOR(weights); + DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); } - return CR_OK; -} -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - SP::onUpdate(out); - return DFHack::CR_OK; + DEBUG(cycle,out).print("now following unit %d: %s\n", unit->id, Units::getReadableName(unit).c_str()); + plotinfo->follow_unit = unit->id; } -command_result spectate (color_ostream &out, std::vector & parameters) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - if (!parameters.empty()) { - if (parameters.size() >= 2 && parameters.size() <= 3) { - bool state =false; - bool set = false; - if (parameters[0] == "enable") { - state = true; - } else if (parameters[0] == "disable") { - state = false; - } else if (parameters[0] == "set") { - set = true; - } else { - return DFHack::CR_WRONG_USAGE; - } - if(parameters[1] == "auto-unpause"){ - SP::SetUnpauseState(state); - } else if (parameters[1] == "auto-disengage") { - config.disengage = state; - } else if (parameters[1] == "animals") { - config.animals = state; - } else if (parameters[1] == "hostiles") { - config.hostiles = state; - } else if (parameters[1] == "visiting") { - config.visitors = state; - } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { - try { - config.tick_threshold = std::abs(std::stol(parameters[2])); - } catch (const std::exception &e) { - out.printerr("%s\n", e.what()); - } - } else { - return DFHack::CR_WRONG_USAGE; - } +///////////////////////////////////////////////////// +// Lua API +// + +static void spectate_setSetting(color_ostream &out, string name, int val) { + DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); + + if (name == "auto-disengage") { + config.auto_disengage = val; + } else if (name == "auto-unpause") { + if (val && !config.auto_unpause) { + save_announcement_settings(out); + scrub_announcements(out); + } else if (!val && config.auto_unpause) { + restore_announcement_settings(out); + } + config.auto_unpause = val; + } else if (name == "cinematic-action") { + config.cinematic_action = val; + } else if (name == "include-animals") { + config.include_animals = val; + } else if (name == "include-hostiles") { + config.include_hostiles = val; + } else if (name == "include-visitors") { + config.include_visitors = val; + } else if (name == "include-wildlife") { + config.include_wildlife = val; + } else if (name == "prefer-conflict") { + config.prefer_conflict = val; + } else if (name == "prefer-new-arrivals") { + config.prefer_new_arrivals = val; + } else if (name == "follow-seconds") { + if (val <= 0) { + WARN(control,out).print("follow-seconds must be a positive integer\n"); + return; } + config.follow_ms = val * 1000; } else { - SP::PrintStatus(out); + WARN(control,out).print("Unknown setting: %s\n", name.c_str()); } - SP::SaveSettings(); - return DFHack::CR_OK; } + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(spectate_setSetting), + DFHACK_LUA_END +}; From 516296d7c3351c778cfd042e12c20dbd52b25d2d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:14:04 -0800 Subject: [PATCH 057/115] add global keybinding to toggle spectate --- data/init/dfhack.keybindings.init | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index f4068a5cd1..1fa7b9facc 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -49,6 +49,9 @@ keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeo # quicksave keybinding add Ctrl-Alt-S@dwarfmode quicksave +# toggle spectate +keybinding add Ctrl-Shift-S@dwarfmode/Default "spectate toggle" + # designate the whole vein for digging keybinding add Ctrl-V@dwarfmode digv keybinding add Ctrl-Shift-V@dwarfmode "digv x" From 70a02dd9c6f17080dd2d90845de797d3b18c1b3e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:27:18 -0800 Subject: [PATCH 058/115] update changelog --- docs/changelog.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3668383d2b..80b8c76045 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,10 +55,14 @@ Template for new versions: ## New Tools ## New Features +- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit +- `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict ## Fixes +- `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled ## Misc Improvements +- `spectate`: player-set configuration is now stored globally instead of per-fort ## Documentation From b934af10a892d05faf15e537a6a74113dbef5bfc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:33:58 -0800 Subject: [PATCH 059/115] add changelog for global keybinding --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 80b8c76045..d521d96745 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Features - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict +- `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S ## Fixes - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled From c762108dade1d7b5a19bd29117b28aaf18bdd38a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:36:01 -0800 Subject: [PATCH 060/115] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6a952903e7..b6e8e30fb5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6a952903e72199e469b44e0c74764c1c89f4b2e3 +Subproject commit b6e8e30fb5f98d1f1925f988675471a44a464375 From 41435de0ad02bcca9660797f7683481cdee5a37f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:18:40 +0000 Subject: [PATCH 061/115] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.30.0 → 0.31.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.30.0...0.31.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01094ed98d..ae54f7cf0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From aa61289ab2d06fab8996ae1f868fadcb0fd89b21 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 6 Feb 2025 04:13:51 -0800 Subject: [PATCH 062/115] Update Gui.cpp - Detect adv look focus string --- library/modules/Gui.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index ed6ddcba7c..6dfe7f920e 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -848,6 +848,9 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.adventure.jump.open) { focusStrings.push_back(baseFocus + "/Jump"); } + if (game->main_interface.adventure.look.open) { + focusStrings.push_back(baseFocus + "/Look"); + } if (game->main_interface.adventure.movement_options.open) { focusStrings.push_back(baseFocus + "/MovementOptions"); } From d6b16e4d7fe5617c7b6d69ce2fe6a7ee46d66633 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 6 Feb 2025 19:38:47 -0500 Subject: [PATCH 063/115] Disallow changing a vein into itself Prevents infinite recursion from occurring while searching for neighboring vein tiles, which would eventually result in a crash. Fixes #5231 --- docs/changelog.txt | 1 + plugins/changevein.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index d521d96745..b421b7042e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled +- `changevein`: fix a crash that could occur when attempting to change a vein into itself ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort diff --git a/plugins/changevein.cpp b/plugins/changevein.cpp index a73d9584e9..a8151f348b 100644 --- a/plugins/changevein.cpp +++ b/plugins/changevein.cpp @@ -254,6 +254,12 @@ command_result df_changevein (color_ostream &out, vector & parameters) return CR_FAILURE; } + if (mineral->inorganic_mat == mi.index) + { + out.printerr("Selected tile is already of the target material.\n"); + return CR_FAILURE; + } + VeinEdgeBitmask mask = VeinEdgeBitmask(mineral); mineral->inorganic_mat = mi.index; ChangeSameBlockVeins(block, mineral, mask, mi.index); From 6b59c65d80fd67c68185905484155554b26db9ab Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 6 Feb 2025 23:01:06 -0800 Subject: [PATCH 064/115] fix incorrect doc comment --- library/include/LuaTools.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 53c17e1b21..51ae098f49 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -513,9 +513,9 @@ namespace DFHack {namespace Lua { } } /** - * High-level wrappers for CallLuaModuleFunction that automatically suspends the - * core and pushes either an argument vector (i.e. single type variable number) or - * an argument tuple (i.e. fixed number of arguments of various types) + * High-level wrappers for CallLuaModuleFunction that pushes either an argument + * vector (i.e. single type variable number) or an argument tuple (i.e. fixed + * number of arguments of various types) */ template bool CallLuaModuleFunction( From d0e03a8dec53e3b8465636f9a65866ad4dc7ef5a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 7 Feb 2025 07:16:48 +0000 Subject: [PATCH 065/115] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index ee9a53f73b..b3202b06e0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ee9a53f73b4d2bf918dca0fc9dcb450f53d6e7c3 +Subproject commit b3202b06e04c03433565e8248f2638dddde4ee22 diff --git a/scripts b/scripts index b6e8e30fb5..ebb22f3adb 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b6e8e30fb5f98d1f1925f988675471a44a464375 +Subproject commit ebb22f3adb6070c931a82b5f6a1749e7bf3ced03 From 9dc6e22d6e714d959e076101b0c2a82e59ca8df2 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 7 Feb 2025 02:44:30 -0600 Subject: [PATCH 066/115] Docs: New Guide for adding art/sprites to Stonesense (#5207) guide for adding art to Stonesense --- docs/changelog.txt | 1 + docs/guides/index.rst | 1 + docs/guides/stonesense-art-guide.rst | 71 +++++++++++++++++++++ docs/images/stonesense-indexed-sprites.png | Bin 0 -> 10256 bytes docs/images/stonesense-sprite-sample.png | Bin 0 -> 694 bytes docs/images/stonesense-sprite-template.png | Bin 0 -> 515 bytes docs/images/stonesense-yellowcubes.png | Bin 0 -> 178548 bytes docs/plugins/stonesense.rst | 17 +++++ 8 files changed, 90 insertions(+) create mode 100644 docs/guides/stonesense-art-guide.rst create mode 100644 docs/images/stonesense-indexed-sprites.png create mode 100644 docs/images/stonesense-sprite-sample.png create mode 100644 docs/images/stonesense-sprite-template.png create mode 100644 docs/images/stonesense-yellowcubes.png diff --git a/docs/changelog.txt b/docs/changelog.txt index b421b7042e..142447527e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: - `spectate`: player-set configuration is now stored globally instead of per-fort ## Documentation +- `stonesense-art-guide`: new guide for making sprite art for Stonesense ## API diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 6660dfae51..f47f9565ce 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -10,3 +10,4 @@ These pages are detailed guides covering DFHack tools. /docs/guides/modding-guide /docs/guides/quickfort-library-guide /docs/guides/quickfort-user-guide + /docs/guides/stonesense-art-guide diff --git a/docs/guides/stonesense-art-guide.rst b/docs/guides/stonesense-art-guide.rst new file mode 100644 index 0000000000..c9e966dba3 --- /dev/null +++ b/docs/guides/stonesense-art-guide.rst @@ -0,0 +1,71 @@ +.. _stonesense-art-guide: + +Stonesense art creation guide +============================= + +Understanding isometric perspective +----------------------------------- + +Stonesense uses an isometric perspective, a form of pseudo-3D projection where objects are displayed at an +angle, typically with a 2:1 pixel ratio for diagonal lines. This perspective allows for a detailed and visually +appealing representation of a 3D world using 2D sprites. Unlike traditional top-down views, isometric projection +simulates depth while maintaining a consistent scale without vanishing points. + +Understanding sprites +--------------------- + +Understanding how Stonesense deals with sprites is central to anyone who wishes to modify the content. The +scheme is not very complicated, and this guide will give a short introduction to how they work. With the +exception of floors, which we will discuss later, all sprites are 32x32 pixels and come in groups known +as sprite sheets. All sprites are loaded and rendered in 32-bit full-color PNGs. The image files should have +a transparent background but pure magenta (RGB: 255,0,255) is also treated as transparent. + + +.. image:: ../images/stonesense-sprite-sample.png + :align: left + +Here's an example of a typical Stonesense sprite. + +When working with Stonesense sprites, it is important to understand how they fit into the isometric grid. +Each sprite is designed to align with the isometric perspective and must fit within a specific bounding area. +To illustrate this, here is a template for the area that should be used by Stonesense sprites: + +.. image:: ../images/stonesense-sprite-template.png + :align: left + +The solid area is the floor space taken up by a sprite, while the dotted box indicates the volume above this +area corresponding to one z-level. + +The way sprites are loaded is fairly generalized: the name of the sprite sheet, and the index of a sprite within that sheet. + +Sprite sheets +------------- +There can be an arbitrary number of sprite sheets for Stonesense, though there are 3 sheets that are +always present as they contain default sprites (see further down). Configuring the XML to use new sheets is +outside the scope of this guide but there may be a guide for such added in the future. + +Sprite index +------------ +The sprite index, or sheet index, is the zero-indexed offset of a sprite on its sprite sheet. +The index starts with the upper left sprite which has index zero. It then increments to the right. Stonesense +is hardcoded to 20 sprite-wide sheets, this means that anything past 20 "sprite slots" is ignored, though less +than 20 slots is fine. The first sprite on the second row always has index 20 (even if there are fewer sprites per row in the sheet), the next row is 40, and so on. This +boundary is hardcoded and changing the size of the sheet will not affect it. + +This image shows how sprites are indexed. Grid added for readability. + +.. figure:: ../images/stonesense-indexed-sprites.png + :align: left + + +Important sprite sheets +----------------------- +`objects.png `_ is the default sheet +for buildings and vegetation. Also used for all hard-coded content, like default plants, the cursor, default +walls and liquid. + +`creatures.png `_ is the default +sprite sheet for creatures. If no file is specified in a creature node, this is the sheet it will use. + +`floors.png `_ holds all the Stonesense +floors. Unlike the other sprite sheet, this sheet is hard-coded with sprite dimensions of 32x20 pixels. diff --git a/docs/images/stonesense-indexed-sprites.png b/docs/images/stonesense-indexed-sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..dc749a21965f0472cf189ecfe1083ec0892ccb87 GIT binary patch literal 10256 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 literal 0 HcmV?d00001 diff --git a/docs/images/stonesense-sprite-sample.png b/docs/images/stonesense-sprite-sample.png new file mode 100644 index 0000000000000000000000000000000000000000..35dcbefd55151f27e30aa656df873d0681d2b253 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCil&0(?ST8O&t`t2nauOECHJd(Bf=5zA0u#~7r?biB#Br15j!LI|B<)rGb&L0pkLQsURC!7eGv!0%U^# z6VO~Hu*x7y3m^-s%h138BrCW67yHpxt!7}bs(QLOhFJKwPHNoNtia=(xpL9}|A+NG zxt+`JMV7m@-bo9*{%-B8Rc4{Tx6R!B^`DE~>}z)po!xaidDXAGcWiZbl^WmnDO1oW z>wfW$JMu1%)pOQGUNd<=PGET6IDgT3o=LqwOW92X1Sij6V4umzl&4yLspw!|@-p^` zvP+Ug8iI{xEYVT6+r(-R<;S`Bbk9s~zm;iTv&#E*otgJNn{@c{DGttx2meaXw0z!e zy+EL8eV1HOvZqKZW0#|R%%S;znID9HZaMr*QEK7y4Ggm$xE$5{KkI5&mP63=A76I` znk@YB=e+GY#jlTLww}G!P$>H&=D7U<332ZueLe{ZzibrevtQ%YOh|ELK2|DN?eQ}v xeN*WtCJ!6qeT)M33`>myuy_`b4FaB$j0`V;o`TV6mNEdP zCa^QG096_o85=MzfS3xhk#zyYq$xl)2rvQ7Wdf@Vva|rQpt=kV3_!B_m-4?!{`z+k z$eiTq;uvD#-#al{s6l~;`RTvu+cY)5vF?z+mBRhF=RAkUnzQWA*Behu^j)>feBisd zac3{v26o;D^>;Qz2D)BfGsAFdSlHWNXJ7ai@lEA#I&x>p98Gf$8P7j#hd62$ghp^? zFN<7r>z=5wH1CUgTe~DWM4fSd50` literal 0 HcmV?d00001 diff --git a/docs/images/stonesense-yellowcubes.png b/docs/images/stonesense-yellowcubes.png new file mode 100644 index 0000000000000000000000000000000000000000..83ae6ce5077a512ae28286bfa0748c62d86374e8 GIT binary patch literal 178548 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 z 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%0A3LFowg*Ew(zB+&5 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 literal 0 HcmV?d00001 diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 5d8dbb6964..9b0a7c2792 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -78,10 +78,27 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. +Yellow cubes and missing sprites +-------------------------------- +If you are seeing yellow cubes in Stonesense, then there is something on the map that +Stonesense does not have a sprite for. + +.. figure:: ../images/stonesense-yellowcubes.png + :align: center + + An example of the yellow cubes. + +If you would like to help us in fixing this, there are two things you can do: + +* Make an issue on `GitHub `_ with what + item is missing and pictures of what it looks like in DF. +* Create the art yourself. For help with this, please see the `stonesense-art-guide`. + Useful links ------------ - Report issues on `Github `_ - `support` +- `Stonesense Subreddit `_ - :forums:`Official Stonesense thread <106497>` - :forums:`Screenshots thread <48172>` - :wiki:`Main wiki page ` From aa6fdb41fc4ee5330ba907a9960dde05cb34d2b1 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 22:48:31 +0800 Subject: [PATCH 067/115] Add Military::removeFromSquad --- library/modules/Military.cpp | 64 ++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 23fc15a871..929285a761 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -7,6 +7,10 @@ #include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" +#include "df/histfig_entity_link_former_positionst.h" +#include "df/histfig_entity_link_former_squadst.h" +#include "df/histfig_entity_link_positionst.h" +#include "df/histfig_entity_link_squadst.h" #include "df/historical_figure.h" #include "df/historical_entity.h" #include "df/entity_position.h" @@ -17,6 +21,7 @@ #include "df/squad_schedule_order.h" #include "df/squad_order.h" #include "df/squad_order_trainst.h" +#include "df/unit.h" #include "df/world.h" using namespace DFHack; @@ -297,46 +302,51 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ for (size_t i = 0; i < hf->entity_links.size(); i++) { auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) continue; - if (link->squad_id != squad->id) continue; - + if (link == nullptr || link->squad_id != squad->id) + continue; + hf->entity_links.erase(hf->entity_links.begin() + i); - delete link; - start_year = link->start_year; + + delete link; break; } - if (start_year == -1) return false; + + if (start_year == -1) + return false; - auto former_squad = new df::histfig_entity_link_former_squadst(); + auto former_squad = df::allocate(); former_squad->squad_id = squad->id; former_squad->entity_id = squad->entity_id; former_squad->start_year = start_year; former_squad->end_year = *df::global::cur_year; former_squad->link_strength = 100; - hf->entity_links.push_back(former_pos); + hf->entity_links.push_back(former_squad); return true; } -static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squad) +static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; - if (! Units::getNoblePositions(&nps, hf)) return false; + if (! Units::getNoblePositions(&nps, hf)) + return false; int32_t assignment_id = -1; for (auto& np : nps) { - if (np.entity.id != squad->entity_id) continue; - if (np.assignment.squad_id != squad.id) continue; - + if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) + continue; + np.assignment->histfig = -1; np.assignment->histfig2 = -1; assignment_id = np.assignment->id; break; } - if (assignment_id == -1) return false; + + if (assignment_id == -1) + return false; int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -346,14 +356,16 @@ static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squ if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - delete link; - start_year = link->start_year; + + delete link; break; } - if (start_year == -1) return false; + + if (start_year == -1) + return false; - auto former_pos = new df::histfig_entity_link_former_positionst(); + auto former_pos = df::allocate(); former_pos->assignment_id = assignment_id; former_pos->entity_id = squad->entity_id; former_pos->start_year = start_year; @@ -366,13 +378,16 @@ static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squ bool Military::removeFromSquad(int32_t unit_id) { - df::unit* unit = df::unit::find(unit_id); - if (unit == nullptr) return false; - if (unit->military.squad_id == -1 || unit->military.squad_position == -1) return false; + df::unit *unit = df::unit::find(unit_id); + if (unit == nullptr) + return false; + if (unit->military.squad_id == -1 || unit->military.squad_position == -1) + return false; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); - if (squad == nullptr) return false; + if (squad == nullptr) + return false; // remove from squad information int32_t squad_pos = unit->military.squad_position; @@ -384,9 +399,10 @@ bool Military::removeFromSquad(int32_t unit_id) unit->military.squad_position = -1; df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); - if (hf == nullptr) return false; + if (hf == nullptr) + return false; return squad_pos == 0 // is unit a commander? - ? remove_captain_entity_link(hf, squad) + ? remove_officer_entity_link(hf, squad) : remove_soldier_entity_link(hf, squad); } From 872cc7cba5a2e599c6e2f081b4ee2a8333d5d9ff Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:01:49 +0800 Subject: [PATCH 068/115] Fix style --- library/modules/Military.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 929285a761..d569250167 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -312,7 +312,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ break; } - if (start_year == -1) + if (start_year == -1) return false; auto former_squad = df::allocate(); @@ -335,7 +335,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ int32_t assignment_id = -1; for (auto& np : nps) { - if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) + if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) continue; np.assignment->histfig = -1; @@ -352,8 +352,10 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ for (size_t i = 0; i < hf->entity_links.size(); i++) { auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) continue; - if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; + if (link == nullptr) + continue; + if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) + continue; hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; From 84370629900dcfca95a984cae8284d11e3d9678b Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:09:19 +0800 Subject: [PATCH 069/115] Attempt to fix trailing whitespace --- library/modules/Military.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index d569250167..d1322735db 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -329,7 +329,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; - if (! Units::getNoblePositions(&nps, hf)) + if (!Units::getNoblePositions(&nps, hf)) return false; int32_t assignment_id = -1; From 7eb27ef350963ebdac9a136bcc291ef989df99aa Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:12:44 +0800 Subject: [PATCH 070/115] Remove trailing whitespace --- library/modules/Military.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index d1322735db..8292c6227b 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -304,14 +304,14 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ auto link = strict_virtual_cast(hf->entity_links[i]); if (link == nullptr || link->squad_id != squad->id) continue; - + hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; delete link; break; } - + if (start_year == -1) return false; @@ -337,14 +337,14 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ { if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) continue; - + np.assignment->histfig = -1; np.assignment->histfig2 = -1; assignment_id = np.assignment->id; break; } - + if (assignment_id == -1) return false; @@ -359,11 +359,11 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; - + delete link; break; } - + if (start_year == -1) return false; From 9a06a27222538bf4cd7536453e0d9e851858a8c2 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 02:43:23 +0800 Subject: [PATCH 071/115] Add Lua bindings --- docs/dev/Lua API.rst | 8 ++++++++ library/LuaApi.cpp | 1 + 2 files changed, 9 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 000840c76b..4cc2c35d06 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1986,6 +1986,14 @@ Military module Returns the name of a squad as a string. +* ``dfhack.military.removeFromSquad(unit_id)`` + + Removes a unit from its squad and returns true if successful. Unsets the unit's + military information (i.e., ``unit.military.squad_id`` and + ``unit.military.squad_pos``), the squad's position information (i.e., + ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links + to indicate former squad membership or command. + Items module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4b82dddc17..6e251dbfcf 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2341,6 +2341,7 @@ static const LuaWrapper::FunctionReg dfhack_military_module[] = { WRAPM(Military, makeSquad), WRAPM(Military, updateRoomAssignments), WRAPM(Military, getSquadName), + WRAPM(Military, removeFromSquad), { NULL, NULL } }; From c9e2dcc2812247cb4f9d1f3a4e3410f70d2172b3 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 02:44:26 +0800 Subject: [PATCH 072/115] Modify Military.cpp --- library/modules/Military.cpp | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 8292c6227b..62d189c4d7 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -301,14 +301,18 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) { - auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr || link->squad_id != squad->id) + df::histfig_entity_link* link = hf->entity_links[i]; + if (link->getType() != df::enums::histfig_entity_link_type::SQUAD) + continue; + + auto squad_link = strict_virtual_cast(link); + if (squad_link == nullptr || squad_link->squad_id != squad->id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - start_year = link->start_year; + start_year = squad_link->start_year; - delete link; + delete squad_link; break; } @@ -351,16 +355,20 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) { - auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) + df::histfig_entity_link* link = hf->entity_links[i]; + if (link->getType() != df::enums::histfig_entity_link_type::POSITION) + continue; + + auto pos_link = strict_virtual_cast(link); + if (pos_link == nullptr) continue; - if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) + if (pos_link->assignment_id != assignment_id && pos_link->entity_id != squad->entity_id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - start_year = link->start_year; + start_year = pos_link->start_year; - delete link; + delete pos_link; break; } @@ -393,9 +401,12 @@ bool Military::removeFromSquad(int32_t unit_id) // remove from squad information int32_t squad_pos = unit->military.squad_position; - df::squad_position* pos = squad->positions.at(squad_pos); - pos->occupant = -1; + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos == nullptr) + return false; + // remove from squad information + pos->occupant = -1; // remove from unit information unit->military.squad_id = -1; unit->military.squad_position = -1; From 76d2af495407cd6cdad1ac9a18854ef3e207e171 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 12:56:43 -0600 Subject: [PATCH 073/115] Update Authors.rst --- docs/about/Authors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 2908cae9d6..bfb8aa02a8 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -242,6 +242,7 @@ thurin thurin Tim Siegel softmoth Tim Walberg twalberg Timothy Collett danaris +Timothy Torres timothymtorres Timur Kelman TymurGubayev Tom Jobbins TheBloke Tom Prince From cfe1dbb79b6b53e3d1f4e03bf92a6b2807b6a9de Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 03:14:43 +0800 Subject: [PATCH 074/115] Update changelog --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d28decc8..e8340baeac 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,9 +35,11 @@ Template for new versions: ## Documentation ## API +- ``Military::removeFromSquad``: removes unit from any squad assignments ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings +- ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` ## Removed From 71f92c6085625497940d1788f60687228d3b9e12 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 7 Feb 2025 16:32:29 -0800 Subject: [PATCH 075/115] Update Gui.cpp - Redundant code --- library/modules/Gui.cpp | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 6dfe7f920e..1d60c335b6 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2962,22 +2962,8 @@ bool Gui::getCursorCoords(int32_t &x, int32_t &y, int32_t &z) bool Gui::getCursorCoords(df::coord &pos) { - using df::global::cursor; - df::coord p; - if (World::isAdventureMode()) - { - if (game) - { - auto &look = game->main_interface.adventure.look; - if (look.open) - p = look.cursor; - } - } - else if (cursor) - p = df::coord(cursor->x, cursor->y, cursor->z); - - pos = p; - return p.isValid(); + pos = getCursorPos(); + return pos.isValid(); } //FIXME: confine writing of coords to map bounds? From 194bde01576571e023ea11255c3aa5a0f80a066a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Feb 2025 07:15:59 +0000 Subject: [PATCH 076/115] Auto-update submodules scripts: master plugins/stonesense: master depends/dfhooks: main --- depends/dfhooks | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 5b70cd886f..5bd392890e 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 5b70cd886f619b503aa770008fb79aa39a3be6fa +Subproject commit 5bd392890e92a70622878c0513ef50e72e215435 diff --git a/plugins/stonesense b/plugins/stonesense index 9d851db080..1756e2f3f7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9d851db080012add38631af37ea5c5b60a893e79 +Subproject commit 1756e2f3f704a0d01b24feccd0089972c245c164 diff --git a/scripts b/scripts index ebb22f3adb..fc67fdc5b6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ebb22f3adb6070c931a82b5f6a1749e7bf3ced03 +Subproject commit fc67fdc5b67a25a776eebfa769b289801679e364 From 5a6de1fc928ecc067267064fab2fd443a36643c8 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 19:15:03 +0800 Subject: [PATCH 077/115] Remove return val --- docs/dev/Lua API.rst | 2 +- library/include/modules/Military.h | 2 +- library/modules/Military.cpp | 35 ++++++++++++++---------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 4cc2c35d06..66516642a9 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1988,7 +1988,7 @@ Military module * ``dfhack.military.removeFromSquad(unit_id)`` - Removes a unit from its squad and returns true if successful. Unsets the unit's + Removes a unit from its squad. Unsets the unit's military information (i.e., ``unit.military.squad_id`` and ``unit.military.squad_pos``), the squad's position information (i.e., ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index dc69dac52b..37855d94db 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,7 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); -DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); +DFHACK_EXPORT void removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 62d189c4d7..9e507a99aa 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -296,7 +296,7 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s } } -static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) +static void remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) { int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -317,7 +317,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ } if (start_year == -1) - return false; + return; auto former_squad = df::allocate(); former_squad->squad_id = squad->id; @@ -327,14 +327,13 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ former_squad->link_strength = 100; hf->entity_links.push_back(former_squad); - return true; } -static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) +static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; if (!Units::getNoblePositions(&nps, hf)) - return false; + return; int32_t assignment_id = -1; for (auto& np : nps) @@ -350,7 +349,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ } if (assignment_id == -1) - return false; + return; int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -373,7 +372,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ } if (start_year == -1) - return false; + return; auto former_pos = df::allocate(); former_pos->assignment_id = assignment_id; @@ -383,27 +382,24 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->link_strength = 100; hf->entity_links.push_back(former_pos); - return true; } -bool Military::removeFromSquad(int32_t unit_id) +void Military::removeFromSquad(int32_t unit_id) { df::unit *unit = df::unit::find(unit_id); - if (unit == nullptr) - return false; - if (unit->military.squad_id == -1 || unit->military.squad_position == -1) - return false; + if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) + return; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); if (squad == nullptr) - return false; + return; // remove from squad information int32_t squad_pos = unit->military.squad_position; df::squad_position* pos = vector_get(squad->positions, squad_pos); if (pos == nullptr) - return false; + return; // remove from squad information pos->occupant = -1; @@ -413,9 +409,10 @@ bool Military::removeFromSquad(int32_t unit_id) df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); if (hf == nullptr) - return false; + return;; - return squad_pos == 0 // is unit a commander? - ? remove_officer_entity_link(hf, squad) - : remove_soldier_entity_link(hf, squad); + if (squad_pos == 0) // is unit a commander? + remove_officer_entity_link(hf, squad); + else + remove_soldier_entity_link(hf, squad); } From 6d44e8f8f0e1eb0cd7b9a42c29dbfa8309d081f5 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sun, 9 Feb 2025 12:51:12 +0800 Subject: [PATCH 078/115] Change return type of Military::removeFromSquad --- library/include/modules/Military.h | 2 +- library/modules/Military.cpp | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 37855d94db..dc69dac52b 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,7 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); -DFHACK_EXPORT void removeFromSquad(int32_t unit_id); +DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 9e507a99aa..3c5708d75e 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -384,22 +384,25 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.push_back(former_pos); } -void Military::removeFromSquad(int32_t unit_id) +bool Military::removeFromSquad(int32_t unit_id) { df::unit *unit = df::unit::find(unit_id); if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) - return; + return false; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); if (squad == nullptr) - return; + return false; - // remove from squad information int32_t squad_pos = unit->military.squad_position; df::squad_position* pos = vector_get(squad->positions, squad_pos); if (pos == nullptr) - return; + return false; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) + return false; // remove from squad information pos->occupant = -1; @@ -407,12 +410,10 @@ void Military::removeFromSquad(int32_t unit_id) unit->military.squad_id = -1; unit->military.squad_position = -1; - df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); - if (hf == nullptr) - return;; - if (squad_pos == 0) // is unit a commander? remove_officer_entity_link(hf, squad); else remove_soldier_entity_link(hf, squad); + + return true; } From c21740929a56691be26401c56cf5ea0ba6df04a7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 9 Feb 2025 07:15:07 +0000 Subject: [PATCH 079/115] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 1756e2f3f7..92794aec77 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 1756e2f3f704a0d01b24feccd0089972c245c164 +Subproject commit 92794aec7768bdafd0f5fc23fed8b30655805b32 From 3844957820893c92bd1cf97cac617f7ef5e451bb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 00:55:35 -0800 Subject: [PATCH 080/115] refactor announcement settings logic --- plugins/spectate.cpp | 141 +++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index ae33031b14..0871e4754c 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -33,42 +33,8 @@ namespace DFHack { } static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle -static bool was_in_settings = false; // whether we were in the vanilla settings screen last update -static const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); -static std::unique_ptr saved_announcement_settings; - -static void save_announcement_settings(color_ostream &out) { - if (!saved_announcement_settings) - saved_announcement_settings = std::make_unique(new uint32_t[announcement_flag_arr_size]); - DEBUG(control,out).print("saving announcement settings\n"); - for (size_t i = 0; i < announcement_flag_arr_size; ++i) - (*saved_announcement_settings)[i] = d_init->announcements.flags[i].whole; -} - -static void restore_announcement_settings(color_ostream &out) { - if (!saved_announcement_settings) - return; - DEBUG(control,out).print("restoring saved announcement settings\n"); - for (size_t i = 0; i < announcement_flag_arr_size; ++i) - d_init->announcements.flags[i].whole = (*saved_announcement_settings)[i]; -} - -static void scrub_announcements(color_ostream &out) { - if (Gui::matchFocusString("dwarfmode/Settings")) { - DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); - return; - } - - DEBUG(control,out).print("removing PAUSE from announcement settings\n"); - for (auto& flag : d_init->announcements.flags) { - flag.bits.DO_MEGA = false; - flag.bits.PAUSE = false; - flag.bits.RECENTER = false; - } -} - -struct Configuration { +static struct Configuration { bool auto_disengage; bool auto_unpause; bool cinematic_action; @@ -94,6 +60,75 @@ struct Configuration { } } config; +static class AnnouncementSettings { + bool was_in_settings = false; // whether we were in the vanilla settings screen last update + + const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); + std::unique_ptr saved; + + void save_settings(color_ostream &out) { + if (!saved) + saved = std::make_unique(new uint32_t[announcement_flag_arr_size]); + DEBUG(control,out).print("saving announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + (*saved)[i] = d_init->announcements.flags[i].whole; + } + +public: + void reset(color_ostream &out, bool skip_restore = false) { + was_in_settings = false; + + if (saved) { + if (!skip_restore) + restore_settings(out); + delete[] *saved; + saved.reset(); + } + } + + void on_update(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + if (!was_in_settings) { + DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); + restore_settings(out); + was_in_settings = true; + } + } else if (was_in_settings) { + was_in_settings = false; + if (config.auto_unpause) { + DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); + save_and_scrub_settings(out); + } + } + } + + void restore_settings(color_ostream &out) { + if (!saved || was_in_settings) + return; + DEBUG(control,out).print("restoring saved announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + d_init->announcements.flags[i].whole = (*saved)[i]; + } + + // remove pausing, popups, and recentering from all announcements + // saves first so the original settings can be restored + void save_and_scrub_settings(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); + return; + } + + save_settings(out); + + DEBUG(control,out).print("scrubbing announcement settings\n"); + for (auto& flag : d_init->announcements.flags) { + flag.bits.DO_MEGA = false; + flag.bits.PAUSE = false; + flag.bits.RECENTER = false; + } + } +} announcement_settings; + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -108,14 +143,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit = -1; - cleanup(out); + announcement_settings.reset(out); } } else { DEBUG(control,out).print("%s from the API, but already %s; no action\n", @@ -148,7 +175,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_shutdown (color_ostream &out) { DEBUG(control,out).print("shutting down %s\n", plugin_name); - cleanup(out); + announcement_settings.reset(out); return CR_OK; } @@ -162,7 +189,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DEBUG(control,out).print("world unloaded; disabling %s\n", plugin_name); is_enabled = false; - cleanup(out); + announcement_settings.reset(out, true); } break; default: @@ -172,25 +199,12 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (Gui::matchFocusString("dwarfmode/Settings")) { - if (!was_in_settings) { - DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); - restore_announcement_settings(out); - was_in_settings = true; - } - } else if (was_in_settings) { - was_in_settings = false; - if (config.auto_unpause) { - DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); - save_announcement_settings(out); - scrub_announcements(out); - } - } + announcement_settings.on_update(out); if (config.auto_disengage && plotinfo->follow_unit < 0) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; - cleanup(out); + announcement_settings.reset(out); return CR_OK; } @@ -354,10 +368,9 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { config.auto_disengage = val; } else if (name == "auto-unpause") { if (val && !config.auto_unpause) { - save_announcement_settings(out); - scrub_announcements(out); + announcement_settings.save_and_scrub_settings(out); } else if (!val && config.auto_unpause) { - restore_announcement_settings(out); + announcement_settings.restore_settings(out); } config.auto_unpause = val; } else if (name == "cinematic-action") { From 2daa28790b5b3dc3e7a052ec61d4768d5350c380 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 02:28:45 -0800 Subject: [PATCH 081/115] implement follow history and zoom to/highlight unit when switching targets when paused --- plugins/spectate.cpp | 92 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 0871e4754c..a3e3c86193 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -2,6 +2,7 @@ #include "LuaTools.h" #include "PluginLua.h" #include "PluginManager.h" +#include "VTableInterpose.h" #include "modules/Gui.h" #include "modules/Units.h" @@ -11,6 +12,7 @@ #include "df/d_init.h" #include "df/plotinfost.h" #include "df/unit.h" +#include "df/viewscreen_dwarfmodest.h" #include "df/world.h" #include @@ -34,6 +36,8 @@ namespace DFHack { static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle +static const size_t MAX_HISTORY = 200; + static struct Configuration { bool auto_disengage; bool auto_unpause; @@ -129,6 +133,77 @@ static class AnnouncementSettings { } } announcement_settings; +static void follow_a_dwarf(color_ostream &out); + +static class UnitHistory { + std::deque history; + size_t offset = 0; + +public: + void reset() { + history.clear(); + offset = 0; + } + + void add(color_ostream &out, df::unit *unit) { + int32_t id = unit->id; + DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = id; + if (offset > 0) { + DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + history.resize(history.size() - offset); + offset = 0; + } + history.push_back(id); + if (history.size() > MAX_HISTORY) { + DEBUG(cycle,out).print("history full, truncating\n"); + history.pop_front(); + } + DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); + } + + void scan_back(color_ostream &out) { + if (history.empty() || offset >= history.size()-1) + return; + int unit_id = history[history.size() - (1 + ++offset)]; + DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); + if (auto unit = df::unit::find(unit_id)) + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = unit_id; + } + + void scan_forward(color_ostream &out) { + if (history.empty() || offset == 0) { + DEBUG(cycle,out).print("already at most recent unit; following new unit\n"); + follow_a_dwarf(out); + return; + } + + int unit_id = history[history.size() - (1 + --offset)]; + DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); + if (auto unit = df::unit::find(unit_id)) + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = unit_id; + } +} unit_history; + +struct forward_back_interceptor : df::viewscreen_dwarfmodest { + typedef df::viewscreen_dwarfmodest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { + bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default", Gui::getDFViewscreen()); + if (is_at_default_view && input->count(df::interface_key::CUSTOM_LEFT)) + unit_history.scan_back(Core::getInstance().getConsole()); + else if (is_at_default_view && input->count(df::interface_key::CUSTOM_RIGHT)) + unit_history.scan_forward(Core::getInstance().getConsole()); + else { + INTERPOSE_NEXT(feed)(input); + } + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -143,6 +218,11 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit = -1; - announcement_settings.reset(out); + on_disable(out); + // don't reset the unit history since we may want to re-enable } } else { DEBUG(control,out).print("%s from the API, but already %s; no action\n", @@ -175,7 +257,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_shutdown (color_ostream &out) { DEBUG(control,out).print("shutting down %s\n", plugin_name); - announcement_settings.reset(out); + on_disable(out); return CR_OK; } @@ -190,6 +272,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan plugin_name); is_enabled = false; announcement_settings.reset(out, true); + unit_history.reset(); } break; default: @@ -204,7 +287,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (config.auto_disengage && plotinfo->follow_unit < 0) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; - announcement_settings.reset(out); + on_disable(out); return CR_OK; } @@ -353,8 +436,7 @@ static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); } - DEBUG(cycle,out).print("now following unit %d: %s\n", unit->id, Units::getReadableName(unit).c_str()); - plotinfo->follow_unit = unit->id; + unit_history.add(out, unit); } ///////////////////////////////////////////////////// From 82e014dd6791d2e8046ea9a15c84f258876a407b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 02:55:03 -0800 Subject: [PATCH 082/115] auto-disengage when the squads menu is opened --- docs/plugins/spectate.rst | 6 +++--- plugins/lua/spectate.lua | 15 +++++++++++++++ plugins/spectate.cpp | 23 +++++++++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index f49849c850..d0b5c8f04e 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -13,9 +13,9 @@ to following a different dwarf. It can also switch to following animals, hostiles, or visiting units. You can switch to the next target (or a previous target) immediately with the left/right arrow keys. -`spectate` will disengage and turn itself off when you move the map, just like -the vanilla follow mechanic. It will also disengage immediately if you open the -squads menu for military action. +By default, `spectate` will disengage and turn itself off when you move the +map, just like the vanilla follow mechanic. It will also disengage immediately +if you open the squads menu for military action. It can also annotate your dwarves on the map with their name, job, and other information, either as floating tooltips or in a panel that comes up when you diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index ba7e9ba7ba..e21016329f 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -1,6 +1,7 @@ local _ENV = mkmodule('plugins.spectate') local argparse = require('argparse') +local dlg = require('gui.dialogs') local json = require('json') local overlay = require('plugins.overlay') local utils = require('utils') @@ -55,6 +56,20 @@ function refresh_cpp_config() end end +function show_squads_warning() + local message = { + 'Cannot start spectate mode while auto-disengage is enabled and', + 'the squads panel is open. The auto-disengage feature automatically', + 'stops spectate mode when you open the squads panel.', + '', + 'Please either close the squads panel or disable auto-disengage by', + 'running the following command:', + '', + 'spectate set auto-disengage false', + } + dlg.showMessage("Spectate", table.concat(message, '\n')) +end + ----------------------------- -- commandline interface diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index a3e3c86193..67a841ed6a 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -79,7 +79,7 @@ static class AnnouncementSettings { } public: - void reset(color_ostream &out, bool skip_restore = false) { + void reset(color_ostream &out, bool skip_restore) { was_in_settings = false; if (saved) { @@ -218,9 +218,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit < 0) { + if (config.auto_disengage && (plotinfo->follow_unit < 0 || is_squads_open())) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; + plotinfo->follow_unit = -1; on_disable(out); return CR_OK; } From 8b28be61602c7c86baff4960024069253b9f2744 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:29:50 -0800 Subject: [PATCH 083/115] first part of prefer recent units feature --- plugins/spectate.cpp | 80 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 67a841ed6a..c7f07b94ce 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -38,6 +38,17 @@ static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; +static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; +static const float JOB_WEIGHT = 3.0f; +static const float OTHER_WEIGHT = 1.0f; + +static const float RECENT_UNIT_MULTIPLIER = 2.0f; +static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes + +///////////////////////////////////////////////////// +// Configuration + static struct Configuration { bool auto_disengage; bool auto_unpause; @@ -64,6 +75,9 @@ static struct Configuration { } } config; +///////////////////////////////////////////////////// +// AnnouncementSettings + static class AnnouncementSettings { bool was_in_settings = false; // whether we were in the vanilla settings screen last update @@ -133,6 +147,9 @@ static class AnnouncementSettings { } } announcement_settings; +///////////////////////////////////////////////////// +// UnitHistory + static void follow_a_dwarf(color_ostream &out); static class UnitHistory { @@ -204,6 +221,37 @@ struct forward_back_interceptor : df::viewscreen_dwarfmodest { }; IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); +///////////////////////////////////////////////////// +// RecentUnits + +static class RecentUnits { + std::unordered_map units; // unit id -> time seen +public: + void add(int32_t unit_id) { + units[unit_id] = Core::getInstance().getUnpausedMs(); + } + + bool contains(int32_t unit_id) { + return units.contains(unit_id); + } + + void trim() { + uint32_t unpaused_ms = Core::getInstance().getUnpausedMs(); + if (unpaused_ms < RECENT_UNIT_MS) + return; + uint32_t cutoff = unpaused_ms - RECENT_UNIT_MS; + for (auto it = units.begin(); it != units.end();) { + if (it->second < cutoff) + it = units.erase(it); + else + ++it; + } + } +} recent_units; + +///////////////////////////////////////////////////// +// plugin API + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -321,7 +369,6 @@ static command_result do_command(color_ostream &out, vector ¶meters) ///////////////////////////////////////////////////// // cycle logic -// static bool is_in_combat(df::unit *unit) { return false; @@ -370,17 +417,18 @@ static void get_dwarf_buckets(color_ostream &out, static std::default_random_engine rng; -static uint32_t get_next_cycle_unpaused_ms(bool has_active_combat) { +static uint32_t get_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat) { int32_t delay_ms = config.follow_ms; if (has_active_combat) { std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); int32_t delay_ms = distribution(rng); delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } + DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); return Core::getInstance().getUnpausedMs() + delay_ms; } -static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { +static void add_bucket_to_vectors(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { if (bucket.empty()) return; intervals.push_back(units.size() + bucket.size()); @@ -388,6 +436,24 @@ static void add_bucket(const vector &bucket, vector &units units.insert(units.end(), bucket.begin(), bucket.end()); } +static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { + if (bucket.empty()) + return; + if (config.prefer_new_arrivals) { + vector new_bucket, old_bucket; + for (auto unit : bucket) { + if (recent_units.contains(unit->id)) + new_bucket.push_back(unit); + else + old_bucket.push_back(unit); + } + add_bucket_to_vectors(new_bucket, units, intervals, weights, weight * RECENT_UNIT_MULTIPLIER); + add_bucket_to_vectors(old_bucket, units, intervals, weights, weight); + } else { + add_bucket_to_vectors(bucket, units, intervals, weights, weight); + } +} + #define DUMP_BUCKET(name) \ DEBUG(cycle,out).print("bucket: " #name ", size: %zd\n", name.size()); \ if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ @@ -402,11 +468,6 @@ static void add_bucket(const vector &bucket, vector &units DEBUG(cycle,out).print(" %d\n", (int)f); \ } -static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; -static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; -static const float JOB_WEIGHT = 3.0f; -static const float OTHER_WEIGHT = 1.0f; - static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("choosing a unit to follow\n"); @@ -416,7 +477,7 @@ static void follow_a_dwarf(color_ostream &out) { vector other_units; get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); - next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(!active_combat_units.empty()); + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !active_combat_units.empty()); // coalesce the buckets and add weights vector units; @@ -452,7 +513,6 @@ static void follow_a_dwarf(color_ostream &out) { ///////////////////////////////////////////////////// // Lua API -// static void spectate_setSetting(color_ostream &out, string name, int val) { DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); From ae305971aad0eba85af8f5760e1a39d8ab7c0e85 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:31:04 -0800 Subject: [PATCH 084/115] only intercept arrow keys when the map has focus --- plugins/spectate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index c7f07b94ce..94c5adb23f 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -209,7 +209,7 @@ struct forward_back_interceptor : df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { - bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default", Gui::getDFViewscreen()); + bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default"); if (is_at_default_view && input->count(df::interface_key::CUSTOM_LEFT)) unit_history.scan_back(Core::getInstance().getConsole()); else if (is_at_default_view && input->count(df::interface_key::CUSTOM_RIGHT)) From ea4b2a2e7a3fd498b986019ba21d219688328fe3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:39:55 -0800 Subject: [PATCH 085/115] collapse two overlays to one since we don't have a good method to control which one gets rendered first --- docs/plugins/spectate.rst | 43 ++++++++++++++++----------------------- plugins/lua/spectate.lua | 29 +++++++------------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index d0b5c8f04e..3d20b1e665 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -37,7 +37,7 @@ Usage spectate [status] spectate toggle spectate set - spectate overlay enable|disable + spectate overlay enable|disable Examples -------- @@ -59,8 +59,9 @@ Examples ``spectate set follow-seconds 30`` Configure `spectate` to switch targets every 30 seconds when in follow mode. -``spectate overlay follow enable`` - Show informative tooltips that follow each unit on the map. +``spectate overlay enable`` + Show informative tooltips that follow each unit on the map. Note that this + can be enabled independently of `spectate` itself. Settings -------- @@ -68,7 +69,8 @@ Settings ``auto-disengage`` (default: enabled) Toggle automatically disabling the plugin when the player moves the map or opens the squad panel. If this is disabled, you will need to manually - disable the plugin to turn off follow mode. + disable the plugin to turn off follow mode. You can still interact normally + with the DF UI. ``auto-unpause`` (default: disabled) Toggle auto-dismissal of announcements that pause the game, like sieges, @@ -102,47 +104,36 @@ Settings arrived on the map. ``tooltip-follow-job`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. ``tooltip-follow-name`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the tooltip. ``tooltip-follow-stress`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the tooltip. ``tooltip-hover-job`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the hover panel. ``tooltip-hover-name`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the hover panel. ``tooltip-hover-stress`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the hover panel. Overlays -------- -``spectate`` provides two overlays via the `overlay` framework to add -information and functionality to the main map. These overlays can be controlled -via the ``spectate overlay`` command or the ``Overlays`` tab in -`gui/control-panel`. +``spectate.tooltip`` -The information displayed by these overlays can be configured via the -``spectate set`` command or the `gui/spectate` interface. +``spectate`` can show informative tooltips that follow each unit on the map +and/or a popup panel with information when your mouse cursor hovers over a unit. -``spectate.follow`` - Show informative tooltips that follow each unit on the map. You can enable - this overlay by running ``spectate overlay follow enable`` or, - equivalently, ``overlay enable spectate.follow``. - -``spectate.hover`` - Show a popup panel with selected information when your mouse cursor hovers - over a unit. You can enable this overlay by running - ``spectate overlay hover enable`` or, equivalently, - ``overlay enable spectate.hover``. +This overlay is managed via the `overlay` framework. It can be controlled via +the ``spectate overlay`` command or the ``Overlays`` tab in `gui/control-panel`. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index e21016329f..d44957dcb1 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -109,15 +109,9 @@ local function set_setting(key, value) end end -local function set_overlay(name, value) - if not name:startswith('spectate.') then - name = 'spectate.' .. name - end - if name ~= 'spectate.follow' and name ~= 'spectate.hover' then - qerror('unknown overlay: ' .. name) - end +local function set_overlay(value) value = argparse.boolean(value, name) - dfhack.run_command('overlay', value and 'enable' or 'disable', name) + dfhack.run_command('overlay', value and 'enable' or 'disable', 'spectate.tooltip') end function parse_commandline(args) @@ -129,7 +123,7 @@ function parse_commandline(args) elseif command == 'set' then set_setting(args[1], args[2]) elseif command == 'overlay' then - set_overlay(args[1], args[2]) + set_overlay(args[1]) else return false end @@ -140,25 +134,16 @@ end ----------------------------- -- overlays -FollowOverlay = defclass(FollowOverlay, overlay.OverlayWidget) -FollowOverlay.ATTRS{ - desc='Adds info tooltips that follow units on the map.', - default_pos={x=1,y=1}, - fullscreen=true, - viewscreens='dwarfmode/Default', -} - -HoverOverlay = defclass(HoverOverlay, overlay.OverlayWidget) -HoverOverlay.ATTRS{ - desc='Shows info popup when hovering the mouse over units on the map.', +TooltipOverlay = defclass(TooltipOverlay, overlay.OverlayWidget) +TooltipOverlay.ATTRS{ + desc='Adds info tooltips that follow units or appear when you hover the mouse.', default_pos={x=1,y=1}, fullscreen=true, viewscreens='dwarfmode/Default', } OVERLAY_WIDGETS = { - follow=FollowOverlay, - hover=HoverOverlay, + tooltip=TooltipOverlay, } return _ENV From 77bbe8c87815e218d5f9bdc2f175572c9c27bc72 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:59:30 -0800 Subject: [PATCH 086/115] implement recent units preference --- plugins/spectate.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 94c5adb23f..abe0fffaf1 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -4,6 +4,7 @@ #include "PluginManager.h" #include "VTableInterpose.h" +#include "modules/EventManager.h" #include "modules/Gui.h" #include "modules/Units.h" #include "modules/World.h" @@ -32,6 +33,7 @@ REQUIRE_GLOBAL(world); namespace DFHack { DBG_DECLARE(spectate, control, DebugCategory::LINFO); DBG_DECLARE(spectate, cycle, DebugCategory::LINFO); + DBG_DECLARE(spectate, event, DebugCategory::LINFO); } static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle @@ -43,6 +45,7 @@ static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; static const float JOB_WEIGHT = 3.0f; static const float OTHER_WEIGHT = 1.0f; +static const int32_t RECENT_UNITS_SCAN_CYCLE = 51; static const float RECENT_UNIT_MULTIPLIER = 2.0f; static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes @@ -226,7 +229,12 @@ IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); static class RecentUnits { std::unordered_map units; // unit id -> time seen + public: + void reset() { + units.clear(); + } + void add(int32_t unit_id) { units[unit_id] = Core::getInstance().getUnpausedMs(); } @@ -249,6 +257,14 @@ static class RecentUnits { } } recent_units; +static void on_new_active_unit(color_ostream& out, void* data) { + int32_t unit_id = reinterpret_cast(data); + DEBUG(event,out).print("unit %d has arrived on map\n", unit_id); + recent_units.add(unit_id); +} + +static EventManager::EventHandler new_unit_handler(plugin_self, on_new_active_unit, RECENT_UNITS_SCAN_CYCLE); + ///////////////////////////////////////////////////// // plugin API @@ -267,6 +283,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) + if ((!config.auto_disengage && plotinfo->follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { + recent_units.trim(); follow_a_dwarf(out); + } return CR_OK; } From a8ebea3c96af3a4909b6a34e817cb01cedb6cf93 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 04:43:10 -0800 Subject: [PATCH 087/115] implement conflict detection and cinematic camera --- plugins/spectate.cpp | 67 ++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index abe0fffaf1..b27db51460 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -13,6 +13,7 @@ #include "df/d_init.h" #include "df/plotinfost.h" #include "df/unit.h" +#include "df/activity_entry.h" #include "df/viewscreen_dwarfmodest.h" #include "df/world.h" @@ -40,14 +41,22 @@ static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; -static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; -static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; -static const float JOB_WEIGHT = 3.0f; +static const float CITIZEN_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float OTHER_COMBAT_PREFERRED_WEIGHT = 10.0f; +static const float JOB_WEIGHT = 5.0f; static const float OTHER_WEIGHT = 1.0f; static const int32_t RECENT_UNITS_SCAN_CYCLE = 51; -static const float RECENT_UNIT_MULTIPLIER = 2.0f; -static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes +static const float RECENT_UNIT_MULTIPLIER = 2.0f; // weight multiplier for recent units +static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes + +// jobs that get "other" weight instad of "job" weight +static const std::unordered_set boring_jobs = { + df::job_type::Eat, + df::job_type::Drink, + df::job_type::Sleep, +}; + ///////////////////////////////////////////////////// // Configuration @@ -392,25 +401,22 @@ static command_result do_command(color_ostream &out, vector ¶meters) // cycle logic static bool is_in_combat(df::unit *unit) { - return false; -} - -static bool is_fleeing(df::unit *unit) { + if (Units::isCrazed(unit) || unit->mood == df::mood_type::Berserk) + return true; + for (auto activity_id : unit->activities) { + auto activity = df::activity_entry::find(activity_id); + if (activity && activity->type == df::activity_entry_type::Conflict) + return true; + } return false; } static void get_dwarf_buckets(color_ostream &out, - vector &active_combat_units, - vector &passive_combat_units, + vector &citizen_combat_units, + vector &other_combat_units, vector &job_units, vector &other_units) { - static const std::unordered_set boring_jobs = { - df::job_type::Eat, - df::job_type::Drink, - df::job_type::Sleep, - }; - for (auto unit : world->units.active) { if (Units::isDead(unit) || !Units::isActive(unit) || unit->flags1.bits.caged || unit->flags1.bits.chained || Units::isHidden(unit)) continue; @@ -424,10 +430,11 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - if (is_fleeing(unit)) - passive_combat_units.push_back(unit); + INFO(cycle).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) + citizen_combat_units.push_back(unit); else - active_combat_units.push_back(unit); + other_combat_units.push_back(unit); } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { job_units.push_back(unit); } else { @@ -440,9 +447,9 @@ static std::default_random_engine rng; static uint32_t get_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat) { int32_t delay_ms = config.follow_ms; - if (has_active_combat) { + if (config.cinematic_action && has_active_combat) { std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); - int32_t delay_ms = distribution(rng); + delay_ms = distribution(rng); delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); @@ -492,21 +499,21 @@ static void add_bucket(const vector &bucket, vector &units static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("choosing a unit to follow\n"); - vector active_combat_units; - vector passive_combat_units; + vector citizen_combat_units; + vector other_combat_units; vector job_units; vector other_units; - get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); + get_dwarf_buckets(out, citizen_combat_units, other_combat_units, job_units, other_units); - next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !active_combat_units.empty()); + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); // coalesce the buckets and add weights vector units; vector intervals; vector weights; intervals.push_back(0); - add_bucket(active_combat_units, units, intervals, weights, config.prefer_conflict ? ACTIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); - add_bucket(passive_combat_units, units, intervals, weights, config.prefer_conflict ? PASSIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(citizen_combat_units, units, intervals, weights, config.prefer_conflict ? CITIZEN_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(other_combat_units, units, intervals, weights, config.prefer_conflict ? OTHER_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); @@ -520,8 +527,8 @@ static void follow_a_dwarf(color_ostream &out) { df::unit *unit = units[unit_idx]; if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { - DUMP_BUCKET(active_combat_units); - DUMP_BUCKET(passive_combat_units); + DUMP_BUCKET(citizen_combat_units); + DUMP_BUCKET(other_combat_units); DUMP_BUCKET(job_units); DUMP_BUCKET(other_units); DUMP_FLOAT_VECTOR(intervals); From ab263e4755bd39aafa8ed5795860f689dd7f2e0f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 04:47:47 -0800 Subject: [PATCH 088/115] clarify function of include-animals setting --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 3d20b1e665..b4d913d9e0 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -84,7 +84,7 @@ Settings include time that the game is paused. ``include-animals`` (default: disabled) - Toggle whether to sometimes follow fort animals. + Toggle whether to sometimes follow fort animals and wildlife. ``include-hostiles`` (default: disabled) Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, From fa8550427d3516bffe2f646c204938022db64326 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 12:30:24 -0800 Subject: [PATCH 089/115] protect chained units from autobutcher --- docs/changelog.txt | 1 + docs/plugins/autobutcher.rst | 1 + plugins/autobutcher.cpp | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 36315052f7..3e5fa6ad90 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort +- `autobutcher`: treat animals on restraints as unavailable for slaughter ## Documentation - `stonesense-art-guide`: new guide for making sprite art for Stonesense diff --git a/docs/plugins/autobutcher.rst b/docs/plugins/autobutcher.rst index b429f57542..f7bea4d6d9 100644 --- a/docs/plugins/autobutcher.rst +++ b/docs/plugins/autobutcher.rst @@ -15,6 +15,7 @@ Units are protected from being automatically butchered if they are: * Untamed * Named or nicknamed * Caged, if and only if the cage is in a zone (to protect zoos) +* On a restraint * Trained for war or hunting * Females who are pregnant or brooding a clutch of fertile eggs diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 9e93018cc9..8fde1cc347 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -666,9 +666,10 @@ static bool isProtectedUnit(df::unit *unit) { || Units::isHunter(unit) // ignore hunting dogs etc || Units::isMarkedForWarTraining(unit) // ignore units marked for any kind of training || Units::isMarkedForHuntTraining(unit) - // ignore creatures in built cages which are defined as rooms to leave zoos alone + || unit->flags1.bits.chained // ignore chained animals + // ignore creatures in built cages which are members of zones to leave zoos alone // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) || (unit->pregnancy_timer != 0) // do not butcher pregnant animals (which includes brooding female egglayers) || Units::isAvailableForAdoption(unit) || unit->name.has_name From eed96caeeea7f8ce1729c41ab7c3b4f89d1199f4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 07:34:55 -0800 Subject: [PATCH 090/115] update to great reorg structures --- library/LuaApi.cpp | 5 +- library/RemoteTools.cpp | 2 +- .../df/custom/creature_handler.methods.inc | 2 +- library/include/modules/Buildings.h | 5 +- library/include/modules/Items.h | 2 +- library/include/modules/Job.h | 2 +- library/include/modules/MapCache.h | 2 +- library/modules/Buildings.cpp | 119 +++++++++--------- library/modules/Gui.cpp | 5 +- library/modules/Items.cpp | 21 ++-- library/modules/Job.cpp | 10 +- library/modules/Maps.cpp | 16 +-- library/modules/Materials.cpp | 18 +-- library/modules/Military.cpp | 35 +++--- library/modules/Units.cpp | 13 +- plugins/3dveins.cpp | 2 +- plugins/blueprint.cpp | 2 +- plugins/buildingplan/buildingplan.cpp | 4 +- plugins/buildingplan/buildingplan_cycle.cpp | 2 +- plugins/cursecheck.cpp | 1 - plugins/devel/eventExample.cpp | 1 + plugins/devel/stripcaged.cpp | 1 - plugins/dwarfmonitor.cpp | 3 +- plugins/embark-assistant/embark-assistant.cpp | 1 - plugins/embark-assistant/finder_ui.cpp | 2 - plugins/embark-assistant/matcher.cpp | 1 - plugins/embark-assistant/overlay.cpp | 1 - plugins/embark-assistant/survey.cpp | 2 - plugins/forceequip.cpp | 2 +- plugins/generated-creature-renamer.cpp | 1 - plugins/infinite-sky.cpp | 4 +- plugins/logistics.cpp | 1 + plugins/nestboxes.cpp | 1 + plugins/orders.cpp | 10 +- plugins/probe.cpp | 2 +- plugins/prospector.cpp | 14 ++- .../remotefortressreader/building_reader.cpp | 4 +- plugins/remotefortressreader/item_reader.cpp | 19 ++- .../remotefortressreader.cpp | 70 +++++------ plugins/stockpiles/OrganicMatLookup.cpp | 6 +- plugins/stockpiles/StockpileSerializer.cpp | 22 ++-- plugins/strangemood.cpp | 20 +-- plugins/tailor.cpp | 4 +- plugins/timestream.cpp | 5 +- 44 files changed, 236 insertions(+), 229 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 6e251dbfcf..637df32211 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -99,6 +99,7 @@ distribution. #include "df/report_zoom_type.h" #include "df/specific_ref.h" #include "df/specific_ref_type.h" +#include "df/squad_use_flags.h" #include "df/squad.h" #include "df/unit.h" #include "df/unit_misc_trait.h" @@ -2443,7 +2444,7 @@ static int items_moveToBuilding(lua_State *state) static int items_moveToInventory(lua_State *state) { auto item = Lua::CheckDFObject(state, 1); auto unit = Lua::CheckDFObject(state, 2); - auto use_mode = (df::unit_inventory_item::T_mode)luaL_optint(state, 3, 0); + auto use_mode = (df::inv_item_role_type)luaL_optint(state, 3, 0); int body_part = luaL_optint(state, 4, -1); lua_pushboolean(state, Items::moveToInventory(item, unit, use_mode, body_part)); return 1; @@ -2816,7 +2817,7 @@ int buildings_setSize(lua_State *state) lua_pushinteger(state, size.x); lua_pushinteger(state, size.y); lua_pushinteger(state, area); - lua_pushinteger(state, Buildings::countExtentTiles(&bld->room, area)); + lua_pushinteger(state, Buildings::countExtentTiles(bld, area)); return 5; } else diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index fbace1ae2e..3df55b847a 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -458,7 +458,7 @@ static command_result ListEnums(color_ostream &stream, BITFIELD(cie_add_tag_mask1); BITFIELD(cie_add_tag_mask2); - describe_bitfield(out->mutable_death_info_flags()); + describe_bitfield(out->mutable_death_info_flags()); ENUM(profession); diff --git a/library/include/df/custom/creature_handler.methods.inc b/library/include/df/custom/creature_handler.methods.inc index 811128d058..14639bd889 100644 --- a/library/include/df/custom/creature_handler.methods.inc +++ b/library/include/df/custom/creature_handler.methods.inc @@ -1 +1 @@ -friend struct world_raws; +friend struct world; diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index ee7fcdf698..80fe443396 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -44,7 +44,6 @@ namespace df { struct building; struct building_cagest; struct building_civzonest; - struct building_extents; struct building_stockpilest; struct item; struct job_item; @@ -112,7 +111,7 @@ DFHACK_EXPORT bool getCorrectSize(df::coord2d &size, df::coord2d ¢er, * Checks if the tiles are free to be built upon. */ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, - df::building_extents *ext = NULL, + df::building *bld, bool create_ext = false, bool allow_occupied = false, bool allow_wall = false, @@ -121,7 +120,7 @@ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, /** * Returns the number of tiles included by the extent, or defval. */ -DFHACK_EXPORT int countExtentTiles(df::building_extents *ext, int defval = -1); +DFHACK_EXPORT int countExtentTiles(df::building *bld, int defval = -1); /** * Checks if the building contains the specified tile. If the building has diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 79aed92c36..6d068e8786 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -157,7 +157,7 @@ DFHACK_EXPORT bool moveToContainer(df::item *item, df::item *container); DFHACK_EXPORT bool moveToBuilding(df::item *item, df::building_actual *building, df::building_item_role_type use_mode = df::building_item_role_type::TEMP, bool force_in_building = false); DFHACK_EXPORT bool moveToInventory(df::item *item, df::unit *unit, - df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Hauled, int body_part = -1); + df::inv_item_role_type mode = df::inv_item_role_type::Hauled, int body_part = -1); /// Remove item from jobs and inventories, hide and forbid. /// Unless no_uncat, item is marked for garbage collection. diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index f0d3e89b97..a0754c4781 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -105,7 +105,7 @@ namespace DFHack DFHACK_EXPORT bool listNewlyCreated(std::vector *pvec, int *id_var); DFHACK_EXPORT bool attachJobItem(df::job *job, df::item *item, - df::job_item_ref::T_role role, + df::job_role_type role, int filter_idx = -1, int insert_idx = -1); DFHACK_EXPORT bool isSuitableItem(const df::job_item *item, df::item_type itype, int isubtype); diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h index 04fddd38bd..0d63627389 100644 --- a/library/include/modules/MapCache.h +++ b/library/include/modules/MapCache.h @@ -84,7 +84,7 @@ class BlockInfo }; static GroundType getGroundType(int material); - typedef df::block_square_event_mineralst::T_flags DFVeinFlags; + typedef df::mineral_event_flag DFVeinFlags; t_veintype veintype; t_blockmaterials veinmats; diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index a560a6fc99..672464ceba 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -48,18 +48,21 @@ distribution. #include "df/building_coffinst.h" #include "df/building_def.h" #include "df/building_design.h" +#include "df/building_extents_type.h" #include "df/building_floodgatest.h" #include "df/building_furnacest.h" #include "df/building_grate_floorst.h" #include "df/building_grate_wallst.h" #include "df/building_rollersst.h" #include "df/building_screw_pumpst.h" +#include "df/building_squad_infost.h" #include "df/building_stockpilest.h" #include "df/building_trapst.h" #include "df/building_water_wheelst.h" #include "df/building_weaponst.h" #include "df/building_wellst.h" #include "df/building_workshopst.h" +#include "df/buildingitemst.h" #include "df/buildings_other_id.h" #include "df/d_init.h" #include "df/dfhack_room_quality_level.h" @@ -77,6 +80,7 @@ distribution. #include "df/plotinfost.h" #include "df/punishment.h" #include "df/squad.h" +#include "df/squad_barracks_infost.h" #include "df/unit.h" #include "df/unit_relationship_type.h" #include "df/world.h" @@ -112,15 +116,15 @@ struct CoordHash { static unordered_map locationToBuilding; -static df::building_extents_type *getExtentTile(df::building_extents &extent, df::coord2d tile) +static df::building_extents_type *getExtentTile(const df::building::T_room &room, df::coord2d tile) { - if (!extent.extents) + if (!room.extents) return NULL; - int dx = tile.x - extent.x; - int dy = tile.y - extent.y; - if (dx < 0 || dy < 0 || dx >= extent.width || dy >= extent.height) + int dx = tile.x - room.x; + int dy = tile.y - room.y; + if (dx < 0 || dy < 0 || dx >= room.width || dy >= room.height) return NULL; - return &extent.extents[dx + dy*extent.width]; + return &room.extents[dx + dy*room.width]; } /* @@ -162,14 +166,14 @@ void buildings_onUpdate(color_ostream &out) for (size_t i = 0; i < job->items.size(); i++) { df::job_item_ref *iref = job->items[i]; - if (iref->role != df::job_item_ref::Reagent) + if (iref->role != df::job_role_type::Reagent) continue; df::job_item *item = vector_get(job->job_items.elements, iref->job_item_idx); if (!item) continue; // Convert Reagent to Hauled, while decrementing quantity item->quantity = std::max(0, item->quantity-1); - iref->role = df::job_item_ref::Hauled; + iref->role = df::job_role_type::Hauled; iref->job_item_idx = -1; } } @@ -219,7 +223,7 @@ static void add_building_to_all_zones(df::building* bld) static void add_zone_to_all_buildings(df::building* zone_as_building) { - if (zone_as_building->getType() != building_type::Civzone) + if (zone_as_building->getType() != df::building_type::Civzone) return; auto zone = strict_virtual_cast(zone_as_building); @@ -259,7 +263,7 @@ static void remove_building_from_zone(df::building* bld, df::building_civzonest* static void remove_zone_from_all_buildings(df::building* zone_as_building) { - if (zone_as_building->getType() != building_type::Civzone) + if (zone_as_building->getType() != df::building_type::Civzone) return; auto zone = strict_virtual_cast(zone_as_building); @@ -478,19 +482,20 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in // Type specific init switch (type) { - case building_type::Well: + using namespace df::enums::building_type; + case Well: { if (VIRTUAL_CAST_VAR(obj, df::building_wellst, bld)) obj->bucket_z = bld->z; break; } - case building_type::Workshop: + case Workshop: { if (VIRTUAL_CAST_VAR(obj, df::building_workshopst, bld)) obj->profile.max_general_orders = 5; break; } - case building_type::Furnace: + case Furnace: { if (VIRTUAL_CAST_VAR(obj, df::building_furnacest, bld)) { @@ -507,7 +512,7 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in break; } */ - case building_type::Trap: + case Trap: { if (VIRTUAL_CAST_VAR(obj, df::building_trapst, bld)) { @@ -516,46 +521,46 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in } break; } - case building_type::Floodgate: + case Floodgate: { if (VIRTUAL_CAST_VAR(obj, df::building_floodgatest, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::GrateWall: + case GrateWall: { if (VIRTUAL_CAST_VAR(obj, df::building_grate_wallst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::GrateFloor: + case GrateFloor: { if (VIRTUAL_CAST_VAR(obj, df::building_grate_floorst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::BarsVertical: + case BarsVertical: { if (VIRTUAL_CAST_VAR(obj, df::building_bars_verticalst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::BarsFloor: + case BarsFloor: { if (VIRTUAL_CAST_VAR(obj, df::building_bars_floorst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::Bridge: + case Bridge: { if (VIRTUAL_CAST_VAR(obj, df::building_bridgest, bld)) - obj->gate_flags.bits.closed = false; + obj->gate_flags.bits.raised = false; break; } - case building_type::Weapon: + case Weapon: { if (VIRTUAL_CAST_VAR(obj, df::building_weaponst, bld)) - obj->gate_flags.bits.closed = false; + obj->gate_flags.bits.retracted = false; break; } default: @@ -711,26 +716,29 @@ bool Buildings::getCorrectSize(df::coord2d &size, df::coord2d ¢er, } } -static void init_extents(df::building_extents *ext, const df::coord &pos, +static void init_extents(df::building::T_room &room, const df::coord &pos, const df::coord2d &size) { - ext->extents = new df::building_extents_type[size.x*size.y]; - ext->x = pos.x; - ext->y = pos.y; - ext->width = size.x; - ext->height = size.y; + room.extents = new df::building_extents_type[size.x*size.y]; + room.x = pos.x; + room.y = pos.y; + room.width = size.x; + room.height = size.y; - memset(ext->extents, 1, size.x*size.y); + memset(room.extents, 1, size.x*size.y); } bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, - df::building_extents *ext, + df::building *bld, bool create_ext, bool allow_occupied, bool allow_wall, bool allow_flow) { + CHECK_NULL_POINTER(bld); + bool found_any = false; + auto & room = bld->room; for (int dx = 0; dx < size.x; dx++) { @@ -740,9 +748,9 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, df::building_extents_type *etile = NULL; // Exclude using extents - if (ext && ext->extents) + if (room.extents) { - etile = getExtentTile(*ext, tile); + etile = getExtentTile(room, tile); if (!etile || !*etile) continue; } @@ -776,13 +784,13 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, found_any = true; else { - if (!ext || !create_ext) + if (!create_ext) return false; - if (!ext->extents) + if (!room.extents) { - init_extents(ext, pos, size); - etile = getExtentTile(*ext, tile); + init_extents(room, pos, size); + etile = getExtentTile(room, tile); } if (!etile) @@ -813,10 +821,10 @@ static bool checkBuildingTiles(df::building *bld, bool can_change, if (force_extents && !bld->room.extents) { // populate the room structure if it's not set already - init_extents(&bld->room, psize.first, psize.second); + init_extents(bld->room, psize.first, psize.second); } - return Buildings::checkFreeTiles(psize.first, psize.second, &bld->room, + return Buildings::checkFreeTiles(psize.first, psize.second, bld, can_change && bld->isExtentShaped(), !bld->isSettingOccupancy(), bld->getType() == @@ -824,14 +832,18 @@ static bool checkBuildingTiles(df::building *bld, bool can_change, !bld->isActual()); } -int Buildings::countExtentTiles(df::building_extents *ext, int defval) +int Buildings::countExtentTiles(df::building *bld, int defval) { - if (!ext || !ext->extents) + CHECK_NULL_POINTER(bld); + + auto & room = bld->room; + + if (!room.extents) return defval; int cnt = 0; - for (int i = 0; i < ext->width * ext->height; i++) - if (ext->extents[i]) + for (int i = 0; i < room.width * room.height; i++) + if (room.extents[i]) cnt++; return cnt; } @@ -881,7 +893,7 @@ static int computeMaterialAmount(df::building *bld) int cnt = size.x * size.y; if (bld->room.extents && bld->isExtentShaped()) - cnt = Buildings::countExtentTiles(&bld->room, cnt); + cnt = Buildings::countExtentTiles(bld, cnt); return cnt/4 + 1; } @@ -911,10 +923,9 @@ bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) auto type = bld->getType(); - using namespace df::enums::building_type; - switch (type) { + using namespace df::enums::building_type; case WaterWheel: { auto obj = (df::building_water_wheelst*)bld; @@ -953,7 +964,7 @@ bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) bool ok = checkBuildingTiles(bld, true); - if (type != Construction) + if (type != df::building_type::Construction) bld->setMaterialAmount(computeMaterialAmount(bld)); return ok; @@ -1158,7 +1169,7 @@ bool Buildings::constructWithItems(df::building *bld, std::vector ite for (size_t i = 0; i < items.size(); i++) { - Job::attachJobItem(job, items[i], df::job_item_ref::Hauled); + Job::attachJobItem(job, items[i], df::job_role_type::Hauled); if (items[i]->getType() == item_type::BOULDER) rough = true; @@ -1224,15 +1235,9 @@ bool Buildings::constructWithFilters(df::building *bld, std::vectorsquad_room_info) + for (auto room_info : zone->squad_room_info) { - int32_t squad_id = room_info->squad_id; - - df::squad* squad = df::squad::find(squad_id); - - //if this is null, something has gone just *terribly* wrong - if (squad) - { + if (auto squad = df::squad::find(room_info->squad_id)) { for (int i=(int)squad->rooms.size() - 1; i >= 0; i--) { if (squad->rooms[i]->building_id == zone->id) @@ -1406,7 +1411,7 @@ bool Buildings::deconstruct(df::building *bld) for (int i = ui_look_list->size()-1; i >= 0; --i) { auto item = (*ui_look_list)[i]; - if (item->type == df::lookinfost::Building && + if (item->type == df::look_info_type::Building && item->data.building.bld_id == id) { vector_erase_at(*ui_look_list, i); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 951482d23f..853d42f3b8 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -84,7 +84,6 @@ distribution. #include "df/route_stockpile_link.h" #include "df/soundst.h" #include "df/stop_depart_condition.h" -#include "df/ui_unit_view_mode.h" #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/viewscreen_choose_start_sitest.h" @@ -1877,7 +1876,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce new_report->text = message; new_report->color = color; new_report->bright = bright; - new_report->flags.whole = adv_unconscious ? df::report::T_flags::mask_unconscious : 0x0; + new_report->flags.whole = adv_unconscious ? df::announcement_flag::mask_unconscious : 0x0; new_report->pos = pos; new_report->id = world->status.next_report_id++; new_report->year = *df::global::cur_year; @@ -2197,7 +2196,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) new_report->text = message; new_report->color = info.color; new_report->bright = info.bright; - new_report->flags.whole = adv_unconscious ? df::report::T_flags::mask_unconscious : 0x0; + new_report->flags.whole = adv_unconscious ? df::announcement_flag::mask_unconscious : 0x0; new_report->zoom_type = info.zoom_type; new_report->pos = info.pos; new_report->zoom_type2 = info.zoom_type2; diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 47d68b1654..a99c2bb761 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -44,6 +44,7 @@ distribution. #include "df/building.h" #include "df/building_actual.h" #include "df/building_tradedepotst.h" +#include "df/buildingitemst.h" #include "df/builtin_mats.h" #include "df/caravan_state.h" #include "df/creature_raw.h" @@ -765,13 +766,13 @@ static bool removeItemOnGround(df::item *item) } static void resetUnitInvFlags(df::unit *unit, df::unit_inventory_item *inv_item) { - if (inv_item->mode == df::unit_inventory_item::Worn || - inv_item->mode == df::unit_inventory_item::WrappedAround) + if (inv_item->mode == df::inv_item_role_type::Worn || + inv_item->mode == df::inv_item_role_type::WrappedAround) { unit->flags2.bits.calculated_inventory = false; unit->flags2.bits.calculated_insulation = false; } - else if (inv_item->mode == df::unit_inventory_item::StuckIn) + else if (inv_item->mode == df::inv_item_role_type::StuckIn) unit->flags3.bits.stuck_weapon_computed = false; } @@ -815,7 +816,7 @@ static bool detachItem(df::item *item) } if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) - return linked_list_remove(&world->proj_list, ref->projectile_id) + return linked_list_remove(&world->projectiles.all, ref->projectile_id) && removeRef(item->general_refs, general_ref_type::PROJECTILE, ref->getID()); if (item->flags.bits.on_ground) { @@ -960,7 +961,7 @@ bool DFHack::Items::moveToBuilding(df::item *item, df::building_actual *building ref->building_id = building->id; item->general_refs.push_back(ref); - auto con = new df::building_actual::T_contained_items; + auto con = new df::buildingitemst; con->item = item; con->use_mode = use_mode; building->contained_items.push_back(con); @@ -969,7 +970,7 @@ bool DFHack::Items::moveToBuilding(df::item *item, df::building_actual *building } bool DFHack::Items::moveToInventory(df::item *item, df::unit *unit, - df::unit_inventory_item::T_mode mode, int body_part) + df::inv_item_role_type mode, int body_part) { CHECK_NULL_POINTER(item); CHECK_NULL_POINTER(unit); @@ -1063,7 +1064,7 @@ df::proj_itemst *Items::makeProjectile(df::item *item) ref->projectile_id = proj->id; item->general_refs.push_back(ref); - linked_list_append(&world->proj_list, proj->link); + linked_list_append(&world->projectiles.all, proj->link); return proj; } @@ -1799,8 +1800,8 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t bool Items::checkMandates(df::item *item) { CHECK_NULL_POINTER(item); - for (auto mandate : world->mandates) { - if ((mandate->mode == df::mandate::Export) && + for (auto mandate : world->mandates.all) { + if ((mandate->mode == df::mandate_type::Export) && (item->getType() == mandate->item_type) && (mandate->item_subtype == -1 || item->getSubtype() == mandate->item_subtype) && (mandate->mat_type == -1 || item->getMaterial() == mandate->mat_type) && @@ -1881,7 +1882,7 @@ bool Items::markForTrade(df::item *item, df::building_tradedepotst *depot) { job->pos = df::coord(depot->centerx, depot->centery, depot->z); // job <-> item link - if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) { + if (!Job::attachJobItem(job, item, df::job_role_type::Hauled)) { delete job; delete href; return false; diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 86809ba5ba..96344ddd6f 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -46,6 +46,8 @@ distribution. #include "df/job.h" #include "df/job_item.h" #include "df/job_list_link.h" +#include "df/job_postingst.h" +#include "df/job_restrictionst.h" #include "df/plotinfost.h" #include "df/specific_ref.h" #include "df/unit.h" @@ -323,11 +325,11 @@ void DFHack::Job::setJobCooldown(df::building *workshop, df::unit *worker, int c if (cooldown <= 0) return; - int idx = linear_index(workshop->job_claim_suppress, &df::building::T_job_claim_suppress::unit, worker); + int idx = linear_index(workshop->job_claim_suppress, &df::job_restrictionst::unit, worker); if (idx < 0) { - auto obj = new df::building::T_job_claim_suppress; + auto obj = new df::job_restrictionst; obj->unit = worker; obj->timer = cooldown; workshop->job_claim_suppress.push_back(obj); @@ -575,7 +577,7 @@ bool DFHack::Job::listNewlyCreated(std::vector *pvec, int *id_var) } bool DFHack::Job::attachJobItem(df::job *job, df::item *item, - df::job_item_ref::T_role role, + df::job_role_type role, int filter_idx, int insert_idx) { CHECK_NULL_POINTER(job); @@ -585,7 +587,7 @@ bool DFHack::Job::attachJobItem(df::job *job, df::item *item, * Functionality 100% reverse-engineered from DF code. */ - if (role != df::job_item_ref::TargetContainer) + if (role != df::job_role_type::TargetContainer) { if (item->flags.bits.in_job) return false; diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index c207360ac4..54fb43c810 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -47,6 +47,8 @@ distribution. #include "df/builtin_mats.h" #include "df/burrow.h" #include "df/feature_init.h" +#include "df/feature_map_shellst.h" +#include "df/feature_mapst.h" #include "df/flow_info.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -515,7 +517,7 @@ df::feature_init *Maps::getLocalInitFeature(df::coord2d rgn_pos, int32_t index) df::coord2d sub = rgn_pos & 15; - vector &features = fptr->feature_init[sub.x][sub.y]; + vector &features = fptr->feature_init[sub.x][sub.y]; return vector_get(features, index); } @@ -947,13 +949,13 @@ static int16_t basic_wet_dry_effect(int16_t region_y, int16_t rain) auto dimy = world->world_data->world_height; auto pole = world->world_data->flip_latitude; - if (dimy > 65 && pole != df::world_data::T_flip_latitude::None) + if (dimy > 65 && pole != df::pole_type::None) { // Medium and Large worlds with at least one pole auto latitude = region_y; - if (pole == df::world_data::T_flip_latitude::South) + if (pole == df::pole_type::South) latitude = dimy - region_y - 1; - else if (pole == df::world_data::T_flip_latitude::Both) + else if (pole == df::pole_type::Both) { if (region_y < dimy / 2) latitude = region_y * 2; @@ -993,7 +995,7 @@ df::enums::biome_type::biome_type Maps::getBiomeTypeWithRef(int16_t region_x, in bool tropical; // Determine tropicality - if (pole == df::world_data::T_flip_latitude::None) + if (pole == df::pole_type::None) { potential_tropical = region->temperature > 74; tropical = region->temperature > 84; @@ -1002,9 +1004,9 @@ df::enums::biome_type::biome_type Maps::getBiomeTypeWithRef(int16_t region_x, in { auto latitude = region_ref_y; // DF just uses region_y, but embark assistant needs region_ref_y - if (pole == df::world_data::T_flip_latitude::South) + if (pole == df::pole_type::South) latitude = dimy - region_ref_y - 1; - else if (pole == df::world_data::T_flip_latitude::Both) + else if (pole == df::pole_type::Both) { if (region_ref_y < dimy / 2) latitude = region_ref_y * 2; diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 341ddeabc4..18dd73b09a 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -99,7 +99,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; } - df::world_raws &raws = world->raws; + auto &raws = world->raws; if (size_t(type) >= sizeof(raws.mat_table.builtin)/sizeof(void*)) return false; @@ -216,7 +216,7 @@ bool MaterialInfo::findBuiltin(const std::string &token) return true; } - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (int i = 0; i < NUM_BUILTIN; i++) { auto obj = raws.mat_table.builtin[i]; @@ -236,10 +236,10 @@ bool MaterialInfo::findInorganic(const std::string &token) return true; } - df::world_raws &raws = world->raws; - for (size_t i = 0; i < raws.inorganics.size(); i++) + auto &raws = world->raws; + for (size_t i = 0; i < raws.inorganics.all.size(); i++) { - df::inorganic_raw *p = raws.inorganics[i]; + df::inorganic_raw *p = raws.inorganics.all[i]; if (p->id == token) return decode(0, i); } @@ -250,7 +250,7 @@ bool MaterialInfo::findPlant(const std::string &token, const std::string &subtok { if (token.empty()) return decode(-1); - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (size_t i = 0; i < raws.plants.all.size(); i++) { df::plant_raw *p = raws.plants.all[i]; @@ -274,7 +274,7 @@ bool MaterialInfo::findCreature(const std::string &token, const std::string &sub { if (token.empty() || subtoken.empty()) return decode(-1); - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (size_t i = 0; i < raws.creatures.all.size(); i++) { df::creature_raw *p = raws.creatures.all[i]; @@ -645,12 +645,12 @@ bool t_matglossInorganic::isGem() bool Materials::CopyInorganicMaterials (std::vector & inorganic) { - size_t size = world->raws.inorganics.size(); + size_t size = world->raws.inorganics.all.size(); inorganic.clear(); inorganic.reserve (size); for (size_t i = 0; i < size;i++) { - df::inorganic_raw *orig = world->raws.inorganics[i]; + df::inorganic_raw *orig = world->raws.inorganics.all[i]; t_matglossInorganic mat; mat.id = orig->id; mat.name = orig->material.stone_name; diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 3c5708d75e..cbb4295562 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -7,6 +7,7 @@ #include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" +#include "df/building_squad_infost.h" #include "df/histfig_entity_link_former_positionst.h" #include "df/histfig_entity_link_former_squadst.h" #include "df/histfig_entity_link_positionst.h" @@ -16,11 +17,16 @@ #include "df/entity_position.h" #include "df/entity_position_assignment.h" #include "df/plotinfost.h" +#include "df/military_routinest.h" #include "df/squad.h" -#include "df/squad_position.h" -#include "df/squad_schedule_order.h" +#include "df/squad_barracks_infost.h" +#include "df/squad_month_positionst.h" #include "df/squad_order.h" #include "df/squad_order_trainst.h" +#include "df/squad_position.h" +#include "df/squad_routine_schedulest.h" +#include "df/squad_schedule_entry.h" +#include "df/squad_schedule_order.h" #include "df/unit.h" #include "df/world.h" @@ -96,7 +102,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) result->id = *df::global::squad_next_id; result->uniform_priority = result->id + 1; //no idea why, but seems to hold result->supplies.carry_food = 2; - result->supplies.carry_water = df::squad::T_supplies::Water; + result->supplies.carry_water = df::squad_water_level_type::Water; result->entity_id = df::global::plotinfo->group_id; result->leader_position = corresponding_position->id; result->leader_assignment = found_assignment->id; @@ -116,7 +122,8 @@ df::squad* Military::makeSquad(int32_t assignment_id) for (const auto& routine : routines) { - df::squad_schedule_entry* asched = (df::squad_schedule_entry*)malloc(sizeof(df::squad_schedule_entry) * 12); + df::squad_routine_schedulest* schedule = new df::squad_routine_schedulest[12]; + auto &asched = schedule->month; for(int kk=0; kk < 12; kk++) { @@ -124,14 +131,14 @@ df::squad* Military::makeSquad(int32_t assignment_id) for(int jj=0; jj < squad_size; jj++) { - int32_t* order_assignments = new int32_t(); - *order_assignments = -1; + df::squad_month_positionst* order_assignments = new df::squad_month_positionst(); + order_assignments->assigned_order_idx = -1; asched[kk].order_assignments.push_back(order_assignments); } } - auto insert_training_order = [asched, squad_size](int month) + auto insert_training_order = [&](int month) { df::squad_schedule_order* order = new df::squad_schedule_order(); order->min_count = squad_size; @@ -212,7 +219,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) } } - result->schedule.push_back(reinterpret_cast(asched)); + result->schedule.routine.push_back(schedule); } //Modify necessary world state @@ -234,8 +241,8 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s if (squad == nullptr || zone == nullptr) return; - df::squad::T_rooms* room_from_squad = nullptr; - df::building_civzonest::T_squad_room_info* room_from_building = nullptr; + df::squad_barracks_infost* room_from_squad = nullptr; + df::building_squad_infost* room_from_building = nullptr; for (auto room : squad->rooms) { @@ -263,18 +270,18 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s if (!avoiding_squad_roundtrip && room_from_squad == nullptr) { - room_from_squad = new df::squad::T_rooms(); + room_from_squad = new df::squad_barracks_infost(); room_from_squad->building_id = civzone_id; - insert_into_vector(squad->rooms, &df::squad::T_rooms::building_id, room_from_squad); + insert_into_vector(squad->rooms, &df::squad_barracks_infost::building_id, room_from_squad); } if (room_from_building == nullptr) { - room_from_building = new df::building_civzonest::T_squad_room_info(); + room_from_building = new df::building_squad_infost(); room_from_building->squad_id = squad_id; - insert_into_vector(zone->squad_room_info, &df::building_civzonest::T_squad_room_info::squad_id, room_from_building); + insert_into_vector(zone->squad_room_info, &df::building_squad_infost::squad_id, room_from_building); } if (room_from_squad) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 933ea53754..1af93c3b7d 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -61,16 +61,20 @@ distribution. #include "df/historical_kills.h" #include "df/history_event_hist_figure_diedst.h" #include "df/identity.h" +#include "df/interaction_profilest.h" #include "df/item.h" #include "df/job.h" #include "df/nemesis_record.h" +#include "df/personality_goalst.h" #include "df/plotinfost.h" +#include "df/reputation_profilest.h" #include "df/syndrome.h" #include "df/tile_occupancy.h" #include "df/training_assignment.h" #include "df/unit.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" +#include "df/unit_active_animationst.h" #include "df/unit_inventory_item.h" #include "df/unit_misc_trait.h" #include "df/unit_path_goal.h" @@ -79,6 +83,7 @@ distribution. #include "df/unit_soul.h" #include "df/unit_syndrome.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" #include "df/world_site.h" @@ -325,7 +330,7 @@ bool Units::isNaked(df::unit *unit, bool no_items) { for (auto inv_item : unit->inventory) { // TODO: Check for proper coverage (bad thought) - if (inv_item->mode == df::unit_inventory_item::Worn) + if (inv_item->mode == df::inv_item_role_type::Worn) return false; } return true; @@ -1247,7 +1252,7 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) { // This is 100% reverse-engineered from DF code int rating = getNominalSkill(unit, skill_id, true); // Apply special states - if (unit->counters.soldier_mood == df::unit::T_counters::None) { + if (unit->counters.soldier_mood == df::soldier_mood_type::None) { if (unit->counters.nausea > 0) rating >>= 1; if (unit->counters.winded > 0) @@ -1260,7 +1265,7 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) { rating >>= 1; } - if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance) { + if (unit->counters.soldier_mood != df::soldier_mood_type::MartialTrance) { if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle && !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged && !hasExtravision(unit)) @@ -1608,7 +1613,7 @@ float Units::computeSlowdownFactor(df::unit *unit) { if (!unit->flags1.bits.marauder && casteFlagSet(unit->race, unit->caste, caste_raw_flags::MEANDERER) && !(unit->following && isCitizen(unit)) && - linear_index(unit->inventory, &df::unit_inventory_item::mode, df::unit_inventory_item::Hauled) < 0) + linear_index(unit->inventory, &df::unit_inventory_item::mode, df::inv_item_role_type::Hauled) < 0) { coeff *= 4.0f; } diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp index 3ed5455d0f..8f75e0549e 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -568,7 +568,7 @@ struct VeinGenerator bool VeinGenerator::init_biomes() { auto &mats = df::inorganic_raw::get_vector(); - materials.resize(world->raws.inorganics.size()); + materials.resize(world->raws.inorganics.all.size()); for (size_t i = 0; i < mats.size(); i++) { diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index c7ce4114e7..b82442a66d 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -679,7 +679,7 @@ static const char * if_pretty(const tile_context &ctx, const char *c) { } static bool is_rectangular(const df::building *bld) { - const df::building_extents &room = bld->room; + const df::building::T_room &room = bld->room; if (!room.extents) return true; for (int32_t y = 0; y < room.height; ++y) { diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index fe317e4705..64ddf89e34 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -166,12 +166,12 @@ static void load_organic_material_cache(df::organic_mat_category cat) { } static void load_material_cache() { - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) if (raws.mat_table.builtin[i]) cache_matched(i, -1); - for (size_t i = 0; i < raws.inorganics.size(); i++) + for (size_t i = 0; i < raws.inorganics.all.size(); i++) cache_matched(0, i); load_organic_material_cache(df::organic_mat_category::Wood); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index d55b5f5da4..1a97a801de 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -337,7 +337,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, } auto item = closest->second; // some item must be closest. - if (Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { + if (Job::attachJobItem(job, item, df::job_role_type::Hauled, filter_idx)) { MaterialInfo material; material.decode(item); ItemTypeInfo item_type; diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 9c41d73912..50b0c7f138 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -29,7 +29,6 @@ #include "df/unit_soul.h" #include "df/unit_syndrome.h" #include "df/world.h" -#include "df/world_raws.h" using std::vector; using std::string; diff --git a/plugins/devel/eventExample.cpp b/plugins/devel/eventExample.cpp index ceaec29921..c0944249f1 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -13,6 +13,7 @@ #include "df/job.h" #include "df/unit.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" #include diff --git a/plugins/devel/stripcaged.cpp b/plugins/devel/stripcaged.cpp index 14189bdc30..fdb11741ca 100644 --- a/plugins/devel/stripcaged.cpp +++ b/plugins/devel/stripcaged.cpp @@ -21,7 +21,6 @@ using namespace std; #include #include "df/world.h" -#include "df/world_raws.h" #include "df/building_def.h" #include "df/unit_inventory_item.h" #include diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index d4fddb4846..35f0238123 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -46,7 +46,6 @@ #include "df/unit_preference.h" #include "df/unit_soul.h" #include "df/viewscreen_unitst.h" -#include "df/world_raws.h" using std::deque; @@ -1184,7 +1183,7 @@ struct preference_map label = ""; typedef df::unit_preference::T_type T_type; - df::world_raws &raws = world->raws; + auto &raws = world->raws; switch (pref.type) { case (T_type::LikeCreature): diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp index ee418363e7..db04308d0b 100644 --- a/plugins/embark-assistant/embark-assistant.cpp +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -18,7 +18,6 @@ #include "df/world.h" #include "df/world_data.h" #include "df/world_geo_biome.h" -#include "df/world_raws.h" #include "defs.h" #include "embark-assistant.h" diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp index b4c55dd823..bb90435343 100644 --- a/plugins/embark-assistant/finder_ui.cpp +++ b/plugins/embark-assistant/finder_ui.cpp @@ -14,9 +14,7 @@ #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" #include "df/world_data.h" -#include "df/world_raws.h" #include "df/world_region_type.h" -#include "df/world_raws.h" #include "embark-assistant.h" #include "finder_ui.h" diff --git a/plugins/embark-assistant/matcher.cpp b/plugins/embark-assistant/matcher.cpp index b540ae80b4..9f3900f0c9 100644 --- a/plugins/embark-assistant/matcher.cpp +++ b/plugins/embark-assistant/matcher.cpp @@ -10,7 +10,6 @@ #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" #include "df/world_data.h" -#include "df/world_raws.h" #include "df/world_region.h" #include "df/world_region_details.h" #include "df/world_region_type.h" diff --git a/plugins/embark-assistant/overlay.cpp b/plugins/embark-assistant/overlay.cpp index d5b50294a9..37130c6897 100644 --- a/plugins/embark-assistant/overlay.cpp +++ b/plugins/embark-assistant/overlay.cpp @@ -8,7 +8,6 @@ #include "df/viewscreen.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" -#include "df/world_raws.h" #include "finder_ui.h" #include "help_ui.h" diff --git a/plugins/embark-assistant/survey.cpp b/plugins/embark-assistant/survey.cpp index 84994b2869..ddfc6d8d49 100644 --- a/plugins/embark-assistant/survey.cpp +++ b/plugins/embark-assistant/survey.cpp @@ -30,7 +30,6 @@ #include "df/interaction_source_type.h" #include "df/interaction_target.h" #include "df/interaction_target_materialst.h" -#include "df/material_common.h" #include "df/reaction.h" #include "df/reaction_product_itemst.h" #include "df/reaction_product_type.h" @@ -42,7 +41,6 @@ #include "df/world_data.h" #include "df/world_geo_biome.h" #include "df/world_geo_layer.h" -#include "df/world_raws.h" #include "df/world_region.h" #include "df/world_region_details.h" #include "df/world_region_feature.h" diff --git a/plugins/forceequip.cpp b/plugins/forceequip.cpp index df716831db..fa8cb8ef12 100644 --- a/plugins/forceequip.cpp +++ b/plugins/forceequip.cpp @@ -238,7 +238,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * return false; } - if (!Items::moveToInventory(item, unit, df::unit_inventory_item::Worn, bpIndex)) + if (!Items::moveToInventory(item, unit, df::inv_item_role_type::Worn, bpIndex)) { if (verbose) { WARN(log).print("\nEquipping failed - failed to retrieve item from its current location/container/inventory. Please move it to the ground and try again.\n"); } return false; diff --git a/plugins/generated-creature-renamer.cpp b/plugins/generated-creature-renamer.cpp index b0a00b3f21..0ea3227b1b 100644 --- a/plugins/generated-creature-renamer.cpp +++ b/plugins/generated-creature-renamer.cpp @@ -3,7 +3,6 @@ #include "Export.h" #include "PluginManager.h" #include "df/world.h" -#include "df/world_raws.h" #include "df/creature_raw.h" #include "df/caste_raw.h" #include "modules/World.h" diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 9b41c2e2ca..ffa62ed830 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -8,6 +8,7 @@ #include "modules/Maps.h" #include "modules/World.h" +#include "df/block_column_print_infost.h" #include "df/construction.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -190,8 +191,7 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { __LINE__, bpos.x, bpos.y); continue; } - df::map_block_column::T_unmined_glyphs *glyphs = - new df::map_block_column::T_unmined_glyphs; + df::block_column_print_infost *glyphs = new df::block_column_print_infost; glyphs->x[0] = 0; glyphs->x[1] = 1; glyphs->x[2] = 2; diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index 52b9bf43a4..bcb59b7ad5 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -10,6 +10,7 @@ #include "modules/World.h" #include "df/building.h" +#include "df/buildingitemst.h" #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" #include "df/caravan_state.h" diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp index 64cfcc301b..b935c34f81 100644 --- a/plugins/nestboxes.cpp +++ b/plugins/nestboxes.cpp @@ -6,6 +6,7 @@ #include "modules/Persistence.h" #include "modules/World.h" +#include "df/buildingitemst.h" #include "df/building_nest_boxst.h" #include "df/item.h" #include "df/item_eggst.h" diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 89df783245..49558c1b88 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -765,8 +765,8 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) if (it2.isMember("bearing")) { std::string bearing(it2["bearing"].asString()); - auto found = std::find_if(world->raws.inorganics.begin(), world->raws.inorganics.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); - if (found == world->raws.inorganics.end()) + auto found = std::find_if(world->raws.inorganics.all.begin(), world->raws.inorganics.all.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); + if (found == world->raws.inorganics.all.end()) { delete condition; @@ -774,7 +774,7 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) continue; } - condition->metal_ore = found - world->raws.inorganics.begin(); + condition->metal_ore = found - world->raws.inorganics.all.begin(); } if (it2.isMember("reaction_class")) @@ -1008,8 +1008,8 @@ static bool orders_compare(df::manager_order *a, df::manager_order *b) return a->workshop_id >= 0; } - if (a->frequency == df::manager_order::T_frequency::OneTime - || b->frequency == df::manager_order::T_frequency::OneTime) + if (a->frequency == df::workquota_frequency_type::OneTime + || b->frequency == df::workquota_frequency_type::OneTime) return a->frequency < b->frequency; return a->frequency > b->frequency; } diff --git a/plugins/probe.cpp b/plugins/probe.cpp index 31c2df5edc..eff0c98c86 100644 --- a/plugins/probe.cpp +++ b/plugins/probe.cpp @@ -46,7 +46,7 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) for (auto inv_item : unit->inventory) { df::item* item = inv_item->item; - if (inv_item->mode == df::unit_inventory_item::T_mode::Worn) { + if (inv_item->mode == df::inv_item_role_type::Worn) { out << " wears item: #" << item->id; if (item->flags.bits.owned) out << " (owned)"; diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index 159b557e4f..e8883fde59 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -223,9 +223,11 @@ void printVeins(color_ostream &con, MatMap &mat_map, MatMap gems; MatMap rest; + auto & inorganics = world->raws.inorganics.all; + for (const auto &kv : mat_map) { - df::inorganic_raw *gloss = vector_get(world->raws.inorganics, kv.first); + df::inorganic_raw *gloss = vector_get(inorganics, kv.first); if (!gloss) { con.printerr("invalid material gloss: %hi\n", kv.first); @@ -242,17 +244,17 @@ void printVeins(color_ostream &con, MatMap &mat_map, if (options.ores) { con << "Ores:" << std::endl; - printMats(con, ores, world->raws.inorganics, options); + printMats(con, ores, inorganics, options); } if (options.gems) { con << "Gems:" << std::endl; - printMats(con, gems, world->raws.inorganics, options); + printMats(con, gems, inorganics, options); } if (options.veins) { con << "Other vein stone:" << std::endl; - printMats(con, rest, world->raws.inorganics, options); + printMats(con, rest, inorganics, options); } } @@ -604,7 +606,7 @@ static command_result embark_prospector(color_ostream &out, // Print the report if (options.layers) { out << "Layer materials:" << std::endl; - printMats(out, layerMats, world->raws.inorganics, options); + printMats(out, layerMats, world->raws.inorganics.all, options); } if (options.hidden) { @@ -837,7 +839,7 @@ static command_result map_prospector(color_ostream &con, if (options.layers) { con << "Layer materials:" << std::endl; - printMats(con, layerMats, world->raws.inorganics, options); + printMats(con, layerMats, world->raws.inorganics.all, options); } if (options.features) { diff --git a/plugins/remotefortressreader/building_reader.cpp b/plugins/remotefortressreader/building_reader.cpp index 7cc715147b..ebf0359afd 100644 --- a/plugins/remotefortressreader/building_reader.cpp +++ b/plugins/remotefortressreader/building_reader.cpp @@ -529,7 +529,7 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re default: break; } - if (actual->gate_flags.bits.closed) + if (actual->gate_flags.bits.raised) remote_build->set_active(1); else remote_build->set_active(0); @@ -671,7 +671,7 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re auto actual = strict_virtual_cast(local_build); if (actual) { - if (actual->gate_flags.bits.closed) + if (actual->gate_flags.bits.retracted) remote_build->set_active(1); else remote_build->set_active(0); diff --git a/plugins/remotefortressreader/item_reader.cpp b/plugins/remotefortressreader/item_reader.cpp index 1353243b2b..58c84b9075 100644 --- a/plugins/remotefortressreader/item_reader.cpp +++ b/plugins/remotefortressreader/item_reader.cpp @@ -14,7 +14,6 @@ #include "df/art_image_property.h" #include "df/art_image_property_intransitive_verbst.h" #include "df/art_image_property_transitive_verbst.h" -#include "df/art_image_ref.h" #include "df/descriptor_shape.h" #include "df/instrument_piece.h" #include "df/instrument_register.h" @@ -240,19 +239,19 @@ void CopyItem(RemoteFortressReader::Item * NetItem, df::item * DfItem) GET_ART_IMAGE_CHUNK GetArtImageChunk = reinterpret_cast(Core::getInstance().vinfo->getAddress("get_art_image_chunk")); if (GetArtImageChunk) { - chunk = GetArtImageChunk(&(world->art_image_chunks), statue->image.id); + chunk = GetArtImageChunk(&(world->art_image_chunks.all), statue->image.id); } else { - for (size_t i = 0; i < world->art_image_chunks.size(); i++) + for (size_t i = 0; i < world->art_image_chunks.all.size(); i++) { - if (world->art_image_chunks[i]->id == statue->image.id) - chunk = world->art_image_chunks[i]; + if (world->art_image_chunks.all[i]->id == statue->image.id) + chunk = world->art_image_chunks.all[i]; } } - if (chunk && chunk->images[statue->image.subid]) + if (chunk && chunk->images[statue->image.subid].art_image) { - CopyImage(chunk->images[statue->image.subid], NetItem->mutable_image()); + CopyImage(chunk->images[statue->image.subid].art_image, NetItem->mutable_image()); } @@ -663,11 +662,11 @@ DFHack::command_result GetItemList(DFHack::color_ostream &stream, const DFHack:: { send_instrument->add_tuning_parm(*(instrument->tuning_parm[j])); } - for (size_t j = 0; j < instrument->registers.size(); j++) + for (size_t j = 0; j < instrument->timbre.registers.size(); j++) { auto reg = send_instrument->add_registers(); - reg->set_pitch_range_min(instrument->registers[j]->pitch_range_min); - reg->set_pitch_range_max(instrument->registers[j]->pitch_range_max); + reg->set_pitch_range_min(instrument->timbre.registers[j]->pitch_range_min); + reg->set_pitch_range_max(instrument->timbre.registers[j]->pitch_range_max); } send_instrument->set_description(DF2UTF(instrument->description)); } diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index ae6683dee9..697eb3fe92 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -42,6 +42,7 @@ #include "df/body_part_layer_raw.h" #include "df/body_part_raw.h" #include "df/bp_appearance_modifier.h" +#include "df/buildingitemst.h" #include "df/builtin_mats.h" #include "df/building_wellst.h" @@ -64,6 +65,7 @@ #include "df/inorganic_raw.h" #include "df/item.h" #include "df/job.h" +#include "df/job_postingst.h" #include "df/job_type.h" #include "df/job_item.h" #include "df/job_material_category.h" @@ -97,6 +99,7 @@ #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_savegamest.h" @@ -194,7 +197,6 @@ const char* growth_locations[] = { #include "df/art_image.h" #include "df/art_image_chunk.h" -#include "df/art_image_ref.h" command_result loadArtImageChunk(color_ostream &out, std::vector & parameters) { if (parameters.size() != 1) @@ -210,7 +212,7 @@ command_result loadArtImageChunk(color_ostream &out, std::vector & if (GetArtImageChunk) { int index = atoi(parameters[0].c_str()); - auto chunk = GetArtImageChunk(&(world->art_image_chunks), index); + auto chunk = GetArtImageChunk(&(world->art_image_chunks.all), index); out.print("Loaded chunk id: %d\n", chunk->id); } return CR_OK; @@ -809,10 +811,10 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage return CR_OK; } - df::world_raws *raws = &world->raws; + auto *raws = &world->raws; // df::world_history *history = &world->history; MaterialInfo mat; - for (size_t i = 0; i < raws->inorganics.size(); i++) + for (size_t i = 0; i < raws->inorganics.all.size(); i++) { mat.decode(0, i); MaterialDefinition *mat_def = out->add_material_list(); @@ -820,9 +822,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; - if (size_t(raws->inorganics[i]->material.state_color[GetState(&raws->inorganics[i]->material)]) < raws->descriptors.colors.size()) + if (size_t(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)]) < raws->descriptors.colors.size()) { - ConvertDFColorDescriptor(raws->inorganics[i]->material.state_color[GetState(&raws->inorganics[i]->material)], mat_def->mutable_state_color()); + ConvertDFColorDescriptor(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)], mat_def->mutable_state_color()); } } for (int i = 0; i < 19; i++) @@ -890,7 +892,7 @@ static command_result GetGrowthList(color_ostream &stream, const EmptyMessage *i - df::world_raws *raws = &world->raws; + auto *raws = &world->raws; if (!raws) return CR_OK;//'. @@ -1154,7 +1156,7 @@ void CopyDesignation(df::map_block * DfBlock, RemoteFortressReader::MapBlock * N void CopyProjectiles(RemoteFortressReader::MapBlock * NetBlock) { - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(projectile, df::proj_itemst, proj->item); if (projectile == NULL) @@ -1187,7 +1189,7 @@ void CopyProjectiles(RemoteFortressReader::MapBlock * NetBlock) { bool isProj = false; auto vehicle = world->vehicles.active[i]; - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(projectile, df::proj_itemst, proj->item); if (!projectile) @@ -1515,14 +1517,14 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in GET_ART_IMAGE_CHUNK GetArtImageChunk = reinterpret_cast(Core::getInstance().vinfo->getAddress("get_art_image_chunk")); if (GetArtImageChunk) { - chunk = GetArtImageChunk(&(world->art_image_chunks), engraving->art_id); + chunk = GetArtImageChunk(&(world->art_image_chunks.all), engraving->art_id); } else { - for (size_t i = 0; i < world->art_image_chunks.size(); i++) + for (size_t i = 0; i < world->art_image_chunks.all.size(); i++) { - if (world->art_image_chunks[i]->id == engraving->art_id) - chunk = world->art_image_chunks[i]; + if (world->art_image_chunks.all[i]->id == engraving->art_id) + chunk = world->art_image_chunks.all[i]; } } if (!chunk) @@ -1534,8 +1536,8 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in ConvertDFCoord(engraving->pos, netEngraving->mutable_pos()); netEngraving->set_quality(engraving->quality); netEngraving->set_tile(engraving->tile); - if (chunk->images[engraving->art_subid]) { - CopyImage(chunk->images[engraving->art_subid], netEngraving->mutable_image()); + if (chunk->images[engraving->art_subid].art_image) { + CopyImage(chunk->images[engraving->art_subid].art_image, netEngraving->mutable_image()); } netEngraving->set_floor(engraving->flags.bits.floor); netEngraving->set_west(engraving->flags.bits.west); @@ -1772,7 +1774,7 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques if (unit->flags1.bits.projectile) { - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(item, df::proj_unitst, proj->item); if (item == NULL) @@ -1985,27 +1987,23 @@ static command_result GetWorldMap(color_ostream &stream, const EmptyMessage *in, out->set_name(DF2UTF(Translation::translateName(&(data->name), false))); out->set_name_english(DF2UTF(Translation::translateName(&(data->name), true))); auto poles = data->flip_latitude; -#if DF_VERSION_INT > 34011 switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif for (int yy = 0; yy < height; yy++) for (int xx = 0; xx < width; xx++) { @@ -2133,28 +2131,24 @@ static command_result GetWorldMapNew(color_ostream &stream, const EmptyMessage * out->set_world_height(height); out->set_name(DF2UTF(Translation::translateName(&(data->name), false))); out->set_name_english(DF2UTF(Translation::translateName(&(data->name), true))); -#if DF_VERSION_INT > 34011 auto poles = data->flip_latitude; switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif for (int yy = 0; yy < height; yy++) for (int xx = 0; xx < width; xx++) { @@ -2272,28 +2266,24 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w sprintf(name, "Region %d, %d", pos_x, pos_y); out->set_name_english(name); out->set_name(name); -#if DF_VERSION_INT > 34011 auto poles = worldData->flip_latitude; switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif df::world_region_details * south = NULL; df::world_region_details * east = NULL; diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 1efdd4df60..05cb776487 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -23,7 +23,7 @@ DBG_EXTERN(stockpiles, log); void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat) { DEBUG(log, out).print("food_lookup: food_idx(%zd)\n", food_idx); - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; int32_t main_idx = table.organic_indexes[mat_category][food_idx]; int16_t type = table.organic_types[mat_category][food_idx]; @@ -58,7 +58,7 @@ size_t OrganicMatLookup::food_max_size(organic_mat_category::organic_mat_categor void OrganicMatLookup::food_build_map() { if (index_built) return; - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; @@ -73,7 +73,7 @@ void OrganicMatLookup::food_build_map() { } int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_category::organic_mat_category mat_category, const std::string& token) { - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; DEBUG(log, out).print("food_idx_by_token:\n"); if (mat_category == organic_mat_category::Fish || diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index f315e7d674..1d11db41e5 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -566,7 +566,7 @@ static void unserialize_list_material(color_ostream& out, const char* subcat, bo vector& pile_list) { // we initialize all disallowed values to 1 // why? because that's how the memory is in DF before we muck with it. - size_t num_elems = world->raws.inorganics.size(); + size_t num_elems = world->raws.inorganics.all.size(); pile_list.resize(num_elems, 0); for (size_t i = 0; i < pile_list.size(); ++i) { MaterialInfo mi(0, i); @@ -814,9 +814,9 @@ void StockpileSerializer::read(color_ostream &out, DeserializeMode mode, const v void StockpileSerializer::write_containers(color_ostream& out) { DEBUG(log, out).print("writing container settings\n"); - mBuffer.set_max_bins(mPile->max_bins); - mBuffer.set_max_barrels(mPile->max_barrels); - mBuffer.set_max_wheelbarrows(mPile->max_wheelbarrows); + mBuffer.set_max_bins(mPile->storage.max_bins); + mBuffer.set_max_barrels(mPile->storage.max_barrels); + mBuffer.set_max_wheelbarrows(mPile->storage.max_wheelbarrows); } template @@ -869,21 +869,21 @@ void StockpileSerializer::read_containers(color_ostream& out, DeserializeMode mo read_elem(out, "max_bins", mode, std::bind(&StockpileSettings::has_max_bins, mBuffer), std::bind(&StockpileSettings::max_bins, mBuffer), - mPile->max_bins); + mPile->storage.max_bins); read_elem(out, "max_barrels", mode, std::bind(&StockpileSettings::has_max_barrels, mBuffer), std::bind(&StockpileSettings::max_barrels, mBuffer), - mPile->max_barrels); + mPile->storage.max_barrels); read_elem(out, "max_wheelbarrows", mode, std::bind(&StockpileSettings::has_max_wheelbarrows, mBuffer), std::bind(&StockpileSettings::max_wheelbarrows, mBuffer), - mPile->max_wheelbarrows); + mPile->storage.max_wheelbarrows); } void StockpileSettingsSerializer::write_general(color_ostream& out) { DEBUG(log, out).print("writing general settings\n"); - mBuffer.set_allow_inorganic(mSettings->allow_inorganic); - mBuffer.set_allow_organic(mSettings->allow_organic); + mBuffer.set_allow_inorganic(mSettings->misc.allow_inorganic); + mBuffer.set_allow_organic(mSettings->misc.allow_organic); } void StockpileSerializer::write_general(color_ostream& out) { @@ -895,11 +895,11 @@ void StockpileSettingsSerializer::read_general(color_ostream& out, DeserializeMo read_elem(out, "allow_inorganic", mode, std::bind(&StockpileSettings::has_allow_inorganic, mBuffer), std::bind(&StockpileSettings::allow_inorganic, mBuffer), - mSettings->allow_inorganic); + mSettings->misc.allow_inorganic); read_elem(out, "allow_organic", mode, std::bind(&StockpileSettings::has_allow_organic, mBuffer), std::bind(&StockpileSettings::allow_organic, mBuffer), - mSettings->allow_organic); + mSettings->misc.allow_organic); } void StockpileSerializer::read_general(color_ostream& out, DeserializeMode mode) { diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 4955d49e96..98b8b4b725 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -644,8 +644,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && pref->mattype == builtin_mats::INORGANIC) { item->mat_type = pref->mattype; @@ -722,8 +722,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial) + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial) { MaterialInfo mat(pref->mattype, pref->matindex); if (mat.material->flags.is_set(material_flags::SILK)) @@ -805,8 +805,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && pref->mattype == 0 && getCreatedMetalBars(pref->matindex) > 0) mats.push_back(pref->matindex); } @@ -835,8 +835,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && ((pref->mattype == builtin_mats::GLASS_GREEN) || (pref->mattype == builtin_mats::GLASS_CLEAR && have_glass[1]) || (pref->mattype == builtin_mats::GLASS_CRYSTAL && have_glass[2]))) @@ -867,8 +867,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial) + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial) { MaterialInfo mat(pref->mattype, pref->matindex); if (mat.material->flags.is_set(material_flags::BONE)) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index ad96302887..ef9b77c67a 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -276,7 +276,7 @@ class Tailor { for (auto inv : u->inventory) { - if (inv->mode != df::unit_inventory_item::Worn) + if (inv->mode != df::inv_item_role_type::Worn) continue; // skip non-clothing if (!inv->item->isClothing()) @@ -456,7 +456,7 @@ class Tailor { order->mat_index == -1 && order->specdata.hist_figure_id == hfid && order->material_category.whole == mcat.whole && - order->frequency == df::manager_order::T_frequency::OneTime) + order->frequency == df::workquota_frequency_type::OneTime) return order; } return NULL; diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index a472c2efec..ea75ced359 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -32,6 +32,7 @@ #include "df/activity_event_worshipst.h" #include "df/building_nest_boxst.h" #include "df/building_trapst.h" +#include "df/buildingitemst.h" #include "df/init.h" #include "df/item_eggst.h" #include "df/unit.h" @@ -409,7 +410,9 @@ static void adjust_activities(color_ostream &out, int32_t timeskip) { for (auto act : world->activities.all) { for (auto ev : act->events) { switch (ev->getType()) { - using namespace df::enums::activity_event_type; + using namespace df::enums::activity_event_type; + case NONE: + break; case TrainingSession: // no counters From 3583334b7aa0039f78dd852f32c2482ec5886672 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 14:55:38 -0800 Subject: [PATCH 091/115] update core/plugin lua for great reorg --- library/lua/gui/dwarfmode.lua | 2 +- library/lua/syndrome-util.lua | 2 +- plugins/lua/buildingplan/planneroverlay.lua | 2 +- plugins/lua/sort/diplomacy.lua | 2 +- test/library/print.lua | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 793acbd520..61317f9ebc 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -249,7 +249,7 @@ end function get_hotkey_target(key) local hk = HOTKEY_KEYS[key] - if hk and hk.cmd == df.ui_hotkey.T_cmd.Zoom then + if hk and hk.cmd == df.hotkey_type.Zoom then return xyz2pos(hk.x, hk.y, hk.z) end end diff --git a/library/lua/syndrome-util.lua b/library/lua/syndrome-util.lua index efbaa5deaa..c0af9f8a72 100644 --- a/library/lua/syndrome-util.lua +++ b/library/lua/syndrome-util.lua @@ -98,7 +98,7 @@ function infectWithSyndrome(target,syndrome,resetPolicy) unitSyndrome.ticks = 0 unitSyndrome.wound_id = -1 for k,v in ipairs(syndrome.ce) do - local symptom = df.unit_syndrome.T_symptoms:new() + local symptom = df.active_creature_interaction_effectst:new() symptom.quantity = 0 symptom.delay = 0 symptom.ticks = 0 diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 62e1a6c49c..f92dc95d84 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -1348,7 +1348,7 @@ function PlannerOverlay:place_building(placement_data, chosen_items) dfhack.printerr(('item no longer available: %d'):format(item_id)) break end - if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then + if not dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, idx-1, -1) then dfhack.printerr(('cannot attach item: %d'):format(item_id)) break end diff --git a/plugins/lua/sort/diplomacy.lua b/plugins/lua/sort/diplomacy.lua index c0ec818505..3e78593862 100644 --- a/plugins/lua/sort/diplomacy.lua +++ b/plugins/lua/sort/diplomacy.lua @@ -64,7 +64,7 @@ local function get_preferences(unit) if not unit then return {} end local preferences = {} for _, pref in ipairs(unit.status.current_soul.preferences) do - if pref.type == df.unit_preference.T_type.LikeItem and pref.active then + if pref.type == df.unitpref_type.LikeItem and pref.active then table.insert(preferences, make_item_description(pref.item_type, pref.item_subtype)) end end diff --git a/test/library/print.lua b/test/library/print.lua index 28a2e70370..79243d8989 100644 --- a/test/library/print.lua +++ b/test/library/print.lua @@ -47,7 +47,7 @@ end local function new_int_vector() -- create a vector of integers by cloning one from world. we do it this way -- because we can't allocate typed vectors from lua directly. - local vector = df.global.world.busy_buildings:new() + local vector = df.global.world.building_uses.buildings:new() vector:resize(0) return vector end From 17be93cb594379622ec8309ef1490c4d35ce2889 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 16:58:00 -0800 Subject: [PATCH 092/115] ignore missing viewscreens unused viewscreens are optimized out of windows builds but not linux builds, so this is necessary to get this test to pass on Windows --- test/structures/misc.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/structures/misc.lua b/test/structures/misc.lua index 13febff566..33d386b832 100644 --- a/test/structures/misc.lua +++ b/test/structures/misc.lua @@ -28,8 +28,10 @@ function test.viewscreenDtors() for name, type in pairs(df) do if name:startswith('viewscreen') then print('testing', name) - local v = type:new() - expect.true_(v:delete(), "destructor returned false: " .. name) + local ok, v = pcall(function() return type:new() end) + if ok then + expect.true_(v:delete(), "destructor returned false: " .. name) + end end end end From df46a55a523606ba005ac61639cbac3b31974989 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 23:13:44 -0800 Subject: [PATCH 093/115] add unit test for allocating and deallocating all types --- test/structures/misc.lua | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/test/structures/misc.lua b/test/structures/misc.lua index 33d386b832..ab58187b31 100644 --- a/test/structures/misc.lua +++ b/test/structures/misc.lua @@ -24,14 +24,39 @@ function test.overlappingGlobals() end end -function test.viewscreenDtors() +local known_bad_types = { + -- renderer base class has non-destructible padding declared + renderer_2d_base=true, + renderer_2d=true, + renderer_offscreen=true, + + -- abstract base classes that aren't instantiable + active_script_varst=true, + widget_sheet_button=true, +} + +if dfhack.getOSType() == 'linux' then + -- empty destructors are declared inline for these types, + -- and gcc appears to optimize them out + known_bad_types.mental_picture_propertyst = true + known_bad_types.region_block_eventst = true +end + +function test.destructors() + local count = 1 for name, type in pairs(df) do - if name:startswith('viewscreen') then - print('testing', name) - local ok, v = pcall(function() return type:new() end) - if ok then - expect.true_(v:delete(), "destructor returned false: " .. name) - end + if known_bad_types[name] then + goto continue + end + print(('testing constructor %5d: %s'):format(count, name)) + local ok, v = pcall(function() return type:new() end) + if not ok then + print(' constructor failed; skipping destructor test') + else + print(' destructor ok') + expect.true_(v:delete(), "destructor returned false: " .. name) end + count = count + 1 + ::continue:: end end From 6518abd6d6384105d5d534e3a56c464ddc0c03aa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 07:34:29 -0800 Subject: [PATCH 094/115] update submodule refs --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index b3202b06e0..2a84adf2fe 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b3202b06e04c03433565e8248f2638dddde4ee22 +Subproject commit 2a84adf2fe1e76f5244f51cd4536442ebce10b5d diff --git a/plugins/stonesense b/plugins/stonesense index 92794aec77..8d445c72d5 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 92794aec7768bdafd0f5fc23fed8b30655805b32 +Subproject commit 8d445c72d5bc797bc2e8f6d0c42091fc068d0893 diff --git a/scripts b/scripts index fc67fdc5b6..fc3f7d044b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit fc67fdc5b67a25a776eebfa769b289801679e364 +Subproject commit fc3f7d044b1b45f3ad7b9f7ba3bcc5a4bb24b09b From 47792ce67181d476c14123232e61d4de2192890a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 17:06:51 -0800 Subject: [PATCH 095/115] finish updating modules for the Great Reorg --- library/lua/custom-raw-tokens.lua | 2 +- library/lua/dfhack/workshops.lua | 2 +- library/lua/gui/materials.lua | 6 +++--- plugins/lua/sort.lua | 4 ++-- plugins/lua/stockflow.lua | 2 +- plugins/rendermax/rendermax.lua | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/library/lua/custom-raw-tokens.lua b/library/lua/custom-raw-tokens.lua index 6c19deb72a..7975ba5e39 100644 --- a/library/lua/custom-raw-tokens.lua +++ b/library/lua/custom-raw-tokens.lua @@ -300,7 +300,7 @@ local function getTokenArg1Else(userdata, token) elseif df.is_instance(df.building_workshopst, userdata) or df.is_instance(df.building_furnacest, userdata) then rawStruct = df.building_def.find(userdata.custom_type) elseif df.is_instance(df.interaction_instance, userdata) then - rawStruct = df.global.world.raws.interactions[userdata.interaction_id] + rawStruct = df.global.world.raws.interactions.all[userdata.interaction_id] else -- Assume raw struct *is* argument 1 rawStruct = userdata diff --git a/library/lua/dfhack/workshops.lua b/library/lua/dfhack/workshops.lua index a09849a57d..f43429642f 100644 --- a/library/lua/dfhack/workshops.lua +++ b/library/lua/dfhack/workshops.lua @@ -509,7 +509,7 @@ local function addReactionJobs(ret,bid,wid,cid) end local function scanRawsOres() local ret={} - for idx,ore in ipairs(df.global.world.raws.inorganics) do + for idx,ore in ipairs(df.global.world.raws.inorganics.all) do if #ore.metal_ore.mat_index~=0 then ret[idx]=ore end diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua index d3a9408a51..7429a78237 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -116,7 +116,7 @@ end function MaterialDialog:initInorganicMode() local choices = {} - for i,mat in ipairs(df.global.world.raws.inorganics) do + for i,mat in ipairs(df.global.world.raws.inorganics.all) do self:addMaterial(choices, mat.material, 0, i, false, mat) end @@ -378,14 +378,14 @@ function ItemTraitsDialog(args) end -------------------------------------- local set_ore_ix = {} - for i, raw in ipairs(df.global.world.raws.inorganics) do + for i, raw in ipairs(df.global.world.raws.inorganics.all) do for _, ix in ipairs(raw.metal_ore.mat_index) do set_ore_ix[ix] = true end end local ores = {} for ix in pairs(set_ore_ix) do - local raw = df.global.world.raws.inorganics[ix] + local raw = df.global.world.raws.inorganics.all[ix] ores[#ores+1] = {mat_index = ix, name = raw.material.state_name.Solid} end table.sort(ores, function(a,b) return a.name < b.name end) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index fe42cbb1ed..5777d2b174 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -43,7 +43,7 @@ local function get_active_idx_cache() end local function is_original_dwarf(unit) - return df.global.plotinfo.fortress_age == unit.curse.time_on_site // 10 + return df.global.plotinfo.fortress_age == unit.curse.interaction.time_on_site // 10 end local WAVE_END_GAP = 10000 @@ -53,7 +53,7 @@ local function get_most_recent_wave_oldest_active_idx(cache) for idx=#active_units-1,0,-1 do local unit = active_units[idx] if not dfhack.units.isCitizen(unit) then goto continue end - if oldest_unit and unit.curse.time_on_site - oldest_unit.curse.time_on_site > WAVE_END_GAP then + if oldest_unit and unit.curse.interaction.time_on_site - oldest_unit.curse.interaction.time_on_site > WAVE_END_GAP then return cache[oldest_unit.id] else oldest_unit = unit diff --git a/plugins/lua/stockflow.lua b/plugins/lua/stockflow.lua index 3b2b0fe669..dc44d23045 100644 --- a/plugins/lua/stockflow.lua +++ b/plugins/lua/stockflow.lua @@ -283,7 +283,7 @@ function collect_reactions() reaction_entry(result, job_types.CatchLiveFish) -- Cutting, encrusting, and metal extraction. - local rock_types = df.global.world.raws.inorganics + local rock_types = df.global.world.raws.inorganics.all for rock_id = #rock_types-1, 0, -1 do local material = rock_types[rock_id].material local rock_name = material.state_adj.Solid diff --git a/plugins/rendermax/rendermax.lua b/plugins/rendermax/rendermax.lua index f1cfa90203..6aacbde64f 100644 --- a/plugins/rendermax/rendermax.lua +++ b/plugins/rendermax/rendermax.lua @@ -164,7 +164,7 @@ function colorFrom16(col16) return {col[0],col[1],col[2]} end function addGems() - for k,v in pairs(df.global.world.raws.inorganics) do + for k,v in pairs(df.global.world.raws.inorganics.all) do if v.material.flags.IS_GEM then addMaterial("INORGANIC:"..v.id,colorFrom16(v.material.tile_color[0]+v.material.tile_color[2]*8)) end From 82c6551ebcb135f73ddb49e7d0d4533b2f794ccf Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 01:35:43 +0000 Subject: [PATCH 096/115] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 2a84adf2fe..9bf00722a7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2a84adf2fe1e76f5244f51cd4536442ebce10b5d +Subproject commit 9bf00722a739e6c00fd2a2c19ac7208e6f337ed3 diff --git a/plugins/stonesense b/plugins/stonesense index 8d445c72d5..9f5aa0f22e 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 8d445c72d5bc797bc2e8f6d0c42091fc068d0893 +Subproject commit 9f5aa0f22e70d3c9c0145fe2c4b4047a54dddca9 diff --git a/scripts b/scripts index fc3f7d044b..795cba2474 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit fc3f7d044b1b45f3ad7b9f7ba3bcc5a4bb24b09b +Subproject commit 795cba2474b928771cf59d6fc3690f4f5622bf99 From db49f6d5873e543ad0f72c0050bd34175de3b7a4 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 9 Feb 2025 21:57:50 -0800 Subject: [PATCH 097/115] Update docs/changelog.txt Co-authored-by: Myk --- docs/changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index aee713f373..6d930a3045 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,7 +59,6 @@ Template for new versions: - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S ## Fixes -- `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command - `createitem`: output items will now end up at look cursor if active - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself From b9f7882a3e310ef8b966ec39284bc5f44fbb91ef Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 9 Feb 2025 22:56:01 -0800 Subject: [PATCH 098/115] Update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6d930a3045..b36fad7284 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -76,6 +76,7 @@ Template for new versions: ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` +- ``gui.dwarfmode`` module: ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` now operate on the adventure mode look cursor, if active. Clearing the cursor sets it to the active adventurer's position. ## Removed From 57d850f2653af01c1bd32bc3733d6e820fe4b473 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:18:09 +0000 Subject: [PATCH 099/115] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 9f5aa0f22e..9bc40021b3 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9f5aa0f22e70d3c9c0145fe2c4b4047a54dddca9 +Subproject commit 9bc40021b3673f59393e4c718949b5575599f01f From 928f2435fec9b0d80f84cc9dd482c30253e3da81 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 02:46:14 -0800 Subject: [PATCH 100/115] add property filters for brewable, millable, and processable --- docs/changelog.txt | 1 + docs/plugins/stockpiles.rst | 6 +++++ plugins/stockpiles/OrganicMatLookup.cpp | 8 +++--- plugins/stockpiles/OrganicMatLookup.h | 2 +- plugins/stockpiles/StockpileSerializer.cpp | 31 +++++++++++++++++++--- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3e5fa6ad90..e8bfa535c4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,7 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort - `autobutcher`: treat animals on restraints as unavailable for slaughter +- `stockpiles`: add property filters for brewable, millable, and processable organic materials ## Documentation - `stonesense-art-guide`: new guide for making sprite art for Stonesense diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index 739cac5b8d..506b9d783c 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -378,6 +378,12 @@ Flags and subcategory prefixes:: paste/ pressed/ +Properties:: + + brewable + millable + processable + Settings files:: preparedmeals diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 05cb776487..879353f3b0 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -39,9 +39,7 @@ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category: DEBUG(log, out).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str()); } } -std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx) { - FoodMat food_mat; - food_mat_by_idx(out, mat_category, idx, food_mat); +std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, const FoodMat& food_mat) { if (food_mat.material.isValid()) { return food_mat.material.getToken(); } @@ -62,11 +60,11 @@ void OrganicMatLookup::food_build_map() { df::special_mat_table table = raws.mat_table; using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; - for (int32_t mat_category = traits::first_item_value; mat_category <= traits::last_item_value; ++mat_category) { + for (int32_t mat_category = 0; mat_category <= traits::last_item_value; ++mat_category) { for (size_t i = 0; i < table.organic_indexes[mat_category].size(); ++i) { int16_t type = table.organic_types[mat_category].at(i); int32_t index = table.organic_indexes[mat_category].at(i); - food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); // wtf.. only in c++ + food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); } } index_built = true; diff --git a/plugins/stockpiles/OrganicMatLookup.h b/plugins/stockpiles/OrganicMatLookup.h index f585ef2de4..15f9838df2 100644 --- a/plugins/stockpiles/OrganicMatLookup.h +++ b/plugins/stockpiles/OrganicMatLookup.h @@ -31,7 +31,7 @@ class OrganicMatLookup { }; static void food_mat_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat); - static std::string food_token_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx); + static std::string food_token_by_idx(DFHack::color_ostream& out, const FoodMat& food_mat); static size_t food_max_size(df::enums::organic_mat_category::organic_mat_category mat_category); static void food_build_map(); diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 1d11db41e5..afd0a6dbc8 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -449,7 +449,9 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v all = false; continue; } - string token = OrganicMatLookup::food_token_by_idx(out, cat, i); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, i, food_mat); + string token = OrganicMatLookup::food_token_by_idx(out, food_mat); if (token.empty()) { DEBUG(log, out).print("food mat invalid :(\n"); continue; @@ -460,6 +462,24 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v return all; } +static string get_filter_string(color_ostream& out, const OrganicMatLookup::FoodMat& food_mat) { + auto str = OrganicMatLookup::food_token_by_idx(out, food_mat); + if (auto plant = food_mat.material.plant) { + if (plant->flags.is_set(df::plant_raw_flags::DRINK)) + str += "/brewable"; + if (plant->flags.is_set(df::plant_raw_flags::MILL)) + str += "/millable"; + if (auto mat = food_mat.material.material) { + if (mat->flags.is_set(df::material_flags::STRUCTURAL_PLANT_MAT) && + (plant->flags.is_set(df::plant_raw_flags::THREAD) || + plant->flags.is_set(df::plant_raw_flags::EXTRACT_VIAL) || + plant->flags.is_set(df::plant_raw_flags::EXTRACT_BARREL))) + str += "/processable"; + } + } + return str; +} + static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, bool all, char val, const vector& filters, FuncReadImport read_value, size_t list_size, vector& pile_list, organic_mat_category::organic_mat_category cat) { @@ -467,8 +487,9 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, pile_list.resize(num_elems, '\0'); if (all) { for (size_t idx = 0; idx < num_elems; ++idx) { - string token = OrganicMatLookup::food_token_by_idx(out, cat, idx); - set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, idx, food_mat); + set_filter_elem(out, subcat, filters, val, get_filter_string(out, food_mat), idx, pile_list.at(idx)); } return; } @@ -480,7 +501,9 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, WARN(log, out).print("organic mat index too large! idx[%d] max_size[%zd]\n", idx, num_elems); continue; } - set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, idx, food_mat); + set_filter_elem(out, subcat, filters, val, get_filter_string(out, food_mat), idx, pile_list.at(idx)); } } From 803cddce9ff40e5b8d17568d719a7bc377f0bfd4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:53:22 +0000 Subject: [PATCH 101/115] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 795cba2474..5890968648 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 795cba2474b928771cf59d6fc3690f4f5622bf99 +Subproject commit 589096864804e5d6758c555851ef12228003f51c From ef3c9785c560057e26bf6d908c2a2411435c1f55 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 08:04:05 -0800 Subject: [PATCH 102/115] changelog editing pass --- docs/changelog.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9586c36d6a..aba2146d48 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,9 +54,10 @@ Template for new versions: ## New Tools ## New Features -- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit +- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S +- `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units ## Fixes - `createitem`: output items will now end up at look cursor if active @@ -66,10 +67,10 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort - `autobutcher`: treat animals on restraints as unavailable for slaughter -- `stockpiles`: add property filters for brewable, millable, and processable organic materials +- `stockpiles`: add property filters for brewable, millable, and processable (e.g. at a Farmer's workshop) organic materials ## Documentation -- `stonesense-art-guide`: new guide for making sprite art for Stonesense +- `stonesense-art-guide`: guide for making sprite art for Stonesense ## API - ``Military::removeFromSquad``: removes unit from any squad assignments @@ -77,7 +78,7 @@ Template for new versions: ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` -- ``gui.dwarfmode`` module: ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` now operate on the adventure mode look cursor, if active. Clearing the cursor sets it to the active adventurer's position. +- ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons ## Removed From 07b43723b7bc39a43faebae3b9faa555b529d34c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:30:55 +0000 Subject: [PATCH 103/115] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 9bf00722a7..13cd875ce9 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9bf00722a739e6c00fd2a2c19ac7208e6f337ed3 +Subproject commit 13cd875ce99fec984b7d654d68c31ef265d05bdd diff --git a/plugins/stonesense b/plugins/stonesense index 9bc40021b3..760d36ec92 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9bc40021b3673f59393e4c718949b5575599f01f +Subproject commit 760d36ec92d8d250ea6a08e588ae8dcc1bbd2367 diff --git a/scripts b/scripts index 5890968648..273494caa5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 589096864804e5d6758c555851ef12228003f51c +Subproject commit 273494caa52ddff73f8eeaed3c037166c70796ca From fb1e54abf822662f1d6590c966ac23f66461d9b0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 14:09:51 -0800 Subject: [PATCH 104/115] bump version to 51.05-r1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c85c190971..7c58b36bf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.04") +set(DF_VERSION "51.05") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 48b75b3f3417225034475fe9d9532a03935b39da Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 14:11:19 -0800 Subject: [PATCH 105/115] update changelog for 51.05-r1 release --- docs/changelog.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index e747e8aacc..838b8270d4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,11 @@ Template for new versions: ## Removed +# 51.05-r1 + +## Misc Improvements +- Compatibility with DF 51.05 + # 51.04-r1 ## Misc Improvements From 537727be276ffd1cc4ac2fe99be245113594ddb4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 11 Feb 2025 07:16:50 +0000 Subject: [PATCH 106/115] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 760d36ec92..17c6be1669 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 760d36ec92d8d250ea6a08e588ae8dcc1bbd2367 +Subproject commit 17c6be1669b2bce22864892892722c625e1d66e3 diff --git a/scripts b/scripts index 273494caa5..70c54c4e48 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 273494caa52ddff73f8eeaed3c037166c70796ca +Subproject commit 70c54c4e48c8005f2a07b68646e1f5b3855a0373 From 3f817a04ef07698c8b0b700466ead859a25555f2 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:03:57 +0800 Subject: [PATCH 107/115] Add missing history event --- library/modules/Military.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index cbb4295562..356c606681 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -14,6 +14,7 @@ #include "df/histfig_entity_link_squadst.h" #include "df/historical_figure.h" #include "df/historical_entity.h" +#include "df/history_event_remove_hf_entity_linkst.h" #include "df/entity_position.h" #include "df/entity_position_assignment.h" #include "df/plotinfost.h" @@ -334,6 +335,8 @@ static void remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ former_squad->link_strength = 100; hf->entity_links.push_back(former_squad); + + // Unassigning a normal soldier does not seem to generate an event record. } static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) @@ -343,6 +346,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ return; int32_t assignment_id = -1; + int32_t pos_id = -1; for (auto& np : nps) { if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) @@ -352,6 +356,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ np.assignment->histfig2 = -1; assignment_id = np.assignment->id; + pos_id = np.position->id; break; } @@ -387,8 +392,21 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->start_year = start_year; former_pos->end_year = *df::global::cur_year; former_pos->link_strength = 100; - + hf->entity_links.push_back(former_pos); + + int32_t event_id = *df::global::hist_event_next_id; + auto former_pos_event = df::allocate(); + former_pos_event->year = *df::global::cur_year; + former_pos_event->seconds = *df::global::cur_year_tick; + former_pos_event->id = event_id; + former_pos_event->civ = squad->entity_id; + former_pos_event->histfig = hf->id; + former_pos_event->position_id = pos_id; + former_pos_event->link_type = df::histfig_entity_link_type::POSITION; + + df::global::world->history.events.push_back(former_pos_event); + *df::global::hist_event_next_id = event_id + 1; } bool Military::removeFromSquad(int32_t unit_id) From 25196c0281487f35b606a197676e94cd95ea6bc9 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:07:38 +0800 Subject: [PATCH 108/115] Trim whitespace --- library/modules/Military.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 356c606681..2f44e04824 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -392,7 +392,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->start_year = start_year; former_pos->end_year = *df::global::cur_year; former_pos->link_strength = 100; - + hf->entity_links.push_back(former_pos); int32_t event_id = *df::global::hist_event_next_id; @@ -404,7 +404,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos_event->histfig = hf->id; former_pos_event->position_id = pos_id; former_pos_event->link_type = df::histfig_entity_link_type::POSITION; - + df::global::world->history.events.push_back(former_pos_event); *df::global::hist_event_next_id = event_id + 1; } From 72819ad3a290ee399d8f86462793e4370c5b2817 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:16:14 +0800 Subject: [PATCH 109/115] Update docs/dev/Lua API.rst --- docs/dev/Lua API.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 66516642a9..bbab5a6c38 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1991,8 +1991,9 @@ Military module Removes a unit from its squad. Unsets the unit's military information (i.e., ``unit.military.squad_id`` and ``unit.military.squad_pos``), the squad's position information (i.e., - ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links - to indicate former squad membership or command. + ``squad.positions[squad_pos].occupant``), modifies the unit's entity links + to indicate former squad membership or command, and creates a corresponding + world history event. Items module ------------ From 4aa4fe9468a6f4175cde36861caa520df94c5d12 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:48:16 +0800 Subject: [PATCH 110/115] Change increment style --- docs/about/Authors.rst | 1 + library/modules/Military.cpp | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index bfb8aa02a8..d806d92aaa 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -158,6 +158,7 @@ NotRexButCaesar NotRexButCaesar Nuno Fernandes UnknowableCoder nuvu vallode Omniclasm +Ong Ying Gao ong-yinggao98 oorzkws oorzkws OwnageIsMagic OwnageIsMagic palenerd dlmarquis diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 2f44e04824..a5946acca5 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -395,7 +395,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.push_back(former_pos); - int32_t event_id = *df::global::hist_event_next_id; + int32_t event_id = (*df::global::hist_event_next_id)++; auto former_pos_event = df::allocate(); former_pos_event->year = *df::global::cur_year; former_pos_event->seconds = *df::global::cur_year_tick; @@ -406,7 +406,6 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos_event->link_type = df::histfig_entity_link_type::POSITION; df::global::world->history.events.push_back(former_pos_event); - *df::global::hist_event_next_id = event_id + 1; } bool Military::removeFromSquad(int32_t unit_id) From 2a00e78d7addb83cf400bcca9e8c37edf4d48946 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 12 Feb 2025 07:16:57 +0000 Subject: [PATCH 111/115] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 70c54c4e48..a76a39a204 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 70c54c4e48c8005f2a07b68646e1f5b3855a0373 +Subproject commit a76a39a2048ea47607794c1ba2f5f0807f5d7a92 From 3cf5586c4a7e3c6eb1d5a3a1d8ba35fe21dbbb57 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:23:37 +0000 Subject: [PATCH 112/115] Auto-update structures ref for 51.05 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 300862ae16..5513dc1ffa 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 300862ae1624b39d73f43c6dfd2ab00e3454434d +Subproject commit 5513dc1ffa70d567d87ce946a0c57ce4760fae7c From cfb2cac22246972bc6e3ec703d65e03e3af3db1b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 12 Feb 2025 14:15:33 -0500 Subject: [PATCH 113/115] Fix regression in checkFreeTiles --- library/include/modules/Buildings.h | 2 +- library/modules/Buildings.cpp | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index 80fe443396..c323958e17 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -111,7 +111,7 @@ DFHACK_EXPORT bool getCorrectSize(df::coord2d &size, df::coord2d ¢er, * Checks if the tiles are free to be built upon. */ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, - df::building *bld, + df::building *bld = nullptr, bool create_ext = false, bool allow_occupied = false, bool allow_wall = false, diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 672464ceba..1135fb5307 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -735,10 +735,7 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, bool allow_wall, bool allow_flow) { - CHECK_NULL_POINTER(bld); - bool found_any = false; - auto & room = bld->room; for (int dx = 0; dx < size.x; dx++) { @@ -748,9 +745,9 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, df::building_extents_type *etile = NULL; // Exclude using extents - if (room.extents) + if (bld && bld->room.extents) { - etile = getExtentTile(room, tile); + etile = getExtentTile(bld->room, tile); if (!etile || !*etile) continue; } @@ -784,13 +781,13 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, found_any = true; else { - if (!create_ext) + if (!bld || !create_ext) return false; - if (!room.extents) + if (!bld->room.extents) { - init_extents(room, pos, size); - etile = getExtentTile(room, tile); + init_extents(bld->room, pos, size); + etile = getExtentTile(bld->room, tile); } if (!etile) From 52a6b891104fff68ac4d2e08f6de7b66e5d81a9f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 12 Feb 2025 14:57:14 -0600 Subject: [PATCH 114/115] do it right this time --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 345d8967db..1e61d7f5f3 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 345d8967db88ff33283111fdfecd0eaebbf09584 +Subproject commit 1e61d7f5f36a0bc937985c5b859d8ee152789bde From ba448949ffac1d933ce4089871f19ce0f1c54ee3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Feb 2025 18:44:14 -0800 Subject: [PATCH 115/115] update docs for checkFreeTiles --- docs/changelog.txt | 2 ++ docs/dev/Lua API.rst | 17 +++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 20e65538d2..20d11103bc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -74,11 +74,13 @@ Template for new versions: ## API - ``Military::removeFromSquad``: removes unit from any squad assignments +- ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons +- ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index bbab5a6c38..f8108e8b2c 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2477,14 +2477,15 @@ General using width and height for flexible dimensions. Returns *is_flexible, width, height, center_x, center_y*. -* ``dfhack.buildings.checkFreeTiles(pos,size[,extents[,change_extents[,allow_occupied[,allow_wall[,allow_flow]]]]])`` - - Checks if the rectangle defined by ``pos`` and ``size``, and possibly extents, - can be used for placing a building. If ``change_extents`` is true, bad tiles - are removed from extents. If ``allow_occupied``, the occupancy test is skipped. - Set ``allow_wall`` to true if the building is unhindered by walls (such as an - activity zone). Set ``allow_flow`` to true if the building can be built even - if there is deep water or any magma on the tile (such as abstract buildings). +* ``dfhack.buildings.checkFreeTiles(pos,size[,bld[,change_extents[,allow_occupied[,allow_wall[,allow_flow]]]]])`` + + Checks if the rectangle defined by ``pos`` and ``size``, and possibly the + extents associated with bld, can be used for placing a building. If + ``change_extents`` is true, bad tiles are removed from extents. If + ``allow_occupied``, the occupancy test is skipped. Set ``allow_wall`` to true + if the building is unhindered by walls (such as an activity zone). Set + ``allow_flow`` to true if the building can be built even if there is deep + water or any magma on the tile (such as abstract buildings). * ``dfhack.buildings.countExtentTiles(extents,defval)``