From 12ae6dd2b0f292831ef95d9bdb956b31f0c25baf Mon Sep 17 00:00:00 2001 From: Alex Noir Date: Fri, 14 Jun 2024 00:09:27 +0300 Subject: [PATCH 001/128] Add paintTileMapPort to support world map tile painting (no getTileMapPort yet) --- library/LuaApi.cpp | 29 ++++++++++++++++++++++++++ library/include/modules/Screen.h | 9 ++++++++ library/modules/Screen.cpp | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 5ed955ae16..6738802a88 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2861,6 +2861,34 @@ static int screen_readTile(lua_State *L) return 1; } +static int screen_paintTileMapPort(lua_State *L) +{ + Pen pen; + Lua::CheckPen(L, &pen, 1); + int x = luaL_checkint(L, 2); + int y = luaL_checkint(L, 3); + if (lua_gettop(L) >= 4 && !lua_isnil(L, 4)) + { + if (lua_type(L, 4) == LUA_TSTRING) + pen.ch = lua_tostring(L, 4)[0]; + else + pen.ch = luaL_checkint(L, 4); + } + if (lua_gettop(L) >= 5 && !lua_isnil(L, 5)) + pen.tile = luaL_checkint(L, 5); + lua_pushboolean(L, Screen::paintTileMapPort(pen, x, y)); + return 1; +} + +// static int screen_readTileMapPort(lua_State *L) +// { +// int x = luaL_checkint(L, 1); +// int y = luaL_checkint(L, 2); +// Pen pen = Screen::readTileMapPort(x, y); +// Lua::Push(L, pen); +// return 1; +// } + static int screen_paintString(lua_State *L) { Pen pen; @@ -3048,6 +3076,7 @@ static const luaL_Reg dfhack_screen_funcs[] = { { "getWindowSize", screen_getWindowSize }, { "paintTile", screen_paintTile }, { "readTile", screen_readTile }, + { "paintTileMapPort", screen_paintTileMapPort }, { "paintString", screen_paintString }, { "fillRect", screen_fillRect }, { "findGraphicsTile", screen_findGraphicsTile }, diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h index f2078e663d..1c60638dce 100644 --- a/library/include/modules/Screen.h +++ b/library/include/modules/Screen.h @@ -35,6 +35,7 @@ distribution. #include "df/viewscreen.h" #include "df/graphic_viewportst.h" +#include "df/graphic_map_portst.h" #include #include @@ -203,6 +204,12 @@ namespace DFHack /// Retrieves one screen tile from the buffer DFHACK_EXPORT Pen readTile(int x, int y, bool map = false, int32_t * df::graphic_viewportst::*texpos_field = NULL); + /// Paint one world map tile with the given pen + DFHACK_EXPORT bool paintTileMapPort(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); + + /// Retrieves one world map tile from the buffer + // DFHACK_EXPORT Pen readTile(int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); + /// Paint a string onto the screen. Ignores ch and tile of pen. DFHACK_EXPORT bool paintString(const Pen &pen, int x, int y, const std::string &text, bool map = false); @@ -315,6 +322,8 @@ namespace DFHack namespace Hooks { GUI_HOOK_DECLARE(get_tile, Pen, (int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field)); GUI_HOOK_DECLARE(set_tile, bool, (const Pen &pen, int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field)); + // GUI_HOOK_DECLARE(get_tile_map_port, Pen, (int x, int y, int32_t * df::graphic_map_portst::*texpos_field)); + GUI_HOOK_DECLARE(set_tile_map_port, bool, (const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field)); } //! Temporary hide a screen until destructor is called diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 8c3745e22f..161b61cc7a 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -365,6 +365,41 @@ Pen Screen::readTile(int x, int y, bool map, int32_t * df::graphic_viewportst::* return doGetTile(x, y, map, texpos_field); } +static bool doSetTile_map_port(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) { + auto &vp = gps->main_map_port; + if (!texpos_field) + texpos_field = &df::graphic_map_portst::screentexpos_interface; + + if (x < 0 || x >= vp->dim_x || y < 0 || y >= vp->dim_y) + return false; + + size_t max_index = vp->dim_y * vp->dim_x - 1; + size_t index = (y * vp->dim_x) + x; + + if (index > max_index) + return false; + + long texpos = pen.tile; + if (!texpos && pen.ch) + texpos = init->font.large_font_texpos[(uint8_t)pen.ch]; + (vp->*texpos_field)[index] = texpos; + return true; +} + +GUI_HOOK_DEFINE(Screen::Hooks::set_tile_map_port, doSetTile_map_port); +static bool doSetTileMapPort(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL) +{ + return GUI_HOOK_TOP(Screen::Hooks::set_tile_map_port)(pen, x, y, texpos_field); +} + +bool Screen::paintTileMapPort(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) +{ + if (!gps || !pen.valid()) return false; + + doSetTileMapPort(pen, x, y, texpos_field); + return true; +} + bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text, bool map) { auto dim = getWindowSize(); From 838ab96b636f5cdd25f832edbe4af2e875df537e Mon Sep 17 00:00:00 2001 From: Alex Noir Date: Sat, 29 Jun 2024 16:40:44 +0300 Subject: [PATCH 002/128] Add readTileMapPort (though it might not be functional right now) Split up doSetTile_char() and doGetTile_char() for non-graphics mode tile grabbing --- library/LuaApi.cpp | 17 +++--- library/include/modules/Screen.h | 4 +- library/modules/Screen.cpp | 98 ++++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 6738802a88..54643e156e 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2880,14 +2880,14 @@ static int screen_paintTileMapPort(lua_State *L) return 1; } -// static int screen_readTileMapPort(lua_State *L) -// { -// int x = luaL_checkint(L, 1); -// int y = luaL_checkint(L, 2); -// Pen pen = Screen::readTileMapPort(x, y); -// Lua::Push(L, pen); -// return 1; -// } +static int screen_readTileMapPort(lua_State *L) +{ + int x = luaL_checkint(L, 1); + int y = luaL_checkint(L, 2); + Pen pen = Screen::readTileMapPort(x, y); + Lua::Push(L, pen); + return 1; +} static int screen_paintString(lua_State *L) { @@ -3077,6 +3077,7 @@ static const luaL_Reg dfhack_screen_funcs[] = { { "paintTile", screen_paintTile }, { "readTile", screen_readTile }, { "paintTileMapPort", screen_paintTileMapPort }, + { "readTileMapPort", screen_readTileMapPort }, { "paintString", screen_paintString }, { "fillRect", screen_fillRect }, { "findGraphicsTile", screen_findGraphicsTile }, diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h index 1c60638dce..e8f375b643 100644 --- a/library/include/modules/Screen.h +++ b/library/include/modules/Screen.h @@ -208,7 +208,7 @@ namespace DFHack DFHACK_EXPORT bool paintTileMapPort(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); /// Retrieves one world map tile from the buffer - // DFHACK_EXPORT Pen readTile(int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); + DFHACK_EXPORT Pen readTileMapPort(int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); /// Paint a string onto the screen. Ignores ch and tile of pen. DFHACK_EXPORT bool paintString(const Pen &pen, int x, int y, const std::string &text, bool map = false); @@ -322,8 +322,6 @@ namespace DFHack namespace Hooks { GUI_HOOK_DECLARE(get_tile, Pen, (int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field)); GUI_HOOK_DECLARE(set_tile, bool, (const Pen &pen, int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field)); - // GUI_HOOK_DECLARE(get_tile_map_port, Pen, (int x, int y, int32_t * df::graphic_map_portst::*texpos_field)); - GUI_HOOK_DECLARE(set_tile_map_port, bool, (const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field)); } //! Temporary hide a screen until destructor is called diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 161b61cc7a..a1fe02c8ba 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -136,13 +136,8 @@ static bool doSetTile_map(const Pen &pen, int x, int y, int32_t * df::graphic_vi return true; } -static bool doSetTile_default(const Pen &pen, int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field) +static bool doSetTile_char(const Pen &pen, int x, int y, bool use_graphics) { - bool use_graphics = Screen::inGraphicsMode(); - - if (map && use_graphics) - return doSetTile_map(pen, x, y, texpos_field); - if (x < 0 || x >= gps->dimx || y < 0 || y >= gps->dimy) return false; @@ -227,6 +222,16 @@ static bool doSetTile_default(const Pen &pen, int x, int y, bool map, int32_t * return true; } +static bool doSetTile_default(const Pen &pen, int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field) +{ + bool use_graphics = Screen::inGraphicsMode(); + + if (map && use_graphics) + return doSetTile_map(pen, x, y, texpos_field); + + return doSetTile_char(pen, x, y, use_graphics); +} + GUI_HOOK_DEFINE(Screen::Hooks::set_tile, doSetTile_default); static bool doSetTile(const Pen &pen, int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field = NULL) { @@ -287,12 +292,7 @@ static uint8_t to_16_bit_color(uint8_t *rgb) { return 0; } -static Pen doGetTile_default(int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field = NULL) { - bool use_graphics = Screen::inGraphicsMode(); - - if (map && use_graphics) - return doGetTile_map(x, y, texpos_field); - +static Pen doGetTile_char(int x, int y, bool use_graphics) { if (x < 0 || x >= gps->dimx || y < 0 || y >= gps->dimy) return Pen(0, 0, 0, -1); @@ -352,6 +352,14 @@ static Pen doGetTile_default(int x, int y, bool map, int32_t * df::graphic_viewp return ret; } +static Pen doGetTile_default(int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field = NULL) { + bool use_graphics = Screen::inGraphicsMode(); + + if (map && use_graphics) + return doGetTile_map(x, y, texpos_field); + return doGetTile_char(x, y, use_graphics); +} + GUI_HOOK_DEFINE(Screen::Hooks::get_tile, doGetTile_default); static Pen doGetTile(int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field = NULL) { @@ -386,20 +394,76 @@ static bool doSetTile_map_port(const Pen &pen, int x, int y, int32_t * df::graph return true; } -GUI_HOOK_DEFINE(Screen::Hooks::set_tile_map_port, doSetTile_map_port); -static bool doSetTileMapPort(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL) -{ - return GUI_HOOK_TOP(Screen::Hooks::set_tile_map_port)(pen, x, y, texpos_field); +static bool doSetTile_map_port_default(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) { + bool use_graphics = Screen::inGraphicsMode(); + + if (use_graphics) + return doSetTile_map_port(pen, x, y, texpos_field); + + return doSetTile_char(pen, x, y, use_graphics); } bool Screen::paintTileMapPort(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) { if (!gps || !pen.valid()) return false; - doSetTileMapPort(pen, x, y, texpos_field); + doSetTile_map_port_default(pen, x, y, texpos_field); return true; } +static Pen doGetTile_map_port(int x, int y, int32_t * df::graphic_map_portst::*texpos_field) { + auto &vp = gps->main_map_port; + + if (x < 0 || x >= vp->dim_x || y < 0 || y >= vp->dim_y) + return Pen(0, 0, 0, -1); + + size_t max_index = vp->dim_x * vp->dim_y - 1; + size_t index = (x * vp->dim_y) + y; + + if (index < 0 || index > max_index) + return Pen(0, 0, 0, -1); + + int tile = 0; + if (!texpos_field) { + // I dunno if any of these are even set, they appear to be 0 in fort mode world map + tile = vp->screentexpos_interface[index]; + if (tile == 0) + tile = vp->screentexpos_base[index]; + if (tile == 0) + tile = vp->screentexpos_detail[index]; + if (tile == 0) + tile = vp->screentexpos_tunnel[index]; + if (tile == 0) + tile = vp->screentexpos_river[index]; + if (tile == 0) + tile = vp->screentexpos_road[index]; + if (tile == 0) + tile = vp->screentexpos_site[index]; + } else { + tile = (vp->*texpos_field)[index]; + } + + char ch = 0; + uint8_t fg = 0; + uint8_t bg = 0; + return Pen(ch, fg, bg, tile, false); +} + +static Pen doGetTile_map_port_default(int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL) { + bool use_graphics = Screen::inGraphicsMode(); + + if (use_graphics) + return doGetTile_map_port(x, y, texpos_field); + return doGetTile_char(x, y, use_graphics); +} + +Pen Screen::readTileMapPort(int x, int y, int32_t * df::graphic_map_portst::*texpos_field) +{ + if (!gps) return Pen(0,0,0,-1); + + return doGetTile_map_port_default(x, y, texpos_field); +} + bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text, bool map) { auto dim = getWindowSize(); From 1b9402b8606ba676a16d0664a71b1a59ff57bc00 Mon Sep 17 00:00:00 2001 From: Alex Noir Date: Sat, 29 Jun 2024 18:41:49 +0300 Subject: [PATCH 003/128] More funny tiles for doGetTile_map_port --- library/modules/Screen.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index a1fe02c8ba..dce537dd2e 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -425,8 +425,6 @@ static Pen doGetTile_map_port(int x, int y, int32_t * df::graphic_map_portst::*t int tile = 0; if (!texpos_field) { - // I dunno if any of these are even set, they appear to be 0 in fort mode world map - tile = vp->screentexpos_interface[index]; if (tile == 0) tile = vp->screentexpos_base[index]; if (tile == 0) @@ -439,6 +437,28 @@ static Pen doGetTile_map_port(int x, int y, int32_t * df::graphic_map_portst::*t tile = vp->screentexpos_road[index]; if (tile == 0) tile = vp->screentexpos_site[index]; + if (tile == 0) + tile = vp->screentexpos_army[index]; + if (tile == 0) + tile = vp->screentexpos_interface[index]; + if (tile == 0) + tile = vp->screentexpos_detail_to_n[index]; + if (tile == 0) + tile = vp->screentexpos_detail_to_s[index]; + if (tile == 0) + tile = vp->screentexpos_detail_to_w[index]; + if (tile == 0) + tile = vp->screentexpos_detail_to_e[index]; + if (tile == 0) + tile = vp->screentexpos_detail_to_nw[index]; + if (tile == 0) + tile = vp->screentexpos_detail_to_ne[index]; + if (tile == 0) + tile = vp->screentexpos_detail_to_sw[index]; + if (tile == 0) + tile = vp->screentexpos_detail_to_se[index]; + if (tile == 0) + tile = vp->screentexpos_site_to_s[index]; } else { tile = (vp->*texpos_field)[index]; } From 0d84266e5b34375eb9ec5be31fdeb12d5882a15b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 22 Feb 2026 11:32:45 +0100 Subject: [PATCH 004/128] Include name in orders export --- docs/changelog.txt | 1 + plugins/orders.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3d78dd8335..6eb5bd1a43 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features - `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators +- `orders`: exported orders now include a human-readable ``name`` field - `sort`: added ``Uniformed`` filter to squad assignment screen to filter dwarves with mining, woodcutting, or hunting labors - `sort`: Add death cause button to dead/missing tab in the creatures screen diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 4bdff21fe2..af504b87c6 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -4,6 +4,7 @@ #include "PluginManager.h" #include "modules/Filesystem.h" +#include "modules/Job.h" #include "modules/Materials.h" #include "modules/World.h" @@ -376,6 +377,7 @@ static command_result orders_export_command(color_ostream & out, const std::stri order["art"] = art; } + order["name"] = Job::getManagerOrderName(it); order["amount_left"] = it->amount_left; order["amount_total"] = it->amount_total; order["is_validated"] = bool(it->status.bits.validated); From 0f0fa85c2642c0889a58dd731a6f5e3b113313a7 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 24 Feb 2026 21:01:25 +0100 Subject: [PATCH 005/128] Fix tests --- test/plugins/orders.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/test/plugins/orders.lua b/test/plugins/orders.lua index ab2ad3235e..c63506e6cd 100644 --- a/test/plugins/orders.lua +++ b/test/plugins/orders.lua @@ -190,6 +190,7 @@ function test.import_export_reaction_condition() } ], "job" : "CustomReaction", + "name" : "Make soap from tallow", "reaction" : "MAKE_SOAP_FROM_TALLOW" } ] From 3213f9d9f457f495200134d9a1055018b8cff1a8 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 Mar 2026 06:17:07 -0800 Subject: [PATCH 006/128] Fix autolabor cycle ticks --- plugins/autolabor/autolabor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index d0cec796fb..4fd73ce181 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -740,7 +740,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) return CR_OK; } - if (world->frame_counter - cycle_timestamp <= CYCLE_TICKS) + if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) return CR_OK; cycle_timestamp = world->frame_counter; From 0622486d4ebc81bb196e9d1a63d62623ba3d257a Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 Mar 2026 06:21:37 -0800 Subject: [PATCH 007/128] Update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 23204c7218..1e70731ebf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `autolabor`: Fix running 1 tick less frequently than intended. ## Misc Improvements From 5b0049a53a1ddcd8905cbd5ed2adc51745d30d8e Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Tue, 10 Mar 2026 20:56:28 +0100 Subject: [PATCH 008/128] Added small tooltip about renaming favorites --- plugins/lua/buildingplan/planneroverlay.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f0fbe17de4..f57057dbdd 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -903,7 +903,7 @@ function PlannerOverlay:init() local favorites_panel = widgets.Panel{ view_id='favorites', - frame={t=15, l=0, r=0, h=9}, + frame={t=15, l=0, r=0, h=11}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('show_favorites'), @@ -940,7 +940,7 @@ function PlannerOverlay:init() is_selected_fn=make_is_selected_filter('0') }, widgets.CycleHotkeyLabel { view_id='slot_select', - frame={b=0, l=2}, + frame={b=2, l=2}, key='CUSTOM_X', key_back='CUSTOM_SHIFT_X', label='next/previous slot', @@ -950,11 +950,15 @@ function PlannerOverlay:init() on_change=function(val) self.selected_favorite = val end, }, widgets.HotkeyLabel{ - frame={b=0, l=28}, + frame={b=2, l=28}, label="set/apply selected", key='CUSTOM_Y', on_activate=function () self:save_restore_filter(self.selected_favorite) end, }, + widgets.Label { + frame={b=0, l=2}, + text="Shift+click to edit the label of a favorite" + }, } } @@ -974,7 +978,7 @@ function PlannerOverlay:show_favorites() end function PlannerOverlay:show_hide_favorites(new) - local errors_frame = {t=15+(new and 9 or 0), l=0, r=0} + local errors_frame = {t=15+(new and 11 or 0), l=0, r=0} self.subviews.errors.frame = errors_frame self:updateLayout() end From 692de1b68c10c9f5f30f9f2666862c5103f81bbc Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Tue, 10 Mar 2026 21:19:56 +0100 Subject: [PATCH 009/128] Add tooltip for renaming favorites in buildingplan Added tooltip text about renaming favorites in the UI for buildingplan. --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index fdde2f1639..74c093afc6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,8 @@ Template for new versions: ## Misc Improvements - General: DFHack will unconditionally use UTF-8 for the console on Windows, now that DF forces the process effective system code page to 65001 during startup +- `buildingplan`: added a small tooltip text about renaming favorites in the UI + ## Documentation From 704c6a19ebc1a28b7773db720ef0485102eefa94 Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Tue, 10 Mar 2026 22:43:43 +0100 Subject: [PATCH 010/128] Fix formatting of label widget in planner overlay --- plugins/lua/buildingplan/planneroverlay.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f57057dbdd..1cb35abdb8 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -955,10 +955,10 @@ function PlannerOverlay:init() key='CUSTOM_Y', on_activate=function () self:save_restore_filter(self.selected_favorite) end, }, - widgets.Label { - frame={b=0, l=2}, - text="Shift+click to edit the label of a favorite" - }, + widgets.Label { + frame={b=0, l=2}, + text="Shift+click to edit the label of a favorite" + }, } } From 39f9613cb6c330d005445bee321527284fb11e06 Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Wed, 11 Mar 2026 00:44:36 +0100 Subject: [PATCH 011/128] Looks better with the in-built TooltipLabel --- plugins/lua/buildingplan/planneroverlay.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 1cb35abdb8..f947b123dd 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -955,9 +955,10 @@ function PlannerOverlay:init() key='CUSTOM_Y', on_activate=function () self:save_restore_filter(self.selected_favorite) end, }, - widgets.Label { + widgets.TooltipLabel { frame={b=0, l=2}, - text="Shift+click to edit the label of a favorite" + show_tooltip=true, + text="Shift+click to edit the label of a favorite", }, } From 5f71a9102008294eb233dfe6b23f94f517bc0c8b Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Wed, 11 Mar 2026 11:27:53 +0100 Subject: [PATCH 012/128] fixed newline mess CRLF->CR --- plugins/lua/buildingplan/planneroverlay.lua | 1384 +------------------ 1 file changed, 1 insertion(+), 1383 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f947b123dd..9ee8feaf1a 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -1,1383 +1 @@ -local _ENV = mkmodule('plugins.buildingplan.planneroverlay') - -local itemselection = require('plugins.buildingplan.itemselection') -local filterselection = require('plugins.buildingplan.filterselection') -local gui = require('gui') -local guidm = require('gui.dwarfmode') -local json = require('json') -local overlay = require('plugins.overlay') -local pens = require('plugins.buildingplan.pens') -local utils = require('utils') -local widgets = require('gui.widgets') -require('dfhack.buildings') - -config = config or json.open('dfhack-config/buildingplan.json') - -local uibs = df.global.buildreq - -reset_counts_flag = false -editing_filters_flag = false - -local function get_cur_filters() - return dfhack.buildings.getFiltersByType({}, uibs.building_type, - uibs.building_subtype, uibs.custom_type) -end - -local function is_choosing_area() - return uibs.selection_pos.x >= 0 -end - --- TODO: reuse data in quickfort database -local function get_selection_size_limits() - local btype = uibs.building_type - if btype == df.building_type.Bridge - or btype == df.building_type.FarmPlot - or btype == df.building_type.RoadPaved - or btype == df.building_type.RoadDirt then - return {w=31, h=31} - elseif btype == df.building_type.AxleHorizontal then - return uibs.direction == 1 and {w=1, h=31} or {w=31, h=1} - elseif btype == df.building_type.Rollers then - return (uibs.direction == 1 or uibs.direction == 3) and {w=31, h=1} or {w=1, h=31} - end -end - -local function get_selected_bounds(selection_pos, pos) - selection_pos = selection_pos or uibs.selection_pos - if not is_choosing_area() then return end - - pos = pos or uibs.pos - - local bounds = { - x1=math.min(selection_pos.x, pos.x), - x2=math.max(selection_pos.x, pos.x), - y1=math.min(selection_pos.y, pos.y), - y2=math.max(selection_pos.y, pos.y), - z1=math.min(selection_pos.z, pos.z), - z2=math.max(selection_pos.z, pos.z), - } - - -- clamp to map edges - bounds = { - x1=math.max(0, bounds.x1), - x2=math.min(df.global.world.map.x_count-1, bounds.x2), - y1=math.max(0, bounds.y1), - y2=math.min(df.global.world.map.y_count-1, bounds.y2), - z1=math.max(0, bounds.z1), - z2=math.min(df.global.world.map.z_count-1, bounds.z2), - } - - local limits = get_selection_size_limits() - if limits then - -- clamp to building type area limit - bounds = { - x1=math.max(selection_pos.x - (limits.w-1), bounds.x1), - x2=math.min(selection_pos.x + (limits.w-1), bounds.x2), - y1=math.max(selection_pos.y - (limits.h-1), bounds.y1), - y2=math.min(selection_pos.y + (limits.h-1), bounds.y2), - z1=bounds.z1, - z2=bounds.z2, - } - end - - return bounds -end - -local function get_cur_area_dims(bounds) - if not bounds and not is_choosing_area() then return 1, 1, 1 end - bounds = bounds or get_selected_bounds() - if not bounds then return 1, 1, 1 end - return bounds.x2 - bounds.x1 + 1, - bounds.y2 - bounds.y1 + 1, - bounds.z2 - bounds.z1 + 1 -end - -local function get_selected_volume(bounds) - local w, h, depth = get_cur_area_dims(bounds) - return w * h * depth -end - -local function is_pressure_plate() - return uibs.building_type == df.building_type.Trap - and uibs.building_subtype == df.trap_type.PressurePlate -end - -local function is_weapon_trap() - return uibs.building_type == df.building_type.Trap - and uibs.building_subtype == df.trap_type.WeaponTrap -end - -local function is_spike_trap() - return uibs.building_type == df.building_type.Weapon -end - -local function is_weapon_or_spike_trap() - return is_weapon_trap() or is_spike_trap() -end - -local function is_construction() - return uibs.building_type == df.building_type.Construction -end - -local function is_siege_engine() - return uibs.building_type == df.building_type.SiegeEngine -end - -local function tile_is_construction(pos) - local tt = dfhack.maps.getTileType(pos) - if not tt then return false end - if df.tiletype.attrs[tt].material ~= df.tiletype_material.CONSTRUCTION then - return false - end - local construction = df.construction.find(pos) - return construction and not construction.flags.top_of_wall -end - -local ONE_BY_ONE = xy2pos(1, 1) - -local function can_reconstruct(bounds) - return get_selected_volume(bounds) == 1 or require('plugins.buildingplan').getGlobalSettings().reconstruct -end - -local function can_place_construction(reconstruct, pos) - return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and (reconstruct or not tile_is_construction(pos)) -end - -local function is_interior(bounds, x, y) - return x ~= bounds.x1 and x ~= bounds.x2 and - y ~= bounds.y1 and y ~= bounds.y2 -end - --- adjusted from CycleHotkeyLabel on the planner panel -local weapon_quantity = 1 - -local function get_quantity(filter, hollow, bounds) - if is_pressure_plate() then - local flags = uibs.plate_info.flags - return (flags.units and 1 or 0) + (flags.water and 1 or 0) + - (flags.magma and 1 or 0) + (flags.track and 1 or 0) - elseif (is_weapon_trap() and filter.vector_id == df.job_item_vector_id.ANY_WEAPON) or is_spike_trap() then - return weapon_quantity - end - local quantity = filter.quantity or 1 - bounds = bounds or get_selected_bounds() - local dimx, dimy, dimz = get_cur_area_dims(bounds) - if quantity < 1 then - return (((dimx * dimy) // 4) + 1) * dimz - end - if bounds and is_construction() then - local reconstruct = can_reconstruct(bounds) - local count = 0 - for z = bounds.z1, bounds.z2 do - for y = bounds.y1, bounds.y2 do - for x = bounds.x1, bounds.x2 do - if hollow and is_interior(bounds, x, y) then goto continue end - if can_place_construction(reconstruct, xyz2pos(x, y, z)) then - count = count + 1 - end - ::continue:: - end - end - end - return quantity * count - end - return quantity * get_selected_volume(bounds) -end - -local function cur_building_has_no_area() - if uibs.building_type == df.building_type.Construction then return false end - local filters = dfhack.buildings.getFiltersByType({}, - uibs.building_type, uibs.building_subtype, uibs.custom_type) - -- this works because all variable-size buildings have either no item - -- filters or a quantity of -1 for their first (and only) item - return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) -end - -local function is_tutorial_open() - local help = df.global.game.main_interface.help - return help.open and - help.context == df.help_context_type.START_TUTORIAL_WORKSHOPS_AND_TASKS -end - -local function is_plannable() - return not is_tutorial_open() and - get_cur_filters() and - not (is_construction() and - uibs.building_subtype == df.construction_type.TrackNSEW) -end - -local function is_slab() - return uibs.building_type == df.building_type.Slab -end - -local function is_cage() - return uibs.building_type == df.building_type.Cage -end - -local function is_stairs() - return is_construction() - and uibs.building_subtype == df.construction_type.UpDownStair -end - -local function is_single_level_stairs() - if not is_stairs() then return false end - local _, _, dimz = get_cur_area_dims() - return dimz == 1 -end - -local function is_multi_level_stairs() - if not is_stairs() then return false end - local _, _, dimz = get_cur_area_dims() - return dimz > 1 -end - -local direction_panel_frame = {t=4, h=13, w=46, r=28} - -local direction_panel_types = utils.invert{ - df.building_type.Bridge, - df.building_type.ScrewPump, - df.building_type.WaterWheel, - df.building_type.AxleHorizontal, - df.building_type.Rollers, - df.building_type.SiegeEngine, -} - -local function has_direction_panel() - return direction_panel_types[uibs.building_type] - or (uibs.building_type == df.building_type.Trap - and uibs.building_subtype == df.trap_type.TrackStop) -end - -local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} - -local function has_pressure_plate_panel() - return is_pressure_plate() -end - -local function is_over_options_panel() - local frame = nil - if has_direction_panel() then - frame = direction_panel_frame - elseif has_pressure_plate_panel() then - frame = pressure_plate_panel_frame - else - return false - end - local v = widgets.Widget{frame=frame} - local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) - v:updateLayout(gui.ViewRect{rect=rect}) - return v:getMousePos() -end - -local function compress(str, len) - if #str <= len then - return str - else - local no_vowels = str:gsub('[aeiou]','') - if #no_vowels <= len then - return no_vowels - else - return no_vowels:sub(1,len-3)..'...' - end - end -end - -local function filter_string(mats, cats, length) - local enabled_mat_names = {} - local enabled_cat_names = {} - for name, props in pairs(mats) do - local enabled = props.enabled == 'true' and cats[props.category] - if enabled then table.insert(enabled_mat_names, name) end - end - if #enabled_mat_names == 1 then - return '['..compress(enabled_mat_names[1], length)..']' - elseif #enabled_mat_names > 1 then - for cat, _ in pairs(cats) do - if cat ~= 'unset' and cats[cat] then - table.insert(enabled_cat_names, cat) - end - end - if #enabled_cat_names == 1 then - return '[' .. enabled_cat_names[1]:gsub("^%l", string.upper) .. ']' - else - return '['..#enabled_cat_names..' mat. categories]' - end - else - -- can result from selecting wood and then toggling "fire safe" etc. - return '[impossible filter]' - end -end --------------------------------- --- ItemLine --- - --- number of characters for item filter summary (excluding surrounding [ ]) -local item_filter_chars = 17 - -ItemLine = defclass(ItemLine, widgets.Panel) -ItemLine.ATTRS{ - idx=DEFAULT_NIL, - is_selected_fn=DEFAULT_NIL, - is_hollow_fn=DEFAULT_NIL, - on_select=DEFAULT_NIL, - on_filter=DEFAULT_NIL, - on_clear_filter=DEFAULT_NIL, -} - -function ItemLine:init() - self.frame.h = 2 - self.visible = function() return #get_cur_filters() >= self.idx end - self:addviews{ - widgets.Label{ - view_id='item_symbol', - frame={t=0, l=0}, - text=string.char(16), -- this is the "►" character - text_pen=COLOR_YELLOW, - auto_width=true, - visible=self.is_selected_fn, - }, - widgets.Label{ - view_id='item_desc', - frame={t=0, l=2}, - text={ - {text=self:callback('get_item_line_text'), - pen=function() return gui.invert_color(COLOR_WHITE, self.is_selected_fn()) end}, - }, - }, - widgets.Label{ - view_id='item_filter', - frame={t=0, l=28}, - text={ - {text=self:callback('get_filter_text'), - width=item_filter_chars+2, - rjustify=true, - pen=function() return - self:is_impossible() and COLOR_RED or - gui.invert_color(COLOR_LIGHTCYAN, self.is_selected_fn()) end}, - }, - auto_width=true, - on_click=function() self.on_filter(self.idx) end, - }, - widgets.Label{ - frame={t=0, l=47}, - text='[clear]', - text_pen=COLOR_LIGHTRED, - auto_width=true, - visible=self:callback('has_filter'), - on_click=function() self.on_clear_filter(self.idx) end, - }, - widgets.Label{ - frame={t=1, l=2}, - text={ - {gap=2, text=function() return self.note end, - pen=function() return self.note_pen end}, - }, - }, - } -end - -function ItemLine:reset() - self.desc = nil - self.available = nil -end - -function ItemLine:onInput(keys) - if keys._MOUSE_L and self:getMousePos() then - self.on_select(self.idx) - end - return ItemLine.super.onInput(self, keys) -end - -function ItemLine:get_item_line_text() - local idx = self.idx - local filter = get_cur_filters()[idx] - local quantity = get_quantity(filter, self.is_hollow_fn()) - - local buildingplan = require('plugins.buildingplan') - self.desc = self.desc or buildingplan.get_desc(filter) - - self.available = self.available or buildingplan.countAvailableItems( - uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) - if self.available >= quantity then - self.note_pen = COLOR_GREEN - self.note = (' %d available now'):format(self.available) - elseif self.available >= 0 then - self.note_pen = COLOR_BROWN - self.note = (' Will link next (need to make %d)'):format(quantity - self.available) - else - self.note_pen = COLOR_BROWN - self.note = (' Will link later (need to make %d)'):format(-self.available + quantity) - end - self.note = string.char(192) .. self.note -- character 192 is "└" - - return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') -end - -function ItemLine:has_filter() - return require('plugins.buildingplan').hasFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) -end - -function ItemLine:get_filter_text() - local buildingplan = require('plugins.buildingplan') - if not buildingplan.hasFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) - then - return '[any material]' - end - local mats = buildingplan.getMaterialFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) - local cats = buildingplan.getMaterialMaskFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) - return filter_string(mats, cats, item_filter_chars) -end - --- short circuit version of the '[impossible filter]' case above -function ItemLine:is_impossible() - local buildingplan = require('plugins.buildingplan') - local mats = buildingplan.getMaterialFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) - local cats = buildingplan.getMaterialMaskFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) - for _,props in pairs(mats) do - local enabled = props.enabled == 'true' and cats[props.category] - if enabled then return false end - end - return true -end - -function ItemLine:reduce_quantity(used_quantity) - if not self.available then return end - local filter = get_cur_filters()[self.idx] - used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn()) - self.available = self.available - used_quantity -end - -local function get_placement_errors() - local out = '' - for _,str in ipairs(uibs.errors) do - if #out > 0 then out = out .. NEWLINE end - out = out .. str.value - end - return out -end - --------------------------------- --- QuickFilter --- - --- Used to store a table of the following format: --- table --- string: quick filter slot (must be strings because of the way persistence works) --- label: string representation of the filter --- mats: list of material names allowed by the filter -BUILDINGPLAN_FILTERS_KEY = "buildingplan/quick-filters" - --- old saves may use numbers as keys, which we convert to string keys on load -dfhack.onStateChange[BUILDINGPLAN_FILTERS_KEY] = function(sc) - if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return - end - local saved_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) - local new_filters = {} - for k, v in pairs(saved_filters) do - if type(k) == 'number' then - new_filters[tostring(k)] = v - elseif type(k) == 'string' then - new_filters[k] = v - end - end - dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, new_filters) -end - -QuickFilter = defclass(QuickFilter, widgets.Panel) -QuickFilter.ATTRS{ - idx=DEFAULT_NIL, - on_click_fn=DEFAULT_NIL, - is_selected_fn=DEFAULT_NIL -} - -function QuickFilter:init() - local label = ('%d.'):format(self.idx) - self.frame.w = 27 - self.renaming = false - - self:addviews { - widgets.Label { - frame = { t = 0, l = 0 }, - text = string.char(16), -- this is the "►" character - text_pen = COLOR_YELLOW, - auto_width = true, - visible = self.is_selected_fn, - }, - widgets.Label { frame = { t = 0, l = 2 }, text = label }, - widgets.Label { - frame = { t = 0, l = 5, w = item_filter_chars + 2 }, - text = { { text = self:callback('get_label_text'), pen = function() return COLOR_CYAN end } }, - visible = function() return self.renaming == false end, - on_click = self:callback("on_click"), - }, - widgets.EditField { - view_id = 'edit_field', - frame = { t = 0, l = 5, w = item_filter_chars + 2 }, - text = "", - visible = function() return self.renaming == true end, - on_submit = function(text) self:submit_name(text) end, - }, - widgets.Label { - frame = { t = 0, r = 0, w = 3 }, - text = "[x]", - text_pen = COLOR_LIGHTRED, - visible = self:callback("slot_used"), - on_click = self:callback("clear") - } - } -end - -function QuickFilter:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R then - if self.renaming then - self.subviews.edit_field:setFocus(false) - self.renaming = false - editing_filters_flag = false - return true - else - return false - end - end - return QuickFilter.super.onInput(self, keys) -end - -function QuickFilter:on_click() - if dfhack.internal.getModifiers().shift and - self:slot_used() and not editing_filters_flag - then - self.subviews.edit_field:setText(self:get_label_text()) - self.renaming = true - editing_filters_flag = true - self.subviews.edit_field:setFocus(true) - else - self.on_click_fn(self.idx) -- save/apply filter based on selected ItemLine - end -end - -function QuickFilter:slot_used() - local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) - return quick_filters[self.idx] ~= nil -end - -function QuickFilter:clear() - local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) - quick_filters[self.idx] = nil - dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) -end - -function QuickFilter:get_label_text() - local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) - local set = quick_filters[self.idx] - if not set then - return "empty" - else - return set.label - end -end - -function QuickFilter:submit_name(text) - local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) - quick_filters[self.idx].label = compress(text, item_filter_chars+2) - dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) - self.renaming = false - editing_filters_flag = false -end --------------------------------- --- PlannerOverlay --- - -PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) -PlannerOverlay.ATTRS{ - desc='Shows the building planner interface panel when building buildings.', - default_pos={x=5,y=9}, - default_enabled=true, - viewscreens='dwarfmode/Building/Placement', - frame={w=56, h=32}, -} - -function PlannerOverlay:init() - self.selected = 1 - self.state = ensure_key(config.data, 'planner') - - self.selected_favorite = '1' - - local main_panel = widgets.Panel{ - view_id='main', - frame={t=1, l=0, r=0, h=14}, - frame_style=gui.FRAME_INTERIOR_MEDIUM, - frame_background=gui.CLEAR_PEN, - visible=self:callback('is_not_minimized'), - } - - local minimized_panel = widgets.Panel{ - frame={t=0, r=1, w=20, h=1}, - subviews={ - widgets.Label{ - frame={t=0, r=3, h=1}, - text={ - {text=' show Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, - {text='['..string.char(31)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, - }, - visible=self:callback('is_minimized'), - on_click=self:callback('toggle_minimized'), - }, - widgets.Label{ - frame={t=0, r=3, h=1}, - text={ - {text=' hide Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, - {text='['..string.char(30)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, - }, - visible=self:callback('is_not_minimized'), - on_click=self:callback('toggle_minimized'), - }, - widgets.HelpButton{ - frame={t=0, r=0}, - command='buildingplan', - } - }, - } - - local function make_is_selected_fn(idx) - return function() return self.selected == idx end - end - - local function on_select_fn(idx) - self.selected = idx - end - - local function is_hollow_fn() - return self.subviews.hollow:getOptionValue() - end - - local buildingplan = require('plugins.buildingplan') - - main_panel:addviews{ - widgets.Label{ - frame={}, - auto_width=true, - text='No items required.', - visible=function() return #get_cur_filters() == 0 end, - }, - ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, - is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn, - on_select=on_select_fn, on_filter=self:callback('set_filter'), - on_clear_filter=self:callback('clear_filter')}, - ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, - is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn, - on_select=on_select_fn, on_filter=self:callback('set_filter'), - on_clear_filter=self:callback('clear_filter')}, - ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, - is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn, - on_select=on_select_fn, on_filter=self:callback('set_filter'), - on_clear_filter=self:callback('clear_filter')}, - ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, - is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn, - on_select=on_select_fn, on_filter=self:callback('set_filter'), - on_clear_filter=self:callback('clear_filter')}, - widgets.CycleHotkeyLabel{ - view_id='hollow', - frame={b=4, l=1, w=21}, - key='CUSTOM_H', - label='Hollow area:', - visible=is_construction, - options={ - {label='No', value=false}, - {label='Yes', value=true, pen=COLOR_GREEN}, - }, - }, - widgets.CycleHotkeyLabel{ - view_id='stairs_top_subtype', - frame={b=7, l=1, w=30}, - key='CUSTOM_R', - label='Top stair type: ', - visible=is_multi_level_stairs, - options={ - {label='Auto', value='auto'}, - {label='UpDown', value=df.construction_type.UpDownStair}, - {label='Down', value=df.construction_type.DownStair}, - }, - }, - widgets.CycleHotkeyLabel { - view_id='stairs_bottom_subtype', - frame={b=6, l=1, w=30}, - key='CUSTOM_B', - label='Bottom Stair Type:', - visible=is_multi_level_stairs, - options={ - {label='Auto', value='auto'}, - {label='UpDown', value=df.construction_type.UpDownStair}, - {label='Up', value=df.construction_type.UpStair}, - }, - }, - widgets.CycleHotkeyLabel{ - view_id='stairs_only_subtype', - frame={b=7, l=1, w=30}, - key='CUSTOM_R', - label='Single level stair:', - visible=is_single_level_stairs, - options={ - {label='Up', value=df.construction_type.UpStair}, - {label='UpDown', value=df.construction_type.UpDownStair}, - {label='Down', value=df.construction_type.DownStair}, - }, - }, - widgets.CycleHotkeyLabel { -- TODO: this thing also needs a slider - view_id='weapons', - frame={b=4, l=1, w=28}, - key='CUSTOM_T', - key_back='CUSTOM_SHIFT_T', - label='Number of weapons:', - visible=is_weapon_or_spike_trap, - options=utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10), - on_change=function(val) weapon_quantity = val end, - }, - widgets.ToggleHotkeyLabel { - view_id='engraved', - frame={b=4, l=1, w=22}, - key='CUSTOM_T', - label='Engraved only:', - visible=is_slab, - on_change=function(val) - buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) - end, - }, - widgets.ToggleHotkeyLabel { - view_id='empty', - frame={b=4, l=1, w=22}, - key='CUSTOM_T', - label='Empty only:', - visible=is_cage, - on_change=function(val) - buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'empty', val) - end, - }, - widgets.Panel{ - visible=function() return #get_cur_filters() > 0 end, - subviews={ - widgets.HotkeyLabel{ - frame={b=2, l=1, w=22}, - key='CUSTOM_F', - label=function() - return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) - and 'Edit filter' or 'Set filter' - end, - on_activate=function() self:set_filter(self.selected) end, - }, - widgets.HotkeyLabel{ - frame={b=1, l=1, w=22}, - key='CUSTOM_CTRL_D', - label='Delete filter', - on_activate=function() self:clear_filter(self.selected) end, - enabled=function() - return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) - end - }, - widgets.CycleHotkeyLabel{ - view_id='show_favorites', - frame={b=0, l=1, w=22}, - key='CUSTOM_CTRL_F', - label="", - option_gap=0, - options={ - { label='Show favorites', value = false }, - { label='Hide favorites', value = true }, - }, - initial_option=false, - on_change=function(new,_) self:show_hide_favorites(new) end, - }, - widgets.CycleHotkeyLabel{ - view_id='choose', - frame={b=0, l=24}, - key='CUSTOM_Z', - label='Choose items:', - label_below=true, - options={ - {label='With filters', value=0}, - { - label=function() - local automaterial = itemselection.get_automaterial_selection(uibs.building_type) - return ('Last used (%s)'):format(automaterial or 'pick manually') - end, - value=2, - }, - {label='Manually', value=1}, - }, - initial_option=0, - on_change=function(choose) - buildingplan.setChooseItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, choose) - end, - }, - widgets.CycleHotkeyLabel{ - view_id='safety', - frame={b=2, l=24, w=25}, - key='CUSTOM_G', - label='Building safety:', - options={ - {label='Any', value=0}, - {label='Magma', value=2, pen=COLOR_RED}, - {label='Fire', value=1, pen=COLOR_LIGHTRED}, - }, - initial_option=0, - on_change=function(heat) - buildingplan.setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) - end, - }, - }, - }, - } - - local divider_widget = widgets.Divider{ - frame={t=10, l=0, r=0, h=1}, - frame_style=gui.FRAME_INTERIOR_MEDIUM, - visible=self:callback('is_not_minimized'), - } - - local error_panel = widgets.ResizingPanel{ - view_id='errors', - frame={t=15, l=0, r=0}, - frame_style=gui.BOLD_FRAME, - frame_background=gui.CLEAR_PEN, - visible=self:callback('is_not_minimized'), - } - - error_panel:addviews{ - widgets.WrappedLabel{ - frame={t=0, l=1, r=0}, - text_pen=COLOR_LIGHTRED, - text_to_wrap=get_placement_errors, - visible=function() return #uibs.errors > 0 end, - }, - widgets.Label{ - frame={t=0, l=1, r=0}, - text_pen=COLOR_GREEN, - text='OK to build', - visible=function() return #uibs.errors == 0 end, - }, - } - - local prev_next_selector = widgets.Panel{ - frame={h=1}, - auto_width=true, - subviews={ - widgets.HotkeyLabel{ - frame={t=0, l=1, w=9}, - key='CUSTOM_SHIFT_Q', - key_sep='\0', - label=': Prev/', - on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, - }, - widgets.HotkeyLabel{ - frame={t=0, l=2, w=1}, - key='CUSTOM_Q', - on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, - }, - widgets.Label{ - frame={t=0,l=10}, - text='next item', - on_click=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, - }, - }, - visible=function() return #get_cur_filters() > 1 end, - } - - local black_bar = widgets.Panel{ - frame={t=0, l=1, w=37, h=1}, - frame_inset=0, - frame_background=gui.CLEAR_PEN, - visible=self:callback('is_not_minimized'), - subviews={ - prev_next_selector, - }, - } - - local function make_is_selected_filter(idx) - return function () return self.selected_favorite == idx end - end - - local favorites_panel = widgets.Panel{ - view_id='favorites', - frame={t=15, l=0, r=0, h=11}, - frame_style=gui.FRAME_INTERIOR_MEDIUM, - frame_background=gui.CLEAR_PEN, - visible=self:callback('show_favorites'), - subviews={ - QuickFilter{idx='1', frame={t=0,l=0}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('1') }, - QuickFilter{idx='2', frame={t=1,l=0}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('2') }, - QuickFilter{idx='3', frame={t=2,l=0}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('3') }, - QuickFilter{idx='4', frame={t=3,l=0}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('4') }, - QuickFilter{idx='5', frame={t=4,l=0}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('5') }, - QuickFilter{idx='6', frame={t=0,l=27}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('6') }, - QuickFilter{idx='7', frame={t=1,l=27}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('7') }, - QuickFilter{idx='8', frame={t=2,l=27}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('8') }, - QuickFilter{idx='9', frame={t=3,l=27}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('9') }, - QuickFilter{idx='0', frame={t=4,l=27}, - on_click_fn=self:callback("save_restore_filter"), - is_selected_fn=make_is_selected_filter('0') }, - widgets.CycleHotkeyLabel { - view_id='slot_select', - frame={b=2, l=2}, - key='CUSTOM_X', - key_back='CUSTOM_SHIFT_X', - label='next/previous slot', - auto_width=true, - options=utils.tabulate(function(i) return {label="", value=tostring(i)} end, 0, 9), - initial_option='1', - on_change=function(val) self.selected_favorite = val end, - }, - widgets.HotkeyLabel{ - frame={b=2, l=28}, - label="set/apply selected", - key='CUSTOM_Y', - on_activate=function () self:save_restore_filter(self.selected_favorite) end, - }, - widgets.TooltipLabel { - frame={b=0, l=2}, - show_tooltip=true, - text="Shift+click to edit the label of a favorite", - }, - - } - } - - self:addviews{ - black_bar, - minimized_panel, - main_panel, - divider_widget, - error_panel, - favorites_panel - } -end - -function PlannerOverlay:show_favorites() - return not self.state.minimized and self.subviews.show_favorites:getOptionValue() -end - -function PlannerOverlay:show_hide_favorites(new) - local errors_frame = {t=15+(new and 11 or 0), l=0, r=0} - self.subviews.errors.frame = errors_frame - self:updateLayout() -end - -function PlannerOverlay:save_restore_filter(slot) - self.selected_favorite = slot - local buildingplan = require('plugins.buildingplan') - local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) - if quick_filters[slot] then -- restore saved filter - buildingplan.setMaterialFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1, - quick_filters[slot].mats - ) - else -- save current filter - - if not buildingplan.hasFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) - then return end - - local mats = buildingplan.getMaterialFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) - local cats = buildingplan.getMaterialMaskFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) - local label = filter_string(mats, cats, item_filter_chars) - local enabled_mats = {} - for mat, props in pairs(mats) do - if props.enabled == "true" and cats[props.category] then - table.insert(enabled_mats, mat) - end - end - if #enabled_mats > 0 then - quick_filters[slot] = { label = label, mats = enabled_mats } - dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) - end - end -end - - -function PlannerOverlay:is_minimized() - return self.state.minimized -end - -function PlannerOverlay:is_not_minimized() - return not self.state.minimized -end - -function PlannerOverlay:toggle_minimized() - self.state.minimized = not self.state.minimized - config:write() - self:reset() -end - -function PlannerOverlay:reset() - self.subviews.item1:reset() - self.subviews.item2:reset() - self.subviews.item3:reset() - self.subviews.item4:reset() - reset_counts_flag = false -end - -function PlannerOverlay:set_filter(idx) - filterselection.FilterSelectionScreen{index=idx, desc=require('plugins.buildingplan').get_desc(get_cur_filters()[idx])}:show() -end - -function PlannerOverlay:clear_filter(idx) - desc=require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) -end - -local function get_placement_data() - local direction = uibs.direction - local bounds = get_selected_bounds() - local width, height, depth = get_cur_area_dims(bounds) - local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( - width, height, uibs.building_type, uibs.building_subtype, - uibs.custom_type, direction) - -- get the upper-left corner of the building/area at min z-level - local start_pos = bounds and xyz2pos(bounds.x1, bounds.y1, bounds.z1) or - xyz2pos( - uibs.pos.x - adjusted_width//2, - uibs.pos.y - adjusted_height//2, - uibs.pos.z) - if uibs.building_type == df.building_type.ScrewPump then - if direction == df.screw_pump_direction.FromSouth then - start_pos.y = start_pos.y + 1 - elseif direction == df.screw_pump_direction.FromEast then - start_pos.x = start_pos.x + 1 - end - end - local min_x, max_x = start_pos.x, start_pos.x - local min_y, max_y = start_pos.y, start_pos.y - local min_z, max_z = start_pos.z, start_pos.z - if adjusted_width == 1 and adjusted_height == 1 - and (width > 1 or height > 1 or depth > 1) then - max_x = min_x + width - 1 - max_y = min_y + height - 1 - max_z = math.max(uibs.selection_pos.z, uibs.pos.z) - end - return { - x1=min_x, y1=min_y, z1=min_z, - x2=max_x, y2=max_y, z2=max_z, - width=adjusted_width, - height=adjusted_height - } -end - -function PlannerOverlay:save_placement() - self.saved_placement = get_placement_data() - if (uibs.selection_pos:isValid()) then - self.saved_selection_pos_valid = true - self.saved_selection_pos = copyall(uibs.selection_pos) - self.saved_pos = copyall(uibs.pos) - uibs.selection_pos:clear() - else - local sp = self.saved_placement - self.saved_selection_pos = xyz2pos(sp.x1, sp.y1, sp.z1) - self.saved_pos = xyz2pos(sp.x2, sp.y2, sp.z2) - self.saved_pos.x = self.saved_pos.x + sp.width - 1 - self.saved_pos.y = self.saved_pos.y + sp.height - 1 - end -end - -function PlannerOverlay:restore_placement() - if self.saved_selection_pos_valid then - uibs.selection_pos = self.saved_selection_pos - self.saved_selection_pos_valid = nil - else - uibs.selection_pos:clear() - end - self.saved_selection_pos = nil - self.saved_pos = nil - local placement_data = self.saved_placement - self.saved_placement = nil - return placement_data -end - -function PlannerOverlay:onInput(keys) - if not is_plannable() then return false end - if PlannerOverlay.super.onInput(self, keys) then - return true - end - if keys.LEAVESCREEN or keys._MOUSE_R then - if uibs.selection_pos:isValid() then - uibs.selection_pos:clear() - return true - end - self.selected = 1 - self.subviews.hollow:setOption(false) - self:reset() - reset_counts_flag = true - return false - end - if keys.CUSTOM_ALT_M then - self:toggle_minimized() - return true - end - if self:is_minimized() then return false end - if keys._MOUSE_L then - if is_over_options_panel() then return false end - local detect_rect = copyall(self.frame_rect) - detect_rect.height = self.subviews.main.frame_rect.height + - self.subviews.errors.frame_rect.height - detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1 - if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect}) - or self.subviews.errors:getMousePos() then - return true - end - if not is_construction() and #uibs.errors > 0 then return true end - if dfhack.gui.getMousePos() then - if is_choosing_area() or cur_building_has_no_area() then - local filters = get_cur_filters() - local num_filters = #filters - local choose = self.subviews.choose:getOptionValue() - if choose == 0 then - self:place_building(get_placement_data()) - else - local bounds = get_selected_bounds() - self:save_placement() - local autoselect = choose == 2 - local is_hollow = self.subviews.hollow:getOptionValue() - local chosen_items, active_screens = {}, {} - local pending = num_filters - df.global.game.main_interface.bottom_mode_selected = -1 - for idx = num_filters,1,-1 do - chosen_items[idx] = {} - local filter = filters[idx] - local get_available_items_fn = function() - return require('plugins.buildingplan').getAvailableItems( - uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) - end - local selection_screen = itemselection.ItemSelectionScreen{ - get_available_items_fn=get_available_items_fn, - desc=require('plugins.buildingplan').get_desc(filter), - quantity=get_quantity(filter, is_hollow, bounds), - autoselect=autoselect, - on_submit=function(items) - chosen_items[idx] = items - if active_screens[idx] then - active_screens[idx]:dismiss() - active_screens[idx] = nil - else - active_screens[idx] = true - end - pending = pending - 1 - if pending == 0 then - df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT - self:place_building(self:restore_placement(), chosen_items) - end - end, - on_cancel=function() - for _,scr in pairs(active_screens) do - scr:dismiss() - end - df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT - self:restore_placement() - end, - } - if active_screens[idx] then - -- we've already returned via autoselect - active_screens[idx] = nil - else - active_screens[idx] = selection_screen:show() - end - end - end - return true - elseif not is_choosing_area() then - return false - end - end - end - return keys._MOUSE_L or keys.SELECT -end - -function PlannerOverlay:render(dc) - if not is_plannable() then return end - self.subviews.errors:updateLayout() - PlannerOverlay.super.render(self, dc) -end - -function PlannerOverlay:onRenderFrame(dc, rect) - PlannerOverlay.super.onRenderFrame(self, dc, rect) - - if reset_counts_flag then - self:reset() - local buildingplan = require('plugins.buildingplan') - self.subviews.engraved:setOption(buildingplan.getSpecials( - uibs.building_type, uibs.building_subtype, uibs.custom_type).engraved or false) - self.subviews.empty:setOption(buildingplan.getSpecials( - uibs.building_type, uibs.building_subtype, uibs.custom_type).empty or false) - self.subviews.choose:setOption(buildingplan.getChooseItems( - uibs.building_type, uibs.building_subtype, uibs.custom_type)) - self.subviews.safety:setOption(buildingplan.getHeatSafetyFilter( - uibs.building_type, uibs.building_subtype, uibs.custom_type)) - end - - if self:is_minimized() then return end - - local bounds = get_selected_bounds(self.saved_selection_pos, self.saved_pos) - if not bounds then return end - - local hollow = self.subviews.hollow:getOptionValue() - local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN - - -- always allow reconstruction if it's a 1x1x1 selection (meaning the player selected that spot specifically) - local reconstruct = can_reconstruct(bounds) - - local get_pen_fn = is_construction() and - function(pos) - return can_place_construction(reconstruct, pos) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN - end or function() - return default_pen - end - - local function get_overlay_pen(pos) - if not hollow then return get_pen_fn(pos) end - if pos.x == bounds.x1 or pos.x == bounds.x2 or - pos.y == bounds.y1 or pos.y == bounds.y2 then - return get_pen_fn(pos) - end - return gui.TRANSPARENT_PEN - end - - guidm.renderMapOverlay(get_overlay_pen, bounds) -end - -function PlannerOverlay:get_stairs_subtype(pos, bounds) - local subtype = uibs.building_subtype - if pos.z == bounds.z1 then - local opt = bounds.z1 == bounds.z2 and self.subviews.stairs_only_subtype:getOptionValue() or - self.subviews.stairs_bottom_subtype:getOptionValue() - if opt == 'auto' then - local tt = dfhack.maps.getTileType(pos) - local shape = df.tiletype.attrs[tt].shape - if shape ~= df.tiletype_shape.STAIR_DOWN and shape ~= df.tiletype_shape.STAIR_UPDOWN then - subtype = df.construction_type.UpStair - end - else - subtype = opt - end - elseif pos.z == bounds.z2 then - local opt = self.subviews.stairs_top_subtype:getOptionValue() - if opt == 'auto' then - local tt = dfhack.maps.getTileType(pos) - local shape = df.tiletype.attrs[tt].shape - if shape ~= df.tiletype_shape.STAIR_UP and shape ~= df.tiletype_shape.STAIR_UPDOWN then - subtype = df.construction_type.DownStair - end - else - subtype = opt - end - end - return subtype -end - -function PlannerOverlay:place_building(placement_data, chosen_items) - local pd = placement_data - local blds = {} - local hollow = self.subviews.hollow:getOptionValue() - local subtype = uibs.building_subtype - local filters = get_cur_filters() - if is_pressure_plate() or is_spike_trap() then - filters[1].quantity = get_quantity(filters[1]) - elseif is_weapon_trap() then - filters[2].quantity = get_quantity(filters[2]) - end - local reconstruct = can_reconstruct(pd) - for z=pd.z1,pd.z2 do for y=pd.y1,pd.y2 do for x=pd.x1,pd.x2 do - if hollow and is_interior(pd, x, y) then - goto continue - end - local pos = xyz2pos(x, y, z) - if is_construction() and not can_place_construction(reconstruct, pos) then - goto continue - end - if is_stairs() then - subtype = self:get_stairs_subtype(pos, pd) - end - local fields = {} - if is_siege_engine() then - local facing = df.global.buildreq.direction - fields.facing = facing - fields.resting_orientation = facing - end - local bld, err = dfhack.buildings.constructBuilding{pos=pos, - type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, - width=pd.width, height=pd.height, - direction=uibs.direction, filters=filters, fields=fields} - if err then - -- it's ok if some buildings fail to build - goto continue - end - -- assign fields for the types that need them. we can't pass them all in - -- to the call to constructBuilding since attempting to assign unrelated - -- fields to building types that don't support them causes errors. - for k in pairs(bld) do - if k == 'track_stop_info' then utils.assign(bld.track_stop_info, uibs.track_stop) end - if k == 'speed' then bld.speed = uibs.speed end - if k == 'plate_info' then utils.assign(bld.plate_info, uibs.plate_info) end - end - table.insert(blds, bld) - ::continue:: - end end end - local used_quantity = is_construction() and #blds or false - self.subviews.item1:reduce_quantity(used_quantity) - self.subviews.item2:reduce_quantity(used_quantity) - self.subviews.item3:reduce_quantity(used_quantity) - self.subviews.item4:reduce_quantity(used_quantity) - local buildingplan = require('plugins.buildingplan') - for _,bld in ipairs(blds) do - -- attach chosen items and reduce job_item quantity - if chosen_items then - local job = bld.jobs[0] - local jitems = job.job_items.elements - local num_filters = #get_cur_filters() - for idx=1,num_filters do - local item_ids = chosen_items[idx] - local jitem = jitems[num_filters-idx] - while jitem.quantity > 0 and #item_ids > 0 do - local item_id = item_ids[#item_ids] - local item = df.item.find(item_id) - if not item then - dfhack.printerr(('item no longer available: %d'):format(item_id)) - break - end - 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 - jitem.quantity = jitem.quantity - 1 - item_ids[#item_ids] = nil - end - end - end - buildingplan.addPlannedBuilding(bld) - end - buildingplan.scheduleCycle() - uibs.selection_pos:clear() -end - - -return _ENV +local _ENV = mkmodule('plugins.buildingplan.planneroverlay') local itemselection = require('plugins.buildingplan.itemselection') local filterselection = require('plugins.buildingplan.filterselection') local gui = require('gui') local guidm = require('gui.dwarfmode') local json = require('json') local overlay = require('plugins.overlay') local pens = require('plugins.buildingplan.pens') local utils = require('utils') local widgets = require('gui.widgets') require('dfhack.buildings') config = config or json.open('dfhack-config/buildingplan.json') local uibs = df.global.buildreq reset_counts_flag = false editing_filters_flag = false local function get_cur_filters() return dfhack.buildings.getFiltersByType({}, uibs.building_type, uibs.building_subtype, uibs.custom_type) end local function is_choosing_area() return uibs.selection_pos.x >= 0 end -- TODO: reuse data in quickfort database local function get_selection_size_limits() local btype = uibs.building_type if btype == df.building_type.Bridge or btype == df.building_type.FarmPlot or btype == df.building_type.RoadPaved or btype == df.building_type.RoadDirt then return {w=31, h=31} elseif btype == df.building_type.AxleHorizontal then return uibs.direction == 1 and {w=1, h=31} or {w=31, h=1} elseif btype == df.building_type.Rollers then return (uibs.direction == 1 or uibs.direction == 3) and {w=31, h=1} or {w=1, h=31} end end local function get_selected_bounds(selection_pos, pos) selection_pos = selection_pos or uibs.selection_pos if not is_choosing_area() then return end pos = pos or uibs.pos local bounds = { x1=math.min(selection_pos.x, pos.x), x2=math.max(selection_pos.x, pos.x), y1=math.min(selection_pos.y, pos.y), y2=math.max(selection_pos.y, pos.y), z1=math.min(selection_pos.z, pos.z), z2=math.max(selection_pos.z, pos.z), } -- clamp to map edges bounds = { x1=math.max(0, bounds.x1), x2=math.min(df.global.world.map.x_count-1, bounds.x2), y1=math.max(0, bounds.y1), y2=math.min(df.global.world.map.y_count-1, bounds.y2), z1=math.max(0, bounds.z1), z2=math.min(df.global.world.map.z_count-1, bounds.z2), } local limits = get_selection_size_limits() if limits then -- clamp to building type area limit bounds = { x1=math.max(selection_pos.x - (limits.w-1), bounds.x1), x2=math.min(selection_pos.x + (limits.w-1), bounds.x2), y1=math.max(selection_pos.y - (limits.h-1), bounds.y1), y2=math.min(selection_pos.y + (limits.h-1), bounds.y2), z1=bounds.z1, z2=bounds.z2, } end return bounds end local function get_cur_area_dims(bounds) if not bounds and not is_choosing_area() then return 1, 1, 1 end bounds = bounds or get_selected_bounds() if not bounds then return 1, 1, 1 end return bounds.x2 - bounds.x1 + 1, bounds.y2 - bounds.y1 + 1, bounds.z2 - bounds.z1 + 1 end local function get_selected_volume(bounds) local w, h, depth = get_cur_area_dims(bounds) return w * h * depth end local function is_pressure_plate() return uibs.building_type == df.building_type.Trap and uibs.building_subtype == df.trap_type.PressurePlate end local function is_weapon_trap() return uibs.building_type == df.building_type.Trap and uibs.building_subtype == df.trap_type.WeaponTrap end local function is_spike_trap() return uibs.building_type == df.building_type.Weapon end local function is_weapon_or_spike_trap() return is_weapon_trap() or is_spike_trap() end local function is_construction() return uibs.building_type == df.building_type.Construction end local function is_siege_engine() return uibs.building_type == df.building_type.SiegeEngine end local function tile_is_construction(pos) local tt = dfhack.maps.getTileType(pos) if not tt then return false end if df.tiletype.attrs[tt].material ~= df.tiletype_material.CONSTRUCTION then return false end local construction = df.construction.find(pos) return construction and not construction.flags.top_of_wall end local ONE_BY_ONE = xy2pos(1, 1) local function can_reconstruct(bounds) return get_selected_volume(bounds) == 1 or require('plugins.buildingplan').getGlobalSettings().reconstruct end local function can_place_construction(reconstruct, pos) return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and (reconstruct or not tile_is_construction(pos)) end local function is_interior(bounds, x, y) return x ~= bounds.x1 and x ~= bounds.x2 and y ~= bounds.y1 and y ~= bounds.y2 end -- adjusted from CycleHotkeyLabel on the planner panel local weapon_quantity = 1 local function get_quantity(filter, hollow, bounds) if is_pressure_plate() then local flags = uibs.plate_info.flags return (flags.units and 1 or 0) + (flags.water and 1 or 0) + (flags.magma and 1 or 0) + (flags.track and 1 or 0) elseif (is_weapon_trap() and filter.vector_id == df.job_item_vector_id.ANY_WEAPON) or is_spike_trap() then return weapon_quantity end local quantity = filter.quantity or 1 bounds = bounds or get_selected_bounds() local dimx, dimy, dimz = get_cur_area_dims(bounds) if quantity < 1 then return (((dimx * dimy) // 4) + 1) * dimz end if bounds and is_construction() then local reconstruct = can_reconstruct(bounds) local count = 0 for z = bounds.z1, bounds.z2 do for y = bounds.y1, bounds.y2 do for x = bounds.x1, bounds.x2 do if hollow and is_interior(bounds, x, y) then goto continue end if can_place_construction(reconstruct, xyz2pos(x, y, z)) then count = count + 1 end ::continue:: end end end return quantity * count end return quantity * get_selected_volume(bounds) end local function cur_building_has_no_area() if uibs.building_type == df.building_type.Construction then return false end local filters = dfhack.buildings.getFiltersByType({}, uibs.building_type, uibs.building_subtype, uibs.custom_type) -- this works because all variable-size buildings have either no item -- filters or a quantity of -1 for their first (and only) item return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) end local function is_tutorial_open() local help = df.global.game.main_interface.help return help.open and help.context == df.help_context_type.START_TUTORIAL_WORKSHOPS_AND_TASKS end local function is_plannable() return not is_tutorial_open() and get_cur_filters() and not (is_construction() and uibs.building_subtype == df.construction_type.TrackNSEW) end local function is_slab() return uibs.building_type == df.building_type.Slab end local function is_cage() return uibs.building_type == df.building_type.Cage end local function is_stairs() return is_construction() and uibs.building_subtype == df.construction_type.UpDownStair end local function is_single_level_stairs() if not is_stairs() then return false end local _, _, dimz = get_cur_area_dims() return dimz == 1 end local function is_multi_level_stairs() if not is_stairs() then return false end local _, _, dimz = get_cur_area_dims() return dimz > 1 end local direction_panel_frame = {t=4, h=13, w=46, r=28} local direction_panel_types = utils.invert{ df.building_type.Bridge, df.building_type.ScrewPump, df.building_type.WaterWheel, df.building_type.AxleHorizontal, df.building_type.Rollers, df.building_type.SiegeEngine, } local function has_direction_panel() return direction_panel_types[uibs.building_type] or (uibs.building_type == df.building_type.Trap and uibs.building_subtype == df.trap_type.TrackStop) end local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} local function has_pressure_plate_panel() return is_pressure_plate() end local function is_over_options_panel() local frame = nil if has_direction_panel() then frame = direction_panel_frame elseif has_pressure_plate_panel() then frame = pressure_plate_panel_frame else return false end local v = widgets.Widget{frame=frame} local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) v:updateLayout(gui.ViewRect{rect=rect}) return v:getMousePos() end local function compress(str, len) if #str <= len then return str else local no_vowels = str:gsub('[aeiou]','') if #no_vowels <= len then return no_vowels else return no_vowels:sub(1,len-3)..'...' end end end local function filter_string(mats, cats, length) local enabled_mat_names = {} local enabled_cat_names = {} for name, props in pairs(mats) do local enabled = props.enabled == 'true' and cats[props.category] if enabled then table.insert(enabled_mat_names, name) end end if #enabled_mat_names == 1 then return '['..compress(enabled_mat_names[1], length)..']' elseif #enabled_mat_names > 1 then for cat, _ in pairs(cats) do if cat ~= 'unset' and cats[cat] then table.insert(enabled_cat_names, cat) end end if #enabled_cat_names == 1 then return '[' .. enabled_cat_names[1]:gsub("^%l", string.upper) .. ']' else return '['..#enabled_cat_names..' mat. categories]' end else -- can result from selecting wood and then toggling "fire safe" etc. return '[impossible filter]' end end -------------------------------- -- ItemLine -- -- number of characters for item filter summary (excluding surrounding [ ]) local item_filter_chars = 17 ItemLine = defclass(ItemLine, widgets.Panel) ItemLine.ATTRS{ idx=DEFAULT_NIL, is_selected_fn=DEFAULT_NIL, is_hollow_fn=DEFAULT_NIL, on_select=DEFAULT_NIL, on_filter=DEFAULT_NIL, on_clear_filter=DEFAULT_NIL, } function ItemLine:init() self.frame.h = 2 self.visible = function() return #get_cur_filters() >= self.idx end self:addviews{ widgets.Label{ view_id='item_symbol', frame={t=0, l=0}, text=string.char(16), -- this is the "►" character text_pen=COLOR_YELLOW, auto_width=true, visible=self.is_selected_fn, }, widgets.Label{ view_id='item_desc', frame={t=0, l=2}, text={ {text=self:callback('get_item_line_text'), pen=function() return gui.invert_color(COLOR_WHITE, self.is_selected_fn()) end}, }, }, widgets.Label{ view_id='item_filter', frame={t=0, l=28}, text={ {text=self:callback('get_filter_text'), width=item_filter_chars+2, rjustify=true, pen=function() return self:is_impossible() and COLOR_RED or gui.invert_color(COLOR_LIGHTCYAN, self.is_selected_fn()) end}, }, auto_width=true, on_click=function() self.on_filter(self.idx) end, }, widgets.Label{ frame={t=0, l=47}, text='[clear]', text_pen=COLOR_LIGHTRED, auto_width=true, visible=self:callback('has_filter'), on_click=function() self.on_clear_filter(self.idx) end, }, widgets.Label{ frame={t=1, l=2}, text={ {gap=2, text=function() return self.note end, pen=function() return self.note_pen end}, }, }, } end function ItemLine:reset() self.desc = nil self.available = nil end function ItemLine:onInput(keys) if keys._MOUSE_L and self:getMousePos() then self.on_select(self.idx) end return ItemLine.super.onInput(self, keys) end function ItemLine:get_item_line_text() local idx = self.idx local filter = get_cur_filters()[idx] local quantity = get_quantity(filter, self.is_hollow_fn()) local buildingplan = require('plugins.buildingplan') self.desc = self.desc or buildingplan.get_desc(filter) self.available = self.available or buildingplan.countAvailableItems( uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) if self.available >= quantity then self.note_pen = COLOR_GREEN self.note = (' %d available now'):format(self.available) elseif self.available >= 0 then self.note_pen = COLOR_BROWN self.note = (' Will link next (need to make %d)'):format(quantity - self.available) else self.note_pen = COLOR_BROWN self.note = (' Will link later (need to make %d)'):format(-self.available + quantity) end self.note = string.char(192) .. self.note -- character 192 is "└" return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end function ItemLine:has_filter() return require('plugins.buildingplan').hasFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) end function ItemLine:get_filter_text() local buildingplan = require('plugins.buildingplan') if not buildingplan.hasFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) then return '[any material]' end local mats = buildingplan.getMaterialFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) local cats = buildingplan.getMaterialMaskFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) return filter_string(mats, cats, item_filter_chars) end -- short circuit version of the '[impossible filter]' case above function ItemLine:is_impossible() local buildingplan = require('plugins.buildingplan') local mats = buildingplan.getMaterialFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) local cats = buildingplan.getMaterialMaskFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) for _,props in pairs(mats) do local enabled = props.enabled == 'true' and cats[props.category] if enabled then return false end end return true end function ItemLine:reduce_quantity(used_quantity) if not self.available then return end local filter = get_cur_filters()[self.idx] used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn()) self.available = self.available - used_quantity end local function get_placement_errors() local out = '' for _,str in ipairs(uibs.errors) do if #out > 0 then out = out .. NEWLINE end out = out .. str.value end return out end -------------------------------- -- QuickFilter -- -- Used to store a table of the following format: -- table -- string: quick filter slot (must be strings because of the way persistence works) -- label: string representation of the filter -- mats: list of material names allowed by the filter BUILDINGPLAN_FILTERS_KEY = "buildingplan/quick-filters" -- old saves may use numbers as keys, which we convert to string keys on load dfhack.onStateChange[BUILDINGPLAN_FILTERS_KEY] = function(sc) if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then return end local saved_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) local new_filters = {} for k, v in pairs(saved_filters) do if type(k) == 'number' then new_filters[tostring(k)] = v elseif type(k) == 'string' then new_filters[k] = v end end dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, new_filters) end QuickFilter = defclass(QuickFilter, widgets.Panel) QuickFilter.ATTRS{ idx=DEFAULT_NIL, on_click_fn=DEFAULT_NIL, is_selected_fn=DEFAULT_NIL } function QuickFilter:init() local label = ('%d.'):format(self.idx) self.frame.w = 27 self.renaming = false self:addviews { widgets.Label { frame = { t = 0, l = 0 }, text = string.char(16), -- this is the "►" character text_pen = COLOR_YELLOW, auto_width = true, visible = self.is_selected_fn, }, widgets.Label { frame = { t = 0, l = 2 }, text = label }, widgets.Label { frame = { t = 0, l = 5, w = item_filter_chars + 2 }, text = { { text = self:callback('get_label_text'), pen = function() return COLOR_CYAN end } }, visible = function() return self.renaming == false end, on_click = self:callback("on_click"), }, widgets.EditField { view_id = 'edit_field', frame = { t = 0, l = 5, w = item_filter_chars + 2 }, text = "", visible = function() return self.renaming == true end, on_submit = function(text) self:submit_name(text) end, }, widgets.Label { frame = { t = 0, r = 0, w = 3 }, text = "[x]", text_pen = COLOR_LIGHTRED, visible = self:callback("slot_used"), on_click = self:callback("clear") } } end function QuickFilter:onInput(keys) if keys.LEAVESCREEN or keys._MOUSE_R then if self.renaming then self.subviews.edit_field:setFocus(false) self.renaming = false editing_filters_flag = false return true else return false end end return QuickFilter.super.onInput(self, keys) end function QuickFilter:on_click() if dfhack.internal.getModifiers().shift and self:slot_used() and not editing_filters_flag then self.subviews.edit_field:setText(self:get_label_text()) self.renaming = true editing_filters_flag = true self.subviews.edit_field:setFocus(true) else self.on_click_fn(self.idx) -- save/apply filter based on selected ItemLine end end function QuickFilter:slot_used() local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) return quick_filters[self.idx] ~= nil end function QuickFilter:clear() local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) quick_filters[self.idx] = nil dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) end function QuickFilter:get_label_text() local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) local set = quick_filters[self.idx] if not set then return "empty" else return set.label end end function QuickFilter:submit_name(text) local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) quick_filters[self.idx].label = compress(text, item_filter_chars+2) dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) self.renaming = false editing_filters_flag = false end -------------------------------- -- PlannerOverlay -- PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ desc='Shows the building planner interface panel when building buildings.', default_pos={x=5,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', frame={w=56, h=32}, } function PlannerOverlay:init() self.selected = 1 self.state = ensure_key(config.data, 'planner') self.selected_favorite = '1' local main_panel = widgets.Panel{ view_id='main', frame={t=1, l=0, r=0, h=14}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), } local minimized_panel = widgets.Panel{ frame={t=0, r=1, w=20, h=1}, subviews={ widgets.Label{ frame={t=0, r=3, h=1}, text={ {text=' show Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, {text='['..string.char(31)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, }, visible=self:callback('is_minimized'), on_click=self:callback('toggle_minimized'), }, widgets.Label{ frame={t=0, r=3, h=1}, text={ {text=' hide Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, {text='['..string.char(30)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, }, visible=self:callback('is_not_minimized'), on_click=self:callback('toggle_minimized'), }, widgets.HelpButton{ frame={t=0, r=0}, command='buildingplan', } }, } local function make_is_selected_fn(idx) return function() return self.selected == idx end end local function on_select_fn(idx) self.selected = idx end local function is_hollow_fn() return self.subviews.hollow:getOptionValue() end local buildingplan = require('plugins.buildingplan') main_panel:addviews{ widgets.Label{ frame={}, auto_width=true, text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn, on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn, on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn, on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn, on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='hollow', frame={b=4, l=1, w=21}, key='CUSTOM_H', label='Hollow area:', visible=is_construction, options={ {label='No', value=false}, {label='Yes', value=true, pen=COLOR_GREEN}, }, }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', frame={b=7, l=1, w=30}, key='CUSTOM_R', label='Top stair type: ', visible=is_multi_level_stairs, options={ {label='Auto', value='auto'}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Down', value=df.construction_type.DownStair}, }, }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', frame={b=6, l=1, w=30}, key='CUSTOM_B', label='Bottom Stair Type:', visible=is_multi_level_stairs, options={ {label='Auto', value='auto'}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Up', value=df.construction_type.UpStair}, }, }, widgets.CycleHotkeyLabel{ view_id='stairs_only_subtype', frame={b=7, l=1, w=30}, key='CUSTOM_R', label='Single level stair:', visible=is_single_level_stairs, options={ {label='Up', value=df.construction_type.UpStair}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Down', value=df.construction_type.DownStair}, }, }, widgets.CycleHotkeyLabel { -- TODO: this thing also needs a slider view_id='weapons', frame={b=4, l=1, w=28}, key='CUSTOM_T', key_back='CUSTOM_SHIFT_T', label='Number of weapons:', visible=is_weapon_or_spike_trap, options=utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10), on_change=function(val) weapon_quantity = val end, }, widgets.ToggleHotkeyLabel { view_id='engraved', frame={b=4, l=1, w=22}, key='CUSTOM_T', label='Engraved only:', visible=is_slab, on_change=function(val) buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) end, }, widgets.ToggleHotkeyLabel { view_id='empty', frame={b=4, l=1, w=22}, key='CUSTOM_T', label='Empty only:', visible=is_cage, on_change=function(val) buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'empty', val) end, }, widgets.Panel{ visible=function() return #get_cur_filters() > 0 end, subviews={ widgets.HotkeyLabel{ frame={b=2, l=1, w=22}, key='CUSTOM_F', label=function() return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) and 'Edit filter' or 'Set filter' end, on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ frame={b=1, l=1, w=22}, key='CUSTOM_CTRL_D', label='Delete filter', on_activate=function() self:clear_filter(self.selected) end, enabled=function() return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) end }, widgets.CycleHotkeyLabel{ view_id='show_favorites', frame={b=0, l=1, w=22}, key='CUSTOM_CTRL_F', label="", option_gap=0, options={ { label='Show favorites', value = false }, { label='Hide favorites', value = true }, }, initial_option=false, on_change=function(new,_) self:show_hide_favorites(new) end, }, widgets.CycleHotkeyLabel{ view_id='choose', frame={b=0, l=24}, key='CUSTOM_Z', label='Choose items:', label_below=true, options={ {label='With filters', value=0}, { label=function() local automaterial = itemselection.get_automaterial_selection(uibs.building_type) return ('Last used (%s)'):format(automaterial or 'pick manually') end, value=2, }, {label='Manually', value=1}, }, initial_option=0, on_change=function(choose) buildingplan.setChooseItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, choose) end, }, widgets.CycleHotkeyLabel{ view_id='safety', frame={b=2, l=24, w=25}, key='CUSTOM_G', label='Building safety:', options={ {label='Any', value=0}, {label='Magma', value=2, pen=COLOR_RED}, {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, initial_option=0, on_change=function(heat) buildingplan.setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) end, }, }, }, } local divider_widget = widgets.Divider{ frame={t=10, l=0, r=0, h=1}, frame_style=gui.FRAME_INTERIOR_MEDIUM, visible=self:callback('is_not_minimized'), } local error_panel = widgets.ResizingPanel{ view_id='errors', frame={t=15, l=0, r=0}, frame_style=gui.BOLD_FRAME, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), } error_panel:addviews{ widgets.WrappedLabel{ frame={t=0, l=1, r=0}, text_pen=COLOR_LIGHTRED, text_to_wrap=get_placement_errors, visible=function() return #uibs.errors > 0 end, }, widgets.Label{ frame={t=0, l=1, r=0}, text_pen=COLOR_GREEN, text='OK to build', visible=function() return #uibs.errors == 0 end, }, } local prev_next_selector = widgets.Panel{ frame={h=1}, auto_width=true, subviews={ widgets.HotkeyLabel{ frame={t=0, l=1, w=9}, key='CUSTOM_SHIFT_Q', key_sep='\0', label=': Prev/', on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, }, widgets.HotkeyLabel{ frame={t=0, l=2, w=1}, key='CUSTOM_Q', on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, widgets.Label{ frame={t=0,l=10}, text='next item', on_click=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, }, visible=function() return #get_cur_filters() > 1 end, } local black_bar = widgets.Panel{ frame={t=0, l=1, w=37, h=1}, frame_inset=0, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), subviews={ prev_next_selector, }, } local function make_is_selected_filter(idx) return function () return self.selected_favorite == idx end end local favorites_panel = widgets.Panel{ view_id='favorites', frame={t=15, l=0, r=0, h=11}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('show_favorites'), subviews={ QuickFilter{idx='1', frame={t=0,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('1') }, QuickFilter{idx='2', frame={t=1,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('2') }, QuickFilter{idx='3', frame={t=2,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('3') }, QuickFilter{idx='4', frame={t=3,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('4') }, QuickFilter{idx='5', frame={t=4,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('5') }, QuickFilter{idx='6', frame={t=0,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('6') }, QuickFilter{idx='7', frame={t=1,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('7') }, QuickFilter{idx='8', frame={t=2,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('8') }, QuickFilter{idx='9', frame={t=3,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('9') }, QuickFilter{idx='0', frame={t=4,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('0') }, widgets.CycleHotkeyLabel { view_id='slot_select', frame={b=2, l=2}, key='CUSTOM_X', key_back='CUSTOM_SHIFT_X', label='next/previous slot', auto_width=true, options=utils.tabulate(function(i) return {label="", value=tostring(i)} end, 0, 9), initial_option='1', on_change=function(val) self.selected_favorite = val end, }, widgets.HotkeyLabel{ frame={b=2, l=28}, label="set/apply selected", key='CUSTOM_Y', on_activate=function () self:save_restore_filter(self.selected_favorite) end, }, widgets.TooltipLabel { frame={b=0, l=2}, show_tooltip=true, text="Shift+click to edit the label of a favorite", }, } } self:addviews{ black_bar, minimized_panel, main_panel, divider_widget, error_panel, favorites_panel } end function PlannerOverlay:show_favorites() return not self.state.minimized and self.subviews.show_favorites:getOptionValue() end function PlannerOverlay:show_hide_favorites(new) local errors_frame = {t=15+(new and 11 or 0), l=0, r=0} self.subviews.errors.frame = errors_frame self:updateLayout() end function PlannerOverlay:save_restore_filter(slot) self.selected_favorite = slot local buildingplan = require('plugins.buildingplan') local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) if quick_filters[slot] then -- restore saved filter buildingplan.setMaterialFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1, quick_filters[slot].mats ) else -- save current filter if not buildingplan.hasFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) then return end local mats = buildingplan.getMaterialFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) local cats = buildingplan.getMaterialMaskFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) local label = filter_string(mats, cats, item_filter_chars) local enabled_mats = {} for mat, props in pairs(mats) do if props.enabled == "true" and cats[props.category] then table.insert(enabled_mats, mat) end end if #enabled_mats > 0 then quick_filters[slot] = { label = label, mats = enabled_mats } dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) end end end function PlannerOverlay:is_minimized() return self.state.minimized end function PlannerOverlay:is_not_minimized() return not self.state.minimized end function PlannerOverlay:toggle_minimized() self.state.minimized = not self.state.minimized config:write() self:reset() end function PlannerOverlay:reset() self.subviews.item1:reset() self.subviews.item2:reset() self.subviews.item3:reset() self.subviews.item4:reset() reset_counts_flag = false end function PlannerOverlay:set_filter(idx) filterselection.FilterSelectionScreen{index=idx, desc=require('plugins.buildingplan').get_desc(get_cur_filters()[idx])}:show() end function PlannerOverlay:clear_filter(idx) desc=require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end local function get_placement_data() local direction = uibs.direction local bounds = get_selected_bounds() local width, height, depth = get_cur_area_dims(bounds) local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( width, height, uibs.building_type, uibs.building_subtype, uibs.custom_type, direction) -- get the upper-left corner of the building/area at min z-level local start_pos = bounds and xyz2pos(bounds.x1, bounds.y1, bounds.z1) or xyz2pos( uibs.pos.x - adjusted_width//2, uibs.pos.y - adjusted_height//2, uibs.pos.z) if uibs.building_type == df.building_type.ScrewPump then if direction == df.screw_pump_direction.FromSouth then start_pos.y = start_pos.y + 1 elseif direction == df.screw_pump_direction.FromEast then start_pos.x = start_pos.x + 1 end end local min_x, max_x = start_pos.x, start_pos.x local min_y, max_y = start_pos.y, start_pos.y local min_z, max_z = start_pos.z, start_pos.z if adjusted_width == 1 and adjusted_height == 1 and (width > 1 or height > 1 or depth > 1) then max_x = min_x + width - 1 max_y = min_y + height - 1 max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end return { x1=min_x, y1=min_y, z1=min_z, x2=max_x, y2=max_y, z2=max_z, width=adjusted_width, height=adjusted_height } end function PlannerOverlay:save_placement() self.saved_placement = get_placement_data() if (uibs.selection_pos:isValid()) then self.saved_selection_pos_valid = true self.saved_selection_pos = copyall(uibs.selection_pos) self.saved_pos = copyall(uibs.pos) uibs.selection_pos:clear() else local sp = self.saved_placement self.saved_selection_pos = xyz2pos(sp.x1, sp.y1, sp.z1) self.saved_pos = xyz2pos(sp.x2, sp.y2, sp.z2) self.saved_pos.x = self.saved_pos.x + sp.width - 1 self.saved_pos.y = self.saved_pos.y + sp.height - 1 end end function PlannerOverlay:restore_placement() if self.saved_selection_pos_valid then uibs.selection_pos = self.saved_selection_pos self.saved_selection_pos_valid = nil else uibs.selection_pos:clear() end self.saved_selection_pos = nil self.saved_pos = nil local placement_data = self.saved_placement self.saved_placement = nil return placement_data end function PlannerOverlay:onInput(keys) if not is_plannable() then return false end if PlannerOverlay.super.onInput(self, keys) then return true end if keys.LEAVESCREEN or keys._MOUSE_R then if uibs.selection_pos:isValid() then uibs.selection_pos:clear() return true end self.selected = 1 self.subviews.hollow:setOption(false) self:reset() reset_counts_flag = true return false end if keys.CUSTOM_ALT_M then self:toggle_minimized() return true end if self:is_minimized() then return false end if keys._MOUSE_L then if is_over_options_panel() then return false end local detect_rect = copyall(self.frame_rect) detect_rect.height = self.subviews.main.frame_rect.height + self.subviews.errors.frame_rect.height detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1 if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect}) or self.subviews.errors:getMousePos() then return true end if not is_construction() and #uibs.errors > 0 then return true end if dfhack.gui.getMousePos() then if is_choosing_area() or cur_building_has_no_area() then local filters = get_cur_filters() local num_filters = #filters local choose = self.subviews.choose:getOptionValue() if choose == 0 then self:place_building(get_placement_data()) else local bounds = get_selected_bounds() self:save_placement() local autoselect = choose == 2 local is_hollow = self.subviews.hollow:getOptionValue() local chosen_items, active_screens = {}, {} local pending = num_filters df.global.game.main_interface.bottom_mode_selected = -1 for idx = num_filters,1,-1 do chosen_items[idx] = {} local filter = filters[idx] local get_available_items_fn = function() return require('plugins.buildingplan').getAvailableItems( uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end local selection_screen = itemselection.ItemSelectionScreen{ get_available_items_fn=get_available_items_fn, desc=require('plugins.buildingplan').get_desc(filter), quantity=get_quantity(filter, is_hollow, bounds), autoselect=autoselect, on_submit=function(items) chosen_items[idx] = items if active_screens[idx] then active_screens[idx]:dismiss() active_screens[idx] = nil else active_screens[idx] = true end pending = pending - 1 if pending == 0 then df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT self:place_building(self:restore_placement(), chosen_items) end end, on_cancel=function() for _,scr in pairs(active_screens) do scr:dismiss() end df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT self:restore_placement() end, } if active_screens[idx] then -- we've already returned via autoselect active_screens[idx] = nil else active_screens[idx] = selection_screen:show() end end end return true elseif not is_choosing_area() then return false end end end return keys._MOUSE_L or keys.SELECT end function PlannerOverlay:render(dc) if not is_plannable() then return end self.subviews.errors:updateLayout() PlannerOverlay.super.render(self, dc) end function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) if reset_counts_flag then self:reset() local buildingplan = require('plugins.buildingplan') self.subviews.engraved:setOption(buildingplan.getSpecials( uibs.building_type, uibs.building_subtype, uibs.custom_type).engraved or false) self.subviews.empty:setOption(buildingplan.getSpecials( uibs.building_type, uibs.building_subtype, uibs.custom_type).empty or false) self.subviews.choose:setOption(buildingplan.getChooseItems( uibs.building_type, uibs.building_subtype, uibs.custom_type)) self.subviews.safety:setOption(buildingplan.getHeatSafetyFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type)) end if self:is_minimized() then return end local bounds = get_selected_bounds(self.saved_selection_pos, self.saved_pos) if not bounds then return end local hollow = self.subviews.hollow:getOptionValue() local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN -- always allow reconstruction if it's a 1x1x1 selection (meaning the player selected that spot specifically) local reconstruct = can_reconstruct(bounds) local get_pen_fn = is_construction() and function(pos) return can_place_construction(reconstruct, pos) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN end or function() return default_pen end local function get_overlay_pen(pos) if not hollow then return get_pen_fn(pos) end if pos.x == bounds.x1 or pos.x == bounds.x2 or pos.y == bounds.y1 or pos.y == bounds.y2 then return get_pen_fn(pos) end return gui.TRANSPARENT_PEN end guidm.renderMapOverlay(get_overlay_pen, bounds) end function PlannerOverlay:get_stairs_subtype(pos, bounds) local subtype = uibs.building_subtype if pos.z == bounds.z1 then local opt = bounds.z1 == bounds.z2 and self.subviews.stairs_only_subtype:getOptionValue() or self.subviews.stairs_bottom_subtype:getOptionValue() if opt == 'auto' then local tt = dfhack.maps.getTileType(pos) local shape = df.tiletype.attrs[tt].shape if shape ~= df.tiletype_shape.STAIR_DOWN and shape ~= df.tiletype_shape.STAIR_UPDOWN then subtype = df.construction_type.UpStair end else subtype = opt end elseif pos.z == bounds.z2 then local opt = self.subviews.stairs_top_subtype:getOptionValue() if opt == 'auto' then local tt = dfhack.maps.getTileType(pos) local shape = df.tiletype.attrs[tt].shape if shape ~= df.tiletype_shape.STAIR_UP and shape ~= df.tiletype_shape.STAIR_UPDOWN then subtype = df.construction_type.DownStair end else subtype = opt end end return subtype end function PlannerOverlay:place_building(placement_data, chosen_items) local pd = placement_data local blds = {} local hollow = self.subviews.hollow:getOptionValue() local subtype = uibs.building_subtype local filters = get_cur_filters() if is_pressure_plate() or is_spike_trap() then filters[1].quantity = get_quantity(filters[1]) elseif is_weapon_trap() then filters[2].quantity = get_quantity(filters[2]) end local reconstruct = can_reconstruct(pd) for z=pd.z1,pd.z2 do for y=pd.y1,pd.y2 do for x=pd.x1,pd.x2 do if hollow and is_interior(pd, x, y) then goto continue end local pos = xyz2pos(x, y, z) if is_construction() and not can_place_construction(reconstruct, pos) then goto continue end if is_stairs() then subtype = self:get_stairs_subtype(pos, pd) end local fields = {} if is_siege_engine() then local facing = df.global.buildreq.direction fields.facing = facing fields.resting_orientation = facing end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, width=pd.width, height=pd.height, direction=uibs.direction, filters=filters, fields=fields} if err then -- it's ok if some buildings fail to build goto continue end -- assign fields for the types that need them. we can't pass them all in -- to the call to constructBuilding since attempting to assign unrelated -- fields to building types that don't support them causes errors. for k in pairs(bld) do if k == 'track_stop_info' then utils.assign(bld.track_stop_info, uibs.track_stop) end if k == 'speed' then bld.speed = uibs.speed end if k == 'plate_info' then utils.assign(bld.plate_info, uibs.plate_info) end end table.insert(blds, bld) ::continue:: end end end local used_quantity = is_construction() and #blds or false self.subviews.item1:reduce_quantity(used_quantity) self.subviews.item2:reduce_quantity(used_quantity) self.subviews.item3:reduce_quantity(used_quantity) self.subviews.item4:reduce_quantity(used_quantity) local buildingplan = require('plugins.buildingplan') for _,bld in ipairs(blds) do -- attach chosen items and reduce job_item quantity if chosen_items then local job = bld.jobs[0] local jitems = job.job_items.elements local num_filters = #get_cur_filters() for idx=1,num_filters do local item_ids = chosen_items[idx] local jitem = jitems[num_filters-idx] while jitem.quantity > 0 and #item_ids > 0 do local item_id = item_ids[#item_ids] local item = df.item.find(item_id) if not item then dfhack.printerr(('item no longer available: %d'):format(item_id)) break end 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 jitem.quantity = jitem.quantity - 1 item_ids[#item_ids] = nil end end end buildingplan.addPlannedBuilding(bld) end buildingplan.scheduleCycle() uibs.selection_pos:clear() end return _ENV \ No newline at end of file From 5321e6d5379755e15604568d593a73b688e58a34 Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Wed, 11 Mar 2026 11:32:39 +0100 Subject: [PATCH 013/128] fixed newline mess correctly this time CR->LF geez --- plugins/lua/buildingplan/planneroverlay.lua | 1384 ++++++++++++++++++- 1 file changed, 1383 insertions(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 9ee8feaf1a..f947b123dd 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -1 +1,1383 @@ -local _ENV = mkmodule('plugins.buildingplan.planneroverlay') local itemselection = require('plugins.buildingplan.itemselection') local filterselection = require('plugins.buildingplan.filterselection') local gui = require('gui') local guidm = require('gui.dwarfmode') local json = require('json') local overlay = require('plugins.overlay') local pens = require('plugins.buildingplan.pens') local utils = require('utils') local widgets = require('gui.widgets') require('dfhack.buildings') config = config or json.open('dfhack-config/buildingplan.json') local uibs = df.global.buildreq reset_counts_flag = false editing_filters_flag = false local function get_cur_filters() return dfhack.buildings.getFiltersByType({}, uibs.building_type, uibs.building_subtype, uibs.custom_type) end local function is_choosing_area() return uibs.selection_pos.x >= 0 end -- TODO: reuse data in quickfort database local function get_selection_size_limits() local btype = uibs.building_type if btype == df.building_type.Bridge or btype == df.building_type.FarmPlot or btype == df.building_type.RoadPaved or btype == df.building_type.RoadDirt then return {w=31, h=31} elseif btype == df.building_type.AxleHorizontal then return uibs.direction == 1 and {w=1, h=31} or {w=31, h=1} elseif btype == df.building_type.Rollers then return (uibs.direction == 1 or uibs.direction == 3) and {w=31, h=1} or {w=1, h=31} end end local function get_selected_bounds(selection_pos, pos) selection_pos = selection_pos or uibs.selection_pos if not is_choosing_area() then return end pos = pos or uibs.pos local bounds = { x1=math.min(selection_pos.x, pos.x), x2=math.max(selection_pos.x, pos.x), y1=math.min(selection_pos.y, pos.y), y2=math.max(selection_pos.y, pos.y), z1=math.min(selection_pos.z, pos.z), z2=math.max(selection_pos.z, pos.z), } -- clamp to map edges bounds = { x1=math.max(0, bounds.x1), x2=math.min(df.global.world.map.x_count-1, bounds.x2), y1=math.max(0, bounds.y1), y2=math.min(df.global.world.map.y_count-1, bounds.y2), z1=math.max(0, bounds.z1), z2=math.min(df.global.world.map.z_count-1, bounds.z2), } local limits = get_selection_size_limits() if limits then -- clamp to building type area limit bounds = { x1=math.max(selection_pos.x - (limits.w-1), bounds.x1), x2=math.min(selection_pos.x + (limits.w-1), bounds.x2), y1=math.max(selection_pos.y - (limits.h-1), bounds.y1), y2=math.min(selection_pos.y + (limits.h-1), bounds.y2), z1=bounds.z1, z2=bounds.z2, } end return bounds end local function get_cur_area_dims(bounds) if not bounds and not is_choosing_area() then return 1, 1, 1 end bounds = bounds or get_selected_bounds() if not bounds then return 1, 1, 1 end return bounds.x2 - bounds.x1 + 1, bounds.y2 - bounds.y1 + 1, bounds.z2 - bounds.z1 + 1 end local function get_selected_volume(bounds) local w, h, depth = get_cur_area_dims(bounds) return w * h * depth end local function is_pressure_plate() return uibs.building_type == df.building_type.Trap and uibs.building_subtype == df.trap_type.PressurePlate end local function is_weapon_trap() return uibs.building_type == df.building_type.Trap and uibs.building_subtype == df.trap_type.WeaponTrap end local function is_spike_trap() return uibs.building_type == df.building_type.Weapon end local function is_weapon_or_spike_trap() return is_weapon_trap() or is_spike_trap() end local function is_construction() return uibs.building_type == df.building_type.Construction end local function is_siege_engine() return uibs.building_type == df.building_type.SiegeEngine end local function tile_is_construction(pos) local tt = dfhack.maps.getTileType(pos) if not tt then return false end if df.tiletype.attrs[tt].material ~= df.tiletype_material.CONSTRUCTION then return false end local construction = df.construction.find(pos) return construction and not construction.flags.top_of_wall end local ONE_BY_ONE = xy2pos(1, 1) local function can_reconstruct(bounds) return get_selected_volume(bounds) == 1 or require('plugins.buildingplan').getGlobalSettings().reconstruct end local function can_place_construction(reconstruct, pos) return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and (reconstruct or not tile_is_construction(pos)) end local function is_interior(bounds, x, y) return x ~= bounds.x1 and x ~= bounds.x2 and y ~= bounds.y1 and y ~= bounds.y2 end -- adjusted from CycleHotkeyLabel on the planner panel local weapon_quantity = 1 local function get_quantity(filter, hollow, bounds) if is_pressure_plate() then local flags = uibs.plate_info.flags return (flags.units and 1 or 0) + (flags.water and 1 or 0) + (flags.magma and 1 or 0) + (flags.track and 1 or 0) elseif (is_weapon_trap() and filter.vector_id == df.job_item_vector_id.ANY_WEAPON) or is_spike_trap() then return weapon_quantity end local quantity = filter.quantity or 1 bounds = bounds or get_selected_bounds() local dimx, dimy, dimz = get_cur_area_dims(bounds) if quantity < 1 then return (((dimx * dimy) // 4) + 1) * dimz end if bounds and is_construction() then local reconstruct = can_reconstruct(bounds) local count = 0 for z = bounds.z1, bounds.z2 do for y = bounds.y1, bounds.y2 do for x = bounds.x1, bounds.x2 do if hollow and is_interior(bounds, x, y) then goto continue end if can_place_construction(reconstruct, xyz2pos(x, y, z)) then count = count + 1 end ::continue:: end end end return quantity * count end return quantity * get_selected_volume(bounds) end local function cur_building_has_no_area() if uibs.building_type == df.building_type.Construction then return false end local filters = dfhack.buildings.getFiltersByType({}, uibs.building_type, uibs.building_subtype, uibs.custom_type) -- this works because all variable-size buildings have either no item -- filters or a quantity of -1 for their first (and only) item return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) end local function is_tutorial_open() local help = df.global.game.main_interface.help return help.open and help.context == df.help_context_type.START_TUTORIAL_WORKSHOPS_AND_TASKS end local function is_plannable() return not is_tutorial_open() and get_cur_filters() and not (is_construction() and uibs.building_subtype == df.construction_type.TrackNSEW) end local function is_slab() return uibs.building_type == df.building_type.Slab end local function is_cage() return uibs.building_type == df.building_type.Cage end local function is_stairs() return is_construction() and uibs.building_subtype == df.construction_type.UpDownStair end local function is_single_level_stairs() if not is_stairs() then return false end local _, _, dimz = get_cur_area_dims() return dimz == 1 end local function is_multi_level_stairs() if not is_stairs() then return false end local _, _, dimz = get_cur_area_dims() return dimz > 1 end local direction_panel_frame = {t=4, h=13, w=46, r=28} local direction_panel_types = utils.invert{ df.building_type.Bridge, df.building_type.ScrewPump, df.building_type.WaterWheel, df.building_type.AxleHorizontal, df.building_type.Rollers, df.building_type.SiegeEngine, } local function has_direction_panel() return direction_panel_types[uibs.building_type] or (uibs.building_type == df.building_type.Trap and uibs.building_subtype == df.trap_type.TrackStop) end local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} local function has_pressure_plate_panel() return is_pressure_plate() end local function is_over_options_panel() local frame = nil if has_direction_panel() then frame = direction_panel_frame elseif has_pressure_plate_panel() then frame = pressure_plate_panel_frame else return false end local v = widgets.Widget{frame=frame} local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) v:updateLayout(gui.ViewRect{rect=rect}) return v:getMousePos() end local function compress(str, len) if #str <= len then return str else local no_vowels = str:gsub('[aeiou]','') if #no_vowels <= len then return no_vowels else return no_vowels:sub(1,len-3)..'...' end end end local function filter_string(mats, cats, length) local enabled_mat_names = {} local enabled_cat_names = {} for name, props in pairs(mats) do local enabled = props.enabled == 'true' and cats[props.category] if enabled then table.insert(enabled_mat_names, name) end end if #enabled_mat_names == 1 then return '['..compress(enabled_mat_names[1], length)..']' elseif #enabled_mat_names > 1 then for cat, _ in pairs(cats) do if cat ~= 'unset' and cats[cat] then table.insert(enabled_cat_names, cat) end end if #enabled_cat_names == 1 then return '[' .. enabled_cat_names[1]:gsub("^%l", string.upper) .. ']' else return '['..#enabled_cat_names..' mat. categories]' end else -- can result from selecting wood and then toggling "fire safe" etc. return '[impossible filter]' end end -------------------------------- -- ItemLine -- -- number of characters for item filter summary (excluding surrounding [ ]) local item_filter_chars = 17 ItemLine = defclass(ItemLine, widgets.Panel) ItemLine.ATTRS{ idx=DEFAULT_NIL, is_selected_fn=DEFAULT_NIL, is_hollow_fn=DEFAULT_NIL, on_select=DEFAULT_NIL, on_filter=DEFAULT_NIL, on_clear_filter=DEFAULT_NIL, } function ItemLine:init() self.frame.h = 2 self.visible = function() return #get_cur_filters() >= self.idx end self:addviews{ widgets.Label{ view_id='item_symbol', frame={t=0, l=0}, text=string.char(16), -- this is the "►" character text_pen=COLOR_YELLOW, auto_width=true, visible=self.is_selected_fn, }, widgets.Label{ view_id='item_desc', frame={t=0, l=2}, text={ {text=self:callback('get_item_line_text'), pen=function() return gui.invert_color(COLOR_WHITE, self.is_selected_fn()) end}, }, }, widgets.Label{ view_id='item_filter', frame={t=0, l=28}, text={ {text=self:callback('get_filter_text'), width=item_filter_chars+2, rjustify=true, pen=function() return self:is_impossible() and COLOR_RED or gui.invert_color(COLOR_LIGHTCYAN, self.is_selected_fn()) end}, }, auto_width=true, on_click=function() self.on_filter(self.idx) end, }, widgets.Label{ frame={t=0, l=47}, text='[clear]', text_pen=COLOR_LIGHTRED, auto_width=true, visible=self:callback('has_filter'), on_click=function() self.on_clear_filter(self.idx) end, }, widgets.Label{ frame={t=1, l=2}, text={ {gap=2, text=function() return self.note end, pen=function() return self.note_pen end}, }, }, } end function ItemLine:reset() self.desc = nil self.available = nil end function ItemLine:onInput(keys) if keys._MOUSE_L and self:getMousePos() then self.on_select(self.idx) end return ItemLine.super.onInput(self, keys) end function ItemLine:get_item_line_text() local idx = self.idx local filter = get_cur_filters()[idx] local quantity = get_quantity(filter, self.is_hollow_fn()) local buildingplan = require('plugins.buildingplan') self.desc = self.desc or buildingplan.get_desc(filter) self.available = self.available or buildingplan.countAvailableItems( uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) if self.available >= quantity then self.note_pen = COLOR_GREEN self.note = (' %d available now'):format(self.available) elseif self.available >= 0 then self.note_pen = COLOR_BROWN self.note = (' Will link next (need to make %d)'):format(quantity - self.available) else self.note_pen = COLOR_BROWN self.note = (' Will link later (need to make %d)'):format(-self.available + quantity) end self.note = string.char(192) .. self.note -- character 192 is "└" return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end function ItemLine:has_filter() return require('plugins.buildingplan').hasFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) end function ItemLine:get_filter_text() local buildingplan = require('plugins.buildingplan') if not buildingplan.hasFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) then return '[any material]' end local mats = buildingplan.getMaterialFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) local cats = buildingplan.getMaterialMaskFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) return filter_string(mats, cats, item_filter_chars) end -- short circuit version of the '[impossible filter]' case above function ItemLine:is_impossible() local buildingplan = require('plugins.buildingplan') local mats = buildingplan.getMaterialFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) local cats = buildingplan.getMaterialMaskFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) for _,props in pairs(mats) do local enabled = props.enabled == 'true' and cats[props.category] if enabled then return false end end return true end function ItemLine:reduce_quantity(used_quantity) if not self.available then return end local filter = get_cur_filters()[self.idx] used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn()) self.available = self.available - used_quantity end local function get_placement_errors() local out = '' for _,str in ipairs(uibs.errors) do if #out > 0 then out = out .. NEWLINE end out = out .. str.value end return out end -------------------------------- -- QuickFilter -- -- Used to store a table of the following format: -- table -- string: quick filter slot (must be strings because of the way persistence works) -- label: string representation of the filter -- mats: list of material names allowed by the filter BUILDINGPLAN_FILTERS_KEY = "buildingplan/quick-filters" -- old saves may use numbers as keys, which we convert to string keys on load dfhack.onStateChange[BUILDINGPLAN_FILTERS_KEY] = function(sc) if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then return end local saved_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) local new_filters = {} for k, v in pairs(saved_filters) do if type(k) == 'number' then new_filters[tostring(k)] = v elseif type(k) == 'string' then new_filters[k] = v end end dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, new_filters) end QuickFilter = defclass(QuickFilter, widgets.Panel) QuickFilter.ATTRS{ idx=DEFAULT_NIL, on_click_fn=DEFAULT_NIL, is_selected_fn=DEFAULT_NIL } function QuickFilter:init() local label = ('%d.'):format(self.idx) self.frame.w = 27 self.renaming = false self:addviews { widgets.Label { frame = { t = 0, l = 0 }, text = string.char(16), -- this is the "►" character text_pen = COLOR_YELLOW, auto_width = true, visible = self.is_selected_fn, }, widgets.Label { frame = { t = 0, l = 2 }, text = label }, widgets.Label { frame = { t = 0, l = 5, w = item_filter_chars + 2 }, text = { { text = self:callback('get_label_text'), pen = function() return COLOR_CYAN end } }, visible = function() return self.renaming == false end, on_click = self:callback("on_click"), }, widgets.EditField { view_id = 'edit_field', frame = { t = 0, l = 5, w = item_filter_chars + 2 }, text = "", visible = function() return self.renaming == true end, on_submit = function(text) self:submit_name(text) end, }, widgets.Label { frame = { t = 0, r = 0, w = 3 }, text = "[x]", text_pen = COLOR_LIGHTRED, visible = self:callback("slot_used"), on_click = self:callback("clear") } } end function QuickFilter:onInput(keys) if keys.LEAVESCREEN or keys._MOUSE_R then if self.renaming then self.subviews.edit_field:setFocus(false) self.renaming = false editing_filters_flag = false return true else return false end end return QuickFilter.super.onInput(self, keys) end function QuickFilter:on_click() if dfhack.internal.getModifiers().shift and self:slot_used() and not editing_filters_flag then self.subviews.edit_field:setText(self:get_label_text()) self.renaming = true editing_filters_flag = true self.subviews.edit_field:setFocus(true) else self.on_click_fn(self.idx) -- save/apply filter based on selected ItemLine end end function QuickFilter:slot_used() local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) return quick_filters[self.idx] ~= nil end function QuickFilter:clear() local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) quick_filters[self.idx] = nil dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) end function QuickFilter:get_label_text() local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) local set = quick_filters[self.idx] if not set then return "empty" else return set.label end end function QuickFilter:submit_name(text) local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) quick_filters[self.idx].label = compress(text, item_filter_chars+2) dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) self.renaming = false editing_filters_flag = false end -------------------------------- -- PlannerOverlay -- PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ desc='Shows the building planner interface panel when building buildings.', default_pos={x=5,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', frame={w=56, h=32}, } function PlannerOverlay:init() self.selected = 1 self.state = ensure_key(config.data, 'planner') self.selected_favorite = '1' local main_panel = widgets.Panel{ view_id='main', frame={t=1, l=0, r=0, h=14}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), } local minimized_panel = widgets.Panel{ frame={t=0, r=1, w=20, h=1}, subviews={ widgets.Label{ frame={t=0, r=3, h=1}, text={ {text=' show Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, {text='['..string.char(31)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, }, visible=self:callback('is_minimized'), on_click=self:callback('toggle_minimized'), }, widgets.Label{ frame={t=0, r=3, h=1}, text={ {text=' hide Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, {text='['..string.char(30)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, }, visible=self:callback('is_not_minimized'), on_click=self:callback('toggle_minimized'), }, widgets.HelpButton{ frame={t=0, r=0}, command='buildingplan', } }, } local function make_is_selected_fn(idx) return function() return self.selected == idx end end local function on_select_fn(idx) self.selected = idx end local function is_hollow_fn() return self.subviews.hollow:getOptionValue() end local buildingplan = require('plugins.buildingplan') main_panel:addviews{ widgets.Label{ frame={}, auto_width=true, text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn, on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn, on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn, on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn, on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='hollow', frame={b=4, l=1, w=21}, key='CUSTOM_H', label='Hollow area:', visible=is_construction, options={ {label='No', value=false}, {label='Yes', value=true, pen=COLOR_GREEN}, }, }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', frame={b=7, l=1, w=30}, key='CUSTOM_R', label='Top stair type: ', visible=is_multi_level_stairs, options={ {label='Auto', value='auto'}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Down', value=df.construction_type.DownStair}, }, }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', frame={b=6, l=1, w=30}, key='CUSTOM_B', label='Bottom Stair Type:', visible=is_multi_level_stairs, options={ {label='Auto', value='auto'}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Up', value=df.construction_type.UpStair}, }, }, widgets.CycleHotkeyLabel{ view_id='stairs_only_subtype', frame={b=7, l=1, w=30}, key='CUSTOM_R', label='Single level stair:', visible=is_single_level_stairs, options={ {label='Up', value=df.construction_type.UpStair}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Down', value=df.construction_type.DownStair}, }, }, widgets.CycleHotkeyLabel { -- TODO: this thing also needs a slider view_id='weapons', frame={b=4, l=1, w=28}, key='CUSTOM_T', key_back='CUSTOM_SHIFT_T', label='Number of weapons:', visible=is_weapon_or_spike_trap, options=utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10), on_change=function(val) weapon_quantity = val end, }, widgets.ToggleHotkeyLabel { view_id='engraved', frame={b=4, l=1, w=22}, key='CUSTOM_T', label='Engraved only:', visible=is_slab, on_change=function(val) buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) end, }, widgets.ToggleHotkeyLabel { view_id='empty', frame={b=4, l=1, w=22}, key='CUSTOM_T', label='Empty only:', visible=is_cage, on_change=function(val) buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'empty', val) end, }, widgets.Panel{ visible=function() return #get_cur_filters() > 0 end, subviews={ widgets.HotkeyLabel{ frame={b=2, l=1, w=22}, key='CUSTOM_F', label=function() return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) and 'Edit filter' or 'Set filter' end, on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ frame={b=1, l=1, w=22}, key='CUSTOM_CTRL_D', label='Delete filter', on_activate=function() self:clear_filter(self.selected) end, enabled=function() return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) end }, widgets.CycleHotkeyLabel{ view_id='show_favorites', frame={b=0, l=1, w=22}, key='CUSTOM_CTRL_F', label="", option_gap=0, options={ { label='Show favorites', value = false }, { label='Hide favorites', value = true }, }, initial_option=false, on_change=function(new,_) self:show_hide_favorites(new) end, }, widgets.CycleHotkeyLabel{ view_id='choose', frame={b=0, l=24}, key='CUSTOM_Z', label='Choose items:', label_below=true, options={ {label='With filters', value=0}, { label=function() local automaterial = itemselection.get_automaterial_selection(uibs.building_type) return ('Last used (%s)'):format(automaterial or 'pick manually') end, value=2, }, {label='Manually', value=1}, }, initial_option=0, on_change=function(choose) buildingplan.setChooseItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, choose) end, }, widgets.CycleHotkeyLabel{ view_id='safety', frame={b=2, l=24, w=25}, key='CUSTOM_G', label='Building safety:', options={ {label='Any', value=0}, {label='Magma', value=2, pen=COLOR_RED}, {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, initial_option=0, on_change=function(heat) buildingplan.setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) end, }, }, }, } local divider_widget = widgets.Divider{ frame={t=10, l=0, r=0, h=1}, frame_style=gui.FRAME_INTERIOR_MEDIUM, visible=self:callback('is_not_minimized'), } local error_panel = widgets.ResizingPanel{ view_id='errors', frame={t=15, l=0, r=0}, frame_style=gui.BOLD_FRAME, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), } error_panel:addviews{ widgets.WrappedLabel{ frame={t=0, l=1, r=0}, text_pen=COLOR_LIGHTRED, text_to_wrap=get_placement_errors, visible=function() return #uibs.errors > 0 end, }, widgets.Label{ frame={t=0, l=1, r=0}, text_pen=COLOR_GREEN, text='OK to build', visible=function() return #uibs.errors == 0 end, }, } local prev_next_selector = widgets.Panel{ frame={h=1}, auto_width=true, subviews={ widgets.HotkeyLabel{ frame={t=0, l=1, w=9}, key='CUSTOM_SHIFT_Q', key_sep='\0', label=': Prev/', on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, }, widgets.HotkeyLabel{ frame={t=0, l=2, w=1}, key='CUSTOM_Q', on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, widgets.Label{ frame={t=0,l=10}, text='next item', on_click=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, }, visible=function() return #get_cur_filters() > 1 end, } local black_bar = widgets.Panel{ frame={t=0, l=1, w=37, h=1}, frame_inset=0, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), subviews={ prev_next_selector, }, } local function make_is_selected_filter(idx) return function () return self.selected_favorite == idx end end local favorites_panel = widgets.Panel{ view_id='favorites', frame={t=15, l=0, r=0, h=11}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('show_favorites'), subviews={ QuickFilter{idx='1', frame={t=0,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('1') }, QuickFilter{idx='2', frame={t=1,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('2') }, QuickFilter{idx='3', frame={t=2,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('3') }, QuickFilter{idx='4', frame={t=3,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('4') }, QuickFilter{idx='5', frame={t=4,l=0}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('5') }, QuickFilter{idx='6', frame={t=0,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('6') }, QuickFilter{idx='7', frame={t=1,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('7') }, QuickFilter{idx='8', frame={t=2,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('8') }, QuickFilter{idx='9', frame={t=3,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('9') }, QuickFilter{idx='0', frame={t=4,l=27}, on_click_fn=self:callback("save_restore_filter"), is_selected_fn=make_is_selected_filter('0') }, widgets.CycleHotkeyLabel { view_id='slot_select', frame={b=2, l=2}, key='CUSTOM_X', key_back='CUSTOM_SHIFT_X', label='next/previous slot', auto_width=true, options=utils.tabulate(function(i) return {label="", value=tostring(i)} end, 0, 9), initial_option='1', on_change=function(val) self.selected_favorite = val end, }, widgets.HotkeyLabel{ frame={b=2, l=28}, label="set/apply selected", key='CUSTOM_Y', on_activate=function () self:save_restore_filter(self.selected_favorite) end, }, widgets.TooltipLabel { frame={b=0, l=2}, show_tooltip=true, text="Shift+click to edit the label of a favorite", }, } } self:addviews{ black_bar, minimized_panel, main_panel, divider_widget, error_panel, favorites_panel } end function PlannerOverlay:show_favorites() return not self.state.minimized and self.subviews.show_favorites:getOptionValue() end function PlannerOverlay:show_hide_favorites(new) local errors_frame = {t=15+(new and 11 or 0), l=0, r=0} self.subviews.errors.frame = errors_frame self:updateLayout() end function PlannerOverlay:save_restore_filter(slot) self.selected_favorite = slot local buildingplan = require('plugins.buildingplan') local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) if quick_filters[slot] then -- restore saved filter buildingplan.setMaterialFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1, quick_filters[slot].mats ) else -- save current filter if not buildingplan.hasFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) then return end local mats = buildingplan.getMaterialFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) local cats = buildingplan.getMaterialMaskFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) local label = filter_string(mats, cats, item_filter_chars) local enabled_mats = {} for mat, props in pairs(mats) do if props.enabled == "true" and cats[props.category] then table.insert(enabled_mats, mat) end end if #enabled_mats > 0 then quick_filters[slot] = { label = label, mats = enabled_mats } dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) end end end function PlannerOverlay:is_minimized() return self.state.minimized end function PlannerOverlay:is_not_minimized() return not self.state.minimized end function PlannerOverlay:toggle_minimized() self.state.minimized = not self.state.minimized config:write() self:reset() end function PlannerOverlay:reset() self.subviews.item1:reset() self.subviews.item2:reset() self.subviews.item3:reset() self.subviews.item4:reset() reset_counts_flag = false end function PlannerOverlay:set_filter(idx) filterselection.FilterSelectionScreen{index=idx, desc=require('plugins.buildingplan').get_desc(get_cur_filters()[idx])}:show() end function PlannerOverlay:clear_filter(idx) desc=require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end local function get_placement_data() local direction = uibs.direction local bounds = get_selected_bounds() local width, height, depth = get_cur_area_dims(bounds) local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( width, height, uibs.building_type, uibs.building_subtype, uibs.custom_type, direction) -- get the upper-left corner of the building/area at min z-level local start_pos = bounds and xyz2pos(bounds.x1, bounds.y1, bounds.z1) or xyz2pos( uibs.pos.x - adjusted_width//2, uibs.pos.y - adjusted_height//2, uibs.pos.z) if uibs.building_type == df.building_type.ScrewPump then if direction == df.screw_pump_direction.FromSouth then start_pos.y = start_pos.y + 1 elseif direction == df.screw_pump_direction.FromEast then start_pos.x = start_pos.x + 1 end end local min_x, max_x = start_pos.x, start_pos.x local min_y, max_y = start_pos.y, start_pos.y local min_z, max_z = start_pos.z, start_pos.z if adjusted_width == 1 and adjusted_height == 1 and (width > 1 or height > 1 or depth > 1) then max_x = min_x + width - 1 max_y = min_y + height - 1 max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end return { x1=min_x, y1=min_y, z1=min_z, x2=max_x, y2=max_y, z2=max_z, width=adjusted_width, height=adjusted_height } end function PlannerOverlay:save_placement() self.saved_placement = get_placement_data() if (uibs.selection_pos:isValid()) then self.saved_selection_pos_valid = true self.saved_selection_pos = copyall(uibs.selection_pos) self.saved_pos = copyall(uibs.pos) uibs.selection_pos:clear() else local sp = self.saved_placement self.saved_selection_pos = xyz2pos(sp.x1, sp.y1, sp.z1) self.saved_pos = xyz2pos(sp.x2, sp.y2, sp.z2) self.saved_pos.x = self.saved_pos.x + sp.width - 1 self.saved_pos.y = self.saved_pos.y + sp.height - 1 end end function PlannerOverlay:restore_placement() if self.saved_selection_pos_valid then uibs.selection_pos = self.saved_selection_pos self.saved_selection_pos_valid = nil else uibs.selection_pos:clear() end self.saved_selection_pos = nil self.saved_pos = nil local placement_data = self.saved_placement self.saved_placement = nil return placement_data end function PlannerOverlay:onInput(keys) if not is_plannable() then return false end if PlannerOverlay.super.onInput(self, keys) then return true end if keys.LEAVESCREEN or keys._MOUSE_R then if uibs.selection_pos:isValid() then uibs.selection_pos:clear() return true end self.selected = 1 self.subviews.hollow:setOption(false) self:reset() reset_counts_flag = true return false end if keys.CUSTOM_ALT_M then self:toggle_minimized() return true end if self:is_minimized() then return false end if keys._MOUSE_L then if is_over_options_panel() then return false end local detect_rect = copyall(self.frame_rect) detect_rect.height = self.subviews.main.frame_rect.height + self.subviews.errors.frame_rect.height detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1 if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect}) or self.subviews.errors:getMousePos() then return true end if not is_construction() and #uibs.errors > 0 then return true end if dfhack.gui.getMousePos() then if is_choosing_area() or cur_building_has_no_area() then local filters = get_cur_filters() local num_filters = #filters local choose = self.subviews.choose:getOptionValue() if choose == 0 then self:place_building(get_placement_data()) else local bounds = get_selected_bounds() self:save_placement() local autoselect = choose == 2 local is_hollow = self.subviews.hollow:getOptionValue() local chosen_items, active_screens = {}, {} local pending = num_filters df.global.game.main_interface.bottom_mode_selected = -1 for idx = num_filters,1,-1 do chosen_items[idx] = {} local filter = filters[idx] local get_available_items_fn = function() return require('plugins.buildingplan').getAvailableItems( uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end local selection_screen = itemselection.ItemSelectionScreen{ get_available_items_fn=get_available_items_fn, desc=require('plugins.buildingplan').get_desc(filter), quantity=get_quantity(filter, is_hollow, bounds), autoselect=autoselect, on_submit=function(items) chosen_items[idx] = items if active_screens[idx] then active_screens[idx]:dismiss() active_screens[idx] = nil else active_screens[idx] = true end pending = pending - 1 if pending == 0 then df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT self:place_building(self:restore_placement(), chosen_items) end end, on_cancel=function() for _,scr in pairs(active_screens) do scr:dismiss() end df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT self:restore_placement() end, } if active_screens[idx] then -- we've already returned via autoselect active_screens[idx] = nil else active_screens[idx] = selection_screen:show() end end end return true elseif not is_choosing_area() then return false end end end return keys._MOUSE_L or keys.SELECT end function PlannerOverlay:render(dc) if not is_plannable() then return end self.subviews.errors:updateLayout() PlannerOverlay.super.render(self, dc) end function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) if reset_counts_flag then self:reset() local buildingplan = require('plugins.buildingplan') self.subviews.engraved:setOption(buildingplan.getSpecials( uibs.building_type, uibs.building_subtype, uibs.custom_type).engraved or false) self.subviews.empty:setOption(buildingplan.getSpecials( uibs.building_type, uibs.building_subtype, uibs.custom_type).empty or false) self.subviews.choose:setOption(buildingplan.getChooseItems( uibs.building_type, uibs.building_subtype, uibs.custom_type)) self.subviews.safety:setOption(buildingplan.getHeatSafetyFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type)) end if self:is_minimized() then return end local bounds = get_selected_bounds(self.saved_selection_pos, self.saved_pos) if not bounds then return end local hollow = self.subviews.hollow:getOptionValue() local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN -- always allow reconstruction if it's a 1x1x1 selection (meaning the player selected that spot specifically) local reconstruct = can_reconstruct(bounds) local get_pen_fn = is_construction() and function(pos) return can_place_construction(reconstruct, pos) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN end or function() return default_pen end local function get_overlay_pen(pos) if not hollow then return get_pen_fn(pos) end if pos.x == bounds.x1 or pos.x == bounds.x2 or pos.y == bounds.y1 or pos.y == bounds.y2 then return get_pen_fn(pos) end return gui.TRANSPARENT_PEN end guidm.renderMapOverlay(get_overlay_pen, bounds) end function PlannerOverlay:get_stairs_subtype(pos, bounds) local subtype = uibs.building_subtype if pos.z == bounds.z1 then local opt = bounds.z1 == bounds.z2 and self.subviews.stairs_only_subtype:getOptionValue() or self.subviews.stairs_bottom_subtype:getOptionValue() if opt == 'auto' then local tt = dfhack.maps.getTileType(pos) local shape = df.tiletype.attrs[tt].shape if shape ~= df.tiletype_shape.STAIR_DOWN and shape ~= df.tiletype_shape.STAIR_UPDOWN then subtype = df.construction_type.UpStair end else subtype = opt end elseif pos.z == bounds.z2 then local opt = self.subviews.stairs_top_subtype:getOptionValue() if opt == 'auto' then local tt = dfhack.maps.getTileType(pos) local shape = df.tiletype.attrs[tt].shape if shape ~= df.tiletype_shape.STAIR_UP and shape ~= df.tiletype_shape.STAIR_UPDOWN then subtype = df.construction_type.DownStair end else subtype = opt end end return subtype end function PlannerOverlay:place_building(placement_data, chosen_items) local pd = placement_data local blds = {} local hollow = self.subviews.hollow:getOptionValue() local subtype = uibs.building_subtype local filters = get_cur_filters() if is_pressure_plate() or is_spike_trap() then filters[1].quantity = get_quantity(filters[1]) elseif is_weapon_trap() then filters[2].quantity = get_quantity(filters[2]) end local reconstruct = can_reconstruct(pd) for z=pd.z1,pd.z2 do for y=pd.y1,pd.y2 do for x=pd.x1,pd.x2 do if hollow and is_interior(pd, x, y) then goto continue end local pos = xyz2pos(x, y, z) if is_construction() and not can_place_construction(reconstruct, pos) then goto continue end if is_stairs() then subtype = self:get_stairs_subtype(pos, pd) end local fields = {} if is_siege_engine() then local facing = df.global.buildreq.direction fields.facing = facing fields.resting_orientation = facing end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, width=pd.width, height=pd.height, direction=uibs.direction, filters=filters, fields=fields} if err then -- it's ok if some buildings fail to build goto continue end -- assign fields for the types that need them. we can't pass them all in -- to the call to constructBuilding since attempting to assign unrelated -- fields to building types that don't support them causes errors. for k in pairs(bld) do if k == 'track_stop_info' then utils.assign(bld.track_stop_info, uibs.track_stop) end if k == 'speed' then bld.speed = uibs.speed end if k == 'plate_info' then utils.assign(bld.plate_info, uibs.plate_info) end end table.insert(blds, bld) ::continue:: end end end local used_quantity = is_construction() and #blds or false self.subviews.item1:reduce_quantity(used_quantity) self.subviews.item2:reduce_quantity(used_quantity) self.subviews.item3:reduce_quantity(used_quantity) self.subviews.item4:reduce_quantity(used_quantity) local buildingplan = require('plugins.buildingplan') for _,bld in ipairs(blds) do -- attach chosen items and reduce job_item quantity if chosen_items then local job = bld.jobs[0] local jitems = job.job_items.elements local num_filters = #get_cur_filters() for idx=1,num_filters do local item_ids = chosen_items[idx] local jitem = jitems[num_filters-idx] while jitem.quantity > 0 and #item_ids > 0 do local item_id = item_ids[#item_ids] local item = df.item.find(item_id) if not item then dfhack.printerr(('item no longer available: %d'):format(item_id)) break end 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 jitem.quantity = jitem.quantity - 1 item_ids[#item_ids] = nil end end end buildingplan.addPlannedBuilding(bld) end buildingplan.scheduleCycle() uibs.selection_pos:clear() end return _ENV \ No newline at end of file +local _ENV = mkmodule('plugins.buildingplan.planneroverlay') + +local itemselection = require('plugins.buildingplan.itemselection') +local filterselection = require('plugins.buildingplan.filterselection') +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local json = require('json') +local overlay = require('plugins.overlay') +local pens = require('plugins.buildingplan.pens') +local utils = require('utils') +local widgets = require('gui.widgets') +require('dfhack.buildings') + +config = config or json.open('dfhack-config/buildingplan.json') + +local uibs = df.global.buildreq + +reset_counts_flag = false +editing_filters_flag = false + +local function get_cur_filters() + return dfhack.buildings.getFiltersByType({}, uibs.building_type, + uibs.building_subtype, uibs.custom_type) +end + +local function is_choosing_area() + return uibs.selection_pos.x >= 0 +end + +-- TODO: reuse data in quickfort database +local function get_selection_size_limits() + local btype = uibs.building_type + if btype == df.building_type.Bridge + or btype == df.building_type.FarmPlot + or btype == df.building_type.RoadPaved + or btype == df.building_type.RoadDirt then + return {w=31, h=31} + elseif btype == df.building_type.AxleHorizontal then + return uibs.direction == 1 and {w=1, h=31} or {w=31, h=1} + elseif btype == df.building_type.Rollers then + return (uibs.direction == 1 or uibs.direction == 3) and {w=31, h=1} or {w=1, h=31} + end +end + +local function get_selected_bounds(selection_pos, pos) + selection_pos = selection_pos or uibs.selection_pos + if not is_choosing_area() then return end + + pos = pos or uibs.pos + + local bounds = { + x1=math.min(selection_pos.x, pos.x), + x2=math.max(selection_pos.x, pos.x), + y1=math.min(selection_pos.y, pos.y), + y2=math.max(selection_pos.y, pos.y), + z1=math.min(selection_pos.z, pos.z), + z2=math.max(selection_pos.z, pos.z), + } + + -- clamp to map edges + bounds = { + x1=math.max(0, bounds.x1), + x2=math.min(df.global.world.map.x_count-1, bounds.x2), + y1=math.max(0, bounds.y1), + y2=math.min(df.global.world.map.y_count-1, bounds.y2), + z1=math.max(0, bounds.z1), + z2=math.min(df.global.world.map.z_count-1, bounds.z2), + } + + local limits = get_selection_size_limits() + if limits then + -- clamp to building type area limit + bounds = { + x1=math.max(selection_pos.x - (limits.w-1), bounds.x1), + x2=math.min(selection_pos.x + (limits.w-1), bounds.x2), + y1=math.max(selection_pos.y - (limits.h-1), bounds.y1), + y2=math.min(selection_pos.y + (limits.h-1), bounds.y2), + z1=bounds.z1, + z2=bounds.z2, + } + end + + return bounds +end + +local function get_cur_area_dims(bounds) + if not bounds and not is_choosing_area() then return 1, 1, 1 end + bounds = bounds or get_selected_bounds() + if not bounds then return 1, 1, 1 end + return bounds.x2 - bounds.x1 + 1, + bounds.y2 - bounds.y1 + 1, + bounds.z2 - bounds.z1 + 1 +end + +local function get_selected_volume(bounds) + local w, h, depth = get_cur_area_dims(bounds) + return w * h * depth +end + +local function is_pressure_plate() + return uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.PressurePlate +end + +local function is_weapon_trap() + return uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.WeaponTrap +end + +local function is_spike_trap() + return uibs.building_type == df.building_type.Weapon +end + +local function is_weapon_or_spike_trap() + return is_weapon_trap() or is_spike_trap() +end + +local function is_construction() + return uibs.building_type == df.building_type.Construction +end + +local function is_siege_engine() + return uibs.building_type == df.building_type.SiegeEngine +end + +local function tile_is_construction(pos) + local tt = dfhack.maps.getTileType(pos) + if not tt then return false end + if df.tiletype.attrs[tt].material ~= df.tiletype_material.CONSTRUCTION then + return false + end + local construction = df.construction.find(pos) + return construction and not construction.flags.top_of_wall +end + +local ONE_BY_ONE = xy2pos(1, 1) + +local function can_reconstruct(bounds) + return get_selected_volume(bounds) == 1 or require('plugins.buildingplan').getGlobalSettings().reconstruct +end + +local function can_place_construction(reconstruct, pos) + return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and (reconstruct or not tile_is_construction(pos)) +end + +local function is_interior(bounds, x, y) + return x ~= bounds.x1 and x ~= bounds.x2 and + y ~= bounds.y1 and y ~= bounds.y2 +end + +-- adjusted from CycleHotkeyLabel on the planner panel +local weapon_quantity = 1 + +local function get_quantity(filter, hollow, bounds) + if is_pressure_plate() then + local flags = uibs.plate_info.flags + return (flags.units and 1 or 0) + (flags.water and 1 or 0) + + (flags.magma and 1 or 0) + (flags.track and 1 or 0) + elseif (is_weapon_trap() and filter.vector_id == df.job_item_vector_id.ANY_WEAPON) or is_spike_trap() then + return weapon_quantity + end + local quantity = filter.quantity or 1 + bounds = bounds or get_selected_bounds() + local dimx, dimy, dimz = get_cur_area_dims(bounds) + if quantity < 1 then + return (((dimx * dimy) // 4) + 1) * dimz + end + if bounds and is_construction() then + local reconstruct = can_reconstruct(bounds) + local count = 0 + for z = bounds.z1, bounds.z2 do + for y = bounds.y1, bounds.y2 do + for x = bounds.x1, bounds.x2 do + if hollow and is_interior(bounds, x, y) then goto continue end + if can_place_construction(reconstruct, xyz2pos(x, y, z)) then + count = count + 1 + end + ::continue:: + end + end + end + return quantity * count + end + return quantity * get_selected_volume(bounds) +end + +local function cur_building_has_no_area() + if uibs.building_type == df.building_type.Construction then return false end + local filters = dfhack.buildings.getFiltersByType({}, + uibs.building_type, uibs.building_subtype, uibs.custom_type) + -- this works because all variable-size buildings have either no item + -- filters or a quantity of -1 for their first (and only) item + return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) +end + +local function is_tutorial_open() + local help = df.global.game.main_interface.help + return help.open and + help.context == df.help_context_type.START_TUTORIAL_WORKSHOPS_AND_TASKS +end + +local function is_plannable() + return not is_tutorial_open() and + get_cur_filters() and + not (is_construction() and + uibs.building_subtype == df.construction_type.TrackNSEW) +end + +local function is_slab() + return uibs.building_type == df.building_type.Slab +end + +local function is_cage() + return uibs.building_type == df.building_type.Cage +end + +local function is_stairs() + return is_construction() + and uibs.building_subtype == df.construction_type.UpDownStair +end + +local function is_single_level_stairs() + if not is_stairs() then return false end + local _, _, dimz = get_cur_area_dims() + return dimz == 1 +end + +local function is_multi_level_stairs() + if not is_stairs() then return false end + local _, _, dimz = get_cur_area_dims() + return dimz > 1 +end + +local direction_panel_frame = {t=4, h=13, w=46, r=28} + +local direction_panel_types = utils.invert{ + df.building_type.Bridge, + df.building_type.ScrewPump, + df.building_type.WaterWheel, + df.building_type.AxleHorizontal, + df.building_type.Rollers, + df.building_type.SiegeEngine, +} + +local function has_direction_panel() + return direction_panel_types[uibs.building_type] + or (uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.TrackStop) +end + +local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} + +local function has_pressure_plate_panel() + return is_pressure_plate() +end + +local function is_over_options_panel() + local frame = nil + if has_direction_panel() then + frame = direction_panel_frame + elseif has_pressure_plate_panel() then + frame = pressure_plate_panel_frame + else + return false + end + local v = widgets.Widget{frame=frame} + local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) + v:updateLayout(gui.ViewRect{rect=rect}) + return v:getMousePos() +end + +local function compress(str, len) + if #str <= len then + return str + else + local no_vowels = str:gsub('[aeiou]','') + if #no_vowels <= len then + return no_vowels + else + return no_vowels:sub(1,len-3)..'...' + end + end +end + +local function filter_string(mats, cats, length) + local enabled_mat_names = {} + local enabled_cat_names = {} + for name, props in pairs(mats) do + local enabled = props.enabled == 'true' and cats[props.category] + if enabled then table.insert(enabled_mat_names, name) end + end + if #enabled_mat_names == 1 then + return '['..compress(enabled_mat_names[1], length)..']' + elseif #enabled_mat_names > 1 then + for cat, _ in pairs(cats) do + if cat ~= 'unset' and cats[cat] then + table.insert(enabled_cat_names, cat) + end + end + if #enabled_cat_names == 1 then + return '[' .. enabled_cat_names[1]:gsub("^%l", string.upper) .. ']' + else + return '['..#enabled_cat_names..' mat. categories]' + end + else + -- can result from selecting wood and then toggling "fire safe" etc. + return '[impossible filter]' + end +end +-------------------------------- +-- ItemLine +-- + +-- number of characters for item filter summary (excluding surrounding [ ]) +local item_filter_chars = 17 + +ItemLine = defclass(ItemLine, widgets.Panel) +ItemLine.ATTRS{ + idx=DEFAULT_NIL, + is_selected_fn=DEFAULT_NIL, + is_hollow_fn=DEFAULT_NIL, + on_select=DEFAULT_NIL, + on_filter=DEFAULT_NIL, + on_clear_filter=DEFAULT_NIL, +} + +function ItemLine:init() + self.frame.h = 2 + self.visible = function() return #get_cur_filters() >= self.idx end + self:addviews{ + widgets.Label{ + view_id='item_symbol', + frame={t=0, l=0}, + text=string.char(16), -- this is the "►" character + text_pen=COLOR_YELLOW, + auto_width=true, + visible=self.is_selected_fn, + }, + widgets.Label{ + view_id='item_desc', + frame={t=0, l=2}, + text={ + {text=self:callback('get_item_line_text'), + pen=function() return gui.invert_color(COLOR_WHITE, self.is_selected_fn()) end}, + }, + }, + widgets.Label{ + view_id='item_filter', + frame={t=0, l=28}, + text={ + {text=self:callback('get_filter_text'), + width=item_filter_chars+2, + rjustify=true, + pen=function() return + self:is_impossible() and COLOR_RED or + gui.invert_color(COLOR_LIGHTCYAN, self.is_selected_fn()) end}, + }, + auto_width=true, + on_click=function() self.on_filter(self.idx) end, + }, + widgets.Label{ + frame={t=0, l=47}, + text='[clear]', + text_pen=COLOR_LIGHTRED, + auto_width=true, + visible=self:callback('has_filter'), + on_click=function() self.on_clear_filter(self.idx) end, + }, + widgets.Label{ + frame={t=1, l=2}, + text={ + {gap=2, text=function() return self.note end, + pen=function() return self.note_pen end}, + }, + }, + } +end + +function ItemLine:reset() + self.desc = nil + self.available = nil +end + +function ItemLine:onInput(keys) + if keys._MOUSE_L and self:getMousePos() then + self.on_select(self.idx) + end + return ItemLine.super.onInput(self, keys) +end + +function ItemLine:get_item_line_text() + local idx = self.idx + local filter = get_cur_filters()[idx] + local quantity = get_quantity(filter, self.is_hollow_fn()) + + local buildingplan = require('plugins.buildingplan') + self.desc = self.desc or buildingplan.get_desc(filter) + + self.available = self.available or buildingplan.countAvailableItems( + uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) + if self.available >= quantity then + self.note_pen = COLOR_GREEN + self.note = (' %d available now'):format(self.available) + elseif self.available >= 0 then + self.note_pen = COLOR_BROWN + self.note = (' Will link next (need to make %d)'):format(quantity - self.available) + else + self.note_pen = COLOR_BROWN + self.note = (' Will link later (need to make %d)'):format(-self.available + quantity) + end + self.note = string.char(192) .. self.note -- character 192 is "└" + + return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') +end + +function ItemLine:has_filter() + return require('plugins.buildingplan').hasFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) +end + +function ItemLine:get_filter_text() + local buildingplan = require('plugins.buildingplan') + if not buildingplan.hasFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) + then + return '[any material]' + end + local mats = buildingplan.getMaterialFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) + local cats = buildingplan.getMaterialMaskFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) + return filter_string(mats, cats, item_filter_chars) +end + +-- short circuit version of the '[impossible filter]' case above +function ItemLine:is_impossible() + local buildingplan = require('plugins.buildingplan') + local mats = buildingplan.getMaterialFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) + local cats = buildingplan.getMaterialMaskFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) + for _,props in pairs(mats) do + local enabled = props.enabled == 'true' and cats[props.category] + if enabled then return false end + end + return true +end + +function ItemLine:reduce_quantity(used_quantity) + if not self.available then return end + local filter = get_cur_filters()[self.idx] + used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn()) + self.available = self.available - used_quantity +end + +local function get_placement_errors() + local out = '' + for _,str in ipairs(uibs.errors) do + if #out > 0 then out = out .. NEWLINE end + out = out .. str.value + end + return out +end + +-------------------------------- +-- QuickFilter +-- + +-- Used to store a table of the following format: +-- table +-- string: quick filter slot (must be strings because of the way persistence works) +-- label: string representation of the filter +-- mats: list of material names allowed by the filter +BUILDINGPLAN_FILTERS_KEY = "buildingplan/quick-filters" + +-- old saves may use numbers as keys, which we convert to string keys on load +dfhack.onStateChange[BUILDINGPLAN_FILTERS_KEY] = function(sc) + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + local saved_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) + local new_filters = {} + for k, v in pairs(saved_filters) do + if type(k) == 'number' then + new_filters[tostring(k)] = v + elseif type(k) == 'string' then + new_filters[k] = v + end + end + dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, new_filters) +end + +QuickFilter = defclass(QuickFilter, widgets.Panel) +QuickFilter.ATTRS{ + idx=DEFAULT_NIL, + on_click_fn=DEFAULT_NIL, + is_selected_fn=DEFAULT_NIL +} + +function QuickFilter:init() + local label = ('%d.'):format(self.idx) + self.frame.w = 27 + self.renaming = false + + self:addviews { + widgets.Label { + frame = { t = 0, l = 0 }, + text = string.char(16), -- this is the "►" character + text_pen = COLOR_YELLOW, + auto_width = true, + visible = self.is_selected_fn, + }, + widgets.Label { frame = { t = 0, l = 2 }, text = label }, + widgets.Label { + frame = { t = 0, l = 5, w = item_filter_chars + 2 }, + text = { { text = self:callback('get_label_text'), pen = function() return COLOR_CYAN end } }, + visible = function() return self.renaming == false end, + on_click = self:callback("on_click"), + }, + widgets.EditField { + view_id = 'edit_field', + frame = { t = 0, l = 5, w = item_filter_chars + 2 }, + text = "", + visible = function() return self.renaming == true end, + on_submit = function(text) self:submit_name(text) end, + }, + widgets.Label { + frame = { t = 0, r = 0, w = 3 }, + text = "[x]", + text_pen = COLOR_LIGHTRED, + visible = self:callback("slot_used"), + on_click = self:callback("clear") + } + } +end + +function QuickFilter:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R then + if self.renaming then + self.subviews.edit_field:setFocus(false) + self.renaming = false + editing_filters_flag = false + return true + else + return false + end + end + return QuickFilter.super.onInput(self, keys) +end + +function QuickFilter:on_click() + if dfhack.internal.getModifiers().shift and + self:slot_used() and not editing_filters_flag + then + self.subviews.edit_field:setText(self:get_label_text()) + self.renaming = true + editing_filters_flag = true + self.subviews.edit_field:setFocus(true) + else + self.on_click_fn(self.idx) -- save/apply filter based on selected ItemLine + end +end + +function QuickFilter:slot_used() + local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) + return quick_filters[self.idx] ~= nil +end + +function QuickFilter:clear() + local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) + quick_filters[self.idx] = nil + dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) +end + +function QuickFilter:get_label_text() + local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) + local set = quick_filters[self.idx] + if not set then + return "empty" + else + return set.label + end +end + +function QuickFilter:submit_name(text) + local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) + quick_filters[self.idx].label = compress(text, item_filter_chars+2) + dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) + self.renaming = false + editing_filters_flag = false +end +-------------------------------- +-- PlannerOverlay +-- + +PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) +PlannerOverlay.ATTRS{ + desc='Shows the building planner interface panel when building buildings.', + default_pos={x=5,y=9}, + default_enabled=true, + viewscreens='dwarfmode/Building/Placement', + frame={w=56, h=32}, +} + +function PlannerOverlay:init() + self.selected = 1 + self.state = ensure_key(config.data, 'planner') + + self.selected_favorite = '1' + + local main_panel = widgets.Panel{ + view_id='main', + frame={t=1, l=0, r=0, h=14}, + frame_style=gui.FRAME_INTERIOR_MEDIUM, + frame_background=gui.CLEAR_PEN, + visible=self:callback('is_not_minimized'), + } + + local minimized_panel = widgets.Panel{ + frame={t=0, r=1, w=20, h=1}, + subviews={ + widgets.Label{ + frame={t=0, r=3, h=1}, + text={ + {text=' show Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, + {text='['..string.char(31)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, + }, + visible=self:callback('is_minimized'), + on_click=self:callback('toggle_minimized'), + }, + widgets.Label{ + frame={t=0, r=3, h=1}, + text={ + {text=' hide Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, + {text='['..string.char(30)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, + }, + visible=self:callback('is_not_minimized'), + on_click=self:callback('toggle_minimized'), + }, + widgets.HelpButton{ + frame={t=0, r=0}, + command='buildingplan', + } + }, + } + + local function make_is_selected_fn(idx) + return function() return self.selected == idx end + end + + local function on_select_fn(idx) + self.selected = idx + end + + local function is_hollow_fn() + return self.subviews.hollow:getOptionValue() + end + + local buildingplan = require('plugins.buildingplan') + + main_panel:addviews{ + widgets.Label{ + frame={}, + auto_width=true, + text='No items required.', + visible=function() return #get_cur_filters() == 0 end, + }, + ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, + is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, + is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, + is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, + is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + widgets.CycleHotkeyLabel{ + view_id='hollow', + frame={b=4, l=1, w=21}, + key='CUSTOM_H', + label='Hollow area:', + visible=is_construction, + options={ + {label='No', value=false}, + {label='Yes', value=true, pen=COLOR_GREEN}, + }, + }, + widgets.CycleHotkeyLabel{ + view_id='stairs_top_subtype', + frame={b=7, l=1, w=30}, + key='CUSTOM_R', + label='Top stair type: ', + visible=is_multi_level_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Down', value=df.construction_type.DownStair}, + }, + }, + widgets.CycleHotkeyLabel { + view_id='stairs_bottom_subtype', + frame={b=6, l=1, w=30}, + key='CUSTOM_B', + label='Bottom Stair Type:', + visible=is_multi_level_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Up', value=df.construction_type.UpStair}, + }, + }, + widgets.CycleHotkeyLabel{ + view_id='stairs_only_subtype', + frame={b=7, l=1, w=30}, + key='CUSTOM_R', + label='Single level stair:', + visible=is_single_level_stairs, + options={ + {label='Up', value=df.construction_type.UpStair}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Down', value=df.construction_type.DownStair}, + }, + }, + widgets.CycleHotkeyLabel { -- TODO: this thing also needs a slider + view_id='weapons', + frame={b=4, l=1, w=28}, + key='CUSTOM_T', + key_back='CUSTOM_SHIFT_T', + label='Number of weapons:', + visible=is_weapon_or_spike_trap, + options=utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10), + on_change=function(val) weapon_quantity = val end, + }, + widgets.ToggleHotkeyLabel { + view_id='engraved', + frame={b=4, l=1, w=22}, + key='CUSTOM_T', + label='Engraved only:', + visible=is_slab, + on_change=function(val) + buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) + end, + }, + widgets.ToggleHotkeyLabel { + view_id='empty', + frame={b=4, l=1, w=22}, + key='CUSTOM_T', + label='Empty only:', + visible=is_cage, + on_change=function(val) + buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'empty', val) + end, + }, + widgets.Panel{ + visible=function() return #get_cur_filters() > 0 end, + subviews={ + widgets.HotkeyLabel{ + frame={b=2, l=1, w=22}, + key='CUSTOM_F', + label=function() + return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + and 'Edit filter' or 'Set filter' + end, + on_activate=function() self:set_filter(self.selected) end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=1, w=22}, + key='CUSTOM_CTRL_D', + label='Delete filter', + on_activate=function() self:clear_filter(self.selected) end, + enabled=function() + return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + end + }, + widgets.CycleHotkeyLabel{ + view_id='show_favorites', + frame={b=0, l=1, w=22}, + key='CUSTOM_CTRL_F', + label="", + option_gap=0, + options={ + { label='Show favorites', value = false }, + { label='Hide favorites', value = true }, + }, + initial_option=false, + on_change=function(new,_) self:show_hide_favorites(new) end, + }, + widgets.CycleHotkeyLabel{ + view_id='choose', + frame={b=0, l=24}, + key='CUSTOM_Z', + label='Choose items:', + label_below=true, + options={ + {label='With filters', value=0}, + { + label=function() + local automaterial = itemselection.get_automaterial_selection(uibs.building_type) + return ('Last used (%s)'):format(automaterial or 'pick manually') + end, + value=2, + }, + {label='Manually', value=1}, + }, + initial_option=0, + on_change=function(choose) + buildingplan.setChooseItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, choose) + end, + }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={b=2, l=24, w=25}, + key='CUSTOM_G', + label='Building safety:', + options={ + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + initial_option=0, + on_change=function(heat) + buildingplan.setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) + end, + }, + }, + }, + } + + local divider_widget = widgets.Divider{ + frame={t=10, l=0, r=0, h=1}, + frame_style=gui.FRAME_INTERIOR_MEDIUM, + visible=self:callback('is_not_minimized'), + } + + local error_panel = widgets.ResizingPanel{ + view_id='errors', + frame={t=15, l=0, r=0}, + frame_style=gui.BOLD_FRAME, + frame_background=gui.CLEAR_PEN, + visible=self:callback('is_not_minimized'), + } + + error_panel:addviews{ + widgets.WrappedLabel{ + frame={t=0, l=1, r=0}, + text_pen=COLOR_LIGHTRED, + text_to_wrap=get_placement_errors, + visible=function() return #uibs.errors > 0 end, + }, + widgets.Label{ + frame={t=0, l=1, r=0}, + text_pen=COLOR_GREEN, + text='OK to build', + visible=function() return #uibs.errors == 0 end, + }, + } + + local prev_next_selector = widgets.Panel{ + frame={h=1}, + auto_width=true, + subviews={ + widgets.HotkeyLabel{ + frame={t=0, l=1, w=9}, + key='CUSTOM_SHIFT_Q', + key_sep='\0', + label=': Prev/', + on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, + }, + widgets.HotkeyLabel{ + frame={t=0, l=2, w=1}, + key='CUSTOM_Q', + on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, + }, + widgets.Label{ + frame={t=0,l=10}, + text='next item', + on_click=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, + }, + }, + visible=function() return #get_cur_filters() > 1 end, + } + + local black_bar = widgets.Panel{ + frame={t=0, l=1, w=37, h=1}, + frame_inset=0, + frame_background=gui.CLEAR_PEN, + visible=self:callback('is_not_minimized'), + subviews={ + prev_next_selector, + }, + } + + local function make_is_selected_filter(idx) + return function () return self.selected_favorite == idx end + end + + local favorites_panel = widgets.Panel{ + view_id='favorites', + frame={t=15, l=0, r=0, h=11}, + frame_style=gui.FRAME_INTERIOR_MEDIUM, + frame_background=gui.CLEAR_PEN, + visible=self:callback('show_favorites'), + subviews={ + QuickFilter{idx='1', frame={t=0,l=0}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('1') }, + QuickFilter{idx='2', frame={t=1,l=0}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('2') }, + QuickFilter{idx='3', frame={t=2,l=0}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('3') }, + QuickFilter{idx='4', frame={t=3,l=0}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('4') }, + QuickFilter{idx='5', frame={t=4,l=0}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('5') }, + QuickFilter{idx='6', frame={t=0,l=27}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('6') }, + QuickFilter{idx='7', frame={t=1,l=27}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('7') }, + QuickFilter{idx='8', frame={t=2,l=27}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('8') }, + QuickFilter{idx='9', frame={t=3,l=27}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('9') }, + QuickFilter{idx='0', frame={t=4,l=27}, + on_click_fn=self:callback("save_restore_filter"), + is_selected_fn=make_is_selected_filter('0') }, + widgets.CycleHotkeyLabel { + view_id='slot_select', + frame={b=2, l=2}, + key='CUSTOM_X', + key_back='CUSTOM_SHIFT_X', + label='next/previous slot', + auto_width=true, + options=utils.tabulate(function(i) return {label="", value=tostring(i)} end, 0, 9), + initial_option='1', + on_change=function(val) self.selected_favorite = val end, + }, + widgets.HotkeyLabel{ + frame={b=2, l=28}, + label="set/apply selected", + key='CUSTOM_Y', + on_activate=function () self:save_restore_filter(self.selected_favorite) end, + }, + widgets.TooltipLabel { + frame={b=0, l=2}, + show_tooltip=true, + text="Shift+click to edit the label of a favorite", + }, + + } + } + + self:addviews{ + black_bar, + minimized_panel, + main_panel, + divider_widget, + error_panel, + favorites_panel + } +end + +function PlannerOverlay:show_favorites() + return not self.state.minimized and self.subviews.show_favorites:getOptionValue() +end + +function PlannerOverlay:show_hide_favorites(new) + local errors_frame = {t=15+(new and 11 or 0), l=0, r=0} + self.subviews.errors.frame = errors_frame + self:updateLayout() +end + +function PlannerOverlay:save_restore_filter(slot) + self.selected_favorite = slot + local buildingplan = require('plugins.buildingplan') + local quick_filters = dfhack.persistent.getSiteData(BUILDINGPLAN_FILTERS_KEY, {}) + if quick_filters[slot] then -- restore saved filter + buildingplan.setMaterialFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1, + quick_filters[slot].mats + ) + else -- save current filter + + if not buildingplan.hasFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + then return end + + local mats = buildingplan.getMaterialFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + local cats = buildingplan.getMaterialMaskFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + local label = filter_string(mats, cats, item_filter_chars) + local enabled_mats = {} + for mat, props in pairs(mats) do + if props.enabled == "true" and cats[props.category] then + table.insert(enabled_mats, mat) + end + end + if #enabled_mats > 0 then + quick_filters[slot] = { label = label, mats = enabled_mats } + dfhack.persistent.saveSiteData(BUILDINGPLAN_FILTERS_KEY, quick_filters) + end + end +end + + +function PlannerOverlay:is_minimized() + return self.state.minimized +end + +function PlannerOverlay:is_not_minimized() + return not self.state.minimized +end + +function PlannerOverlay:toggle_minimized() + self.state.minimized = not self.state.minimized + config:write() + self:reset() +end + +function PlannerOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_counts_flag = false +end + +function PlannerOverlay:set_filter(idx) + filterselection.FilterSelectionScreen{index=idx, desc=require('plugins.buildingplan').get_desc(get_cur_filters()[idx])}:show() +end + +function PlannerOverlay:clear_filter(idx) + desc=require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) +end + +local function get_placement_data() + local direction = uibs.direction + local bounds = get_selected_bounds() + local width, height, depth = get_cur_area_dims(bounds) + local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( + width, height, uibs.building_type, uibs.building_subtype, + uibs.custom_type, direction) + -- get the upper-left corner of the building/area at min z-level + local start_pos = bounds and xyz2pos(bounds.x1, bounds.y1, bounds.z1) or + xyz2pos( + uibs.pos.x - adjusted_width//2, + uibs.pos.y - adjusted_height//2, + uibs.pos.z) + if uibs.building_type == df.building_type.ScrewPump then + if direction == df.screw_pump_direction.FromSouth then + start_pos.y = start_pos.y + 1 + elseif direction == df.screw_pump_direction.FromEast then + start_pos.x = start_pos.x + 1 + end + end + local min_x, max_x = start_pos.x, start_pos.x + local min_y, max_y = start_pos.y, start_pos.y + local min_z, max_z = start_pos.z, start_pos.z + if adjusted_width == 1 and adjusted_height == 1 + and (width > 1 or height > 1 or depth > 1) then + max_x = min_x + width - 1 + max_y = min_y + height - 1 + max_z = math.max(uibs.selection_pos.z, uibs.pos.z) + end + return { + x1=min_x, y1=min_y, z1=min_z, + x2=max_x, y2=max_y, z2=max_z, + width=adjusted_width, + height=adjusted_height + } +end + +function PlannerOverlay:save_placement() + self.saved_placement = get_placement_data() + if (uibs.selection_pos:isValid()) then + self.saved_selection_pos_valid = true + self.saved_selection_pos = copyall(uibs.selection_pos) + self.saved_pos = copyall(uibs.pos) + uibs.selection_pos:clear() + else + local sp = self.saved_placement + self.saved_selection_pos = xyz2pos(sp.x1, sp.y1, sp.z1) + self.saved_pos = xyz2pos(sp.x2, sp.y2, sp.z2) + self.saved_pos.x = self.saved_pos.x + sp.width - 1 + self.saved_pos.y = self.saved_pos.y + sp.height - 1 + end +end + +function PlannerOverlay:restore_placement() + if self.saved_selection_pos_valid then + uibs.selection_pos = self.saved_selection_pos + self.saved_selection_pos_valid = nil + else + uibs.selection_pos:clear() + end + self.saved_selection_pos = nil + self.saved_pos = nil + local placement_data = self.saved_placement + self.saved_placement = nil + return placement_data +end + +function PlannerOverlay:onInput(keys) + if not is_plannable() then return false end + if PlannerOverlay.super.onInput(self, keys) then + return true + end + if keys.LEAVESCREEN or keys._MOUSE_R then + if uibs.selection_pos:isValid() then + uibs.selection_pos:clear() + return true + end + self.selected = 1 + self.subviews.hollow:setOption(false) + self:reset() + reset_counts_flag = true + return false + end + if keys.CUSTOM_ALT_M then + self:toggle_minimized() + return true + end + if self:is_minimized() then return false end + if keys._MOUSE_L then + if is_over_options_panel() then return false end + local detect_rect = copyall(self.frame_rect) + detect_rect.height = self.subviews.main.frame_rect.height + + self.subviews.errors.frame_rect.height + detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1 + if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect}) + or self.subviews.errors:getMousePos() then + return true + end + if not is_construction() and #uibs.errors > 0 then return true end + if dfhack.gui.getMousePos() then + if is_choosing_area() or cur_building_has_no_area() then + local filters = get_cur_filters() + local num_filters = #filters + local choose = self.subviews.choose:getOptionValue() + if choose == 0 then + self:place_building(get_placement_data()) + else + local bounds = get_selected_bounds() + self:save_placement() + local autoselect = choose == 2 + local is_hollow = self.subviews.hollow:getOptionValue() + local chosen_items, active_screens = {}, {} + local pending = num_filters + df.global.game.main_interface.bottom_mode_selected = -1 + for idx = num_filters,1,-1 do + chosen_items[idx] = {} + local filter = filters[idx] + local get_available_items_fn = function() + return require('plugins.buildingplan').getAvailableItems( + uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) + end + local selection_screen = itemselection.ItemSelectionScreen{ + get_available_items_fn=get_available_items_fn, + desc=require('plugins.buildingplan').get_desc(filter), + quantity=get_quantity(filter, is_hollow, bounds), + autoselect=autoselect, + on_submit=function(items) + chosen_items[idx] = items + if active_screens[idx] then + active_screens[idx]:dismiss() + active_screens[idx] = nil + else + active_screens[idx] = true + end + pending = pending - 1 + if pending == 0 then + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT + self:place_building(self:restore_placement(), chosen_items) + end + end, + on_cancel=function() + for _,scr in pairs(active_screens) do + scr:dismiss() + end + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT + self:restore_placement() + end, + } + if active_screens[idx] then + -- we've already returned via autoselect + active_screens[idx] = nil + else + active_screens[idx] = selection_screen:show() + end + end + end + return true + elseif not is_choosing_area() then + return false + end + end + end + return keys._MOUSE_L or keys.SELECT +end + +function PlannerOverlay:render(dc) + if not is_plannable() then return end + self.subviews.errors:updateLayout() + PlannerOverlay.super.render(self, dc) +end + +function PlannerOverlay:onRenderFrame(dc, rect) + PlannerOverlay.super.onRenderFrame(self, dc, rect) + + if reset_counts_flag then + self:reset() + local buildingplan = require('plugins.buildingplan') + self.subviews.engraved:setOption(buildingplan.getSpecials( + uibs.building_type, uibs.building_subtype, uibs.custom_type).engraved or false) + self.subviews.empty:setOption(buildingplan.getSpecials( + uibs.building_type, uibs.building_subtype, uibs.custom_type).empty or false) + self.subviews.choose:setOption(buildingplan.getChooseItems( + uibs.building_type, uibs.building_subtype, uibs.custom_type)) + self.subviews.safety:setOption(buildingplan.getHeatSafetyFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type)) + end + + if self:is_minimized() then return end + + local bounds = get_selected_bounds(self.saved_selection_pos, self.saved_pos) + if not bounds then return end + + local hollow = self.subviews.hollow:getOptionValue() + local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN + + -- always allow reconstruction if it's a 1x1x1 selection (meaning the player selected that spot specifically) + local reconstruct = can_reconstruct(bounds) + + local get_pen_fn = is_construction() and + function(pos) + return can_place_construction(reconstruct, pos) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN + end or function() + return default_pen + end + + local function get_overlay_pen(pos) + if not hollow then return get_pen_fn(pos) end + if pos.x == bounds.x1 or pos.x == bounds.x2 or + pos.y == bounds.y1 or pos.y == bounds.y2 then + return get_pen_fn(pos) + end + return gui.TRANSPARENT_PEN + end + + guidm.renderMapOverlay(get_overlay_pen, bounds) +end + +function PlannerOverlay:get_stairs_subtype(pos, bounds) + local subtype = uibs.building_subtype + if pos.z == bounds.z1 then + local opt = bounds.z1 == bounds.z2 and self.subviews.stairs_only_subtype:getOptionValue() or + self.subviews.stairs_bottom_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_DOWN and shape ~= df.tiletype_shape.STAIR_UPDOWN then + subtype = df.construction_type.UpStair + end + else + subtype = opt + end + elseif pos.z == bounds.z2 then + local opt = self.subviews.stairs_top_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_UP and shape ~= df.tiletype_shape.STAIR_UPDOWN then + subtype = df.construction_type.DownStair + end + else + subtype = opt + end + end + return subtype +end + +function PlannerOverlay:place_building(placement_data, chosen_items) + local pd = placement_data + local blds = {} + local hollow = self.subviews.hollow:getOptionValue() + local subtype = uibs.building_subtype + local filters = get_cur_filters() + if is_pressure_plate() or is_spike_trap() then + filters[1].quantity = get_quantity(filters[1]) + elseif is_weapon_trap() then + filters[2].quantity = get_quantity(filters[2]) + end + local reconstruct = can_reconstruct(pd) + for z=pd.z1,pd.z2 do for y=pd.y1,pd.y2 do for x=pd.x1,pd.x2 do + if hollow and is_interior(pd, x, y) then + goto continue + end + local pos = xyz2pos(x, y, z) + if is_construction() and not can_place_construction(reconstruct, pos) then + goto continue + end + if is_stairs() then + subtype = self:get_stairs_subtype(pos, pd) + end + local fields = {} + if is_siege_engine() then + local facing = df.global.buildreq.direction + fields.facing = facing + fields.resting_orientation = facing + end + local bld, err = dfhack.buildings.constructBuilding{pos=pos, + type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, + width=pd.width, height=pd.height, + direction=uibs.direction, filters=filters, fields=fields} + if err then + -- it's ok if some buildings fail to build + goto continue + end + -- assign fields for the types that need them. we can't pass them all in + -- to the call to constructBuilding since attempting to assign unrelated + -- fields to building types that don't support them causes errors. + for k in pairs(bld) do + if k == 'track_stop_info' then utils.assign(bld.track_stop_info, uibs.track_stop) end + if k == 'speed' then bld.speed = uibs.speed end + if k == 'plate_info' then utils.assign(bld.plate_info, uibs.plate_info) end + end + table.insert(blds, bld) + ::continue:: + end end end + local used_quantity = is_construction() and #blds or false + self.subviews.item1:reduce_quantity(used_quantity) + self.subviews.item2:reduce_quantity(used_quantity) + self.subviews.item3:reduce_quantity(used_quantity) + self.subviews.item4:reduce_quantity(used_quantity) + local buildingplan = require('plugins.buildingplan') + for _,bld in ipairs(blds) do + -- attach chosen items and reduce job_item quantity + if chosen_items then + local job = bld.jobs[0] + local jitems = job.job_items.elements + local num_filters = #get_cur_filters() + for idx=1,num_filters do + local item_ids = chosen_items[idx] + local jitem = jitems[num_filters-idx] + while jitem.quantity > 0 and #item_ids > 0 do + local item_id = item_ids[#item_ids] + local item = df.item.find(item_id) + if not item then + dfhack.printerr(('item no longer available: %d'):format(item_id)) + break + end + 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 + jitem.quantity = jitem.quantity - 1 + item_ids[#item_ids] = nil + end + end + end + buildingplan.addPlannedBuilding(bld) + end + buildingplan.scheduleCycle() + uibs.selection_pos:clear() +end + + +return _ENV From 9b378020e80ac935ef93dda9584a98f2b4b562e9 Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Wed, 11 Mar 2026 15:52:15 +0100 Subject: [PATCH 014/128] Logic for slider ready define new Class WeaponSpikeTrapPanel outside of main_panel --- plugins/lua/buildingplan/planneroverlay.lua | 42 +++++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f0fbe17de4..13b6e1d06f 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -148,7 +148,7 @@ local function is_interior(bounds, x, y) y ~= bounds.y1 and y ~= bounds.y2 end --- adjusted from CycleHotkeyLabel on the planner panel +-- adjusted from CycleHotkeyLabel on the planner panel later local weapon_quantity = 1 local function get_quantity(filter, hollow, bounds) @@ -657,6 +657,31 @@ function PlannerOverlay:init() end local buildingplan = require('plugins.buildingplan') + + -- WeaponSpikeTrapPanel defined outside of main_panel, otherwise addviews breaks -> addviews expects table + local WeaponSpikeTrapPanel = defclass(WeaponSpikeTrapPanel, widgets.Panel) + WeaponSpikeTrapPanel.ATTRS{ + view_id='weapons', + visible=is_weapon_or_spike_trap, + } + + function WeaponSpikeTrapPanel:init() + self.options = utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10) + self.selected_idx = weapon_quantity + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='weapons_hotkey', + frame={b=4, l=1, w=28}, + key='CUSTOM_T', + key_back='CUSTOM_SHIFT_T', + label='Number of weapons:', + options=self.options, + initial_option=self.selected_idx, + on_change=function(val) weapon_quantity = val end, + }, + } + end main_panel:addviews{ widgets.Label{ @@ -716,7 +741,7 @@ function PlannerOverlay:init() {label='Up', value=df.construction_type.UpStair}, }, }, - widgets.CycleHotkeyLabel{ + widgets.CycleHotkeyLabel { view_id='stairs_only_subtype', frame={b=7, l=1, w=30}, key='CUSTOM_R', @@ -728,16 +753,9 @@ function PlannerOverlay:init() {label='Down', value=df.construction_type.DownStair}, }, }, - widgets.CycleHotkeyLabel { -- TODO: this thing also needs a slider - view_id='weapons', - frame={b=4, l=1, w=28}, - key='CUSTOM_T', - key_back='CUSTOM_SHIFT_T', - label='Number of weapons:', - visible=is_weapon_or_spike_trap, - options=utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10), - on_change=function(val) weapon_quantity = val end, - }, + + WeaponSpikeTrapPanel{}, + widgets.ToggleHotkeyLabel { view_id='engraved', frame={b=4, l=1, w=22}, From 86a7e7981e498f51e4b9d367c8bb9fb7791d0fb2 Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Fri, 13 Mar 2026 15:06:01 +0100 Subject: [PATCH 015/128] Update .gitattributes --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 50361afbf6..2125666142 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -docs/changelog.txt merge=union +* text=auto \ No newline at end of file From 563fa58f0319f817828f7d8068b2a5908f5d0c06 Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Fri, 13 Mar 2026 15:08:36 +0100 Subject: [PATCH 016/128] Preparatory work Not working yet --- plugins/lua/buildingplan/planneroverlay.lua | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 13b6e1d06f..409633d709 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -148,7 +148,7 @@ local function is_interior(bounds, x, y) y ~= bounds.y1 and y ~= bounds.y2 end --- adjusted from CycleHotkeyLabel on the planner panel later +-- adjusted from WeaponSpiketrapPanel (HotKey & Slider) on the planner panel local weapon_quantity = 1 local function get_quantity(filter, hollow, bounds) @@ -658,14 +658,14 @@ function PlannerOverlay:init() local buildingplan = require('plugins.buildingplan') - -- WeaponSpikeTrapPanel defined outside of main_panel, otherwise addviews breaks -> addviews expects table - local WeaponSpikeTrapPanel = defclass(WeaponSpikeTrapPanel, widgets.Panel) - WeaponSpikeTrapPanel.ATTRS{ + -- WeaponSpiketrapPanel defined outside of main_panel, otherwise addviews breaks -> addviews expects table + local WeaponSpiketrapPanel = defclass(WeaponSpiketrapPanel, widgets.Panel) + WeaponSpiketrapPanel.ATTRS{ view_id='weapons', visible=is_weapon_or_spike_trap, } - function WeaponSpikeTrapPanel:init() + function WeaponSpiketrapPanel:init() self.options = utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10) self.selected_idx = weapon_quantity @@ -680,7 +680,13 @@ function PlannerOverlay:init() initial_option=self.selected_idx, on_change=function(val) weapon_quantity = val end, }, - } + widgets.Slider{ + view_id='weapons_slider', + frame={b=8, l=1, w=28}, + num_stops=#self.options, + + }, + }, end main_panel:addviews{ @@ -754,7 +760,7 @@ function PlannerOverlay:init() }, }, - WeaponSpikeTrapPanel{}, + WeaponSpiketrapPanel{}, widgets.ToggleHotkeyLabel { view_id='engraved', From 929399a601b6b64f008ef641203c5f4fa94bfa3c Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Fri, 13 Mar 2026 15:29:19 +0100 Subject: [PATCH 017/128] Slider commented out, new layout Working, no Slider --- plugins/lua/buildingplan/planneroverlay.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 409633d709..280694edbe 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -680,13 +680,14 @@ function PlannerOverlay:init() initial_option=self.selected_idx, on_change=function(val) weapon_quantity = val end, }, + --[[ widgets.Slider{ view_id='weapons_slider', frame={b=8, l=1, w=28}, num_stops=#self.options, - }, - }, + --]] + } end main_panel:addviews{ From 001356f72573a8133868ac22466d82da9b5511b3 Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Fri, 13 Mar 2026 20:39:50 +0100 Subject: [PATCH 018/128] Corrected pressure_plate_panel_frame numbers --- docs/changelog.txt | 1 + plugins/lua/buildingplan/planneroverlay.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf2ca7d998..da89aef279 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `buildingplan`: fixed non-clickable pressure plates's triggers (issue #5736) ## Misc Improvements diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f0fbe17de4..abf0626a0d 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -248,7 +248,7 @@ local function has_direction_panel() and uibs.building_subtype == df.trap_type.TrackStop) end -local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} +local pressure_plate_panel_frame = {t=4, h=38, w=50, r=28} local function has_pressure_plate_panel() return is_pressure_plate() From 68f5d6d4f6f9b67894f474b1a764d2caf5c4782c Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Sat, 14 Mar 2026 01:16:47 +0100 Subject: [PATCH 019/128] Update .gitattributes --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 2125666142..86db03c22b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -* text=auto \ No newline at end of file +docs/changelog.txt merge=union \ No newline at end of file From 503b8d3fee466d25d58893685f28622436956aa5 Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Sat, 14 Mar 2026 01:27:47 +0100 Subject: [PATCH 020/128] added a slider on the weapontrap overlay --- docs/changelog.txt | 19 +++++++++++++++++++ plugins/lua/buildingplan/planneroverlay.lua | 19 ++++++++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index fdde2f1639..dc1dca964d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,25 @@ Template for new versions: ## New Tools +## New Features +- `buildingplan`: added a slider on the weapontrap overlay + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.11-r2 + +## New Tools + ## New Features ## Fixes diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 280694edbe..cd226aa548 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -667,7 +667,6 @@ function PlannerOverlay:init() function WeaponSpiketrapPanel:init() self.options = utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10) - self.selected_idx = weapon_quantity self:addviews{ widgets.CycleHotkeyLabel{ @@ -677,16 +676,22 @@ function PlannerOverlay:init() key_back='CUSTOM_SHIFT_T', label='Number of weapons:', options=self.options, - initial_option=self.selected_idx, - on_change=function(val) weapon_quantity = val end, + initial_option=weapon_quantity, + on_change=function(val) + weapon_quantity = val + end }, - --[[ + widgets.Slider{ view_id='weapons_slider', - frame={b=8, l=1, w=28}, + frame={b=6, l=4, w=35}, num_stops=#self.options, - - --]] + get_idx_fn=function() return weapon_quantity end, + on_change=function(val) + weapon_quantity = val + self.subviews.weapons_hotkey:setOption(val) + end + } } end From 7dbaa37171fa5801ebee8d3ae85955a68addd88c Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Sat, 14 Mar 2026 01:32:29 +0100 Subject: [PATCH 021/128] Update .gitattributes --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 86db03c22b..50361afbf6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -docs/changelog.txt merge=union \ No newline at end of file +docs/changelog.txt merge=union From 55bc11bf6b9ebbee20535b3dcee0464135052f2e Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Sat, 14 Mar 2026 01:36:06 +0100 Subject: [PATCH 022/128] Update planneroverlay.lua --- plugins/lua/buildingplan/planneroverlay.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index cd226aa548..9fa556f7ea 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -753,7 +753,7 @@ function PlannerOverlay:init() {label='Up', value=df.construction_type.UpStair}, }, }, - widgets.CycleHotkeyLabel { + widgets.CycleHotkeyLabel{ view_id='stairs_only_subtype', frame={b=7, l=1, w=30}, key='CUSTOM_R', From a263d13cf50c90da37fb214ab36d4f99e18b6838 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:44:42 +0000 Subject: [PATCH 023/128] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/lua/buildingplan/planneroverlay.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 9fa556f7ea..1bda1923b1 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -657,7 +657,7 @@ function PlannerOverlay:init() end local buildingplan = require('plugins.buildingplan') - + -- WeaponSpiketrapPanel defined outside of main_panel, otherwise addviews breaks -> addviews expects table local WeaponSpiketrapPanel = defclass(WeaponSpiketrapPanel, widgets.Panel) WeaponSpiketrapPanel.ATTRS{ @@ -667,7 +667,7 @@ function PlannerOverlay:init() function WeaponSpiketrapPanel:init() self.options = utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10) - + self:addviews{ widgets.CycleHotkeyLabel{ view_id='weapons_hotkey', @@ -677,7 +677,7 @@ function PlannerOverlay:init() label='Number of weapons:', options=self.options, initial_option=weapon_quantity, - on_change=function(val) + on_change=function(val) weapon_quantity = val end }, @@ -765,9 +765,9 @@ function PlannerOverlay:init() {label='Down', value=df.construction_type.DownStair}, }, }, - + WeaponSpiketrapPanel{}, - + widgets.ToggleHotkeyLabel { view_id='engraved', frame={b=4, l=1, w=22}, From bcc0d46dda602cdfbe9054aad3f185e3b2036bdd Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 31 Mar 2026 04:10:46 -0700 Subject: [PATCH 024/128] Create graphic_button.lua --- .../gui/widgets/buttons/graphic_button.lua | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 library/lua/gui/widgets/buttons/graphic_button.lua diff --git a/library/lua/gui/widgets/buttons/graphic_button.lua b/library/lua/gui/widgets/buttons/graphic_button.lua new file mode 100644 index 0000000000..f2ffa36be8 --- /dev/null +++ b/library/lua/gui/widgets/buttons/graphic_button.lua @@ -0,0 +1,68 @@ +local textures = require('gui.textures') +local Panel = require('gui.widgets.containers.panel') +local Label = require('gui.widgets.labels.label') + +local to_pen = dfhack.pen.parse + +local button_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} +local button_pen_center = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 10) or nil, ch=string.byte('=')} +local button_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} + +------------------- +-- GraphicButton -- +------------------- + +---@class widgets.GraphicButton.attrs: widgets.Panel.attrs +---@field on_click? function +---@field pen_left dfhack.pen|fun(): dfhack.pen +---@field pen_center dfhack.pen|fun(): dfhack.pen +---@field pen_right dfhack.pen|fun(): dfhack.pen + +---@class widgets.GraphicButton.attrs.partial: widgets.GraphicButton.attrs + +---@class widgets.GraphicButton: widgets.Panel, widgets.GraphicButton.attrs +---@field super widgets.Panel +---@field ATTRS widgets.GraphicButton.attrs|fun(attributes: widgets.GraphicButton.attrs.partial) +---@overload fun(init_table: widgets.GraphicButton.attrs.partial): self +GraphicButton = defclass(GraphicButton, Panel) + +GraphicButton.ATTRS{ + on_click=DEFAULT_NIL, + pen_left=button_pen_left, + pen_center=button_pen_center, + pen_right=button_pen_right, +} + +function GraphicButton:init() + self.frame.w = self.frame.w or 3 + self.frame.h = self.frame.h or 1 + + self:addviews{ + Label{ + view_id='label', + frame={t=0, l=0, w=3, h=1}, + text={ + {tile=self.pen_left}, + {tile=self.pen_center}, + {tile=self.pen_right}, + }, + on_click=self.on_click, + }, + } +end + +function GraphicButton:refresh() + local l = self.subviews.label + + l.on_click = self.on_click + l.pen_left = self.pen_left + l.pen_center = self.pen_center + l.pen_right = self.pen_right + + l:setText({{tile=self.pen_left}, {tile=self.pen_center}, {tile=self.pen_right}}) +end + +return GraphicButton From 6367685819cd0cf37e5fb48bb99555f0457e413b Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 31 Mar 2026 04:13:14 -0700 Subject: [PATCH 025/128] Update help_button.lua - Derive from GraphicButton widget --- .../lua/gui/widgets/buttons/help_button.lua | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/library/lua/gui/widgets/buttons/help_button.lua b/library/lua/gui/widgets/buttons/help_button.lua index 9f9b7dc989..99aa15e7fb 100644 --- a/library/lua/gui/widgets/buttons/help_button.lua +++ b/library/lua/gui/widgets/buttons/help_button.lua @@ -1,53 +1,35 @@ local textures = require('gui.textures') -local Panel = require('gui.widgets.containers.panel') -local Label = require('gui.widgets.labels.label') +local GraphicButton = require('gui.widgets.buttons.graphic_button') -local to_pen = dfhack.pen.parse +local help_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} ---------------- -- HelpButton -- ---------------- ----@class widgets.HelpButton.attrs: widgets.Panel.attrs +---@class widgets.HelpButton.attrs: widgets.GraphicButton.attrs ---@field command? string ---@class widgets.HelpButton.attrs.partial: widgets.HelpButton.attrs ----@class widgets.HelpButton: widgets.Panel, widgets.HelpButton.attrs ----@field super widgets.Panel +---@class widgets.HelpButton: widgets.GraphicButton, widgets.HelpButton.attrs +---@field super widgets.GraphicButton ---@field ATTRS widgets.HelpButton.attrs|fun(attributes: widgets.HelpButton.attrs.partial) ---@overload fun(init_table: widgets.HelpButton.attrs.partial): self -HelpButton = defclass(HelpButton, Panel) +HelpButton = defclass(HelpButton, GraphicButton) HelpButton.ATTRS{ frame={t=0, r=1, w=3, h=1}, command=DEFAULT_NIL, + pen_center=help_pen_center, } -local button_pen_left = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} -local button_pen_right = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} -local help_pen_center = to_pen{ - tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} - function HelpButton:init() - self.frame.w = self.frame.w or 3 - self.frame.h = self.frame.h or 1 - local command = self.command .. ' ' - self:addviews{ - Label{ - frame={t=0, l=0, w=3, h=1}, - text={ - {tile=button_pen_left}, - {tile=help_pen_center}, - {tile=button_pen_right}, - }, - on_click=function() dfhack.run_command('gui/launcher', command) end, - }, - } + self.on_click = function() dfhack.run_command('gui/launcher', command) end + self:refresh() end return HelpButton From 8badbf43952be756953c249e725fadfcf78d6106 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 31 Mar 2026 04:14:05 -0700 Subject: [PATCH 026/128] Update configure_button.lua - Derive from GraphicButton widget --- .../gui/widgets/buttons/configure_button.lua | 46 +++---------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/library/lua/gui/widgets/buttons/configure_button.lua b/library/lua/gui/widgets/buttons/configure_button.lua index b93b1e18b2..bfca49eeb5 100644 --- a/library/lua/gui/widgets/buttons/configure_button.lua +++ b/library/lua/gui/widgets/buttons/configure_button.lua @@ -1,53 +1,19 @@ local textures = require('gui.textures') -local Panel = require('gui.widgets.containers.panel') -local Label = require('gui.widgets.labels.label') +local GraphicButton = require('gui.widgets.buttons.graphic_button') -local to_pen = dfhack.pen.parse - -local button_pen_left = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} -local button_pen_right = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} -local configure_pen_center = to_pen{ +local configure_pen_center = dfhack.pen.parse{ tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol --------------------- -- ConfigureButton -- --------------------- ----@class widgets.ConfigureButton.attrs: widgets.Panel.attrs ----@field on_click? function - ----@class widgets.ConfigureButton.attrs.partial: widgets.ConfigureButton.attrs - ----@class widgets.ConfigureButton: widgets.Panel, widgets.ConfigureButton.attrs ----@field super widgets.Panel ----@field ATTRS widgets.ConfigureButton.attrs|fun(attributes: widgets.ConfigureButton.attrs.partial) ----@overload fun(init_table: widgets.ConfigureButton.attrs.partial): self -ConfigureButton = defclass(ConfigureButton, Panel) +---@class widgets.ConfigureButton.attrs: widgets.GraphicButton.attrs +---@field super widgets.GraphicButton +ConfigureButton = defclass(ConfigureButton, GraphicButton) ConfigureButton.ATTRS{ - on_click=DEFAULT_NIL, + pen_center=configure_pen_center, } -function ConfigureButton:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 - init_table.frame.w = init_table.frame.w or 3 -end - -function ConfigureButton:init() - self:addviews{ - Label{ - frame={t=0, l=0, w=3, h=1}, - text={ - {tile=button_pen_left}, - {tile=configure_pen_center}, - {tile=button_pen_right}, - }, - on_click=self.on_click, - }, - } -end - return ConfigureButton From f7debea7a39562fc56370baa56b39dca69c8f313 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 31 Mar 2026 04:14:56 -0700 Subject: [PATCH 027/128] Create toggle_button.lua --- .../lua/gui/widgets/buttons/toggle_button.lua | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 library/lua/gui/widgets/buttons/toggle_button.lua diff --git a/library/lua/gui/widgets/buttons/toggle_button.lua b/library/lua/gui/widgets/buttons/toggle_button.lua new file mode 100644 index 0000000000..21c2122476 --- /dev/null +++ b/library/lua/gui/widgets/buttons/toggle_button.lua @@ -0,0 +1,49 @@ +local textures = require('gui.textures') +local GraphicButton = require('gui.widgets.buttons.graphic_button') + +local to_pen = dfhack.pen.parse + +local enabled_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} +local enabled_pen_center = to_pen{fg=COLOR_LIGHTGREEN, + tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check +local enabled_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} +local disabled_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')} +local disabled_pen_center = to_pen{fg=COLOR_RED, + tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} +local disabled_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} + +------------------ +-- ToggleButton -- +------------------ + +---@class widgets.ToggleButton.attrs: widgets.GraphicButton.attrs +---@field initial_state boolean + +---@class widgets.ToggleButton.attrs.partial: widgets.ToggleButton.attrs + +---@class widgets.ToggleButton: widgets.GraphicButton, widgets.ToggleButton.attrs +---@field super widgets.GraphicButton +---@field ATTRS widgets.ToggleButton.attrs|fun(attributes: widgets.ToggleButton.attrs.partial) +---@overload fun(init_table: widgets.ToggleButton.attrs.partial): self +ToggleButton = defclass(ToggleButton, GraphicButton) + +ToggleButton.ATTRS{ + initial_state=true, +} + +function ToggleButton:init() + self.toggle_state = self.initial_state + + self.on_click = function() self.toggle_state = not self.toggle_state end + self.pen_left = function() return self.toggle_state and enabled_pen_left or disabled_pen_left end + self.pen_center = function() return self.toggle_state and enabled_pen_center or disabled_pen_center end + self.pen_right = function() return self.toggle_state and enabled_pen_right or disabled_pen_right end + + self:refresh() +end + +return ToggleButton From d7fb882a520d2d586fdebaf328a5a63698af3f01 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 31 Mar 2026 04:16:04 -0700 Subject: [PATCH 028/128] Update widgets.lua - Add ToggleButton widget --- library/lua/gui/widgets.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index e7870f88e8..744827d9bb 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -18,6 +18,7 @@ WrappedLabel = require('gui.widgets.labels.wrapped_label') TooltipLabel = require('gui.widgets.labels.tooltip_label') HelpButton = require('gui.widgets.buttons.help_button') ConfigureButton = require('gui.widgets.buttons.configure_button') +ToggleButton = require('gui.widgets.buttons.toggle_button') BannerPanel = require('gui.widgets.containers.banner_panel') TextButton = require('gui.widgets.buttons.text_button') CycleHotkeyLabel = require('gui.widgets.labels.cycle_hotkey_label') From 39cc838655f3a97c74cb6f4cc57d17c9f59cec8d Mon Sep 17 00:00:00 2001 From: sizzlins Date: Wed, 22 Apr 2026 12:28:01 +0700 Subject: [PATCH 029/128] buildingplan: fix workorders queuing unknown/invalid materials --- plugins/lua/buildingplan/planneroverlay.lua | 151 +++++++++++++++++++- 1 file changed, 147 insertions(+), 4 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f0fbe17de4..75fa917093 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -409,6 +409,8 @@ function ItemLine:get_item_line_text() self.note = (' Will link later (need to make %d)'):format(-self.available + quantity) end self.note = string.char(192) .. self.note -- character 192 is "└" + + self.quantity = quantity return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end @@ -610,7 +612,7 @@ function PlannerOverlay:init() local main_panel = widgets.Panel{ view_id='main', - frame={t=1, l=0, r=0, h=14}, + frame={t=1, l=0, r=0, h=15}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), @@ -761,6 +763,16 @@ function PlannerOverlay:init() widgets.Panel{ visible=function() return #get_cur_filters() > 0 end, subviews={ + widgets.HotkeyLabel{ + frame={b=3, l=1, w=22}, + key='CUSTOM_CTRL_Q', + label='Queue order', + on_activate=function() self:queue_order(self.selected) end, + visible=function() + local item = self.subviews['item'..tostring(self.selected)] + return item and item.available and item.quantity and (item.available < item.quantity) + end + }, widgets.HotkeyLabel{ frame={b=2, l=1, w=22}, key='CUSTOM_F', @@ -841,7 +853,7 @@ function PlannerOverlay:init() local error_panel = widgets.ResizingPanel{ view_id='errors', - frame={t=15, l=0, r=0}, + frame={t=16, l=0, r=0}, frame_style=gui.BOLD_FRAME, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), @@ -903,7 +915,7 @@ function PlannerOverlay:init() local favorites_panel = widgets.Panel{ view_id='favorites', - frame={t=15, l=0, r=0, h=9}, + frame={t=16, l=0, r=0, h=9}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('show_favorites'), @@ -974,7 +986,7 @@ function PlannerOverlay:show_favorites() end function PlannerOverlay:show_hide_favorites(new) - local errors_frame = {t=15+(new and 9 or 0), l=0, r=0} + local errors_frame = {t=16+(new and 9 or 0), l=0, r=0} self.subviews.errors.frame = errors_frame self:updateLayout() end @@ -1043,6 +1055,137 @@ function PlannerOverlay:clear_filter(idx) desc=require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end +function PlannerOverlay:queue_order(idx) + local item = self.subviews['item'..tostring(idx)] + if not item or not item.available or not item.quantity or item.available >= item.quantity then return end + local missing = item.quantity - item.available + if missing <= 0 then return end + + local filter = get_cur_filters()[idx] + + local item_to_job = { + [df.item_type.BED] = 'ConstructBed', + [df.item_type.DOOR] = 'ConstructDoor', + [df.item_type.CABINET] = 'ConstructCabinet', + [df.item_type.TABLE] = 'ConstructTable', + [df.item_type.CHAIR] = 'ConstructThrone', + [df.item_type.BOX] = 'ConstructChest', + [df.item_type.ARMORSTAND] = 'ConstructArmorStand', + [df.item_type.WEAPONRACK] = 'ConstructWeaponRack', + [df.item_type.STATUE] = 'ConstructStatue', + [df.item_type.COFFIN] = 'ConstructCoffin', + [df.item_type.HATCH_COVER] = 'ConstructHatchCover', + [df.item_type.GRATE] = 'ConstructGrate', + [df.item_type.QUERN] = 'ConstructQuern', + [df.item_type.MILLSTONE] = 'ConstructMillstone', + [df.item_type.TRACTION_BENCH] = 'ConstructTractionBench', + [df.item_type.SLAB] = 'ConstructSlab', + [df.item_type.ANVIL] = 'ForgeAnvil', + [df.item_type.WINDOW] = 'MakeWindow', + [df.item_type.CAGE] = 'MakeCage', + [df.item_type.BARREL] = 'MakeBarrel', + [df.item_type.BUCKET] = 'MakeBucket', + [df.item_type.ANIMALTRAP] = 'MakeAnimalTrap', + [df.item_type.CHAIN] = 'MakeChain', + [df.item_type.FLASK] = 'MakeFlask', + [df.item_type.GOBLET] = 'MakeGoblet', + [df.item_type.BLOCKS] = 'ConstructBlocks', + } + + local job_name = "ConstructBlocks" + local item_type = nil + if filter.item_type and filter.item_type ~= -1 then + item_type = filter.item_type + elseif filter.vector_id and filter.vector_id ~= -1 then + local mapping_vector = { + [df.job_item_vector_id.ANY_WEAPON] = df.item_type.WEAPON, + [df.job_item_vector_id.ANY_ARMOR] = df.item_type.ARMOR, + } + item_type = mapping_vector[filter.vector_id] + end + + if item_type and item_to_job[item_type] then + job_name = item_to_job[item_type] + end + + local order_json = { + job = job_name, + amount_total = missing + } + + local buildingplan = require('plugins.buildingplan') + local cats_list = {} + if buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) then + local cats = buildingplan.getMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) + for cat, enabled in pairs(cats) do + if enabled and cat ~= 'unset' then + table.insert(cats_list, cat) + end + end + end + + if #cats_list == 0 then + local job_defaults = { + ConstructBed = {'wood'}, + ConstructDoor = {'stone'}, + ConstructCabinet = {'stone'}, + ConstructTable = {'stone'}, + ConstructThrone = {'stone'}, + ConstructChest = {'stone'}, + ConstructArmorStand = {'stone'}, + ConstructWeaponRack = {'stone'}, + ConstructStatue = {'stone'}, + ConstructCoffin = {'stone'}, + ConstructHatchCover = {'stone'}, + ConstructGrate = {'stone'}, + ConstructQuern = {'stone'}, + ConstructMillstone = {'stone'}, + ConstructTractionBench = {'wood'}, + ConstructSlab = {'stone'}, + ForgeAnvil = {'iron'}, + MakeWindow = {'glass'}, + MakeCage = {'wood'}, + MakeBarrel = {'wood'}, + MakeBucket = {'wood'}, + MakeAnimalTrap = {'wood'}, + MakeChain = {'iron'}, + MakeFlask = {'iron'}, + MakeGoblet = {'stone'}, + ConstructBlocks = {'stone'}, + } + if job_defaults[job_name] then + cats_list = job_defaults[job_name] + end + end + + local valid_mat_cats = { + wood=true, bone=true, shell=true, horn=true, pearl=true, tooth=true, + leather=true, silk=true, yarn=true, cloth=true, plant=true + } + + local mat_cats = {} + for _, cat in ipairs(cats_list) do + if valid_mat_cats[cat] then + table.insert(mat_cats, cat) + elseif cat == 'stone' then + order_json.material = "INORGANIC" + elseif cat == 'glass' then + order_json.material = "GLASS_GREEN" + elseif cat == 'metal' or cat == 'iron' then + order_json.material = "IRON" + end + end + + if #mat_cats > 0 then + order_json.material_category = mat_cats + end + + dfhack.run_command_silent('workorder', json.encode(order_json)) + + local desc = item.desc or "item" + dfhack.gui.showAnnouncement('Work order queued for ' .. tostring(missing) .. ' ' .. desc .. '.', COLOR_YELLOW, true) +end + local function get_placement_data() local direction = uibs.direction local bounds = get_selected_bounds() From cc98819cb95534c20bc32e8fcc3b9e7ab9d54dd6 Mon Sep 17 00:00:00 2001 From: sizzlins Date: Wed, 22 Apr 2026 12:34:14 +0700 Subject: [PATCH 030/128] fix trailing whitespace --- plugins/lua/buildingplan/planneroverlay.lua | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 75fa917093..16bf582ac6 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -409,7 +409,7 @@ function ItemLine:get_item_line_text() self.note = (' Will link later (need to make %d)'):format(-self.available + quantity) end self.note = string.char(192) .. self.note -- character 192 is "└" - + self.quantity = quantity return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') @@ -1060,9 +1060,9 @@ function PlannerOverlay:queue_order(idx) if not item or not item.available or not item.quantity or item.available >= item.quantity then return end local missing = item.quantity - item.available if missing <= 0 then return end - + local filter = get_cur_filters()[idx] - + local item_to_job = { [df.item_type.BED] = 'ConstructBed', [df.item_type.DOOR] = 'ConstructDoor', @@ -1091,7 +1091,7 @@ function PlannerOverlay:queue_order(idx) [df.item_type.GOBLET] = 'MakeGoblet', [df.item_type.BLOCKS] = 'ConstructBlocks', } - + local job_name = "ConstructBlocks" local item_type = nil if filter.item_type and filter.item_type ~= -1 then @@ -1103,16 +1103,16 @@ function PlannerOverlay:queue_order(idx) } item_type = mapping_vector[filter.vector_id] end - + if item_type and item_to_job[item_type] then job_name = item_to_job[item_type] end - + local order_json = { job = job_name, amount_total = missing } - + local buildingplan = require('plugins.buildingplan') local cats_list = {} if buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) then @@ -1123,7 +1123,7 @@ function PlannerOverlay:queue_order(idx) end end end - + if #cats_list == 0 then local job_defaults = { ConstructBed = {'wood'}, @@ -1157,12 +1157,12 @@ function PlannerOverlay:queue_order(idx) cats_list = job_defaults[job_name] end end - + local valid_mat_cats = { wood=true, bone=true, shell=true, horn=true, pearl=true, tooth=true, leather=true, silk=true, yarn=true, cloth=true, plant=true } - + local mat_cats = {} for _, cat in ipairs(cats_list) do if valid_mat_cats[cat] then @@ -1175,13 +1175,13 @@ function PlannerOverlay:queue_order(idx) order_json.material = "IRON" end end - + if #mat_cats > 0 then order_json.material_category = mat_cats end - + dfhack.run_command_silent('workorder', json.encode(order_json)) - + local desc = item.desc or "item" dfhack.gui.showAnnouncement('Work order queued for ' .. tostring(missing) .. ' ' .. desc .. '.', COLOR_YELLOW, true) end From a99a2ed52b412e8e916ab2bb715ca8986b82a425 Mon Sep 17 00:00:00 2001 From: sizzlins Date: Sat, 25 Apr 2026 11:00:22 +0700 Subject: [PATCH 031/128] docs: Document Ctrl-Q queue work order feature in buildingplan --- docs/plugins/buildingplan.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index c00569365c..fd239aaed6 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -204,6 +204,18 @@ other available items (or from items produced in the future if not all items are available yet). If there are multiple item types to choose for the current building, one dialog will appear per item type. +Queueing work orders +-------------------- + +If you are planning a building but do not have the required items in stock, you can +automatically queue a manager work order to produce the missing quantity. After +selecting your desired item types and filters, press :kbd:`Ctrl`:kbd:`q` (or click +"Queue order") to generate a work order. + +`buildingplan` will attempt to automatically determine the correct job (e.g. making +a wooden bed if you are planning a bed) and will respect the material categories +you have selected in your filters. + Building status --------------- From a0e630371e23d97634af231b0a74f00329519e5b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 12:37:07 -0500 Subject: [PATCH 032/128] move infinite sky's actual work code to `Maps` the infinite sky plugin will still handle the "automatic" extension, but the code to actually extend the map block columns moves to the Maps module this is due to a request from a modder to be able to extend map block columns from scripts without having to rely on a plugin API call --- library/LuaApi.cpp | 1 + library/include/modules/Maps.h | 2 + library/modules/Maps.cpp | 135 +++++++++++++++++++++++++++++++++ plugins/infinite-sky.cpp | 128 +------------------------------ 4 files changed, 141 insertions(+), 125 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d95b6c1a2a..a6d0627c4e 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2653,6 +2653,7 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = { WRAPM(Maps, getWalkableGroup), WRAPM(Maps, canWalkBetween), WRAPM(Maps, spawnFlow), + WRAPM(Maps, addBlockColumns), WRAPN(hasTileAssignment, hasTileAssignment), WRAPN(getTileAssignment, getTileAssignment), WRAPN(setTileAssignment, setTileAssignment), diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index 1e7eac89e1..bf7a947150 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -405,6 +405,8 @@ DFHACK_EXPORT bool removeTileAquifer(int32_t x, int32_t y, int32_t z); inline bool removeTileAquifer(df::coord pos) { return removeTileAquifer(pos.x, pos.y, pos.z); } DFHACK_EXPORT int removeAreaAquifer(df::coord pos1, df::coord pos2, std::function filter = [](df::coord pos, df::map_block *block) { return true; }); + +DFHACK_EXPORT void addBlockColumns(int32_t new_height); } } #endif diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 2376936054..b8aa6c5c85 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -40,6 +40,7 @@ distribution. #include "df/biome_type.h" #include "df/block_burrow.h" #include "df/block_burrow_link.h" +#include "df/block_column_print_infost.h" #include "df/block_square_event_grassst.h" #include "df/block_square_event_item_spatterst.h" #include "df/block_square_event_material_spatterst.h" @@ -48,10 +49,13 @@ distribution. #include "df/building_type.h" #include "df/builtin_mats.h" #include "df/burrow.h" +#include "df/entity_plot_invasion_mapst.h" #include "df/feature_init.h" #include "df/feature_map_shellst.h" #include "df/feature_mapst.h" #include "df/flow_info.h" +#include "df/historical_entity.h" +#include "df/invasion_info.h" #include "df/map_block.h" #include "df/map_block_column.h" #include "df/material.h" @@ -59,6 +63,8 @@ distribution. #include "df/plant_root_tile.h" #include "df/plant_tree_info.h" #include "df/plant_tree_tile.h" +#include "df/plotinfost.h" +#include "df/plot_invasion_mapst.h" #include "df/region_map_entry.h" #include "df/world.h" #include "df/world_data.h" @@ -68,9 +74,12 @@ distribution. #include "df/world_underground_region.h" #include "df/z_level_flags.h" + +#include #include #include #include +#include #include #include #include @@ -1522,3 +1531,129 @@ int Maps::removeAreaAquifer(df::coord pos1, df::coord pos2, std::functionmap.z_count_block; + if (quantity <= 0) + return; + + auto world = df::global::world; + int32_t z_count_block = world->map.z_count_block; + df::map_block**** block_index = world->map.block_index; + + cuboid last_air_layer( + 0, 0, world->map.z_count_block - 1, + world->map.x_count_block - 1, world->map.y_count_block - 1, world->map.z_count_block - 1); + + last_air_layer.forCoord([&] (df::coord bpos) { + // Allocate a new block column and copy over data from the old + df::map_block** blockColumn = + new df::map_block * [z_count_block + quantity]; + std::memcpy(blockColumn, block_index[bpos.x][bpos.y], + z_count_block * sizeof(df::map_block*)); + delete[] block_index[bpos.x][bpos.y]; + block_index[bpos.x][bpos.y] = blockColumn; + + df::map_block* last_air_block = blockColumn[bpos.z]; + for (int32_t count = 0; count < quantity; count++) + { + df::map_block* air_block = new df::map_block(); + std::fill(&air_block->tiletype[0][0], + &air_block->tiletype[0][0] + (16 * 16), + df::tiletype::OpenSpace); + + // Set block positions properly (based on prior air layer) + air_block->map_pos = last_air_block->map_pos; + air_block->map_pos.z += count + 1; + air_block->region_pos = last_air_block->region_pos; + + // Copy other potentially important metadata from prior air + // layer + std::memcpy(air_block->lighting, last_air_block->lighting, + sizeof(air_block->lighting)); + std::memcpy(air_block->temperature_1, last_air_block->temperature_1, + sizeof(air_block->temperature_1)); + std::memcpy(air_block->temperature_2, last_air_block->temperature_2, + sizeof(air_block->temperature_2)); + std::memcpy(air_block->region_offset, last_air_block->region_offset, + sizeof(air_block->region_offset)); + + // Create tile designations to inform lighting and + // outside markers + df::tile_designation designation{}; + designation.bits.light = true; + designation.bits.outside = true; + std::fill(&air_block->designation[0][0], + &air_block->designation[0][0] + (16 * 16), designation); + + blockColumn[z_count_block + count] = air_block; + world->map.map_blocks.push_back(air_block); + + // deal with map_block_column stuff even though it'd probably be + // fine + df::map_block_column* column = + world->map.column_index[bpos.x][bpos.y]; + if (!column) + { + continue; + } + df::block_column_print_infost* glyphs = new df::block_column_print_infost; + std::ranges::copy(std::array{0,1,2,3}, glyphs->x); + std::ranges::copy(std::array{0,0,0,0}, glyphs->y); + std::ranges::copy(std::array{'e','x','p','^'}, glyphs->tile); + column->unmined_glyphs.push_back(glyphs); + } + return true; + }); + + // Update global z level flags + df::z_level_flags* flags = new df::z_level_flags[z_count_block + quantity]; + memcpy(flags, world->map_extras.z_level_flags, + z_count_block * sizeof(df::z_level_flags)); + for (int32_t count = 0; count < quantity; count++) + { + flags[z_count_block + count].whole = 0; + flags[z_count_block + count].bits.update = 1; + } + world->map.z_count_block += quantity; + world->map.z_count += quantity; + delete[] world->map_extras.z_level_flags; + world->map_extras.z_level_flags = flags; + + auto updateInvasionMap = [](int32_t new_height, df::plot_invasion_mapst & map) -> void + { + if (map.blockz == 0) + return; // Unused invasion map + if (map.blockz >= new_height) + return; // No change required + + cuboid blocks(0, 0, 0, map.blockx - 1, map.blocky - 1, 0); + blocks.forCoord([&] (df::coord bpos) { + // Create new vertical block + df::pim_blockst** new_block = new df::pim_blockst * [new_height](); + std::memcpy(new_block, map.block_index[bpos.x][bpos.y], map.blockz * sizeof(df::pim_blockst*)); + // Fill new block with nullptr (no information) + std::fill_n(&new_block[map.blockz], new_height - map.blockz, nullptr); + delete[] map.block_index[bpos.x][bpos.y]; + map.block_index[bpos.x][bpos.y] = new_block; + return true; + }); + + map.blockz = new_height; + }; + + auto plotinfo = df::global::plotinfo; + + for (auto& invasion : plotinfo->invasions.list) + { + updateInvasionMap(world->map.z_count, invasion->map); + } + for (auto& entity : world->entities.all) + { + for (auto& map : entity->plot_invasion_map | std::views::filter([&](df::entity_plot_invasion_mapst* map) { return map->site_id == plotinfo->site_id; })) + { + updateInvasionMap(world->map.z_count, map->map); + } + } +} diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 402a4a7a7b..50c0563ef3 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -136,132 +136,10 @@ static void constructionEventHandler(color_ostream &out, void *ptr) { doInfiniteSky(out, 1); } -void addBlockColumns(color_ostream& out, int32_t quantity) { - int32_t z_count_block = world->map.z_count_block; - df::map_block ****block_index = world->map.block_index; - - cuboid last_air_layer( - 0, 0, world->map.z_count_block - 1, - world->map.x_count_block - 1, world->map.y_count_block - 1, world->map.z_count_block - 1); - - last_air_layer.forCoord([&](df::coord bpos) { - // Allocate a new block column and copy over data from the old - df::map_block **blockColumn = - new df::map_block *[z_count_block + quantity]; - memcpy(blockColumn, block_index[bpos.x][bpos.y], - z_count_block * sizeof(df::map_block *)); - delete[] block_index[bpos.x][bpos.y]; - block_index[bpos.x][bpos.y] = blockColumn; - - df::map_block *last_air_block = blockColumn[bpos.z]; - for (int32_t count = 0; count < quantity; count++) { - df::map_block *air_block = new df::map_block(); - std::fill(&air_block->tiletype[0][0], - &air_block->tiletype[0][0] + (16 * 16), - df::tiletype::OpenSpace); - - // Set block positions properly (based on prior air layer) - air_block->map_pos = last_air_block->map_pos; - air_block->map_pos.z += count + 1; - air_block->region_pos = last_air_block->region_pos; - - // Copy other potentially important metadata from prior air - // layer - std::memcpy(air_block->lighting, last_air_block->lighting, - sizeof(air_block->lighting)); - std::memcpy(air_block->temperature_1, last_air_block->temperature_1, - sizeof(air_block->temperature_1)); - std::memcpy(air_block->temperature_2, last_air_block->temperature_2, - sizeof(air_block->temperature_2)); - std::memcpy(air_block->region_offset, last_air_block->region_offset, - sizeof(air_block->region_offset)); - - // Create tile designations to inform lighting and - // outside markers - df::tile_designation designation{}; - designation.bits.light = true; - designation.bits.outside = true; - std::fill(&air_block->designation[0][0], - &air_block->designation[0][0] + (16 * 16), designation); - - blockColumn[z_count_block + count] = air_block; - world->map.map_blocks.push_back(air_block); - - // deal with map_block_column stuff even though it'd probably be - // fine - df::map_block_column *column = - world->map.column_index[bpos.x][bpos.y]; - if (!column) { - DEBUG(cycle, out) - .print("{}, line {}: column is null ({}).\n", __FILE__, __LINE__, bpos); - continue; - } - df::block_column_print_infost *glyphs = new df::block_column_print_infost; - glyphs->x[0] = 0; - glyphs->x[1] = 1; - glyphs->x[2] = 2; - glyphs->x[3] = 3; - glyphs->y[0] = 0; - glyphs->y[1] = 0; - glyphs->y[2] = 0; - glyphs->y[3] = 0; - glyphs->tile[0] = 'e'; - glyphs->tile[1] = 'x'; - glyphs->tile[2] = 'p'; - glyphs->tile[3] = '^'; - column->unmined_glyphs.push_back(glyphs); - } - return true; - }); - - // Update global z level flags - df::z_level_flags *flags = new df::z_level_flags[z_count_block + quantity]; - memcpy(flags, world->map_extras.z_level_flags, - z_count_block * sizeof(df::z_level_flags)); - for (int32_t count = 0; count < quantity; count++) { - flags[z_count_block + count].whole = 0; - flags[z_count_block + count].bits.update = 1; - } - world->map.z_count_block += quantity; - world->map.z_count += quantity; - delete[] world->map_extras.z_level_flags; - world->map_extras.z_level_flags = flags; -} - -void updateInvasionMap(color_ostream &out, int32_t new_height, df::plot_invasion_mapst& map) { - if (map.blockz == 0) - return; // Unused invasion map - if (map.blockz >= new_height) - return; // No change required - - cuboid blocks(0, 0, 0, map.blockx - 1, map.blocky - 1, 0); - blocks.forCoord([&](df::coord bpos) { - // Create new vertical block - df::pim_blockst **new_block = new df::pim_blockst *[new_height](); - memcpy(new_block, map.block_index[bpos.x][bpos.y], map.blockz * sizeof(df::pim_blockst*)); - // Fill new block with nullptr (no information) - std::fill_n(&new_block[map.blockz], new_height - map.blockz, nullptr); - delete[] map.block_index[bpos.x][bpos.y]; - map.block_index[bpos.x][bpos.y] = new_block; - return true; - }); - - map.blockz = new_height; -} -void doInfiniteSky(color_ostream &out, int32_t quantity) { - addBlockColumns(out, quantity); - - for (auto& invasion : plotinfo->invasions.list) { - updateInvasionMap(out, world->map.z_count, invasion->map); - } - for (auto& entity : world->entities.all) { - for (auto& map : entity->plot_invasion_map) { - if (map->site_id != plotinfo->site_id) - continue; - updateInvasionMap(out, world->map.z_count, map->map); - } - } +void doInfiniteSky(color_ostream& out, int32_t quantity) +{ + Maps::addBlockColumns(world->map.z_count_block + quantity); } struct infinitesky_options { From d057f2b8b843f770dcb7f6783dd578bcbcc56b12 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 13:33:08 -0500 Subject: [PATCH 033/128] make `getConfigPath` a public `Core` API also export to Lua as `dfhack.getConfigPath` mainly so that we can move `dfhack-config` without having to update hundreds of source locations --- library/Core.cpp | 27 ++++++++------------------- library/LuaApi.cpp | 2 ++ library/include/Core.h | 12 ++++++++++++ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 00419bd7ae..0099cae23b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -135,16 +135,6 @@ namespace DFHack { DBG_DECLARE(core, keybinding, DebugCategory::LINFO); DBG_DECLARE(core, script, DebugCategory::LINFO); - static const std::filesystem::path getConfigPath() - { - return Filesystem::getInstallDir() / "dfhack-config"; - }; - - static const std::filesystem::path getConfigDefaultsPath() - { - return Core::getInstance().getHackPath() / "data" / "dfhack-config-defaults"; - }; - class MainThread { public: //! MainThread::suspend keeps the main DF thread suspended from Core::Init to @@ -538,9 +528,9 @@ std::filesystem::path Core::findScript(std::string name) return {}; } -bool loadScriptPaths(color_ostream &out, bool silent = false) +bool loadScriptPathsCore(Core& core, color_ostream &out, bool silent = false) { - std::filesystem::path filename{ getConfigPath() / "script-paths.txt" }; + std::filesystem::path filename{ core.getConfigPath() / "script-paths.txt" }; std::ifstream file(filename); if (!file) { @@ -563,7 +553,7 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) getline(ss, path); if (ch == '+' || ch == '-') { - if (!Core::getInstance().addScriptPath(path, ch == '+') && !silent) + if (!core.addScriptPath(path, ch == '+') && !silent) out.printerr("{}:{}: Failed to add path: {}\n", filename, line, path); } else if (!silent) @@ -935,12 +925,11 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // load baseline defaults - core->loadScriptFile(out, getConfigPath() / "init" / "default.dfhack.init", false); + core->loadScriptFile(out, core->getConfigPath() / "init" / "default.dfhack.init", false); // load user overrides std::vector prefixes(1, "dfhack"); - loadScriptFiles(core, out, prefixes, getConfigPath() / "init"); - + loadScriptFiles(core, out, prefixes, core->getConfigPath() / "init"); // show the terminal if requested auto L = DFHack::Core::getInstance().getLuaState(); Lua::CallLuaModuleFunction(out, L, "dfhack", "getHideConsoleOnStartup", 0, 1, @@ -962,9 +951,9 @@ static void fInitthread(IODATA * iod) // A thread function... for the interactive console. static void fIOthread(IODATA * iod) { - static const std::filesystem::path HISTORY_FILE = getConfigPath() / "dfhack.history"; - Core * core = iod->core; + std::filesystem::path HISTORY_FILE = core->getConfigPath() / "dfhack.history"; + PluginManager * plug_mgr = iod->plug_mgr; CommandHistory main_history; @@ -1388,7 +1377,7 @@ bool Core::InitSimulationThread() #endif } - loadScriptPaths(con); + loadScriptPathsCore(*this, con); // initialize common lua context // Calls InitCoreContext after checking IsCoreContext diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d95b6c1a2a..670dddce6c 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1359,6 +1359,7 @@ static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); } static std::filesystem::path getDFPath() { return Core::getInstance().p->getPath(); } static std::filesystem::path getHackPath() { return Core::getInstance().getHackPath(); } +static std::filesystem::path getConfigPath() { return Core::getInstance().getConfigPath(); } static bool isWorldLoaded() { return Core::getInstance().isWorldLoaded(); } static bool isMapLoaded() { return Core::getInstance().isMapLoaded(); } @@ -1384,6 +1385,7 @@ static const LuaWrapper::FunctionReg dfhack_module[] = { WRAP(getDFPath), WRAP(getTickCount), WRAP(getHackPath), + WRAP(getConfigPath), WRAP(isWorldLoaded), WRAP(isMapLoaded), WRAP(isSiteLoaded), diff --git a/library/include/Core.h b/library/include/Core.h index 8b78e58097..5548793132 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -29,6 +29,8 @@ distribution. #include "Export.h" #include "Hooks.h" +#include "modules/Filesystem.h" + #include #include #include @@ -251,6 +253,16 @@ namespace DFHack return false; } + const std::filesystem::path getConfigPath() + { + return Filesystem::getInstallDir() / "dfhack-config"; + } + + const std::filesystem::path getConfigDefaultsPath() + { + return getHackPath() / "data" / "dfhack-config-defaults"; + } + private: DFHack::Console con; From 449c2db26c7789f1b198790e43ac08cc980e0bd9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:05:53 -0500 Subject: [PATCH 034/128] remove a hardcoded reference to `dfhack-config` plus some light code cleanup --- library/LuaTools.cpp | 19 ++++++++++--------- library/include/LuaTools.h | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 69242ede5f..0a172a10e5 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1281,29 +1281,30 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, DFHack: return (rv == LUA_OK); } -static bool init_interpreter(color_ostream &out, lua_State *state, const char* prompt, const char* hfile) +static bool init_interpreter(color_ostream &out, lua_State *state, const std::string& prompt, const std::filesystem::path& hfile) { lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_DFHACK_TOKEN); lua_getfield(state, -1, "interpreter"); lua_remove(state, -2); - lua_pushstring(state, prompt); - lua_pushstring(state, hfile); + lua_pushlstring(state, prompt.c_str(), prompt.size()); + lua_pushlstring(state, hfile.string().c_str(), hfile.string().size()); return true; } bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, - const char *prompt, const char *hfile) + std::string prompt, std::filesystem::path hfile) { if (!out.is_console()) return false; - if (!hfile) - hfile = "dfhack-config/lua.history"; - if (!prompt) + if (hfile.empty()) + hfile = DFHack::Core::getInstance().getConfigPath() / "lua.history"; + if (prompt.empty()) prompt = "lua"; - using namespace std::placeholders; - auto init_fn = std::bind(init_interpreter, _1, _2, prompt, hfile); + auto init_fn = [&](color_ostream& out, lua_State* state) { + return init_interpreter(out, state, prompt, hfile); + }; return RunCoreQueryLoop(out, state, init_fn); } diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 93853468e4..d31315b2b0 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -287,7 +287,7 @@ namespace DFHack::Lua { * Uses RunCoreQueryLoop internally. */ DFHACK_EXPORT bool InterpreterLoop(color_ostream &out, lua_State *state, - const char *prompt = NULL, const char *hfile = NULL); + std::string prompt = {}, std::filesystem::path hfile = {}); /** * Run an interactive prompt loop. All access to the lua state From 52ae4b7eb8200fcdeb13e74ee4c61ee9884bf072 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:26:02 -0500 Subject: [PATCH 035/128] remove hardcoded `dfhack-config` in `script-manager.lua` --- library/lua/script-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 80774c53e8..cb4abf1f53 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -246,7 +246,7 @@ function getModSourcePath(mod_id) end function getModStatePath(mod_id) - local path = ('dfhack-config/mods/%s/'):format(mod_id) + local path = (dfhack.getConfigPath() + ('/mods/%s/')):format(mod_id) if not dfhack.filesystem.mkdir_recursive(path) then error(('failed to create mod state directory: "%s"'):format(path)) end From 9079fdb19d25f3759d721ba3daf3f03ebef248be Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:27:46 -0500 Subject: [PATCH 036/128] remove hardcoded `dfhack_config` in `blueprints.cpp` also modernize code --- plugins/blueprint.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index f21198c9d5..3f046bc142 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -6,6 +6,7 @@ */ #include "Console.h" +#include "Core.h" #include "DataDefs.h" #include "DataFuncs.h" #include "DataIdentity.h" @@ -59,8 +60,6 @@ using namespace DFHack; DFHACK_PLUGIN("blueprint"); REQUIRE_GLOBAL(world); -static const string BLUEPRINT_USER_DIR = "dfhack-config/blueprints/"; - namespace DFHack { DBG_DECLARE(blueprint,log); } @@ -1370,9 +1369,9 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons static bool create_output_dir(color_ostream &out, const blueprint_options &opts) { - string basename = BLUEPRINT_USER_DIR + opts.name; - size_t last_slash = basename.find_last_of("/"); - string parent_path = basename.substr(0, last_slash); + std::filesystem::path BLUEPRINT_USER_DIR = Core::getInstance().getConfigPath() / "blueprints"; + std::filesystem::path basename = BLUEPRINT_USER_DIR / opts.name; + std::filesystem::path parent_path = basename.parent_path(); // create output directory if it doesn't already exist if (!Filesystem::mkdir_recursive(parent_path)) { From e1e3c469d0f3fa45285372151ffa78b0db1945a1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:37:06 -0500 Subject: [PATCH 037/128] remove hardcoded `dfhack-config` reference in `debug` plugin --- plugins/debug.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/debug.cpp b/plugins/debug.cpp index 51e4a8300b..d0e5e0f5a0 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -21,6 +21,7 @@ redistribute it freely, subject to the following restrictions: distribution. */ +#include "Core.h" #include "PluginManager.h" #include "DebugManager.h" #include "Debug.h" @@ -352,7 +353,9 @@ struct FilterManager : public std::map //! Current configuration version implemented by the code constexpr static Json::UInt configVersion{1}; //! Path to the configuration file - constexpr static const char* configPath{"dfhack-config/runtime-debug.json"}; + const inline std::filesystem::path getConfigPath() const { + return DFHack::Core::getInstance().getConfigPath() / "runtime-debug.json"; + } //! Get reference to the singleton static FilterManager& getInstance() noexcept @@ -434,8 +437,6 @@ struct FilterManager : public std::map DebugManager::categorySignal_t::Connection connection_; }; -constexpr const char* FilterManager::configPath; - FilterManager::~FilterManager() { } @@ -443,6 +444,7 @@ FilterManager::~FilterManager() command_result FilterManager::loadConfig(DFHack::color_ostream& out) noexcept { nextId_ = 1; + auto configPath = getConfigPath(); if (!Filesystem::isfile(configPath)) return CR_OK; try { @@ -463,6 +465,7 @@ command_result FilterManager::loadConfig(DFHack::color_ostream& out) noexcept command_result FilterManager::saveConfig(DFHack::color_ostream& out) const noexcept { + auto configPath = getConfigPath(); try { DEBUG(command, out) << "Save config to '" << configPath << "'" << std::endl; JsonArchive archive; From eed65c39055711c91727e32383f669698f8f28a1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:40:01 -0500 Subject: [PATCH 038/128] remove hardcoded `dfhack-config` in `liquids` plugin --- plugins/liquids.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/liquids.cpp b/plugins/liquids.cpp index a814bbd5b0..0b5526d996 100644 --- a/plugins/liquids.cpp +++ b/plugins/liquids.cpp @@ -58,7 +58,8 @@ using namespace df::enums; DFHACK_PLUGIN("liquids"); REQUIRE_GLOBAL(world); -static const char * HISTORY_FILE = "dfhack-config/liquids.history"; +auto constexpr HISTORY_FILE = "liquids.history"; + CommandHistory liquids_hist; command_result df_liquids (color_ostream &out, vector & parameters); @@ -66,7 +67,7 @@ command_result df_liquids_here (color_ostream &out, vector & parameters DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - liquids_hist.load(HISTORY_FILE); + liquids_hist.load(DFHack::Core::getInstance().getConfigPath() / HISTORY_FILE); commands.push_back(PluginCommand( "liquids", "Place magma, water or obsidian.", @@ -82,7 +83,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector Date: Mon, 1 Jun 2026 15:47:33 -0500 Subject: [PATCH 039/128] reemove hardcoded `dfhack_config` in `orders` plugin --- plugins/orders.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 95932acb52..f6a5b6b091 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -44,8 +44,15 @@ DFHACK_PLUGIN("orders"); REQUIRE_GLOBAL(world); -static std::filesystem::path ORDERS_DIR = std::filesystem::path("dfhack-config") / "orders"; -static std::filesystem::path ORDERS_LIBRARY_DIR = Core::getInstance().getHackPath() / "data" / "orders"; +static const std::filesystem::path get_orders_dir() +{ + return Core::getInstance().getConfigPath() / "orders"; +} + +static std::filesystem::path get_orders_library_dir() +{ + return Core::getInstance().getHackPath() / "data" / "orders"; +} static command_result orders_command(color_ostream & out, std::vector & parameters); @@ -135,7 +142,7 @@ static command_result orders_command(color_ostream & out, std::vector files; - if (0 < Filesystem::listdir_recursive(ORDERS_LIBRARY_DIR, files, 0, false)) { + if (0 < Filesystem::listdir_recursive(get_orders_library_dir(), files, 0, false)) { // if the library directory doesn't exist, just skip it return; } @@ -163,7 +170,7 @@ static command_result orders_list_command(color_ostream & out) // support subdirs so we can identify and ignore subdirs with ".json" names. // also listdir_recursive will alphabetize the list for us. std::map files; - Filesystem::listdir_recursive(ORDERS_DIR, files, 0, false); + Filesystem::listdir_recursive(get_orders_dir(), files, 0, false); for (auto& it : files) { if (it.second) @@ -504,9 +511,9 @@ static command_result orders_export_command(color_ostream & out, const std::stri orders.append(order); } - Filesystem::mkdir(ORDERS_DIR); + Filesystem::mkdir(get_orders_dir()); - std::ofstream file(ORDERS_DIR / ( name + ".json")); + std::ofstream file(get_orders_dir() / ( name + ".json")); file << orders << std::endl; @@ -924,7 +931,7 @@ static command_result orders_import_command(color_ostream & out, const std::stri return CR_WRONG_USAGE; } - auto filename((is_library ? ORDERS_LIBRARY_DIR : ORDERS_DIR) / (fname + ".json")); + auto filename((is_library ? get_orders_library_dir() : get_orders_dir()) / (fname + ".json")); Json::Value orders; { From 2e13395c041b32790d95996ea316adabe8d57762 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:53:14 -0500 Subject: [PATCH 040/128] remove hardcoded `dfhack-config` from `tiletypes` plugin --- plugins/tiletypes.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/tiletypes.cpp b/plugins/tiletypes.cpp index 8c8407d189..dc93bea4bb 100644 --- a/plugins/tiletypes.cpp +++ b/plugins/tiletypes.cpp @@ -82,7 +82,8 @@ static const std::map, df::til }; static const uint16_t UNDERGROUND_TEMP = 10015; -static const char * HISTORY_FILE = "dfhack-config/tiletypes.history"; +static const std::filesystem::path get_history_file() { return Core::getInstance().getConfigPath() / "tiletypes.history"; } + CommandHistory tiletypes_hist; command_result df_tiletypes (color_ostream &out, vector & parameters); @@ -92,7 +93,7 @@ command_result df_tiletypes_here_point (color_ostream &out, vector & pa DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - tiletypes_hist.load(HISTORY_FILE); + tiletypes_hist.load(get_history_file()); commands.push_back(PluginCommand("tiletypes", "Paints tiles of specified types onto the map.", df_tiletypes, true, true)); commands.push_back(PluginCommand("tiletypes-command", "Run tiletypes commands (seperated by ' ; ')", df_tiletypes_command)); commands.push_back(PluginCommand("tiletypes-here", "Repeat tiletypes command at cursor (with brush)", df_tiletypes_here)); @@ -102,7 +103,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector Date: Mon, 1 Jun 2026 15:54:17 -0500 Subject: [PATCH 041/128] remove hardcoded `dfhack-config` from `blueprints.lua` --- plugins/lua/blueprint.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 8c765d563f..58e7421c51 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -204,7 +204,7 @@ end -- returns the name of the output file for the given context function get_filename(opts, phase, ordinal) - local fullname = 'dfhack-config/blueprints/' .. opts.name + local fullname = dfhack.getConfigPath() .. '/blueprints/' .. opts.name local _,_,basename = opts.name:find('([^/]+)/*$') if not basename then -- should not happen since opts.name should already be validated From 21ab900436890bc8c29ef8d03d9c0fd4ea97cb0d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:55:27 -0500 Subject: [PATCH 042/128] `dfhack-config` in `dwarfmonitor.lua` --- plugins/lua/dwarfmonitor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/dwarfmonitor.lua b/plugins/lua/dwarfmonitor.lua index d98bfd80f5..fd18178a51 100644 --- a/plugins/lua/dwarfmonitor.lua +++ b/plugins/lua/dwarfmonitor.lua @@ -5,7 +5,7 @@ local guidm = require('gui.dwarfmode') local overlay = require('plugins.overlay') local utils = require('utils') -local DWARFMONITOR_CONFIG_FILE = 'dfhack-config/dwarfmonitor.json' +local DWARFMONITOR_CONFIG_FILE = dfhack.getConfigPath() .. '/dwarfmonitor.json' -- ------------- -- -- WeatherWidget -- From 5a7477cb81760c244bac4caa8780ff27efaea5f9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:56:01 -0500 Subject: [PATCH 043/128] `dfhack-config` in `orders.lua` --- plugins/lua/orders.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 80a6196e13..a470e3e2ce 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -38,7 +38,7 @@ local function do_import() dismiss_on_select2=false, on_select2=function(_, choice) if choice.text:startswith('library/') then return end - local fname = 'dfhack-config/orders/'..choice.text..'.json' + local fname = dfhack.getConfigPath() .. '/orders/' .. choice.text .. '.json' if not dfhack.filesystem.isfile(fname) then return end dialogs.showYesNoPrompt('Delete orders file?', 'Are you sure you want to delete "' .. fname .. '"?', nil, From 5dc823be04b7a6d4a9cb8465b0a53e2e604f7654 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:57:14 -0500 Subject: [PATCH 044/128] `dfhack-config` in `overlay.lua` --- plugins/lua/overlay.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index c1d1bcb434..cdfaac89e1 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -6,7 +6,7 @@ local scriptmanager = require('script-manager') local utils = require('utils') local widgets = require('gui.widgets') -local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json' +local OVERLAY_CONFIG_FILE = dfhack.getConfigPath() .. '/overlay.json' local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS' local GLOBAL_KEY = 'OVERLAY' From 1bf20c2ee09854adc8bb5d93e01d71425fe5fd19 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:57:39 -0500 Subject: [PATCH 045/128] `dfhack-config` in `spectate.lua` --- plugins/lua/spectate.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 953895eab0..1e0d81f917 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -75,7 +75,7 @@ end local function load_state() local state = get_default_state() - local config_file = json.open('dfhack-config/spectate.json') + local config_file = json.open(dfhack.getConfigPath() .. '/spectate.json') for key in pairs(config_file.data) do if state[key] == nil then config_file.data[key] = nil From 7171a8d0bc47ba78cfeac6601cbf8655d3658ea8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:58:08 -0500 Subject: [PATCH 046/128] `dfhack-config` in `stockpiles.lua` --- plugins/lua/stockpiles.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index 33f6e375ad..4b56418861 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -7,7 +7,7 @@ local logistics = require('plugins.logistics') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local STOCKPILES_DIR = 'dfhack-config/stockpiles' +local STOCKPILES_DIR = dfhack.getConfigPath() .. '/stockpiles' local STOCKPILES_LIBRARY_DIR = dfhack.getHackPath() .. '/data/stockpiles' local BAD_FILENAME_REGEX = '[^%w._]' From 6e8f9f52dd0dd35acf0e81bc87920787aa61559d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 15:58:39 -0500 Subject: [PATCH 047/128] `dfhack-config` in `planneroverlay.lua` --- plugins/lua/buildingplan/planneroverlay.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f0fbe17de4..65a7b1ff35 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -11,7 +11,7 @@ local utils = require('utils') local widgets = require('gui.widgets') require('dfhack.buildings') -config = config or json.open('dfhack-config/buildingplan.json') +config = config or json.open(dfhack.getConfigPath() .. '/buildingplan.json') local uibs = df.global.buildreq From 863ef160187c5897cb3a060f188cd8c11d82a3f1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 16:01:02 -0500 Subject: [PATCH 048/128] `dfhack-config` in tests for `blueprint` and `orders` --- test/plugins/blueprint.lua | 8 ++++---- test/plugins/orders.lua | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/plugins/blueprint.lua b/test/plugins/blueprint.lua index a08844bbee..d5ebd2031d 100644 --- a/test/plugins/blueprint.lua +++ b/test/plugins/blueprint.lua @@ -243,16 +243,16 @@ end function test.get_filename() local opts = {name='a', split_strategy='none'} - expect.eq('dfhack-config/blueprints/a.csv', b.get_filename(opts, 'dig', 1)) + expect.eq(dfhack.getConfigPath() .. '/blueprints/a.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a/', split_strategy='none'} - expect.eq('dfhack-config/blueprints/a/a.csv', b.get_filename(opts, 'dig', 1)) + expect.eq(dfhack.getConfigPath() .. '/blueprints/a/a.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a', split_strategy='phase'} - expect.eq('dfhack-config/blueprints/a-1-dig.csv', b.get_filename(opts, 'dig', 1)) + expect.eq(dfhack.getConfigPath() .. '/blueprints/a-1-dig.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a/', split_strategy='phase'} - expect.eq('dfhack-config/blueprints/a/a-5-dig.csv', b.get_filename(opts, 'dig', 5)) + expect.eq(dfhack.getConfigPath() .. '/blueprints/a/a-5-dig.csv', b.get_filename(opts, 'dig', 5)) expect.error_match('could not parse basename', function() b.get_filename({name='', split_strategy='none'}) diff --git a/test/plugins/orders.lua b/test/plugins/orders.lua index ab2ad3235e..86d03101dd 100644 --- a/test/plugins/orders.lua +++ b/test/plugins/orders.lua @@ -1,7 +1,7 @@ config.mode = 'fortress' config.target = 'orders' -local FILE_PATH_PATTERN = 'dfhack-config/orders/%s.json' +local FILE_PATH_PATTERN = dfhack.getConfigPath() .. '/orders/%s.json' local BACKUP_FILE_NAME = 'tmp-backup' local BACKUP_FILE_PATH = FILE_PATH_PATTERN:format(BACKUP_FILE_NAME) From 7fa756c69e3fb840ffef5c0aec52ff73a78553dd Mon Sep 17 00:00:00 2001 From: gwilymtv <126210878+gwilymtv@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:42:30 +0930 Subject: [PATCH 049/128] buildingplan: add Pull button for linked levers Add a Pull/Queued button next to linked levers on the "Show linked buildings" tab so a pull-lever job can be queued without navigating to the lever. Uses lever.leverPullJob from the existing lever script. --- docs/changelog.txt | 1 + docs/plugins/buildingplan.rst | 5 ++ .../lua/buildingplan/unlink_mechanisms.lua | 83 ++++++++++++++++++- 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 70e55aff58..f1ea40d4f9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -75,6 +75,7 @@ Template for new versions: ## New Tools ## New Features +- `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a pull-lever job without navigating to the lever ## Fixes diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index c00569365c..45bd3c5035 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -237,3 +237,8 @@ usual) unless freed via the ``Free`` buttons on the ``Show items`` tab on both buildings. This will remove the mechanism from the building and drop it onto the ground, allowing it to be reused elsewhere. There is an option to auto-free mechanisms when unlinking to perform this step automatically. + +For any linked building that is a lever, a ``Pull`` button also appears next to it +on the ``Show linked buildings`` tab. Clicking it queues a "pull the lever" job on +that lever without having to navigate to the lever itself. The button changes to +``Queued`` once a pull job is pending. diff --git a/plugins/lua/buildingplan/unlink_mechanisms.lua b/plugins/lua/buildingplan/unlink_mechanisms.lua index 86ae80940f..02a8d72648 100644 --- a/plugins/lua/buildingplan/unlink_mechanisms.lua +++ b/plugins/lua/buildingplan/unlink_mechanisms.lua @@ -4,6 +4,7 @@ local dialogs = require('gui.dialogs') local overlay = require("plugins.overlay") local utils = require("utils") local widgets = require("gui.widgets") +local lever = reqscript("lever") local function mech_iter(b) --iterate mechanisms backwards local t = b.contained_items @@ -35,6 +36,18 @@ local function get_mech_target(m) --mechanism target building if exists return i and df.building.find(m.general_refs[i].building_id) or nil end +local function is_lever(b) --building is a lever + return b and b._type == df.building_trapst and b.trap_type == df.trap_type.Lever +end + +local function has_pull_job(b) --lever already has a pending pull job + for _, j in ipairs(b.jobs) do + if j.job_type == df.job_type.PullLever then + return true + end + end +end + local function has_link_tab(b) --linked building tab exists if not b then return @@ -148,7 +161,7 @@ local valid_build = { MechLinkOverlay = defclass(MechLinkOverlay, overlay.OverlayWidget) MechLinkOverlay.ATTRS { - desc = "Allows unlinking mechanisms from buildings.", + desc = "Allows unlinking mechanisms and pulling linked levers from buildings.", default_enabled = true, default_pos = {x=-41, y=-4}, frame = {w=56, h=27}, @@ -303,6 +316,60 @@ function MechLinkOverlay:activate_button(n) end end +function MechLinkOverlay:get_pull_button(n, ensure) + local button = self.subviews["pull_"..n] + if not button and ensure then + self:addviews + { + widgets.TextButton + { + view_id = "pull_"..n, + frame = {t=0, r=17, w=8, h=1}, + label = function() return self:pull_label(n) end, + enabled = function() return self:pull_enabled(n) end, + on_activate = function() self:activate_pull(n) end, + visible = false, + }, + } + button = self.subviews["pull_"..n] + button:updateLayout(self.frame_body) + end + + return button +end + +function MechLinkOverlay:pull_target(n) --linked lever for button n, or nil + local button = self:get_pull_button(n) + if not button then + return + end + + local idx = self:idx_from_offset(button.frame.t) + if idx > 0 and idx < #self.building.contained_items then + local target = get_mech_target(self.building.contained_items[idx].item) + if is_lever(target) then + return target + end + end +end + +function MechLinkOverlay:pull_label(n) + local target = self:pull_target(n) + return target and has_pull_job(target) and "Queued" or "Pull" +end + +function MechLinkOverlay:pull_enabled(n) + local target = self:pull_target(n) + return target ~= nil and not has_pull_job(target) +end + +function MechLinkOverlay:activate_pull(n) + local target = self:pull_target(n) + if target and not has_pull_job(target) then + lever.leverPullJob(target, false) + end +end + function MechLinkOverlay:ask_unlink_all() local saved_mode = self.subviews.unlink_mode:getOptionValue() local message = { @@ -356,6 +423,16 @@ function MechLinkOverlay:update_buttons() button.visible = true end button:updateLayout() + + local pbutton = self:get_pull_button(i, true) + pbutton.visible = false + if idx > 0 and idx < bci_len and + is_lever(get_mech_target(self.building.contained_items[idx].item)) then + pbutton.frame.t = offset + pbutton.frame.r = h_offset + 9 + pbutton.visible = true + end + pbutton:updateLayout() end local b = (self.frame.h % 3) == 1 and #self.links >= self.num_buttons and 0 or 1 @@ -371,6 +448,10 @@ function MechLinkOverlay:preUpdateLayout(parent_rect) if button then button.visible = false end + local pbutton = self:get_pull_button(i) + if pbutton then + pbutton.visible = false + end end local h = parent_rect.height - 49 From 9d9235003b6a77114ca8446197f7a069b92a9d92 Mon Sep 17 00:00:00 2001 From: gwilymtv <126210878+gwilymtv@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:17:41 +0930 Subject: [PATCH 050/128] buildingplan: move changelog entry to # Future section The Pull-button entry was filed under the released 53.14-r2 section; move it under # Future where unreleased changes belong. --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f1ea40d4f9..6df256607d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a pull-lever job without navigating to the lever ## Fixes @@ -75,7 +76,6 @@ Template for new versions: ## New Tools ## New Features -- `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a pull-lever job without navigating to the lever ## Fixes From ea0e45d0d15ac856a6afe74c7f04807836e9cc70 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 5 Jun 2026 21:49:48 -0500 Subject: [PATCH 051/128] Add some C++ code standards to `Contributing.rst` --- docs/dev/Contributing.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/dev/Contributing.rst b/docs/dev/Contributing.rst index 7b34dc20ce..3ab620c1f9 100644 --- a/docs/dev/Contributing.rst +++ b/docs/dev/Contributing.rst @@ -61,6 +61,19 @@ Code format * ``#include`` directives should be sorted: C++ libraries first, then DFHack modules, then ``df/`` headers, then local includes. Within each category they should be sorted alphabetically. +General C++ code guidelines +--------------------------- +* This project is currently built at the C++20 feature level, and C++20 features should be used when appropriate. C++23 features will be allowed once all of our build platforms support them. +* NEVER use ``using namespace`` in a header file. In source files, do not use ``using namespace std``; instead, import each STL identifier you need specifically (e.g. ``using std::string;``). +* Avoid platform specific code as much as possible. +* Avoid including ``Windows.h``; if you must, ensure that ``NOMINMAX`` and ``WIN32_LEAN_AND_MEAN`` are defined before including it. +* Do not include C headers (e.g. ````); use the C++ versions (e.g. ````) instead. +* Do not use ``std::string`` (or ``char *``) for path names; always use ``std::filesystem::path``. This avoids issues with encoding, especially on the Windows platform, which is roughly 80% of our user base. +* Do not use ``printf`` or similar functions for formatting strings; use C++ streams or ``fmt::format`` instead. We use the `fmt library `__ for formatting strings; this dependency is automatically fetched by our build system. +* Avoid out parameters; prefer returning a struct, pair, or tuple, or using ``std::optional`` instead. +* Prefer range for loops to traditional for loops when iterating over a container. +* Avoid macros when possible; prefer ``constexpr`` variables for constants and functions or templates for code generation. + .. _contributing-pr-guidelines: Pull request guidelines From 3b45cdd3ddf253680fc6a2dbe8988412299bc940 Mon Sep 17 00:00:00 2001 From: gwilymtv <126210878+gwilymtv@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:12:28 +0930 Subject: [PATCH 052/128] buildingplan: queue lever Pull jobs as 'do now' Pass priority=true to lever.leverPullJob so the Pull button creates a high-priority (do_now) pull-lever job instead of a normal-priority one. --- docs/changelog.txt | 2 +- docs/plugins/buildingplan.rst | 6 +++--- plugins/lua/buildingplan/unlink_mechanisms.lua | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6df256607d..350dfabdd1 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: ## New Tools ## New Features -- `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a pull-lever job without navigating to the lever +- `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a high-priority pull-lever job without navigating to the lever ## Fixes diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 45bd3c5035..7c5fb1b249 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -239,6 +239,6 @@ ground, allowing it to be reused elsewhere. There is an option to auto-free mechanisms when unlinking to perform this step automatically. For any linked building that is a lever, a ``Pull`` button also appears next to it -on the ``Show linked buildings`` tab. Clicking it queues a "pull the lever" job on -that lever without having to navigate to the lever itself. The button changes to -``Queued`` once a pull job is pending. +on the ``Show linked buildings`` tab. Clicking it queues a high-priority ("do now") +"pull the lever" job on that lever without having to navigate to the lever itself. +The button changes to ``Queued`` once a pull job is pending. diff --git a/plugins/lua/buildingplan/unlink_mechanisms.lua b/plugins/lua/buildingplan/unlink_mechanisms.lua index 02a8d72648..a54dbc3990 100644 --- a/plugins/lua/buildingplan/unlink_mechanisms.lua +++ b/plugins/lua/buildingplan/unlink_mechanisms.lua @@ -366,7 +366,7 @@ end function MechLinkOverlay:activate_pull(n) local target = self:pull_target(n) if target and not has_pull_job(target) then - lever.leverPullJob(target, false) + lever.leverPullJob(target, true) --do now end end From b59b320487ab7bbe4fe3be6a4c1de180ab1c9d1f Mon Sep 17 00:00:00 2001 From: gwilymtv <126210878+gwilymtv@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:12:59 +0930 Subject: [PATCH 053/128] buildingplan: click Queued to cancel a lever pull job When a pull-lever job is already pending, the button shows 'Queued' and is now clickable: activating it removes the job via dfhack.job.removeJob so it can be cancelled without navigating to the lever. The button stays enabled in both states and toggles between queuing and cancelling. --- docs/changelog.txt | 2 +- docs/plugins/buildingplan.rst | 3 ++- plugins/lua/buildingplan/unlink_mechanisms.lua | 17 +++++++++++------ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 350dfabdd1..c9329232a8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: ## New Tools ## New Features -- `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a high-priority pull-lever job without navigating to the lever +- `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a high-priority pull-lever job (or cancel a queued one) without navigating to the lever ## Fixes diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 7c5fb1b249..37f5130521 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -241,4 +241,5 @@ mechanisms when unlinking to perform this step automatically. For any linked building that is a lever, a ``Pull`` button also appears next to it on the ``Show linked buildings`` tab. Clicking it queues a high-priority ("do now") "pull the lever" job on that lever without having to navigate to the lever itself. -The button changes to ``Queued`` once a pull job is pending. +The button changes to ``Queued`` once a pull job is pending; click it again to +cancel that job, again without navigating to the lever. diff --git a/plugins/lua/buildingplan/unlink_mechanisms.lua b/plugins/lua/buildingplan/unlink_mechanisms.lua index a54dbc3990..c0304c74b5 100644 --- a/plugins/lua/buildingplan/unlink_mechanisms.lua +++ b/plugins/lua/buildingplan/unlink_mechanisms.lua @@ -40,10 +40,10 @@ local function is_lever(b) --building is a lever return b and b._type == df.building_trapst and b.trap_type == df.trap_type.Lever end -local function has_pull_job(b) --lever already has a pending pull job +local function get_pull_job(b) --pending pull job on lever, or nil for _, j in ipairs(b.jobs) do if j.job_type == df.job_type.PullLever then - return true + return j end end end @@ -355,17 +355,22 @@ end function MechLinkOverlay:pull_label(n) local target = self:pull_target(n) - return target and has_pull_job(target) and "Queued" or "Pull" + return target and get_pull_job(target) and "Queued" or "Pull" end function MechLinkOverlay:pull_enabled(n) - local target = self:pull_target(n) - return target ~= nil and not has_pull_job(target) + return self:pull_target(n) ~= nil end function MechLinkOverlay:activate_pull(n) local target = self:pull_target(n) - if target and not has_pull_job(target) then + if not target then + return + end + local job = get_pull_job(target) + if job then + dfhack.job.removeJob(job) --cancel queued pull + else lever.leverPullJob(target, true) --do now end end From a31197bcc787d43ab49ec84a7977962320d4a9b4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 8 Jun 2026 19:21:37 -0500 Subject: [PATCH 054/128] `string_view` instead of `string` also reorder includes om `LuaTools.h` to match our coding standards --- library/LuaTools.cpp | 8 ++++---- library/include/LuaTools.h | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 0a172a10e5..56c5079838 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1281,18 +1281,18 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, DFHack: return (rv == LUA_OK); } -static bool init_interpreter(color_ostream &out, lua_State *state, const std::string& prompt, const std::filesystem::path& hfile) +static bool init_interpreter(color_ostream &out, lua_State *state, std::string_view prompt, const std::filesystem::path& hfile) { lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_DFHACK_TOKEN); lua_getfield(state, -1, "interpreter"); lua_remove(state, -2); - lua_pushlstring(state, prompt.c_str(), prompt.size()); - lua_pushlstring(state, hfile.string().c_str(), hfile.string().size()); + lua_pushlstring(state, prompt.data(), prompt.size()); + lua_pushlstring(state, hfile.string().data(), hfile.string().size()); return true; } bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, - std::string prompt, std::filesystem::path hfile) + std::string_view prompt, std::filesystem::path hfile) { if (!out.is_console()) return false; diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index d31315b2b0..09672e1c02 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -24,16 +24,6 @@ distribution. #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include "Core.h" #include "ColorText.h" #include "DataDefs.h" @@ -43,6 +33,17 @@ distribution. #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace DFHack { class function_identity_base; struct MaterialInfo; @@ -287,7 +288,7 @@ namespace DFHack::Lua { * Uses RunCoreQueryLoop internally. */ DFHACK_EXPORT bool InterpreterLoop(color_ostream &out, lua_State *state, - std::string prompt = {}, std::filesystem::path hfile = {}); + std::string_view prompt = {}, std::filesystem::path hfile = {}); /** * Run an interactive prompt loop. All access to the lua state From 57f322aecfef9a5d2fa71b8b165c145619d34ad2 Mon Sep 17 00:00:00 2001 From: gwilymtv <126210878+gwilymtv@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:29:43 +0930 Subject: [PATCH 055/128] buildingplan: show job state and lever state --- .../lua/buildingplan/unlink_mechanisms.lua | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/plugins/lua/buildingplan/unlink_mechanisms.lua b/plugins/lua/buildingplan/unlink_mechanisms.lua index c0304c74b5..cfe67e90db 100644 --- a/plugins/lua/buildingplan/unlink_mechanisms.lua +++ b/plugins/lua/buildingplan/unlink_mechanisms.lua @@ -48,6 +48,28 @@ local function get_pull_job(b) --pending pull job on lever, or nil end end +local ASCII_LEVER_OFF = string.char(0x95) --ò +local ASCII_LEVER_ON = string.char(0xA2) --ó + +local function get_lever_state_char(lever) --lever position glyph for the current tileset + -- match the current mode because ASCII and premium lever glyphs differ in directions + if dfhack.screen.inGraphicsMode() then + return lever.state == 0 and "/" or "\\" + end + return lever.state == 0 and ASCII_LEVER_OFF or ASCII_LEVER_ON +end + +local function pull_label(lever) --button label and pen for the lever's pull state + local state_char = get_lever_state_char(lever) --current lever position + local job = get_pull_job(lever) + if not job then + return "Pull "..state_char, COLOR_WHITE + elseif dfhack.job.getWorker(job) then + return "Pulling "..state_char, COLOR_GREEN --a citizen has taken the job + end + return "Queued "..state_char, COLOR_YELLOW --queued, not yet taken +end + local function has_link_tab(b) --linked building tab exists if not b then return @@ -324,9 +346,8 @@ function MechLinkOverlay:get_pull_button(n, ensure) widgets.TextButton { view_id = "pull_"..n, - frame = {t=0, r=17, w=8, h=1}, - label = function() return self:pull_label(n) end, - enabled = function() return self:pull_enabled(n) end, + frame = {t=0, r=17, w=11, h=1}, + label = "", --set per-frame in update_buttons on_activate = function() self:activate_pull(n) end, visible = false, }, @@ -353,15 +374,6 @@ function MechLinkOverlay:pull_target(n) --linked lever for button n, or nil end end -function MechLinkOverlay:pull_label(n) - local target = self:pull_target(n) - return target and get_pull_job(target) and "Queued" or "Pull" -end - -function MechLinkOverlay:pull_enabled(n) - return self:pull_target(n) ~= nil -end - function MechLinkOverlay:activate_pull(n) local target = self:pull_target(n) if not target then @@ -431,11 +443,15 @@ function MechLinkOverlay:update_buttons() local pbutton = self:get_pull_button(i, true) pbutton.visible = false - if idx > 0 and idx < bci_len and - is_lever(get_mech_target(self.building.contained_items[idx].item)) then - pbutton.frame.t = offset - pbutton.frame.r = h_offset + 9 - pbutton.visible = true + local target = idx > 0 and idx < bci_len and + get_mech_target(self.building.contained_items[idx].item) + if is_lever(target) then + local label, pen = pull_label(target) + pbutton:setLabel(label) + pbutton.label.text_pen = pen + pbutton.frame.t = offset + pbutton.frame.r = h_offset + 9 + pbutton.visible = true end pbutton:updateLayout() end From 770d333e0e9acf6d9dd1d84fa3dfe720eb29c84d Mon Sep 17 00:00:00 2001 From: gwilymtv <126210878+gwilymtv@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:40:51 +0930 Subject: [PATCH 056/128] update buildingplan.rst --- docs/plugins/buildingplan.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 37f5130521..cb0511d982 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -239,7 +239,6 @@ ground, allowing it to be reused elsewhere. There is an option to auto-free mechanisms when unlinking to perform this step automatically. For any linked building that is a lever, a ``Pull`` button also appears next to it -on the ``Show linked buildings`` tab. Clicking it queues a high-priority ("do now") -"pull the lever" job on that lever without having to navigate to the lever itself. -The button changes to ``Queued`` once a pull job is pending; click it again to -cancel that job, again without navigating to the lever. +on the ``Show linked buildings`` tab, with a glyph showing the lever's current +position. Clicking it queues a high-priority ("do now") pull-lever job without +having to navigate to the lever itself; click it again to cancel the job. From 804c4c021b4ec9343dd2c93a7e860a232bb0e7e7 Mon Sep 17 00:00:00 2001 From: gwilymtv <126210878+gwilymtv@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:44:57 +0930 Subject: [PATCH 057/128] pull_label -> get_pull_label --- plugins/lua/buildingplan/unlink_mechanisms.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/buildingplan/unlink_mechanisms.lua b/plugins/lua/buildingplan/unlink_mechanisms.lua index cfe67e90db..81bd0fdabe 100644 --- a/plugins/lua/buildingplan/unlink_mechanisms.lua +++ b/plugins/lua/buildingplan/unlink_mechanisms.lua @@ -59,7 +59,7 @@ local function get_lever_state_char(lever) --lever position glyph for the curren return lever.state == 0 and ASCII_LEVER_OFF or ASCII_LEVER_ON end -local function pull_label(lever) --button label and pen for the lever's pull state +local function get_pull_label(lever) --button label and pen for the lever's pull state local state_char = get_lever_state_char(lever) --current lever position local job = get_pull_job(lever) if not job then @@ -446,7 +446,7 @@ function MechLinkOverlay:update_buttons() local target = idx > 0 and idx < bci_len and get_mech_target(self.building.contained_items[idx].item) if is_lever(target) then - local label, pen = pull_label(target) + local label, pen = get_pull_label(target) pbutton:setLabel(label) pbutton.label.text_pen = pen pbutton.frame.t = offset From b3ef18bf7c53b5316b4bcf1cdc39323c00bcd687 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:23:09 +0000 Subject: [PATCH 058/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 92aec15dd7..4ffbea2204 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 92aec15dd7eff76b76ff74efb3164e31b7f0e5bd +Subproject commit 4ffbea2204b85c95baa1c630137f71fe3a4e6217 From ca374cd36f7752d3f4b26746e2d797b374999af0 Mon Sep 17 00:00:00 2001 From: sizzlins Date: Thu, 25 Jun 2026 10:59:22 +0700 Subject: [PATCH 059/128] buildingplan: compress job_defaults dictionary and hoist static vars --- plugins/lua/buildingplan/planneroverlay.lua | 100 +++++++------------- 1 file changed, 32 insertions(+), 68 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 16bf582ac6..e852f85730 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -15,6 +15,33 @@ config = config or json.open('dfhack-config/buildingplan.json') local uibs = df.global.buildreq +local ITEM_TO_JOB = { + [df.item_type.BED] = 'ConstructBed', [df.item_type.DOOR] = 'ConstructDoor', + [df.item_type.CABINET] = 'ConstructCabinet', [df.item_type.TABLE] = 'ConstructTable', + [df.item_type.CHAIR] = 'ConstructThrone', [df.item_type.BOX] = 'ConstructChest', + [df.item_type.ARMORSTAND] = 'ConstructArmorStand', [df.item_type.WEAPONRACK] = 'ConstructWeaponRack', + [df.item_type.STATUE] = 'ConstructStatue', [df.item_type.COFFIN] = 'ConstructCoffin', + [df.item_type.HATCH_COVER] = 'ConstructHatchCover', [df.item_type.GRATE] = 'ConstructGrate', + [df.item_type.QUERN] = 'ConstructQuern', [df.item_type.MILLSTONE] = 'ConstructMillstone', + [df.item_type.TRACTION_BENCH] = 'ConstructTractionBench', [df.item_type.SLAB] = 'ConstructSlab', + [df.item_type.ANVIL] = 'ForgeAnvil', [df.item_type.WINDOW] = 'MakeWindow', + [df.item_type.CAGE] = 'MakeCage', [df.item_type.BARREL] = 'MakeBarrel', + [df.item_type.BUCKET] = 'MakeBucket', [df.item_type.ANIMALTRAP] = 'MakeAnimalTrap', + [df.item_type.CHAIN] = 'MakeChain', [df.item_type.FLASK] = 'MakeFlask', + [df.item_type.GOBLET] = 'MakeGoblet', [df.item_type.BLOCKS] = 'ConstructBlocks', +} + +local JOB_DEFAULTS = { + ConstructBed='wood', ConstructTractionBench='wood', MakeCage='wood', + MakeBarrel='wood', MakeBucket='wood', MakeAnimalTrap='wood', + ForgeAnvil='iron', MakeChain='iron', MakeFlask='iron', MakeWindow='glass' +} + +local VALID_MAT_CATS = { + wood=true, bone=true, shell=true, horn=true, pearl=true, tooth=true, + leather=true, silk=true, yarn=true, cloth=true, plant=true +} + reset_counts_flag = false editing_filters_flag = false @@ -1063,35 +1090,6 @@ function PlannerOverlay:queue_order(idx) local filter = get_cur_filters()[idx] - local item_to_job = { - [df.item_type.BED] = 'ConstructBed', - [df.item_type.DOOR] = 'ConstructDoor', - [df.item_type.CABINET] = 'ConstructCabinet', - [df.item_type.TABLE] = 'ConstructTable', - [df.item_type.CHAIR] = 'ConstructThrone', - [df.item_type.BOX] = 'ConstructChest', - [df.item_type.ARMORSTAND] = 'ConstructArmorStand', - [df.item_type.WEAPONRACK] = 'ConstructWeaponRack', - [df.item_type.STATUE] = 'ConstructStatue', - [df.item_type.COFFIN] = 'ConstructCoffin', - [df.item_type.HATCH_COVER] = 'ConstructHatchCover', - [df.item_type.GRATE] = 'ConstructGrate', - [df.item_type.QUERN] = 'ConstructQuern', - [df.item_type.MILLSTONE] = 'ConstructMillstone', - [df.item_type.TRACTION_BENCH] = 'ConstructTractionBench', - [df.item_type.SLAB] = 'ConstructSlab', - [df.item_type.ANVIL] = 'ForgeAnvil', - [df.item_type.WINDOW] = 'MakeWindow', - [df.item_type.CAGE] = 'MakeCage', - [df.item_type.BARREL] = 'MakeBarrel', - [df.item_type.BUCKET] = 'MakeBucket', - [df.item_type.ANIMALTRAP] = 'MakeAnimalTrap', - [df.item_type.CHAIN] = 'MakeChain', - [df.item_type.FLASK] = 'MakeFlask', - [df.item_type.GOBLET] = 'MakeGoblet', - [df.item_type.BLOCKS] = 'ConstructBlocks', - } - local job_name = "ConstructBlocks" local item_type = nil if filter.item_type and filter.item_type ~= -1 then @@ -1104,8 +1102,8 @@ function PlannerOverlay:queue_order(idx) item_type = mapping_vector[filter.vector_id] end - if item_type and item_to_job[item_type] then - job_name = item_to_job[item_type] + if item_type and ITEM_TO_JOB[item_type] then + job_name = ITEM_TO_JOB[item_type] end local order_json = { @@ -1125,47 +1123,13 @@ function PlannerOverlay:queue_order(idx) end if #cats_list == 0 then - local job_defaults = { - ConstructBed = {'wood'}, - ConstructDoor = {'stone'}, - ConstructCabinet = {'stone'}, - ConstructTable = {'stone'}, - ConstructThrone = {'stone'}, - ConstructChest = {'stone'}, - ConstructArmorStand = {'stone'}, - ConstructWeaponRack = {'stone'}, - ConstructStatue = {'stone'}, - ConstructCoffin = {'stone'}, - ConstructHatchCover = {'stone'}, - ConstructGrate = {'stone'}, - ConstructQuern = {'stone'}, - ConstructMillstone = {'stone'}, - ConstructTractionBench = {'wood'}, - ConstructSlab = {'stone'}, - ForgeAnvil = {'iron'}, - MakeWindow = {'glass'}, - MakeCage = {'wood'}, - MakeBarrel = {'wood'}, - MakeBucket = {'wood'}, - MakeAnimalTrap = {'wood'}, - MakeChain = {'iron'}, - MakeFlask = {'iron'}, - MakeGoblet = {'stone'}, - ConstructBlocks = {'stone'}, - } - if job_defaults[job_name] then - cats_list = job_defaults[job_name] - end + -- df manager requires a material category for generic jobs or it queues "unknown material" orders + table.insert(cats_list, JOB_DEFAULTS[job_name] or 'stone') end - local valid_mat_cats = { - wood=true, bone=true, shell=true, horn=true, pearl=true, tooth=true, - leather=true, silk=true, yarn=true, cloth=true, plant=true - } - local mat_cats = {} for _, cat in ipairs(cats_list) do - if valid_mat_cats[cat] then + if VALID_MAT_CATS[cat] then table.insert(mat_cats, cat) elseif cat == 'stone' then order_json.material = "INORGANIC" From 4d7a258994d85f60eafdc66f1fbadc24f14cd868 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:57:37 +0000 Subject: [PATCH 060/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 4ffbea2204..48759c31bf 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4ffbea2204b85c95baa1c630137f71fe3a4e6217 +Subproject commit 48759c31bf7cd34a2787c5b2f796304bf769eb56 From 95e9d462066ee646e8faf4b27d528349b9230f66 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:43:47 +0000 Subject: [PATCH 061/128] Auto-update structures ref for 53.15 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 01aae95cac..44527aad30 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 01aae95cacd98850e4f477c45a4b75f800bacecc +Subproject commit 44527aad30d4ff8eecf11cc86cadf2a868ad17b4 From 3754ec319e9a9cda315ae4e9bcc190256919e8fb Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:48:08 +0000 Subject: [PATCH 062/128] Auto-update structures ref for 53.15 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 44527aad30..7c54a444f2 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 44527aad30d4ff8eecf11cc86cadf2a868ad17b4 +Subproject commit 7c54a444f28f132b0056bff206efadc78683ef88 From f184bd8732bc3a5db19792a83ee060782922d787 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:53:54 +0000 Subject: [PATCH 063/128] Auto-update structures ref for 53.15 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 7c54a444f2..618819a4ff 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7c54a444f28f132b0056bff206efadc78683ef88 +Subproject commit 618819a4ffeee7ef9f9c1d22812b20d8316b3007 From d47dcc3b9b3adfce2738e8def92732f8a34a0630 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 25 Jun 2026 12:02:51 -0500 Subject: [PATCH 064/128] Update CMakeLists.txt for 53.15 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 837020bc5b..ac66abe35c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. -set(DF_VERSION "53.14") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "53.15") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 421106a88d885db96f36ec73f36a368888193724 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:12:15 +0000 Subject: [PATCH 065/128] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 618819a4ff..7f4244eb88 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 618819a4ffeee7ef9f9c1d22812b20d8316b3007 +Subproject commit 7f4244eb8893901d718c65325de1c72353873221 From b12f73a323863761992faf93f15a70a3e8ad2acd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 25 Jun 2026 12:50:05 -0500 Subject: [PATCH 066/128] Changelog and modules for 53.15-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ library/xml | 2 +- scripts | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 699e635b79..8e299189e7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.15-r1 + +## New Tools + +## New Features + ## Fixes - `autoclothing`: will no longer count gloves and pants as if they were helms - `timestream`: do not skip ticks when a caravan is loading or unloading, and be more careful about skipping ticks when flows are active diff --git a/library/xml b/library/xml index 7f4244eb88..80a6267fad 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7f4244eb8893901d718c65325de1c72353873221 +Subproject commit 80a6267faddb7aa99759c9df94186de3f873dd97 diff --git a/scripts b/scripts index 48759c31bf..3455f6848b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 48759c31bf7cd34a2787c5b2f796304bf769eb56 +Subproject commit 3455f6848be9eb924acecb64df8bbc3b4a324efc From 5b92c46a4601ebf5be4e401112e9c99386c07efe Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:23:22 +0000 Subject: [PATCH 067/128] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 80a6267fad..c3ccf5fa72 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 80a6267faddb7aa99759c9df94186de3f873dd97 +Subproject commit c3ccf5fa722d561c10745fa46ea2dcabb22bfad0 From 0fdc3b748d8c277c618b17a426fe20a2c4f13eaa Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 4 Jul 2026 22:35:46 -0700 Subject: [PATCH 068/128] ConfigureButton replaces GraphicButton; rename ToggleButton; docs * Update widgets.lua - RadioButton * Update and rename toggle_button.lua to radio_button.lua * Update help_button.lua * Update configure_button.lua - Replace GraphicButton * Delete library/lua/gui/widgets/buttons/graphic_button.lua * Update Lua API.rst - Add RadioButton; HelpButton now subclass --- docs/dev/Lua API.rst | 33 ++++++--- library/lua/gui/widgets.lua | 4 +- .../gui/widgets/buttons/configure_button.lua | 64 +++++++++++++++-- .../gui/widgets/buttons/graphic_button.lua | 68 ------------------- .../lua/gui/widgets/buttons/help_button.lua | 12 ++-- .../{toggle_button.lua => radio_button.lua} | 30 ++++---- 6 files changed, 108 insertions(+), 103 deletions(-) delete mode 100644 library/lua/gui/widgets/buttons/graphic_button.lua rename library/lua/gui/widgets/buttons/{toggle_button.lua => radio_button.lua} (64%) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 0fbcb41971..9071a1db6b 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -6346,12 +6346,27 @@ This is a specialized subclass of CycleHotkeyLabel that has two options: ``On`` (with a value of ``true``) and ``Off`` (with a value of ``false``). The ``On`` option is rendered in green. +ConfigureButton class +--------------------- + +A 3x1 tile button with a gear symbol on it, intended to represent a configure +icon. Clicking on the icon will run the given callback. The graphics can also +be overridden to create custom buttons. + +It has the following attributes: + +:on_click: The function to run when the icon is clicked. +:pen_left: Pen or function returning a pen to overwrite the left tile of the button. +:pen_center: As above, but for the center tile (gear symbol). +:pen_right: As above, but for the right tile. + HelpButton class ---------------- -A 3x1 tile button with a question mark on it, intended to represent a help -icon. Clicking on the icon will launch `gui/launcher` with a given command -string, showing the help text for that command. +Subclass of ConfigureButton; a 3x1 tile button with a question mark on it, +intended to represent a help icon. Clicking on the icon will launch +`gui/launcher` with a given command string, showing the help text for that +command. It has the following attributes: @@ -6361,15 +6376,17 @@ It also sets the ``frame`` attribute so the button appears in the upper right corner of the parent, but you can override this to your liking if you want a different position. -ConfigureButton class ---------------------- +RadioButton class +----------------- -A 3x1 tile button with a gear mark on it, intended to represent a configure -icon. Clicking on the icon will run the given callback. +Subclass of ConfigureButton; a 3x1 tile button that resembles a radio button +(or check box in ASCII mode), identical to the ones found in +`gui/control-panel`. Clicking on the button will toggle its enabled state. +This state is represented by the boolean value ``toggle_state``. It has the following attributes: -:on_click: The function on run when the icon is clicked. +:initial_state: Start in the ``true`` or ``false`` state. Defaults to ``true``. BannerPanel class ----------------- diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 744827d9bb..726b52003a 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -16,9 +16,9 @@ Label = require('gui.widgets.labels.label') Scrollbar = require('gui.widgets.scrollbar') WrappedLabel = require('gui.widgets.labels.wrapped_label') TooltipLabel = require('gui.widgets.labels.tooltip_label') -HelpButton = require('gui.widgets.buttons.help_button') ConfigureButton = require('gui.widgets.buttons.configure_button') -ToggleButton = require('gui.widgets.buttons.toggle_button') +HelpButton = require('gui.widgets.buttons.help_button') +RadioButton = require('gui.widgets.buttons.radio_button') BannerPanel = require('gui.widgets.containers.banner_panel') TextButton = require('gui.widgets.buttons.text_button') CycleHotkeyLabel = require('gui.widgets.labels.cycle_hotkey_label') diff --git a/library/lua/gui/widgets/buttons/configure_button.lua b/library/lua/gui/widgets/buttons/configure_button.lua index bfca49eeb5..f3199618ca 100644 --- a/library/lua/gui/widgets/buttons/configure_button.lua +++ b/library/lua/gui/widgets/buttons/configure_button.lua @@ -1,19 +1,71 @@ +-- A 3x1 tile button with a gear symbol on it. Clicking on it will run a callback + local textures = require('gui.textures') -local GraphicButton = require('gui.widgets.buttons.graphic_button') +local Panel = require('gui.widgets.containers.panel') +local Label = require('gui.widgets.labels.label') + +local to_pen = dfhack.pen.parse -local configure_pen_center = dfhack.pen.parse{ +local button_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} +local button_pen_center = to_pen{ tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol +local button_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} --------------------- -- ConfigureButton -- --------------------- ----@class widgets.ConfigureButton.attrs: widgets.GraphicButton.attrs ----@field super widgets.GraphicButton -ConfigureButton = defclass(ConfigureButton, GraphicButton) +---@class widgets.ConfigureButton.attrs: widgets.Panel.attrs +---@field on_click? function +---@field pen_left dfhack.pen|fun(): dfhack.pen +---@field pen_center dfhack.pen|fun(): dfhack.pen +---@field pen_right dfhack.pen|fun(): dfhack.pen + +---@class widgets.ConfigureButton.attrs.partial: widgets.ConfigureButton.attrs + +---@class widgets.ConfigureButton: widgets.Panel, widgets.ConfigureButton.attrs +---@field super widgets.Panel +---@field ATTRS widgets.ConfigureButton.attrs|fun(attributes: widgets.ConfigureButton.attrs.partial) +---@overload fun(init_table: widgets.ConfigureButton.attrs.partial): self +ConfigureButton = defclass(ConfigureButton, Panel) ConfigureButton.ATTRS{ - pen_center=configure_pen_center, + frame={t=0, l=0, w=3, h=1}, + on_click=DEFAULT_NIL, + pen_left=button_pen_left, + pen_center=button_pen_center, + pen_right=button_pen_right, } +function ConfigureButton:init() + self.frame.h = self.frame.h or 1 + self.frame.w = self.frame.w or 3 + + self:addviews{ + Label{ + view_id='label', + frame={t=0, l=0, w=3, h=1}, + text={ + {tile=self.pen_left}, + {tile=self.pen_center}, + {tile=self.pen_right}, + }, + on_click=self.on_click, + }, + } +end + +function ConfigureButton:refresh() + local l = self.subviews.label + + l.on_click = self.on_click + l.pen_left = self.pen_left + l.pen_center = self.pen_center + l.pen_right = self.pen_right + + l:setText({{tile=self.pen_left}, {tile=self.pen_center}, {tile=self.pen_right}}) +end + return ConfigureButton diff --git a/library/lua/gui/widgets/buttons/graphic_button.lua b/library/lua/gui/widgets/buttons/graphic_button.lua deleted file mode 100644 index f2ffa36be8..0000000000 --- a/library/lua/gui/widgets/buttons/graphic_button.lua +++ /dev/null @@ -1,68 +0,0 @@ -local textures = require('gui.textures') -local Panel = require('gui.widgets.containers.panel') -local Label = require('gui.widgets.labels.label') - -local to_pen = dfhack.pen.parse - -local button_pen_left = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} -local button_pen_center = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 10) or nil, ch=string.byte('=')} -local button_pen_right = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} - -------------------- --- GraphicButton -- -------------------- - ----@class widgets.GraphicButton.attrs: widgets.Panel.attrs ----@field on_click? function ----@field pen_left dfhack.pen|fun(): dfhack.pen ----@field pen_center dfhack.pen|fun(): dfhack.pen ----@field pen_right dfhack.pen|fun(): dfhack.pen - ----@class widgets.GraphicButton.attrs.partial: widgets.GraphicButton.attrs - ----@class widgets.GraphicButton: widgets.Panel, widgets.GraphicButton.attrs ----@field super widgets.Panel ----@field ATTRS widgets.GraphicButton.attrs|fun(attributes: widgets.GraphicButton.attrs.partial) ----@overload fun(init_table: widgets.GraphicButton.attrs.partial): self -GraphicButton = defclass(GraphicButton, Panel) - -GraphicButton.ATTRS{ - on_click=DEFAULT_NIL, - pen_left=button_pen_left, - pen_center=button_pen_center, - pen_right=button_pen_right, -} - -function GraphicButton:init() - self.frame.w = self.frame.w or 3 - self.frame.h = self.frame.h or 1 - - self:addviews{ - Label{ - view_id='label', - frame={t=0, l=0, w=3, h=1}, - text={ - {tile=self.pen_left}, - {tile=self.pen_center}, - {tile=self.pen_right}, - }, - on_click=self.on_click, - }, - } -end - -function GraphicButton:refresh() - local l = self.subviews.label - - l.on_click = self.on_click - l.pen_left = self.pen_left - l.pen_center = self.pen_center - l.pen_right = self.pen_right - - l:setText({{tile=self.pen_left}, {tile=self.pen_center}, {tile=self.pen_right}}) -end - -return GraphicButton diff --git a/library/lua/gui/widgets/buttons/help_button.lua b/library/lua/gui/widgets/buttons/help_button.lua index 99aa15e7fb..61e9e16239 100644 --- a/library/lua/gui/widgets/buttons/help_button.lua +++ b/library/lua/gui/widgets/buttons/help_button.lua @@ -1,5 +1,7 @@ +-- A 3x1 tile button with a question mark on it. Clicking on it will show help text for a command + local textures = require('gui.textures') -local GraphicButton = require('gui.widgets.buttons.graphic_button') +local ConfigureButton = require('gui.widgets.buttons.configure_button') local help_pen_center = dfhack.pen.parse{ tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} @@ -8,16 +10,16 @@ local help_pen_center = dfhack.pen.parse{ -- HelpButton -- ---------------- ----@class widgets.HelpButton.attrs: widgets.GraphicButton.attrs +---@class widgets.HelpButton.attrs: widgets.ConfigureButton.attrs ---@field command? string ---@class widgets.HelpButton.attrs.partial: widgets.HelpButton.attrs ----@class widgets.HelpButton: widgets.GraphicButton, widgets.HelpButton.attrs ----@field super widgets.GraphicButton +---@class widgets.HelpButton: widgets.ConfigureButton, widgets.HelpButton.attrs +---@field super widgets.ConfigureButton ---@field ATTRS widgets.HelpButton.attrs|fun(attributes: widgets.HelpButton.attrs.partial) ---@overload fun(init_table: widgets.HelpButton.attrs.partial): self -HelpButton = defclass(HelpButton, GraphicButton) +HelpButton = defclass(HelpButton, ConfigureButton) HelpButton.ATTRS{ frame={t=0, r=1, w=3, h=1}, diff --git a/library/lua/gui/widgets/buttons/toggle_button.lua b/library/lua/gui/widgets/buttons/radio_button.lua similarity index 64% rename from library/lua/gui/widgets/buttons/toggle_button.lua rename to library/lua/gui/widgets/buttons/radio_button.lua index 21c2122476..df0dcb82ec 100644 --- a/library/lua/gui/widgets/buttons/toggle_button.lua +++ b/library/lua/gui/widgets/buttons/radio_button.lua @@ -1,5 +1,7 @@ +-- A 3x1 tile button that toggles state when clicked + local textures = require('gui.textures') -local GraphicButton = require('gui.widgets.buttons.graphic_button') +local ConfigureButton = require('gui.widgets.buttons.configure_button') local to_pen = dfhack.pen.parse @@ -16,26 +18,26 @@ local disabled_pen_center = to_pen{fg=COLOR_RED, local disabled_pen_right = to_pen{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} ------------------- --- ToggleButton -- ------------------- +----------------- +-- RadioButton -- +----------------- ----@class widgets.ToggleButton.attrs: widgets.GraphicButton.attrs +---@class widgets.RadioButton.attrs: widgets.ConfigureButton.attrs ---@field initial_state boolean ----@class widgets.ToggleButton.attrs.partial: widgets.ToggleButton.attrs +---@class widgets.RadioButton.attrs.partial: widgets.RadioButton.attrs ----@class widgets.ToggleButton: widgets.GraphicButton, widgets.ToggleButton.attrs ----@field super widgets.GraphicButton ----@field ATTRS widgets.ToggleButton.attrs|fun(attributes: widgets.ToggleButton.attrs.partial) ----@overload fun(init_table: widgets.ToggleButton.attrs.partial): self -ToggleButton = defclass(ToggleButton, GraphicButton) +---@class widgets.RadioButton: widgets.ConfigureButton, widgets.RadioButton.attrs +---@field super widgets.ConfigureButton +---@field ATTRS widgets.RadioButton.attrs|fun(attributes: widgets.RadioButton.attrs.partial) +---@overload fun(init_table: widgets.RadioButton.attrs.partial): self +RadioButton = defclass(RadioButton, ConfigureButton) -ToggleButton.ATTRS{ +RadioButton.ATTRS{ initial_state=true, } -function ToggleButton:init() +function RadioButton:init() self.toggle_state = self.initial_state self.on_click = function() self.toggle_state = not self.toggle_state end @@ -46,4 +48,4 @@ function ToggleButton:init() self:refresh() end -return ToggleButton +return RadioButton From 3fb5af13e70edd559fe4e09ad27b0b078ec927ff Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 5 Jul 2026 20:41:07 -0700 Subject: [PATCH 069/128] Use postinit * Update radio_button.lua * Update configure_button.lua * Update help_button.lua --- library/lua/gui/widgets/buttons/configure_button.lua | 2 +- library/lua/gui/widgets/buttons/help_button.lua | 2 -- library/lua/gui/widgets/buttons/radio_button.lua | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/library/lua/gui/widgets/buttons/configure_button.lua b/library/lua/gui/widgets/buttons/configure_button.lua index f3199618ca..7d865b27ad 100644 --- a/library/lua/gui/widgets/buttons/configure_button.lua +++ b/library/lua/gui/widgets/buttons/configure_button.lua @@ -57,7 +57,7 @@ function ConfigureButton:init() } end -function ConfigureButton:refresh() +function ConfigureButton:postinit() local l = self.subviews.label l.on_click = self.on_click diff --git a/library/lua/gui/widgets/buttons/help_button.lua b/library/lua/gui/widgets/buttons/help_button.lua index 61e9e16239..9fa45f7a70 100644 --- a/library/lua/gui/widgets/buttons/help_button.lua +++ b/library/lua/gui/widgets/buttons/help_button.lua @@ -29,9 +29,7 @@ HelpButton.ATTRS{ function HelpButton:init() local command = self.command .. ' ' - self.on_click = function() dfhack.run_command('gui/launcher', command) end - self:refresh() end return HelpButton diff --git a/library/lua/gui/widgets/buttons/radio_button.lua b/library/lua/gui/widgets/buttons/radio_button.lua index df0dcb82ec..04d45732cb 100644 --- a/library/lua/gui/widgets/buttons/radio_button.lua +++ b/library/lua/gui/widgets/buttons/radio_button.lua @@ -44,8 +44,6 @@ function RadioButton:init() self.pen_left = function() return self.toggle_state and enabled_pen_left or disabled_pen_left end self.pen_center = function() return self.toggle_state and enabled_pen_center or disabled_pen_center end self.pen_right = function() return self.toggle_state and enabled_pen_right or disabled_pen_right end - - self:refresh() end return RadioButton From d25d4383223393f4c07725bbdb5da4d229750b14 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 5 Jul 2026 20:46:53 -0700 Subject: [PATCH 070/128] Update changelog.txt --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8e299189e7..e3c42fec28 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,10 +61,12 @@ Template for new versions: ## Fixes ## Misc Improvements +- ``widgets.HelpButton``: Now derives from ``widgets.ConfigureButton`` instead of using redundant code ## Documentation ## API +- ``widgets.RadioButton``: New button widget resembling those used in ``gui/control-panel`` ## Lua From bdeaa9974cdaf8d223c1dff6812612cdb50fad14 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 6 Jul 2026 01:20:30 -0500 Subject: [PATCH 071/128] add protective code against vanilla misbehavior --- docs/changelog.txt | 1 + plugins/getplants.cpp | 41 ++++++++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8e299189e7..3956615ec5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `getplants`: added protective code to avoid misoperation when a plant has an invalid material (which should never happen, but...) ## Misc Improvements diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 69eba4b1d1..6ac8b429cb 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -478,41 +478,52 @@ command_result df_getplants(color_ostream& out, vector & parameters) { } count = 0; - for (size_t i = 0; i < world->plants.all.size(); i++) { - const df::plant* plant = world->plants.all[i]; + for (auto* plant : world->plants.all) + { df::map_block* cur = Maps::getTileBlock(plant->pos); - TRACE(log, out).print("Examining {} at ({}, {}, {}) [index={}]\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z, (int)i); + auto mat = plant->material; + if (mat < 0 || mat > world->raws.plants.all.size()) + { + WARN(log, out).print("plant with invalid material {} in plant vector", mat); + continue; + } + + TRACE(log, out).print("Examining {} at ({}, {}, {})\n", world->raws.plants.all[mat]->id, plant->pos.x, plant->pos.y, plant->pos.z); int x = plant->pos.x % 16; int y = plant->pos.y % 16; - if (plantSelections[plant->material] == selectability::OutOfSeason || - plantSelections[plant->material] == selectability::Selectable) { + if (plantSelections[mat] == selectability::OutOfSeason || + plantSelections[mat] == selectability::Selectable) + { if (exclude || - plantSelections[plant->material] == selectability::OutOfSeason) + plantSelections[mat] == selectability::OutOfSeason) continue; } - else { + else + { if (!exclude) continue; } df::tiletype tt = cur->tiletype[x][y]; - df::tiletype_material mat = tileMaterial(tt); + df::tiletype_material tile_mat = tileMaterial(tt); if ((treesonly || tt != tiletype::Shrub) && ENUM_ATTR(plant_type, is_shrub, plant->type)) continue; - if ((shrubsonly || mat != tiletype_material::TREE) && !ENUM_ATTR(plant_type, is_shrub, plant->type)) + if ((shrubsonly || tile_mat != tiletype_material::TREE) && !ENUM_ATTR(plant_type, is_shrub, plant->type)) continue; if (cur->designation[x][y].bits.hidden) continue; - if (collectionCount[plant->material] >= maxCount) + if (collectionCount[mat] >= maxCount) continue; - if (deselect && Designations::unmarkPlant(plant)) { - collectionCount[plant->material]++; + if (deselect && Designations::unmarkPlant(plant)) + { + collectionCount[mat]++; ++count; } - if (!deselect && designate(out, plant, farming)) { - DEBUG(log, out).print("Designated {} at ({}, {}, {}), {}\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z, (int)i); - collectionCount[plant->material]++; + if (!deselect && designate(out, plant, farming)) + { + DEBUG(log, out).print("Designated {} at ({}, {}, {})\n", world->raws.plants.all[mat]->id, plant->pos.x, plant->pos.y, plant->pos.z); + collectionCount[mat]++; ++count; } } From 3ae8e72f616d497158a4892af51f06dfce01565a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 6 Jul 2026 01:33:55 -0500 Subject: [PATCH 072/128] add explicit cast --- plugins/getplants.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 6ac8b429cb..72cefda5c4 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -483,7 +483,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { df::map_block* cur = Maps::getTileBlock(plant->pos); auto mat = plant->material; - if (mat < 0 || mat > world->raws.plants.all.size()) + if (mat < 0 || mat > int16_t(world->raws.plants.all.size())) { WARN(log, out).print("plant with invalid material {} in plant vector", mat); continue; From 6da219eb23e7e0501b0de8d8cd44afe37e44016a Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 6 Jul 2026 01:10:00 -0700 Subject: [PATCH 073/128] Update Lua API.rst * Fix backslash not appearing for "\n" * Advise gui/control-panel for FILTER_FULL_TEXT --- 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 0fbcb41971..d4e9575305 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -5757,7 +5757,7 @@ TextArea Functions: * ``textarea:getText()`` Returns the current text content of the ``TextArea`` widget as a string. - "\n" characters (``string.char(10)``) should be interpreted as new lines + ``\n`` characters (``string.char(10)``) should be interpreted as new lines * ``textarea:setText(text)`` @@ -6526,7 +6526,8 @@ Filter behavior: By default, the filter matches substrings that start at the beginning of a word (or after any punctuation). You can instead configure filters to match any -substring across the full text with a command like:: +substring across the full text by setting ``FILTER_FULL_TEXT`` in `gui/control-panel` +or set it for the session by running a command like:: :lua require('utils').FILTER_FULL_TEXT=true From d6a19d5713b7c64e5709ddf482699c5f7d9e2e77 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:10:19 +0000 Subject: [PATCH 074/128] [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.37.2 → 0.37.4](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.2...0.37.4) --- .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 91a34bdee2..47251d8ed3 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.37.2 + rev: 0.37.4 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 68025fcf7095857c24e43a2a6a08c76a3fe16ca2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:37:32 +0000 Subject: [PATCH 075/128] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master depends/dfhooks: main --- depends/dfhooks | 2 +- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 8a578206fb..5a904e30a8 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 8a578206fb9b1dd32b04c8c7c35217e2b83e369e +Subproject commit 5a904e30a8bace81c662b44ec7ff076b92edafd1 diff --git a/library/xml b/library/xml index c3ccf5fa72..4955a64887 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit c3ccf5fa722d561c10745fa46ea2dcabb22bfad0 +Subproject commit 4955a6488713c30081acebfc2fbe9a6417f7820c diff --git a/plugins/stonesense b/plugins/stonesense index 8791e2c266..b693d5904b 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 8791e2c26693cea552c42700e38c87503b7ac7da +Subproject commit b693d5904b967385ad74b1c63003aa1308ee0d18 diff --git a/scripts b/scripts index 3455f6848b..8d0d033148 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3455f6848be9eb924acecb64df8bbc3b4a324efc +Subproject commit 8d0d0331487da879e25e4f4530ddf761cf4310eb From 5b57f752fe28e6d522a012a0e634b2bd38b93ba9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 7 Jul 2026 10:00:57 -0500 Subject: [PATCH 076/128] `>=` not `>` Co-Authored-By: Quietust <1005195+quietust@users.noreply.github.com> --- plugins/getplants.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 72cefda5c4..53219276bb 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -483,7 +483,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { df::map_block* cur = Maps::getTileBlock(plant->pos); auto mat = plant->material; - if (mat < 0 || mat > int16_t(world->raws.plants.all.size())) + if (mat < 0 || mat >= int16_t(world->raws.plants.all.size())) { WARN(log, out).print("plant with invalid material {} in plant vector", mat); continue; From d3b5f26c6c1322f9e71ab80f7d10f5928333993f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Jul 2026 22:52:57 -0500 Subject: [PATCH 077/128] refactor based on review should be slightly more clear --- library/modules/Maps.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index b8aa6c5c85..a90bb86724 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -1564,8 +1564,7 @@ void Maps::addBlockColumns(int32_t new_height) df::tiletype::OpenSpace); // Set block positions properly (based on prior air layer) - air_block->map_pos = last_air_block->map_pos; - air_block->map_pos.z += count + 1; + air_block->map_pos = last_air_block->map_pos + df::coord{0, 0, uint16_t(count + 1)}; air_block->region_pos = last_air_block->region_pos; // Copy other potentially important metadata from prior air From fe95b29bf1a9b63a321d50a88af54998c070a715 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Jul 2026 19:23:05 -0500 Subject: [PATCH 078/128] changes required as part of changing codegen to use `std::array` Note: no separate changelog, the changelog on structures is enough to cover --- library/DataStatics.cpp | 4 +- library/include/DataIdentity.h | 12 ++++ .../df/custom/tile_bitmask.methods.inc | 4 +- library/include/modules/MapCache.h | 4 +- library/include/modules/Maps.h | 14 ++-- library/modules/MapCache.cpp | 68 ++++++++++--------- library/modules/Maps.cpp | 24 +++---- library/xml | 2 +- .../remotefortressreader.cpp | 13 ++-- plugins/stockpiles/StockpileSerializer.cpp | 8 +-- 10 files changed, 86 insertions(+), 67 deletions(-) diff --git a/library/DataStatics.cpp b/library/DataStatics.cpp index fe60e54544..af7613cdaa 100644 --- a/library/DataStatics.cpp +++ b/library/DataStatics.cpp @@ -22,8 +22,8 @@ namespace { DFHack::VersionInfo *global_table_ = DFHack::Core::getInstance().vinfo.get(); \ void * tmp_; -#define INIT_GLOBAL_FUNCTION_ITEM(type,name) \ - if (global_table_->getAddress(#name,tmp_)) name = (type*)tmp_; +#define INIT_GLOBAL_FUNCTION_ITEM(name, ...) \ + if (global_table_->getAddress(#name,tmp_)) name = (__VA_ARGS__*)tmp_; #define TID(type) (&identity_traits< type >::identity) diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index e58247fdaa..cf486e6188 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -713,6 +713,11 @@ namespace df static const container_identity *get(); }; + template struct identity_traits> + { + static const container_identity* get(); + }; + template struct identity_traits > { static const container_identity *get(); }; @@ -797,6 +802,13 @@ namespace df return &identity; } + template + inline const container_identity* identity_traits>::get() + { + static const buffer_container_identity identity(sz, identity_traits::get()); + return &identity; + } + template inline const container_identity *identity_traits >::get() { using container = std::vector; diff --git a/library/include/df/custom/tile_bitmask.methods.inc b/library/include/df/custom/tile_bitmask.methods.inc index b991819b94..ade00e7218 100644 --- a/library/include/df/custom/tile_bitmask.methods.inc +++ b/library/include/df/custom/tile_bitmask.methods.inc @@ -6,11 +6,11 @@ inline uint16_t &operator[] (int y) } void clear() { - memset(bits,0,sizeof(bits)); + bits.fill(0); } void set_all() { - memset(bits,0xFF,sizeof(bits)); + bits.fill(-1); } inline bool getassignment( const df::coord2d &xy ) { diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h index 0d63627389..31516dbb7f 100644 --- a/library/include/modules/MapCache.h +++ b/library/include/modules/MapCache.h @@ -68,8 +68,8 @@ struct BiomeInfo { int16_t layer_stone[MAX_LAYERS]; }; -typedef uint8_t t_veintype[16][16]; -typedef df::tiletype t_tilearr[16][16]; +using t_veintype = arr40d; +using t_tilearr = arr40d; class BlockInfo { diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index bf7a947150..aa8907ac7e 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -126,28 +126,32 @@ enum BiomeOffset { */ typedef df::block_flags t_blockflags; +template +using arr40d = std::array, 16>; + /** * 16x16 array of tile types * \ingroup grp_maps */ -typedef df::tiletype tiletypes40d [16][16]; +using tiletypes40d = arr40d; /** * 16x16 array used for squashed block materials * \ingroup grp_maps */ -typedef int16_t t_blockmaterials [16][16]; +using t_blockmaterials = arr40d; /** * 16x16 array of designation flags * \ingroup grp_maps */ typedef df::tile_designation t_designation; -typedef t_designation designations40d [16][16]; +using designations40d = arr40d; + /** * 16x16 array of occupancy flags * \ingroup grp_maps */ typedef df::tile_occupancy t_occupancy; -typedef t_occupancy occupancies40d [16][16]; +using occupancies40d = arr40d; /** * array of 16 biome indexes valid for the block * \ingroup grp_maps @@ -157,7 +161,7 @@ typedef uint8_t biome_indices40d [9]; * 16x16 array of temperatures * \ingroup grp_maps */ -typedef uint16_t t_temperatures [16][16]; +using t_temperatures = arr40d; /** * Index a tile array by a 2D coordinate, clipping it to mod 16. diff --git a/library/modules/MapCache.cpp b/library/modules/MapCache.cpp index 603d1d0da2..84ee4affa0 100644 --- a/library/modules/MapCache.cpp +++ b/library/modules/MapCache.cpp @@ -96,8 +96,6 @@ const BiomeInfo MapCache::biome_stub = { -1, -1, -1, -1, -1, -1, -1, -1 } }; -#define COPY(a,b) memcpy(&a,&b,sizeof(a)) - MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent), designated_tiles{} @@ -123,20 +121,19 @@ void MapExtras::Block::init() if(block) { - COPY(designation, block->designation); - COPY(occupancy, block->occupancy); - - COPY(temp1, block->temperature_1); - COPY(temp2, block->temperature_2); + designation = block->designation; + occupancy = block->occupancy; + temp1 = block->temperature_1; + temp2 = block->temperature_2; valid = true; } else { - memset(designation,0,sizeof(designation)); - memset(occupancy,0,sizeof(occupancy)); - memset(temp1,0,sizeof(temp1)); - memset(temp2,0,sizeof(temp2)); + designation.fill({}); + occupancy.fill({}); + temp1.fill({}); + temp2.fill({}); } } @@ -198,10 +195,10 @@ void MapExtras::Block::init_tiles(bool basemat) MapExtras::Block::TileInfo::TileInfo() { dirty_raw.clear(); - memset(raw_tiles,0,sizeof(raw_tiles)); + raw_tiles.fill({}); ice_info = NULL; con_info = NULL; - memset(base_tiles,0,sizeof(base_tiles)); + base_tiles.fill({}); } MapExtras::Block::TileInfo::~TileInfo() @@ -218,6 +215,15 @@ void MapExtras::Block::TileInfo::init_iceinfo() ice_info = new IceInfo(); } +template +constexpr T arr40d_neg1() { + T tmp{}; + std::remove_reference_t tmp2{}; + tmp2.fill(-1); + tmp.fill(tmp2); + return tmp; +}; + void MapExtras::Block::TileInfo::init_coninfo() { if (con_info) @@ -225,17 +231,17 @@ void MapExtras::Block::TileInfo::init_coninfo() con_info = new ConInfo(); con_info->constructed.clear(); - COPY(con_info->tiles, base_tiles); - memset(con_info->mat_type, -1, sizeof(con_info->mat_type)); - memset(con_info->mat_index, -1, sizeof(con_info->mat_index)); + con_info->tiles = base_tiles; + con_info->mat_type = arr40d_neg1(); + con_info->mat_index = arr40d_neg1(); } MapExtras::Block::BasematInfo::BasematInfo() { vein_dirty.clear(); - memset(mat_type,0,sizeof(mat_type)); - memset(mat_index,-1,sizeof(mat_index)); - memset(veinmat,-1,sizeof(veinmat)); + mat_type.fill({}); + mat_index = arr40d_neg1(); + veinmat = arr40d_neg1(); } bool MapExtras::Block::setFlagAt(df::coord2d p, df::tile_designation::Mask mask, bool set) @@ -481,7 +487,7 @@ void MapExtras::Block::ParseTiles(TileInfo *tiles) tiletypes40d icetiles; BlockInfo::SquashFrozenLiquids(block, icetiles); - COPY(tiles->raw_tiles, block->tiletype); + tiles->raw_tiles = block->tiletype; for (int x = 0; x < 16; x++) { @@ -598,7 +604,7 @@ void MapExtras::Block::WriteTiles(TileInfo *tiles) if (tiles->ice_info && tiles->ice_info->dirty.has_assignments()) { - df::tiletype (*newtiles)[16] = (tiles->con_info ? tiles->con_info->tiles : tiles->base_tiles); + auto newtiles = (tiles->con_info ? tiles->con_info->tiles : tiles->base_tiles); for (int i = block->block_events.size()-1; i >= 0; i--) { @@ -646,8 +652,8 @@ void MapExtras::Block::ParseBasemats(TileInfo *tiles, BasematInfo *bmats) info.prepare(this); - COPY(bmats->veinmat, info.veinmats); - COPY(bmats->veintype, info.veintype); + bmats->veinmat = info.veinmats; + bmats->veintype = info.veintype; for (int x = 0; x < 16; x++) { @@ -779,7 +785,7 @@ bool MapExtras::Block::Write () if(dirty_designations) { - COPY(block->designation, designation); + block->designation = designation; block->flags.bits.designated = true; block->dsgn_check_cooldown = 0; dirty_designations = false; @@ -798,13 +804,13 @@ bool MapExtras::Block::Write () } if(dirty_temperatures) { - COPY(block->temperature_1, temp1); - COPY(block->temperature_2, temp2); + block->temperature_1 = temp1; + block->temperature_2 = temp2; dirty_temperatures = false; } if(dirty_occupancies) { - COPY(block->occupancy, occupancy); + block->occupancy = occupancy; dirty_occupancies = false; } return true; @@ -1034,8 +1040,8 @@ void MapExtras::BlockInfo::SquashVeins(df::map_block *mb, t_blockmaterials & mat { std::vector veins; Maps::SortBlockEvents(mb,&veins); - memset(materials,-1,sizeof(materials)); - memset(veintype, 0, sizeof(t_veintype)); + materials = arr40d_neg1(); + veintype.fill({}); for (uint32_t x = 0;x<16;x++) for (uint32_t y = 0; y< 16;y++) { @@ -1054,7 +1060,7 @@ void MapExtras::BlockInfo::SquashFrozenLiquids(df::map_block *mb, tiletypes40d & { std::vector ices; Maps::SortBlockEvents(mb,NULL,&ices); - memset(frozen,0,sizeof(frozen)); + frozen.fill({}); for (uint32_t x = 0; x < 16; x++) for (uint32_t y = 0; y < 16; y++) { for (size_t i = 0; i < ices.size(); i++) @@ -1089,7 +1095,7 @@ void MapExtras::BlockInfo::SquashGrass(df::map_block *mb, t_blockmaterials &mate { std::vector grasses; Maps::SortBlockEvents(mb, NULL, NULL, NULL, &grasses); - memset(materials,-1,sizeof(materials)); + materials = arr40d_neg1(); for (uint32_t x = 0; x < 16; x++) for (uint32_t y = 0; y < 16; y++) { int amount = 0; diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index a90bb86724..d915d963a5 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -756,7 +756,7 @@ int32_t Maps::addMaterialSpatter (df::coord pos, int16_t mat, int32_t matg, df:: spatter->mat_type = mat; spatter->mat_index = matg; spatter->mat_state = state; - memset(spatter->amount, 0, sizeof(spatter->amount)); + spatter->amount.fill({}); spatter->min_temperature = spatter->max_temperature = 60001; uint16_t melt = matinfo.material->heat.melting_point; @@ -876,8 +876,8 @@ int32_t Maps::addItemSpatter (df::coord pos, df::item_type i_type, int16_t i_sub spatter->mattype = i_subcat1; spatter->matindex = i_subcat2; spatter->print_variant = print_variant; - memset(spatter->amount, 0, sizeof(spatter->amount)); - memset(spatter->flag, 0, sizeof(spatter->flag)); + spatter->amount.fill({}); + spatter->flag.fill({}); spatter->min_temperature = spatter->max_temperature = 60001; if (Items::usesStandardMaterial(i_type)) @@ -1569,14 +1569,10 @@ void Maps::addBlockColumns(int32_t new_height) // Copy other potentially important metadata from prior air // layer - std::memcpy(air_block->lighting, last_air_block->lighting, - sizeof(air_block->lighting)); - std::memcpy(air_block->temperature_1, last_air_block->temperature_1, - sizeof(air_block->temperature_1)); - std::memcpy(air_block->temperature_2, last_air_block->temperature_2, - sizeof(air_block->temperature_2)); - std::memcpy(air_block->region_offset, last_air_block->region_offset, - sizeof(air_block->region_offset)); + air_block->lighting = last_air_block->lighting; + air_block->temperature_1 = last_air_block->temperature_1; + air_block->temperature_2 = last_air_block->temperature_2; + air_block->region_offset = last_air_block->region_offset; // Create tile designations to inform lighting and // outside markers @@ -1598,9 +1594,9 @@ void Maps::addBlockColumns(int32_t new_height) continue; } df::block_column_print_infost* glyphs = new df::block_column_print_infost; - std::ranges::copy(std::array{0,1,2,3}, glyphs->x); - std::ranges::copy(std::array{0,0,0,0}, glyphs->y); - std::ranges::copy(std::array{'e','x','p','^'}, glyphs->tile); + glyphs->x = {0,1,2,3}; + glyphs->y = {0,0,0,0}; + glyphs->tile = {'e','x','p','^'}; column->unmined_glyphs.push_back(glyphs); } return true; diff --git a/library/xml b/library/xml index 4955a64887..ca346feff1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 4955a6488713c30081acebfc2fbe9a6417f7820c +Subproject commit ca346feff1d26a440dae3a9b09778277e904678d diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index 60c04ac25b..f8b54f01fb 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -304,15 +304,16 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) return CR_OK; } -uint16_t fletcher16(uint8_t const *data, size_t bytes) +uint16_t fletcher16(const void *data_, size_t bytes) { + auto data = static_cast(data_); uint16_t sum1 = 0xff, sum2 = 0xff; while (bytes) { size_t tlen = bytes > 20 ? 20 : bytes; bytes -= tlen; do { - sum2 += sum1 += *data++; + sum2 += sum1 += static_cast(*data++); } while (--tlen); sum1 = (sum1 & 0xff) + (sum1 >> 8); sum2 = (sum2 & 0xff) + (sum2 >> 8); @@ -335,7 +336,7 @@ void ConvertDfColor(int16_t index, RemoteFortressReader::ColorDefinition * out) out->set_blue(gps->uccolor[index][2]); } -void ConvertDfColor(int16_t in[3], RemoteFortressReader::ColorDefinition * out) +void ConvertDfColor(std::array& in, RemoteFortressReader::ColorDefinition * out) { int index = in[0] | (8 * in[2]); ConvertDfColor(index, out); @@ -623,7 +624,7 @@ static command_result CheckHashes(color_ostream &stream, const EmptyMessage *in) for (size_t i = 0; i < world->map.map_blocks.size(); i++) { df::map_block * block = world->map.map_blocks[i]; - fletcher16((uint8_t*)(block->tiletype), 16 * 16 * sizeof(df::enums::tiletype::tiletype)); + fletcher16((block->tiletype).data(), 16 * 16 * sizeof(df::enums::tiletype::tiletype)); } clock_t end = clock(); double elapsed_secs = double(end - start) / CLOCKS_PER_SEC; @@ -654,7 +655,7 @@ bool IsTiletypeChanged(DFCoord pos) uint16_t hash; df::map_block * block = Maps::getBlock(pos); if (block) - hash = fletcher16((uint8_t*)(block->tiletype), 16 * 16 * (sizeof(df::enums::tiletype::tiletype))); + hash = fletcher16((block->tiletype).data(), 16 * 16 * (sizeof(df::enums::tiletype::tiletype))); else hash = 0; if (hashes[pos] != hash) @@ -672,7 +673,7 @@ bool IsDesignationChanged(DFCoord pos) uint16_t hash; df::map_block * block = Maps::getBlock(pos); if (block) - hash = fletcher16((uint8_t*)(block->designation), 16 * 16 * (sizeof(df::tile_designation))); + hash = fletcher16((block->designation).data(), 16 * 16 * (sizeof(df::tile_designation))); else hash = 0; if (waterHashes[pos] != hash) diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 46efd95044..6e90cb797e 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -320,7 +320,7 @@ static void unserialize_list_itemdef(color_ostream& out, const char* subcat, boo } static bool serialize_list_quality(color_ostream& out, FuncWriteExport add_value, - const bool(&quality_list)[7]) { + const std::array &quality_list) { using df::enums::item_quality::item_quality; using quality_traits = df::enum_traits; @@ -337,12 +337,12 @@ static bool serialize_list_quality(color_ostream& out, FuncWriteExport add_value return all; } -static void quality_clear(bool(&pile_list)[7]) { - std::fill(pile_list, pile_list + 7, false); +static void quality_clear(std::array &pile_list) { + pile_list.fill(false); } static void unserialize_list_quality(color_ostream& out, const char* subcat, bool all, bool val, const vector& filters, - FuncReadImport read_value, int32_t list_size, bool(&pile_list)[7]) { + FuncReadImport read_value, int32_t list_size, std::array &pile_list) { if (all) { for (auto idx = 0; idx < 7; ++idx) { string id = ENUM_KEY_STR(item_quality, (df::item_quality)idx); From 5606a5ed77eab6a0e757a3eaa5c8dc6d1c34270f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 10 Jul 2026 16:29:51 -0500 Subject: [PATCH 079/128] address review comments * change `loadScriptPaths` to a `Core` private method * add comments to header file for `getHackPath` and `getConfigPath` regarding validity of the return values of these methods * fix include ordering in `LuaTools.h` * add/update documentation for Lua exports `getHackPath` and `getConfigPath` * add changelog --- docs/changelog.txt | 2 ++ docs/dev/Lua API.rst | 12 +++++++++++- library/Core.cpp | 8 ++++---- library/include/Core.h | 6 ++++++ library/include/LuaTools.h | 18 +++++++++--------- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 70e55aff58..4b5ce9670f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,8 +65,10 @@ Template for new versions: ## Documentation ## API +- Core: added ``getConfigPath()`` API for obtaining the path to user-specific configuration files ## Lua +- Added ``dfhack.getConfigPath()`` API, proxying `Core::getConfigPath` ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 0fbcb41971..bdb872049f 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -938,7 +938,17 @@ can be omitted. * ``dfhack.getHackPath()`` - Returns the dfhack directory path, i.e., ``".../df/hack/"``. + Returns the DFHack installation directory path (the folder where DFHack is installed). + This may be the ``hack`` folder within the DF installation, but you should not rely on this. + Specifically, the installation folder is extremely likely to be somewhere else when DFHack is installed from Steam. + Always use this function to get the DFHack installation directory path instead of hardcoding it. + +* ``dfhack.getConfigPath()`` + + Returns the DFHack config directory path (the folder where user-specific configuration files are stored). + This is currently the ``dfhack-config`` folder within the DF installation, but you should not rely on this as it is likely to change in the future. + Always use this function to get the DFHack config directory path instead of hardcoding it. + Avoid storing this value in a long-lived variable, as it's possible that in future versions of DFHack, it may be possible for the config directory to be changed at runtime. * ``dfhack.getSavePath()`` diff --git a/library/Core.cpp b/library/Core.cpp index 0099cae23b..29adbc7ef4 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -528,9 +528,9 @@ std::filesystem::path Core::findScript(std::string name) return {}; } -bool loadScriptPathsCore(Core& core, color_ostream &out, bool silent = false) +bool Core::loadScriptPaths(color_ostream &out, bool silent) { - std::filesystem::path filename{ core.getConfigPath() / "script-paths.txt" }; + std::filesystem::path filename{ getConfigPath() / "script-paths.txt" }; std::ifstream file(filename); if (!file) { @@ -553,7 +553,7 @@ bool loadScriptPathsCore(Core& core, color_ostream &out, bool silent = false) getline(ss, path); if (ch == '+' || ch == '-') { - if (!core.addScriptPath(path, ch == '+') && !silent) + if (!addScriptPath(path, ch == '+') && !silent) out.printerr("{}:{}: Failed to add path: {}\n", filename, line, path); } else if (!silent) @@ -1377,7 +1377,7 @@ bool Core::InitSimulationThread() #endif } - loadScriptPathsCore(*this, con); + loadScriptPaths(con); // initialize common lua context // Calls InitCoreContext after checking IsCoreContext diff --git a/library/include/Core.h b/library/include/Core.h index 5548793132..5842b29038 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -191,6 +191,8 @@ namespace DFHack std::map> ListAliases(); std::string GetAliasCommand(const std::string &name, bool ignore_params = false); + // note that this isn't valid until after DFHack is initialized by DF calling `dfhooks_init` + // that means that it's invalid during at-init static initialization std::filesystem::path getHackPath(); bool isWorldLoaded() { return (last_world_data_ptr != nullptr); } @@ -253,6 +255,8 @@ namespace DFHack return false; } + // Note that this path should be treated as potentially changeable over the life of a Core instance + // Consumers should not cache this path in long-lived local variables const std::filesystem::path getConfigPath() { return Filesystem::getInstallDir() / "dfhack-config"; @@ -287,6 +291,8 @@ namespace DFHack void onStateChange(color_ostream &out, state_change_event event); void handleLoadAndUnloadScripts(color_ostream &out, state_change_event event); + bool loadScriptPaths(color_ostream& out, bool silent = false); + Core(Core const&) = delete; void operator=(Core const&) = delete; diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 09672e1c02..f95095d55b 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -24,15 +24,6 @@ distribution. #pragma once -#include "Core.h" -#include "ColorText.h" -#include "DataDefs.h" - -#include "df/interface_key.h" - -#include -#include - #include #include #include @@ -44,6 +35,15 @@ distribution. #include #include +#include "Core.h" +#include "ColorText.h" +#include "DataDefs.h" + +#include "df/interface_key.h" + +#include +#include + namespace DFHack { class function_identity_base; struct MaterialInfo; From 7c64a1b36bd2f87576e1168ef8aa3d613fc1d67d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 10 Jul 2026 16:33:23 -0500 Subject: [PATCH 080/128] fix changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4b5ce9670f..0c29a14396 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,7 +68,7 @@ Template for new versions: - Core: added ``getConfigPath()`` API for obtaining the path to user-specific configuration files ## Lua -- Added ``dfhack.getConfigPath()`` API, proxying `Core::getConfigPath` +- Added ``dfhack.getConfigPath()`` API, proxying ``Core::getConfigPath`` ## Removed From a2fa1c0fe08353570ebaa4be9f624a6f758e611d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 10 Jul 2026 18:09:32 -0500 Subject: [PATCH 081/128] `autoclothing`: correct command line parsing of material specification --- docs/changelog.txt | 1 + plugins/autoclothing.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3956615ec5..5d1e6d8e4f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `autoclothing`: correct defect in validating material specification on command line - `getplants`: added protective code to avoid misoperation when a plant has an invalid material (which should never happen, but...) ## Misc Improvements diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index 01b6290aff..d9b718f0ba 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -145,19 +145,22 @@ struct ClothingRequirement { return std::nullopt; } - if (auto req = setItem(parameters[idx+1]); !req) + auto req = setItem(parameters[idx + 1]); + + if (!req) { out << "Unrecognized item name or token: " << parameters[idx+1] << endl; return std::nullopt; } - else if (!validateMaterialCategory(*req)) { + req->material_category = material_category; + + if (!validateMaterialCategory(*req)) { out << parameters[idx] << " is not a valid material category for " << parameters[idx+1] << endl; return std::nullopt; } else { - req->material_category = material_category; return req; } } From 8d67c6d36dcb5bfccbcba3ba56763cd266e5fd65 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 11 Jul 2026 16:18:14 -0500 Subject: [PATCH 082/128] change `Core` lifecycle instead of having `Core` be a static singleton with indeterminate construction and destruction sequencing, make Core's instance lifecycle explicit by tying it to `dfhooks_init` and` dfhooks_shutdown` (squashed patch) --- library/Console-windows.cpp | 1 + library/Core.cpp | 20 +++++++++++++++--- library/Debug.cpp | 3 --- library/Hooks.cpp | 21 +++++++++++++------ library/include/Core.h | 12 ++++++++--- library/include/Debug.h | 9 +++++++- library/include/modules/DFSteam.h | 2 +- library/modules/DFSteam.cpp | 35 +++++++++++++++++++++---------- 8 files changed, 75 insertions(+), 28 deletions(-) diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 058cedebe0..29d8f93c83 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -516,6 +516,7 @@ bool Console::init(bool) // FIXME: looks awfully empty, doesn't it? bool Console::shutdown(void) { + assert(inited); std::lock_guard lock{*wlock}; FreeConsole(); inited = false; diff --git a/library/Core.cpp b/library/Core.cpp index 29adbc7ef4..9ff34386c8 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -135,6 +135,8 @@ namespace DFHack { DBG_DECLARE(core, keybinding, DebugCategory::LINFO); DBG_DECLARE(core, script, DebugCategory::LINFO); + Core* Core::active_instance = nullptr; + class MainThread { public: //! MainThread::suspend keeps the main DF thread suspended from Core::Init to @@ -1083,6 +1085,11 @@ df::viewscreen * Core::getTopViewscreen() { } bool Core::InitMainThread(std::filesystem::path path) { + assert(active_instance == nullptr); + + // set this instance as the active instance + active_instance = this; + // this hook is always called from DF's main (render) thread, so capture this thread id df_render_thread = std::this_thread::get_id(); hack_path = path; @@ -1496,8 +1503,8 @@ bool Core::InitSimulationThread() } Core& Core::getInstance() { - static Core instance; - return instance; + assert(Core::active_instance != nullptr); + return *Core::active_instance; } bool Core::isSuspended(void) @@ -1927,6 +1934,7 @@ int Core::Shutdown ( void ) if (hotkey_mgr) { delete hotkey_mgr; + hotkey_mgr = nullptr; } if(plug_mgr) @@ -1934,11 +1942,17 @@ int Core::Shutdown ( void ) delete plug_mgr; plug_mgr = nullptr; } + // invalidate all modules Textures::cleanup(); DFSDL::cleanup(); - DFSteam::cleanup(getConsole()); + DFSteam::cleanup(); + d.reset(); + + // clear active instance + Core::active_instance = nullptr; + return -1; } diff --git a/library/Debug.cpp b/library/Debug.cpp index dafbeb5ce3..50bef6e538 100644 --- a/library/Debug.cpp +++ b/library/Debug.cpp @@ -66,9 +66,6 @@ void DebugManager::unregisterCategory(DebugCategory& cat) DebugRegisterBase::DebugRegisterBase(DebugCategory* cat) { - // Make sure Core lives at least as long any DebugCategory to - // allow debug prints until all Debugcategories has been destructed - Core::getInstance(); DebugManager::getInstance().registerCategory(*cat); } diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 42e9f859af..0bb6b04426 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -15,6 +15,8 @@ static bool disabled = false; DFhackCExport const int32_t dfhooks_priority = 100; +static std::unique_ptr core_instance; + static std::filesystem::path getModulePath() { #ifdef _WIN32 @@ -49,8 +51,11 @@ DFhackCExport void dfhooks_init() { return; } + // construct DFHack core instance + core_instance = std::make_unique(); + // we need to init DF globals before we can check the commandline - if (!DFHack::Core::getInstance().InitMainThread(std::filesystem::canonical(basepath)) || !df::global::game) { + if (!core_instance->InitMainThread(std::filesystem::canonical(basepath)) || !df::global::game) { // we don't set disabled to true here so symbol generation can work return; } @@ -59,6 +64,8 @@ DFhackCExport void dfhooks_init() { if (cmdline.find("--disable-dfhack") != std::string::npos) { fprintf(stderr, "dfhack: --disable-dfhack specified on commandline; disabling\n"); disabled = true; + core_instance->Shutdown(); + core_instance.reset(); return; } @@ -69,14 +76,16 @@ DFhackCExport void dfhooks_init() { DFhackCExport void dfhooks_shutdown() { if (disabled) return; - DFHack::Core::getInstance().Shutdown(); + core_instance->Shutdown(); + // release DFHack core instance + core_instance.reset(); } // called from the simulation thread in the main event loop DFhackCExport void dfhooks_update() { if (disabled) return; - DFHack::Core::getInstance().Update(); + core_instance->Update(); } // called from the simulation thread just before adding the macro @@ -92,7 +101,7 @@ DFhackCExport void dfhooks_prerender() { DFhackCExport bool dfhooks_sdl_event(SDL_Event* event) { if (disabled) return false; - return DFHack::Core::getInstance().DFH_SDL_Event(event); + return core_instance->DFH_SDL_Event(event); } // called from the main thread just after setting mouse state in gps and just @@ -101,7 +110,7 @@ DFhackCExport void dfhooks_sdl_loop() { if (disabled) return; // TODO: wire this up to the new SDL-based console once it is merged - DFHack::Core::getInstance().DFH_SDL_Loop(); + core_instance->DFH_SDL_Loop(); } // called from the main thread for each utf-8 char read from the ncurses input @@ -111,5 +120,5 @@ DFhackCExport void dfhooks_sdl_loop() { DFhackCExport bool dfhooks_ncurses_key(int key) { if (disabled) return false; - return DFHack::Core::getInstance().DFH_ncurses_key(key); + return core_instance->DFH_ncurses_key(key); } diff --git a/library/include/Core.h b/library/include/Core.h index 5842b29038..2957a1347a 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -156,8 +156,11 @@ namespace DFHack friend void ::dfhooks_sdl_loop(); friend bool ::dfhooks_ncurses_key(int key); public: - /// Get the single Core instance or make one. + /// Get the current active Core instance. will assert if none exists + /// Use noInstance() to check first if unsure static Core& getInstance(); + static bool noInstance() { return active_instance == nullptr; } + /// check if the activity lock is owned by this thread bool isSuspended(void); /// Is everything OK? @@ -267,11 +270,14 @@ namespace DFHack return getHackPath() / "data" / "dfhack-config-defaults"; } + Core(); + ~Core(); + private: + static Core* active_instance; + DFHack::Console con; - Core(); - ~Core(); struct Private; std::unique_ptr d; diff --git a/library/include/Debug.h b/library/include/Debug.h index 48e661acc4..632d4910b6 100644 --- a/library/include/Debug.h +++ b/library/include/Debug.h @@ -183,7 +183,7 @@ class DFHACK_EXPORT DebugCategory final { }; /*! - * Fetch a steam object proxy object for output. It also adds standard + * Fetch a stream object proxy object for output. It also adds standard * message components like time and plugin and category names to the line. * * User must make sure that the line is terminated with a line end. @@ -194,6 +194,13 @@ class DFHACK_EXPORT DebugCategory final { */ ostream_proxy_prefix getStream(const level msgLevel) const { + // if the core instance is unavailable, use stderr as a fallback + if (Core::noInstance()) + { + static color_ostream_wrapper fallback{std::cerr}; + return {*this,fallback,msgLevel}; + } + return {*this,Core::getInstance().getConsole(),msgLevel}; } /*! diff --git a/library/include/modules/DFSteam.h b/library/include/modules/DFSteam.h index e604294f8a..7080a94e2e 100644 --- a/library/include/modules/DFSteam.h +++ b/library/include/modules/DFSteam.h @@ -24,7 +24,7 @@ bool init(DFHack::color_ostream& out); /** * Call this when DFHack is being unloaded. */ -void cleanup(DFHack::color_ostream& out); +void cleanup(); DFHACK_EXPORT void launchSteamDFHackIfNecessary(DFHack::color_ostream& out); diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 31cacfea63..9c61b2a7fc 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -54,17 +54,20 @@ bool (*g_SteamAPI_RestartAppIfNecessary)(uint32_t unOwnAppID) = nullptr; void* (*g_SteamInternal_FindOrCreateUserInterface)(int, const char*) = nullptr; bool (*g_SteamAPI_ISteamApps_BIsAppInstalled)(void *iSteamApps, uint32_t appID) = nullptr; -static void bind_all(color_ostream& out, DFLibrary* handle) { -#define bind(name) \ - if (!handle) { \ - g_##name = nullptr; \ - } else { \ - g_##name = (decltype(g_##name))LookupPlugin(handle, #name); \ - if (!g_##name) { \ - WARN(dfsteam, out).print("steam library function not found: " #name "\n"); \ - } \ +template +static void bind_(color_ostream& out, DFLibrary* handle, const char* name, Ptr& func_ptr) { + if (!handle) { + func_ptr = nullptr; + } else { + func_ptr = (Ptr)LookupPlugin(handle, name); + if (!func_ptr) { + WARN(dfsteam, out).print("steam library function not found: {}\n", name); } + } +} +static void bind_all(color_ostream& out, DFLibrary* handle) { +#define bind(name) bind_(out, handle, #name, g_##name) bind(SteamAPI_Init); bind(SteamAPI_Shutdown); bind(SteamAPI_GetHSteamUser); @@ -75,6 +78,16 @@ static void bind_all(color_ostream& out, DFLibrary* handle) { #undef bind } +static void unbind_all() +{ + g_SteamAPI_Init = nullptr; + g_SteamAPI_Shutdown = nullptr; + g_SteamAPI_GetHSteamUser = nullptr; + g_SteamInternal_FindOrCreateUserInterface = nullptr; + g_SteamAPI_RestartAppIfNecessary = nullptr; + g_SteamAPI_ISteamApps_BIsAppInstalled = nullptr; +} + bool DFSteam::init(color_ostream& out) { char *steam_client_launch = getenv("SteamClientLaunch"); if (!steam_client_launch || strncmp(steam_client_launch, "1", 2) != 0) { @@ -103,7 +116,7 @@ bool DFSteam::init(color_ostream& out) { return true; } -void DFSteam::cleanup(color_ostream& out) { +void DFSteam::cleanup() { if (!g_steam_handle) return; @@ -113,7 +126,7 @@ void DFSteam::cleanup(color_ostream& out) { ClosePlugin(g_steam_handle); g_steam_handle = nullptr; - bind_all(out, nullptr); + unbind_all(); g_steam_initialized = false; } From 736292fc3604e44d1bc2af813272ce59a48e4535 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 11 Jul 2026 17:20:29 -0500 Subject: [PATCH 083/128] add missed changelog for #5791 --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 72fd30304a..2946ae9356 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -103,6 +103,7 @@ Template for new versions: ## Misc Improvements - Core: attempts to delete a pool-allocated DF object will now throw an exception instead of corrupting the heap +- `buildingplan`: buildingplan can now generate work orders ## Documentation From 63b62c2e5030d7e39b287ddd7998798bac676ff8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 11 Jul 2026 17:21:37 -0500 Subject: [PATCH 084/128] recategorize changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f3c9df2af9..41d0caf6de 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,11 +57,11 @@ Template for new versions: ## New Tools ## New Features -- `buildingplan`: added a slider on the weapontrap overlay ## Fixes ## Misc Improvements +- `buildingplan`: added a slider on the weapontrap overlay ## Documentation From 49ce780f06343830a73b57ca2160fa3c9847caad Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 11 Jul 2026 17:54:22 -0500 Subject: [PATCH 085/128] unscramble changelog --- docs/changelog.txt | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 41d0caf6de..f5ccac717a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,29 +58,11 @@ Template for new versions: ## New Features -## Fixes - -## Misc Improvements -- `buildingplan`: added a slider on the weapontrap overlay - -## Documentation - -## API - -## Lua - -## Removed - -# 53.11-r2 - -## New Tools - -## New Features - ## Fixes - `getplants`: added protective code to avoid misoperation when a plant has an invalid material (which should never happen, but...) ## Misc Improvements +- `buildingplan`: added a slider on the weapontrap overlay ## Documentation From 4b0d5391c376df837b8843d790bf78bd2c90a6d9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 11 Jul 2026 17:56:37 -0500 Subject: [PATCH 086/128] moved to correct section of changelog --- docs/changelog.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1ac22f3b1b..cfae87d7c4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: - `getplants`: added protective code to avoid misoperation when a plant has an invalid material (which should never happen, but...) ## Misc Improvements +- `buildingplan`: added a small tooltip text about renaming favorites in the UI ## Documentation @@ -226,8 +227,6 @@ Template for new versions: ## Misc Improvements - General: DFHack will unconditionally use UTF-8 for the console on Windows, now that DF forces the process effective system code page to 65001 during startup -- `buildingplan`: added a small tooltip text about renaming favorites in the UI - ## Documentation From f5fc5c72271a3b52d36aa308a0535e1c044d5da9 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 12 Jul 2026 09:02:37 +0000 Subject: [PATCH 087/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8d0d033148..d1e329d4fe 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8d0d0331487da879e25e4f4530ddf761cf4310eb +Subproject commit d1e329d4fe346637a980f6b399458616dc2ecd7d From 45136abcaf8c3feae4a51d6f00187e60112fcb9d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:50:34 +0000 Subject: [PATCH 088/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index d1e329d4fe..910f31f4aa 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d1e329d4fe346637a980f6b399458616dc2ecd7d +Subproject commit 910f31f4aa41c860b62a87b4647d7da0e6653038 From 0cdd3f7871fd797ce4b0abce2df1c82b1cb2d3e7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 12 Jul 2026 13:41:42 -0500 Subject: [PATCH 089/128] Relocate misplaced changelog entries --- docs/changelog.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ede847df39..56638a96cb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,13 +66,17 @@ Template for new versions: ## Misc Improvements - `buildingplan`: added a slider on the weapontrap overlay - `buildingplan`: added a small tooltip text about renaming favorites in the UI +- `buildingplan`: buildingplan can now generate work orders +- `orders`: exported orders now include a human-readable ``name`` field ## Documentation ## API +- Core: added ``getConfigPath()`` API for obtaining the path to user-specific configuration files - ``widgets.RadioButton``: New button widget resembling those used in ``gui/control-panel`` ## Lua +- Added ``dfhack.getConfigPath()`` API, proxying ``Core::getConfigPath`` ## Removed @@ -91,10 +95,8 @@ Template for new versions: ## Documentation ## API -- Core: added ``getConfigPath()`` API for obtaining the path to user-specific configuration files ## Lua -- Added ``dfhack.getConfigPath()`` API, proxying ``Core::getConfigPath`` ## Removed @@ -108,7 +110,6 @@ Template for new versions: ## Misc Improvements - Core: attempts to delete a pool-allocated DF object will now throw an exception instead of corrupting the heap -- `buildingplan`: buildingplan can now generate work orders ## Documentation @@ -265,7 +266,6 @@ Template for new versions: ## New Features - `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators -- `orders`: exported orders now include a human-readable ``name`` field - `sort`: added ``Uniformed`` filter to squad assignment screen to filter dwarves with mining, woodcutting, or hunting labors - `sort`: Add death cause button to dead/missing tab in the creatures screen From cdb202ec437ce017bb184aa9eb85c7e7eb98c2f3 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 12 Jul 2026 17:20:55 -0700 Subject: [PATCH 090/128] Update radio_button.lua - use on_change function --- .../lua/gui/widgets/buttons/radio_button.lua | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/library/lua/gui/widgets/buttons/radio_button.lua b/library/lua/gui/widgets/buttons/radio_button.lua index 04d45732cb..d27d54edc3 100644 --- a/library/lua/gui/widgets/buttons/radio_button.lua +++ b/library/lua/gui/widgets/buttons/radio_button.lua @@ -6,9 +6,9 @@ local ConfigureButton = require('gui.widgets.buttons.configure_button') local to_pen = dfhack.pen.parse local enabled_pen_left = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} + tile=curry(textures.tp_control_panel, 1) or nil, ch=string.byte('[')} local enabled_pen_center = to_pen{fg=COLOR_LIGHTGREEN, - tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check + tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check mark local enabled_pen_right = to_pen{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} local disabled_pen_left = to_pen{fg=COLOR_CYAN, @@ -24,6 +24,7 @@ local disabled_pen_right = to_pen{fg=COLOR_CYAN, ---@class widgets.RadioButton.attrs: widgets.ConfigureButton.attrs ---@field initial_state boolean +---@field on_change? fun(val: boolean) ---@class widgets.RadioButton.attrs.partial: widgets.RadioButton.attrs @@ -35,15 +36,24 @@ RadioButton = defclass(RadioButton, ConfigureButton) RadioButton.ATTRS{ initial_state=true, + on_change=DEFAULT_NIL, } -function RadioButton:init() - self.toggle_state = self.initial_state +function RadioButton:setState(val) + self.toggle_state = not not val + + if self.on_change then + self.on_change(self.toggle_state) + end +end - self.on_click = function() self.toggle_state = not self.toggle_state end +function RadioButton:init() + self.on_click = function() self:setState(not self.toggle_state) end self.pen_left = function() return self.toggle_state and enabled_pen_left or disabled_pen_left end self.pen_center = function() return self.toggle_state and enabled_pen_center or disabled_pen_center end self.pen_right = function() return self.toggle_state and enabled_pen_right or disabled_pen_right end + + self:setState(self.initial_state) end return RadioButton From a607ea368c7e9cf1c4d07a5e4eb044cc53cf4380 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 12 Jul 2026 17:36:44 -0700 Subject: [PATCH 091/128] Update Lua API.rst --- docs/dev/Lua API.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index b232706d77..eaefea06f9 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -6392,11 +6392,17 @@ RadioButton class Subclass of ConfigureButton; a 3x1 tile button that resembles a radio button (or check box in ASCII mode), identical to the ones found in `gui/control-panel`. Clicking on the button will toggle its enabled state. -This state is represented by the boolean value ``toggle_state``. It has the following attributes: -:initial_state: Start in the ``true`` or ``false`` state. Defaults to ``true``. +:initial_state: Whether to start in the ``true`` or ``false`` state. Defaults to ``true``. +:on_change: Callback to call when state changes, including initialization. Called as `on_change(val)`. + +It implements the following method: + +* ``RadioButton:setState(val)`` + + Sets the state to boolean `val` and calls `on_change` (if defined). BannerPanel class ----------------- From 7477d076ef0450977a5f05f871097d37d71a7c4e Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 12 Jul 2026 17:39:16 -0700 Subject: [PATCH 092/128] Update Lua API.rst --- docs/dev/Lua API.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index eaefea06f9..6ec394112f 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -6396,13 +6396,13 @@ Subclass of ConfigureButton; a 3x1 tile button that resembles a radio button It has the following attributes: :initial_state: Whether to start in the ``true`` or ``false`` state. Defaults to ``true``. -:on_change: Callback to call when state changes, including initialization. Called as `on_change(val)`. +:on_change: Callback to call when state changes, including initialization. Called as ``on_change(val)``. It implements the following method: * ``RadioButton:setState(val)`` - Sets the state to boolean `val` and calls `on_change` (if defined). + Sets the state to boolean ``val`` and calls ``on_change`` (if defined). BannerPanel class ----------------- From 44fd10894f39e9e5ba5bd750f9cd11211f511e3e Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:28:00 +0000 Subject: [PATCH 093/128] 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 b693d5904b..389b423ee3 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit b693d5904b967385ad74b1c63003aa1308ee0d18 +Subproject commit 389b423ee3ac5d6345e037cbaaccd85670705e50 diff --git a/scripts b/scripts index 910f31f4aa..2b407d2364 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 910f31f4aa41c860b62a87b4647d7da0e6653038 +Subproject commit 2b407d23641ea3dece2ca6a024d1a9b505812aa9 From fa5974167eecac2083d9c5050f88a9aca489212a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:43:57 +0000 Subject: [PATCH 094/128] 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 389b423ee3..7f5867ed80 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 389b423ee3ac5d6345e037cbaaccd85670705e50 +Subproject commit 7f5867ed805a526c22839e1a4a7f8a3a89d91807 From 4db67b3ef4a81a768b4ba7bddc1dc4f71bdc0aca Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 13 Jul 2026 11:06:15 -0500 Subject: [PATCH 095/128] Fix markup in Removed.rst --- docs/about/Removed.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 96eaa3c142..bf3d0118df 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -261,7 +261,7 @@ Replaced by `gui/create-item`. .. _gui/logcleaner: gui/logcleaner -=============== +============== Removed because changes to Dwarf Fortress internals made the functionality impossible to implement safely. From efea458260bd930bc2f3b55171cab30cbbb0d100 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 13 Jul 2026 11:57:07 -0500 Subject: [PATCH 096/128] CMakeList and changelogs for 53.15-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ac66abe35c..61bddcc0b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. set(DF_VERSION "53.15") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index c8a7e52f7e..7d51a61dad 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,24 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.15-r2 + +## New Tools + ## New Features - `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a high-priority pull-lever job (or cancel a queued one) without navigating to the lever From 425442d4411c29040420af0aacd8d73f13a85545 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 13 Jul 2026 12:01:40 -0500 Subject: [PATCH 097/128] submodules for 53.15-r2 --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 7f5867ed80..0acbcfbd2f 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 7f5867ed805a526c22839e1a4a7f8a3a89d91807 +Subproject commit 0acbcfbd2fe966b42cbf34bccde69406033d7aeb diff --git a/scripts b/scripts index 2b407d2364..b0e865cbb1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 2b407d23641ea3dece2ca6a024d1a9b505812aa9 +Subproject commit b0e865cbb1303a491177d03916185c1d9a202740 From deb3c57cccf56b8c649d008bff2132b14484f84b Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Thu, 16 Jul 2026 23:18:18 -0500 Subject: [PATCH 098/128] buildingplan/planneroverlay: adjust bottom-anchored widget positions When, in 39cc838655f3a97c74cb6f4cc57d17c9f59cec8d, the main_panel gained an extra UI line to accommodate the "queue order" hotkey label, the other bottom-anchored widgets in the upper portion of the main_panel were not adjusted. Several of these (conditional) widgets end up being drawn "under" the divider. They are still present (their hotkeys still work (and they are still clickable!), but they are not visible). The inadvertently hidden widgets are: - the hotkeys (and weapon count) for weapon traps - the hollow toggle for constructions (walls, floors, etc.) - the engraved-only toggle for slabs - the empty-only toggle for (non-trap) cages The other bottom-anchored "upper" widgets are: - the slider for weapon traps - the up/down/up-down/auto selectors for single- and multi-level stairs Move all these widgets up one UI line to unhide the "hidden" widgets and preserve their relative vertical layouts. --- docs/changelog.txt | 1 + plugins/lua/buildingplan/planneroverlay.lua | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7d51a61dad..0d6cc38214 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `buildingplan`: restore planner UI elements: hollow constructions, only engraved slabs, only empty cages, weapon count ## Misc Improvements diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f33722b3b3..4d7180afed 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -700,7 +700,7 @@ function PlannerOverlay:init() self:addviews{ widgets.CycleHotkeyLabel{ view_id='weapons_hotkey', - frame={b=4, l=1, w=28}, + frame={b=5, l=1, w=28}, key='CUSTOM_T', key_back='CUSTOM_SHIFT_T', label='Number of weapons:', @@ -713,7 +713,7 @@ function PlannerOverlay:init() widgets.Slider{ view_id='weapons_slider', - frame={b=6, l=4, w=35}, + frame={b=7, l=4, w=35}, num_stops=#self.options, get_idx_fn=function() return weapon_quantity end, on_change=function(val) @@ -749,7 +749,7 @@ function PlannerOverlay:init() on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='hollow', - frame={b=4, l=1, w=21}, + frame={b=5, l=1, w=21}, key='CUSTOM_H', label='Hollow area:', visible=is_construction, @@ -760,7 +760,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', - frame={b=7, l=1, w=30}, + frame={b=8, l=1, w=30}, key='CUSTOM_R', label='Top stair type: ', visible=is_multi_level_stairs, @@ -772,7 +772,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', - frame={b=6, l=1, w=30}, + frame={b=7, l=1, w=30}, key='CUSTOM_B', label='Bottom Stair Type:', visible=is_multi_level_stairs, @@ -784,7 +784,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='stairs_only_subtype', - frame={b=7, l=1, w=30}, + frame={b=8, l=1, w=30}, key='CUSTOM_R', label='Single level stair:', visible=is_single_level_stairs, @@ -799,7 +799,7 @@ function PlannerOverlay:init() widgets.ToggleHotkeyLabel { view_id='engraved', - frame={b=4, l=1, w=22}, + frame={b=5, l=1, w=22}, key='CUSTOM_T', label='Engraved only:', visible=is_slab, @@ -809,7 +809,7 @@ function PlannerOverlay:init() }, widgets.ToggleHotkeyLabel { view_id='empty', - frame={b=4, l=1, w=22}, + frame={b=5, l=1, w=22}, key='CUSTOM_T', label='Empty only:', visible=is_cage, From c3ae0c34b481dd0a755576a495488ed6d55524fb Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 18 Jul 2026 08:49:03 +0000 Subject: [PATCH 099/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index b0e865cbb1..68b39325b2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b0e865cbb1303a491177d03916185c1d9a202740 +Subproject commit 68b39325b2a94e6401ab5dc32770e5ccf0f0bf52 From 2705daeeb3cf8508d20e60eda3490b402839c431 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:18:53 +0200 Subject: [PATCH 100/128] revise #4767, implementing review feedback and other improvements --- docs/changelog.txt | 6 +++ docs/dev/Lua API.rst | 27 +++++++--- library/LuaApi.cpp | 12 ++--- library/include/modules/Screen.h | 4 +- library/modules/Screen.cpp | 89 +++++++------------------------- 5 files changed, 52 insertions(+), 86 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7d51a61dad..c3f75a4a5d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,8 +66,14 @@ Template for new versions: ## API +``Screen``: new functions ``paintMapPortTile`` and ``readMapPortTile`` to write and read world and region map tiles. + ## Lua +Added ``Screen::paintMapPortTile`` as ``dfhack.screen.paintMapPortTile`` +Added ``Screen::readMapPortTile`` as ``dfhack.screen.readMapPortTile`` + + ## Removed # 53.15-r2 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 6ec394112f..f729be2743 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2872,12 +2872,9 @@ Common parameters to these functions include: * ``x``, ``y``: screen coordinates in tiles; the upper left corner of the screen is ``x = 0, y = 0`` * ``pen``: a `pen object ` -* ``map``: a boolean indicating whether to draw to a separate map buffer - (defaults to false, which is suitable for off-map text or a screen that hides - the map entirely). Note that only third-party plugins like TWBT currently - implement a separate map buffer. If no such plugins are enabled, passing - ``true`` has no effect. However, this parameter should still be used to ensure - that scripts work properly with such plugins. +* ``map``: a boolean (defaults to false) indicating whether to draw to a + separate map buffer. The Steam version uses separate map buffers with square + tiles for for all types of maps (i.e. fort, region, and world). Functions: @@ -2903,15 +2900,31 @@ Functions: * ``dfhack.screen.paintTile(pen,x,y[,char[,tile[,map]]])`` Paints a tile using given parameters. `See below ` for a - description of ``pen``. + description of ``pen``. The map argument is only supported for local maps + (i.e. fort mode and adventure mode outside of fast travel). The ``char`` and + ``tile`` arguments allow overriding the respective parts of the ``pen`` + without constructing a new pen beforehand. Returns *false* on error, e.g., if coordinates are out of bounds +* ``dfhack.screen.paintMapPortTile(pen,x,y[,char[,tile]])`` + + Paints a tile using given parameters onto the interface texpos layer of a map + port (e.g., the world map or the zoomed-in map for embark selection). The + ``char`` and ``tile`` arguments work as above. + * ``dfhack.screen.readTile(x,y[,map])`` Retrieves the contents of the specified tile from the screen buffers. Returns a `pen object `, or *nil* if invalid or TrueType. +* ``dfhack.screen.readMapPortTile(x,y)`` + + Retrieves the contents of the specified tile from the screen buffers. Returns + a `pen object `, or *nil* if invalid. + + For now only looks at the ``sites`` textpos layer. + * ``dfhack.screen.paintString(pen,x,y,text[,map])`` Paints the string starting at *x,y*. Uses the string characters diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index f8c373df15..d321ca7891 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3129,7 +3129,7 @@ static int screen_readTile(lua_State *L) return 1; } -static int screen_paintTileMapPort(lua_State *L) +static int screen_paintMapPortTile(lua_State *L) { Pen pen; Lua::CheckPen(L, &pen, 1); @@ -3144,15 +3144,15 @@ static int screen_paintTileMapPort(lua_State *L) } if (lua_gettop(L) >= 5 && !lua_isnil(L, 5)) pen.tile = luaL_checkint(L, 5); - lua_pushboolean(L, Screen::paintTileMapPort(pen, x, y)); + lua_pushboolean(L, Screen::paintMapPortTile(pen, x, y)); return 1; } -static int screen_readTileMapPort(lua_State *L) +static int screen_readMapPortTile(lua_State *L) { int x = luaL_checkint(L, 1); int y = luaL_checkint(L, 2); - Pen pen = Screen::readTileMapPort(x, y); + Pen pen = Screen::readMapPortTile(x, y, &df::graphic_map_portst::screentexpos_site); Lua::Push(L, pen); return 1; } @@ -3344,8 +3344,8 @@ static const luaL_Reg dfhack_screen_funcs[] = { { "getWindowSize", screen_getWindowSize }, { "paintTile", screen_paintTile }, { "readTile", screen_readTile }, - { "paintTileMapPort", screen_paintTileMapPort }, - { "readTileMapPort", screen_readTileMapPort }, + { "paintMapPortTile", screen_paintMapPortTile }, + { "readMapPortTile", screen_readMapPortTile }, { "paintString", screen_paintString }, { "fillRect", screen_fillRect }, { "findGraphicsTile", screen_findGraphicsTile }, diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h index af7224dea7..173cda6aa8 100644 --- a/library/include/modules/Screen.h +++ b/library/include/modules/Screen.h @@ -202,10 +202,10 @@ namespace DFHack DFHACK_EXPORT Pen readTile(int x, int y, bool map = false, int32_t * df::graphic_viewportst::*texpos_field = NULL); /// Paint one world map tile with the given pen - DFHACK_EXPORT bool paintTileMapPort(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); + DFHACK_EXPORT bool paintMapPortTile(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); /// Retrieves one world map tile from the buffer - DFHACK_EXPORT Pen readTileMapPort(int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); + DFHACK_EXPORT Pen readMapPortTile(int x, int y, int32_t * df::graphic_map_portst::*texpos_field); /// Paint a string onto the screen. Ignores ch and tile of pen. DFHACK_EXPORT bool paintString(const Pen &pen, int x, int y, const std::string &text, bool map = false); diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 37d4c10748..338a66925e 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -372,11 +372,18 @@ Pen Screen::readTile(int x, int y, bool map, int32_t * df::graphic_viewportst::* return doGetTile(x, y, map, texpos_field); } -static bool doSetTile_map_port(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) { - auto &vp = gps->main_map_port; +bool Screen::paintMapPortTile(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) +{ + if (!gps || !pen.valid()) return false; + + bool use_graphics = Screen::inGraphicsMode(); + if (!use_graphics) + return doSetTile_char(pen, x, y, use_graphics); + if (!texpos_field) texpos_field = &df::graphic_map_portst::screentexpos_interface; + auto &vp = gps->main_map_port; if (x < 0 || x >= vp->dim_x || y < 0 || y >= vp->dim_y) return false; @@ -393,74 +400,29 @@ static bool doSetTile_map_port(const Pen &pen, int x, int y, int32_t * df::graph return true; } -static bool doSetTile_map_port_default(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) { - bool use_graphics = Screen::inGraphicsMode(); - - if (use_graphics) - return doSetTile_map_port(pen, x, y, texpos_field); +Pen Screen::readMapPortTile(int x, int y, int32_t * df::graphic_map_portst::*texpos_field) +{ + CHECK_NULL_POINTER(texpos_field) - return doSetTile_char(pen, x, y, use_graphics); -} + if (!gps) return Pen(0,0,0,-1); -bool Screen::paintTileMapPort(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) -{ - if (!gps || !pen.valid()) return false; + bool use_graphics = Screen::inGraphicsMode(); - doSetTile_map_port_default(pen, x, y, texpos_field); - return true; -} + if (!use_graphics) + return doGetTile_char(x, y, use_graphics); -static Pen doGetTile_map_port(int x, int y, int32_t * df::graphic_map_portst::*texpos_field) { auto &vp = gps->main_map_port; if (x < 0 || x >= vp->dim_x || y < 0 || y >= vp->dim_y) return Pen(0, 0, 0, -1); size_t max_index = vp->dim_x * vp->dim_y - 1; - size_t index = (x * vp->dim_y) + y; + size_t index = (y * vp->dim_x) + x; if (index < 0 || index > max_index) return Pen(0, 0, 0, -1); - int tile = 0; - if (!texpos_field) { - if (tile == 0) - tile = vp->screentexpos_base[index]; - if (tile == 0) - tile = vp->screentexpos_detail[index]; - if (tile == 0) - tile = vp->screentexpos_tunnel[index]; - if (tile == 0) - tile = vp->screentexpos_river[index]; - if (tile == 0) - tile = vp->screentexpos_road[index]; - if (tile == 0) - tile = vp->screentexpos_site[index]; - if (tile == 0) - tile = vp->screentexpos_army[index]; - if (tile == 0) - tile = vp->screentexpos_interface[index]; - if (tile == 0) - tile = vp->screentexpos_detail_to_n[index]; - if (tile == 0) - tile = vp->screentexpos_detail_to_s[index]; - if (tile == 0) - tile = vp->screentexpos_detail_to_w[index]; - if (tile == 0) - tile = vp->screentexpos_detail_to_e[index]; - if (tile == 0) - tile = vp->screentexpos_detail_to_nw[index]; - if (tile == 0) - tile = vp->screentexpos_detail_to_ne[index]; - if (tile == 0) - tile = vp->screentexpos_detail_to_sw[index]; - if (tile == 0) - tile = vp->screentexpos_detail_to_se[index]; - if (tile == 0) - tile = vp->screentexpos_site_to_s[index]; - } else { - tile = (vp->*texpos_field)[index]; - } + auto tile = (vp->*texpos_field)[index]; char ch = 0; uint8_t fg = 0; @@ -468,21 +430,6 @@ static Pen doGetTile_map_port(int x, int y, int32_t * df::graphic_map_portst::*t return Pen(ch, fg, bg, tile, false); } -static Pen doGetTile_map_port_default(int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL) { - bool use_graphics = Screen::inGraphicsMode(); - - if (use_graphics) - return doGetTile_map_port(x, y, texpos_field); - return doGetTile_char(x, y, use_graphics); -} - -Pen Screen::readTileMapPort(int x, int y, int32_t * df::graphic_map_portst::*texpos_field) -{ - if (!gps) return Pen(0,0,0,-1); - - return doGetTile_map_port_default(x, y, texpos_field); -} - bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text, bool map) { auto dim = getWindowSize(); From ef152d32506d2caaf8d46912312efac1839a5642 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:36:40 +0200 Subject: [PATCH 101/128] add missing dashes --- docs/changelog.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index c3f75a4a5d..41acd4dc86 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,12 +66,12 @@ Template for new versions: ## API -``Screen``: new functions ``paintMapPortTile`` and ``readMapPortTile`` to write and read world and region map tiles. +- ``Screen``: new functions ``paintMapPortTile`` and ``readMapPortTile`` to write and read world and region map tiles. ## Lua -Added ``Screen::paintMapPortTile`` as ``dfhack.screen.paintMapPortTile`` -Added ``Screen::readMapPortTile`` as ``dfhack.screen.readMapPortTile`` +- Added ``Screen::paintMapPortTile`` as ``dfhack.screen.paintMapPortTile`` +- Added ``Screen::readMapPortTile`` as ``dfhack.screen.readMapPortTile`` ## Removed From 46b2561a6f1e49ebb8487d2790ed3bb2000dca16 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:27:20 +0000 Subject: [PATCH 102/128] Auto-update submodules library/xml: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index ca346feff1..a06e4c1c81 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ca346feff1d26a440dae3a9b09778277e904678d +Subproject commit a06e4c1c81f94eaf0d735cfe5a8ed2c64e0bcb81 diff --git a/plugins/stonesense b/plugins/stonesense index 0acbcfbd2f..de2cd37da6 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 0acbcfbd2fe966b42cbf34bccde69406033d7aeb +Subproject commit de2cd37da6e64d8b53523e873889b82b6182b6a5 From efff8d9a0b635c196ba2c3e8183d5b16fdc103c5 Mon Sep 17 00:00:00 2001 From: Quietust Date: Tue, 21 Jul 2026 17:43:17 -0600 Subject: [PATCH 103/128] Remove MaterialInfo consts in favor of using df::builtin_mats enum entries Also adjust some comparisons to use more easily understandable values (e.g. <= CREATURE_200 instead of < HIST_FIG_1) --- docs/changelog.txt | 3 ++- library/LuaApi.cpp | 3 ++- library/RemoteTools.cpp | 6 ++--- library/include/modules/Materials.h | 7 ----- library/lua/gui/materials.lua | 9 ++++--- library/lua/tile-material.lua | 4 +-- library/modules/Items.cpp | 2 +- library/modules/Kitchen.cpp | 2 +- library/modules/MapCache.cpp | 4 +-- library/modules/Materials.cpp | 22 ++++++++-------- plugins/autochop.cpp | 2 +- plugins/buildingplan/buildingplan.cpp | 2 +- .../remotefortressreader.cpp | 26 +++++++++---------- 13 files changed, 44 insertions(+), 48 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index af1e77905d..3f1340168f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,12 +68,13 @@ Template for new versions: ## API - ``Screen``: new functions ``paintMapPortTile`` and ``readMapPortTile`` to write and read world and region map tiles. +- ``Materials``: ``MaterialInfo`` constants ``NUM_BUILTIN``, ``GROUP_SIZE``, ``CREATURE_BASE``, ``FIGURE_BASE``, ``PLANT_BASE``, and ``END_BASE`` removed. New plugins should use appropriate members of the ``df::builtin_mats`` enum. ## Lua - Added ``Screen::paintMapPortTile`` as ``dfhack.screen.paintMapPortTile`` - Added ``Screen::readMapPortTile`` as ``dfhack.screen.readMapPortTile`` - +- Deprecated ``gui.materials.CREATURE_BASE`` and ``gui.materials.PLANT_BASE`` - scripts should instead use ``df.builtin_mats.CREATURE_1`` and ``df.builtin_mats.PLANT_1``, respectively. ## Removed diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d321ca7891..6a08756156 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -72,6 +72,7 @@ distribution. #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" #include "df/building_workshopst.h" +#include "df/builtin_mats.h" #include "df/burrow.h" #include "df/caravan_state.h" #include "df/construction.h" @@ -491,7 +492,7 @@ static bool decode_matinfo(lua_State *state, MaterialInfo *info, bool numpair = if (auto item = Lua::GetDFObject(state, 1)) return info->decode(item); if (auto plant = Lua::GetDFObject(state, 1)) - return info->decode(MaterialInfo::PLANT_BASE, plant->material); + return info->decode(df::builtin_mats::PLANT_1, plant->material); if (auto mvec = Lua::GetDFObject(state, 1)) return info->decode(*mvec, luaL_checkint(state, 2)); } diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index 5dab76e8cb..30e24c51b5 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -530,7 +530,7 @@ static command_result ListMaterials(color_ostream &stream, if (in->builtin()) { - for (int i = 0; i < MaterialInfo::NUM_BUILTIN; i++) + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) listMaterial(out, i, -1, mask); } @@ -549,7 +549,7 @@ static command_result ListMaterials(color_ostream &stream, auto praw = vec[i]; for (size_t j = 0; j < praw->material.size(); j++) - listMaterial(out, MaterialInfo::CREATURE_BASE+j, i, mask); + listMaterial(out, df::builtin_mats::CREATURE_1+j, i, mask); } } @@ -561,7 +561,7 @@ static command_result ListMaterials(color_ostream &stream, auto praw = vec[i]; for (size_t j = 0; j < praw->material.size(); j++) - listMaterial(out, MaterialInfo::PLANT_BASE+j, i, mask); + listMaterial(out, df::builtin_mats::PLANT_1+j, i, mask); } } diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 1f87f548ff..61763ae295 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -66,13 +66,6 @@ namespace DFHack struct DFHACK_EXPORT MaterialInfo { - static const int NUM_BUILTIN = 19; - static const int GROUP_SIZE = 200; - static const int CREATURE_BASE = NUM_BUILTIN; - static const int FIGURE_BASE = NUM_BUILTIN + GROUP_SIZE; - static const int PLANT_BASE = NUM_BUILTIN + GROUP_SIZE * 2; - static const int END_BASE = NUM_BUILTIN + GROUP_SIZE * 3; - int16_t type; int32_t index; diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua index 7429a78237..99e4e6449b 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -8,8 +8,9 @@ local dlg = require('gui.dialogs') ARROW = string.char(26) -CREATURE_BASE = 19 -PLANT_BASE = 419 +-- For backwards compatibility with older scripts +CREATURE_BASE = df.builtin_mats.CREATURE_1 +PLANT_BASE = df.builtin_mats.PLANT_1 MaterialDialog = defclass(MaterialDialog, gui.FramedScreen) @@ -127,7 +128,7 @@ function MaterialDialog:initCreatureMode() local choices = {} for i,v in ipairs(df.global.world.raws.creatures.all) do - self:addObjectChoice(choices, v, v.name[0], CREATURE_BASE, i) + self:addObjectChoice(choices, v, v.name[0], df.builtin_mats.CREATURE_1, i) end self:pushContext('Creature materials', choices) @@ -137,7 +138,7 @@ function MaterialDialog:initPlantMode() local choices = {} for i,v in ipairs(df.global.world.raws.plants.all) do - self:addObjectChoice(choices, v, v.name, PLANT_BASE, i) + self:addObjectChoice(choices, v, v.name, df.builtin_mats.PLANT_1, i) end self:pushContext('Plant materials', choices) diff --git a/library/lua/tile-material.lua b/library/lua/tile-material.lua index c0fe2e7cb6..5c847c463d 100644 --- a/library/lua/tile-material.lua +++ b/library/lua/tile-material.lua @@ -194,7 +194,7 @@ function GetTreeMat(x, y, z) for _, tree in ipairs(df.global.world.plants.all) do if tree.tree_info ~= nil then if coordInTree(pos, tree) then - return dfhack.matinfo.decode(419, tree.material) + return dfhack.matinfo.decode(df.builtin_mats.PLANT_1, tree.material) end end end @@ -209,7 +209,7 @@ function GetShrubMat(x, y, z) for _, shrub in ipairs(df.global.world.plants.all) do if shrub.tree_info == nil then if shrub.pos.x == pos.x and shrub.pos.y == pos.y and shrub.pos.z == pos.z then - return dfhack.matinfo.decode(419, shrub.material) + return dfhack.matinfo.decode(df.builtin_mats.PLANT_1, shrub.material) end end end diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 6b5b3ee1d2..0e7f1ffdf3 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1756,7 +1756,7 @@ int32_t Items::pickGrowthPrint(int16_t subtype, int16_t mat, int32_t matg) { int growth_print = -1; // Make sure it's made of a valid plant material, then grab its definition - if (mat >= 419 && mat <= 618 && matg >= 0 && (unsigned)matg < world->raws.plants.all.size()) + if (mat >= df::builtin_mats::PLANT_1 && mat <= df::builtin_mats::PLANT_200 && matg >= 0 && (unsigned)matg < world->raws.plants.all.size()) { auto plant_def = world->raws.plants.all[matg]; // Make sure it subtype is also valid diff --git a/library/modules/Kitchen.cpp b/library/modules/Kitchen.cpp index c9d4c7d5c6..fd1bfe002f 100644 --- a/library/modules/Kitchen.cpp +++ b/library/modules/Kitchen.cpp @@ -39,7 +39,7 @@ void Kitchen::debug_print(color_ostream &out) plotinfo->kitchen.mat_types[i], plotinfo->kitchen.mat_indices[i], plotinfo->kitchen.exc_types[i].whole, - (plotinfo->kitchen.mat_types[i] >= 419 && plotinfo->kitchen.mat_types[i] <= 618) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id : "n/a" + (plotinfo->kitchen.mat_types[i] >= df::builtin_mats::PLANT_1 && plotinfo->kitchen.mat_types[i] <= df::builtin_mats::PLANT_200) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id : "n/a" ); } out.print("\n"); diff --git a/library/modules/MapCache.cpp b/library/modules/MapCache.cpp index 84ee4affa0..ff0bea9eb4 100644 --- a/library/modules/MapCache.cpp +++ b/library/modules/MapCache.cpp @@ -943,7 +943,7 @@ t_matpair MapExtras::BlockInfo::getBaseMaterial(df::tiletype tt, df::coord2d pos case ROOT: case TREE: case PLANT: - rv.mat_type = MaterialInfo::PLANT_BASE; + rv.mat_type = df::builtin_mats::PLANT_1; if (auto plant = plants[block->map_pos + df::coord(x,y,0)]) { if (auto raw = df::plant_raw::find(plant->material)) @@ -958,7 +958,7 @@ t_matpair MapExtras::BlockInfo::getBaseMaterial(df::tiletype tt, df::coord2d pos case GRASS_DARK: case GRASS_DRY: case GRASS_DEAD: - rv.mat_type = MaterialInfo::PLANT_BASE; + rv.mat_type = df::builtin_mats::PLANT_1; if (auto raw = df::plant_raw::find(grass[x][y])) { rv.mat_type = raw->material_defs.type[plant_material_def::basic_mat]; diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index dba8a78132..f91882337a 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -108,7 +108,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) { material = raws.mat_table.builtin[type]; } - else if (type == 0) + else if (type == df::builtin_mats::INORGANIC) { mode = Inorganic; inorganic = df::inorganic_raw::find(index); @@ -116,23 +116,23 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; material = &inorganic->material; } - else if (type < CREATURE_BASE) + else if (type < df::builtin_mats::CREATURE_1) { material = raws.mat_table.builtin[type]; } - else if (type < FIGURE_BASE) + else if (type <= df::builtin_mats::CREATURE_200) { mode = Creature; - subtype = type - CREATURE_BASE; + subtype = type - df::builtin_mats::CREATURE_1; creature = df::creature_raw::find(index); if (!creature || size_t(subtype) >= creature->material.size()) return false; material = creature->material[subtype]; } - else if (type < PLANT_BASE) + else if (type <= df::builtin_mats::HIST_FIG_200) { mode = Creature; - subtype = type - FIGURE_BASE; + subtype = type - df::builtin_mats::HIST_FIG_1; figure = df::historical_figure::find(index); if (!figure) return false; @@ -141,10 +141,10 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; material = creature->material[subtype]; } - else if (type < END_BASE) + else if (type <= df::builtin_mats::PLANT_200) { mode = Plant; - subtype = type - PLANT_BASE; + subtype = type - df::builtin_mats::PLANT_1; plant = df::plant_raw::find(index); if (!plant || size_t(subtype) >= plant->material.size()) return false; @@ -219,7 +219,7 @@ bool MaterialInfo::findBuiltin(const std::string& token) } auto& raws = world->raws; - for (int i = 0; i < NUM_BUILTIN; i++) + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) { auto obj = raws.mat_table.builtin[i]; if (obj && obj->id == token) @@ -266,7 +266,7 @@ bool MaterialInfo::findPlant(const std::string& token, const std::string& subtok for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(PLANT_BASE + j, i); + return decode(df::builtin_mats::PLANT_1 + j, i); break; } @@ -286,7 +286,7 @@ bool MaterialInfo::findCreature(const std::string& token, const std::string& sub for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(CREATURE_BASE + j, i); + return decode(df::builtin_mats::CREATURE_1 + j, i); break; } diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index 811a3d1cb0..f85bbdd756 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -301,7 +301,7 @@ static int32_t estimate_logs(const df::plant *plant) { return 0; MaterialInfo mi; - mi.decode(MaterialInfo::PLANT_BASE, plant->material); + mi.decode(df::builtin_mats::PLANT_1, plant->material); bool is_shroom = mi.plant->flags.is_set(df::plant_raw_flags::TREE_HAS_MUSHROOM_CAP); int32_t trunks = 0, parent_dir = 0; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 6f5a13a8c7..61ee882071 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -167,7 +167,7 @@ static void load_organic_material_cache(df::organic_mat_category cat) { static void load_material_cache() { auto &raws = world->raws; - for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) + for (int i = 1; i < df::builtin_mats::CREATURE_1; ++i) if (raws.mat_table.builtin[i]) cache_matched(i, -1); diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index f8b54f01fb..9a8f034c5b 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -634,12 +634,12 @@ static command_result CheckHashes(color_ostream &stream, const EmptyMessage *in) void CopyMat(RemoteFortressReader::MatPair * mat, int type, int index) { - if (type >= MaterialInfo::FIGURE_BASE && type < MaterialInfo::PLANT_BASE) + if (type >= df::builtin_mats::HIST_FIG_1 && type <= df::builtin_mats::HIST_FIG_200) { df::historical_figure * figure = df::historical_figure::find(index); if (figure) { - type -= MaterialInfo::GROUP_SIZE; + type = (type - df::builtin_mats::HIST_FIG_1) + df::builtin_mats::CREATURE_1; index = figure->race; } } @@ -817,9 +817,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage MaterialInfo mat; for (size_t i = 0; i < raws->inorganics.all.size(); i++) { - mat.decode(0, i); + mat.decode(df::builtin_mats::INORGANIC, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(0); + mat_def->mutable_mat_pair()->set_mat_type(df::builtin_mats::INORGANIC); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; @@ -828,11 +828,11 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage ConvertDFColorDescriptor(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)], mat_def->mutable_state_color()); } } - for (int i = 0; i < 19; i++) + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) { int k = -1; - if (i == 7) - k = 1;// for coal. + if (i == df::builtin_mats::COAL) + k = 1;// for coke and charcoal for (int j = -1; j <= k; j++) { mat.decode(i, j); @@ -852,9 +852,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage df::creature_raw * creature = raws->creatures.all[i]; for (size_t j = 0; j < creature->material.size(); j++) { - mat.decode(j + MaterialInfo::CREATURE_BASE, i); + mat.decode(j + df::builtin_mats::CREATURE_1, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(j + 19); + mat_def->mutable_mat_pair()->set_mat_type(j + df::builtin_mats::CREATURE_1); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; @@ -869,9 +869,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage df::plant_raw * plant = raws->plants.all[i]; for (size_t j = 0; j < plant->material.size(); j++) { - mat.decode(j + 419, i); + mat.decode(j + df::builtin_mats::PLANT_1, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(j + 419); + mat_def->mutable_mat_pair()->set_mat_type(j + df::builtin_mats::PLANT_1); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; @@ -2096,14 +2096,14 @@ static void SetRegionTile(RegionTile * out, df::region_map_entry * e1) auto plantMat = out->add_plant_materials(); plantMat->set_mat_index(pop->plant); - plantMat->set_mat_type(419); + plantMat->set_mat_type(df::builtin_mats::PLANT_1); } else if (pop->type == world_population_type::Tree) { auto plantMat = out->add_tree_materials(); plantMat->set_mat_index(pop->plant); - plantMat->set_mat_type(419); + plantMat->set_mat_type(df::builtin_mats::PLANT_1); } } #if DF_VERSION_INT >= 43005 From c9a137c6dca3330e424dc37ef83524b762d2567c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 30 Jul 2026 17:05:37 -0500 Subject: [PATCH 104/128] extend DFSDL API to provide the DFLIbrary so a plugin that needs to use an SDL method not already mapped by DFSDL doesn't have to manually locate and open the SDL libs itself --- docs/changelog.txt | 2 +- library/include/modules/DFSDL.h | 7 +++++++ library/modules/DFSDL.cpp | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3f1340168f..300ae435f2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,7 +66,7 @@ Template for new versions: ## Documentation ## API - +- ``DFSDL``: added ``obtain_library_handle`` and ``obtain_image_library_handle`` so that a plugin can obtain DFHack's already-open handle to these libs instead of having to do it itself. - ``Screen``: new functions ``paintMapPortTile`` and ``readMapPortTile`` to write and read world and region map tiles. - ``Materials``: ``MaterialInfo`` constants ``NUM_BUILTIN``, ``GROUP_SIZE``, ``CREATURE_BASE``, ``FIGURE_BASE``, ``PLANT_BASE``, and ``END_BASE`` removed. New plugins should use appropriate members of the ``df::builtin_mats`` enum. diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 7d53242ad0..d47d588fc6 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -35,6 +35,13 @@ namespace DFHack::DFSDL */ void cleanup(); + /** + * Obtain DFHack's handle to the SDL or IMG libraries, in case a plugin needs + * to map a SDL API not mapped here + */ + DFHACK_EXPORT DFLibrary* obtain_library_handle(); + DFHACK_EXPORT DFLibrary* obtain_image_library_handle(); + DFHACK_EXPORT SDL_Surface* DFIMG_Load(const char* file); DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceFrom(void* pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index f23cde5511..ae0e6abbd6 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -25,6 +25,17 @@ using std::vector; static DFLibrary *g_sdl_handle = nullptr; static DFLibrary *g_sdl_image_handle = nullptr; + +DFLibrary* obtain_library_handle() +{ + return g_sdl_handle; +} + +DFLibrary* obtain_image_library_handle() +{ + return g_sdl_image_handle; +} + static const vector SDL_LIBS { #ifdef WIN32 "SDL2.dll" From e872d74a763fce333d87c0d70b0f14b1a6202724 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 30 Jul 2026 17:12:33 -0500 Subject: [PATCH 105/128] add missing header oops --- library/include/modules/DFSDL.h | 1 + 1 file changed, 1 insertion(+) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index d47d588fc6..95837b6889 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -2,6 +2,7 @@ #include "ColorText.h" #include "Export.h" +#include "PluginManager.h" #include #include From d460446d2fe332869d602db65c9ded6810dacaa8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 30 Jul 2026 18:41:13 -0500 Subject: [PATCH 106/128] correct namespace for these functions do not commit while hungry --- library/modules/DFSDL.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index ae0e6abbd6..01b89720cf 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -26,12 +26,12 @@ using std::vector; static DFLibrary *g_sdl_handle = nullptr; static DFLibrary *g_sdl_image_handle = nullptr; -DFLibrary* obtain_library_handle() +DFLibrary* DFHack::DFSDL::obtain_library_handle() { return g_sdl_handle; } -DFLibrary* obtain_image_library_handle() +DFLibrary* DFHack::DFSDL::obtain_image_library_handle() { return g_sdl_image_handle; } From fb8d4e900d228d02da1e483aec45d8f3036d32e1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 1 Aug 2026 13:56:45 -0500 Subject: [PATCH 107/128] some updates to `Contributing.rst` --- docs/dev/Contributing.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/dev/Contributing.rst b/docs/dev/Contributing.rst index 3ab620c1f9..216d204969 100644 --- a/docs/dev/Contributing.rst +++ b/docs/dev/Contributing.rst @@ -120,6 +120,37 @@ Pull request guidelines or add "WIP" to the title. Otherwise, your pull request may be reviewed and/or merged prematurely. +* Avoid using force pushes to your pull request branch after it has been reviewed, + as this can make it difficult for reviewers to see what has changed since their + last review. If you need to make changes, consider creating a new commit instead + of amending or rebasing. We neither enforce nor recommend a "single commit" rule; if you do + choose to squash your commits, please ensure that the commit message is clear and descriptive of the changes made. + If your pull request has an unusually large number of commits, a maintainer may + request that you squash your commits into a smaller number of commits before merging. + +* All pull requests must be accompanied by a description of the changes made, and + any relevant information for reviewers. If your pull request addresses an + issue, please include a reference to that issue in the description (e.g. + "Fixes #1234"). If your pull request is related to another pull request, please + include a reference to that pull request in the description (e.g. "Related to + #1234"). + +* All pull requests which have user facing changes, including all new features, bug fixes, or + changes to existing functionality, must include an entry in the "Future" section of + the changelog for the relevant repository. If your pull request is merged, this entry + will be added to the appropriate changelog. These entries are used when preparing the release + notes for each release, so please be sure to include a clear and concise description + of the changes made. See `build-changelog` for more information on the changelog format. + Changes that do not require a changelog entry are mainly those that are purely internal, + such as refactoring not intended to change semantics, code cleanup, changes to CI implementation + or to documentation, or changes directly related to the release process. When in doubt, + assume a changelog entry will be required. + +* Pull requests that add or modify tools must include a corresponding update to the documentation + for that tool. Similarly, pull requests that add or modify either the C++ or Lua APIs + must include a corresponding update to the appropriate API documentation. + See `docs-standards` for details. + Other ways to help ================== DFHack is a software project, but there's a lot more to it than programming. From e868d02edd4991b9a74891fe2ce67fad953f7bbd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 1 Aug 2026 21:48:48 -0500 Subject: [PATCH 108/128] add a safety check to `manageUnitAttackEvent` to avoid a potential crash based on a bug report in which DFHack crashed due to getting an erroneous result from `binsearch`, presumably due to crud in the DF vector --- docs/changelog.txt | 1 + library/modules/EventManager.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 300ae435f2..92f023062f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: - `buildingplan`: restore planner UI elements: hollow constructions, only engraved slabs, only empty cages, weapon count ## Misc Improvements +- `EventManager`: add safety check to potentially avoid a DFHack crash when DF's ``reports`` table is out of order ## Documentation diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 3c926645a2..12aa4e7ed5 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -1136,6 +1136,11 @@ static void manageUnitAttackEvent(color_ostream& out) { multimap copy(handlers[EventType::UNIT_ATTACK].begin(), handlers[EventType::UNIT_ATTACK].end()); std::vector& reports = df::global::world->status.reports; size_t idx = df::report::binsearch_index(reports, lastReportUnitAttack, false); + if (idx >= reports.size()) + { + WARN(log, out).print("manageUnitAttackEvent: last reported unit attack lookup failed ({} -> {})\n", lastReportUnitAttack, idx); + return; + } // returns the index to the key equal to or greater than the key provided idx = reports[idx]->id == lastReportUnitAttack ? idx + 1 : idx; // we need the index after (where the new stuff is) From 4c2130595c54fac19b30ce713918f2e84fbda97c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Aug 2026 09:46:33 -0500 Subject: [PATCH 109/128] version to 53.15-r3 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 61bddcc0b4..87b47f2d04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. set(DF_VERSION "53.15") -set(DFHACK_RELEASE "r2") +set(DFHACK_RELEASE "r3") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 58c2636545d7b22e7b78fa74aa79a1d5a830cdac Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Aug 2026 09:58:26 -0500 Subject: [PATCH 110/128] Changelog for 53.15-r3 --- docs/changelog.txt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 92f023062f..14efc37d1a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.15-r3 + +## New Tools + +## New Features + ## Fixes - `buildingplan`: restore planner UI elements: hollow constructions, only engraved slabs, only empty cages, weapon count @@ -72,7 +90,6 @@ Template for new versions: - ``Materials``: ``MaterialInfo`` constants ``NUM_BUILTIN``, ``GROUP_SIZE``, ``CREATURE_BASE``, ``FIGURE_BASE``, ``PLANT_BASE``, and ``END_BASE`` removed. New plugins should use appropriate members of the ``df::builtin_mats`` enum. ## Lua - - Added ``Screen::paintMapPortTile`` as ``dfhack.screen.paintMapPortTile`` - Added ``Screen::readMapPortTile`` as ``dfhack.screen.readMapPortTile`` - Deprecated ``gui.materials.CREATURE_BASE`` and ``gui.materials.PLANT_BASE`` - scripts should instead use ``df.builtin_mats.CREATURE_1`` and ``df.builtin_mats.PLANT_1``, respectively. From 679a99a99b4e047a71929a1d6c4ef4794ca20233 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Aug 2026 09:59:00 -0500 Subject: [PATCH 111/128] library/xml for 53.15-r3 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a06e4c1c81..a488fe8f5a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a06e4c1c81f94eaf0d735cfe5a8ed2c64e0bcb81 +Subproject commit a488fe8f5ae3475d7731eb8169498900a32e4b0b From e069b16a8362e53b304daaccb5cfb2074455cdc8 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:38:38 +0000 Subject: [PATCH 112/128] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a488fe8f5a..47bacb367d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a488fe8f5ae3475d7731eb8169498900a32e4b0b +Subproject commit 47bacb367d26cadca1a99a1ecd74fde88e4ed1a4 From 7f5304709fba9c3cbc13ebf4b1882bc99ba3fde7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 4 Aug 2026 10:43:21 -0500 Subject: [PATCH 113/128] block gcc 16 for being used for builds gcc 16 has a defect in which enabling `-Warray-bounds` causes it to spuriously warn about object slicing in a case where no object slicing can possibly occur will reenable when gcc fixes this defect --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 87b47f2d04..852d21f07d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,12 @@ macro(CHECK_GCC compiler_path) if(${GCC_VERSION_OUT} VERSION_LESS "11") message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 11 or later") endif() + # GCC 16 currently has a defect that prevents it from compiling DFHack + # -Warray-bounds is broken in GCC 16 and we'd rather disable the compiler than remove this warning + # will reconsider when this defect is fixed in a future GCC release + if(${GCC_VERSION_OUT} VERSION_GREATER_EQUAL "16") + message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 15 or earlier") + endif() endmacro() if(UNIX) From 8e98ed9c530530e0562471ad80baaaa48b86e65f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:13:36 +0000 Subject: [PATCH 114/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 68b39325b2..769a842ba5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 68b39325b2a94e6401ab5dc32770e5ccf0f0bf52 +Subproject commit 769a842ba5064d91d893373ba493710fea9958a5 From 69cb7a9c6a084a26cbeeda927701b7a923d182bb Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:35:18 +0000 Subject: [PATCH 115/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 769a842ba5..eb0d5d633d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 769a842ba5064d91d893373ba493710fea9958a5 +Subproject commit eb0d5d633d74b77ab6287393d61ea866b190b814 From e274bc76eb8ef2f1d1937e6e1fee2dca4a5ec695 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 03:59:31 -0500 Subject: [PATCH 116/128] prevent out of bound color values in `doSetTile_char` some Lua code is leaking a bg of -1 into this function, resulting in an out of bounds reference to `uccolor`. this logic prohibits such out of bound reads the upstream problem still needs to be found --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 3 +++ .../widgets/text_area/text_area_content.lua | 2 +- library/modules/Screen.cpp | 19 +++++++++++-------- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 14efc37d1a..49518fdcb4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- a safety check was added to ``Screen::doSetTile_char`` fur out of bound pen color values ## Misc Improvements diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index f729be2743..e8a1cd9f67 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3754,6 +3754,9 @@ environment by the mandatory init file dfhack.lua: ``COLOR_GREY`` and ``COLOR_DARKGREY`` can also be spelled ``COLOR_GRAY`` and ``COLOR_DARKGRAY``. + Note: ``COLOR_RESET`` is not valid in a `Pen `, and using it in a Pen color field + will result in runtime warnings and may result in color flashing or other unexpected results. + * State change event codes, used by ``dfhack.onStateChange`` Available only in the `core context `, as is the event itself: 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 83099fac76..cd35f751ee 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -37,7 +37,7 @@ function TextAreaContent:init() self.cursor = nil self.main_pen = dfhack.pen.parse({ - bg=COLOR_RESET, + bg=COLOR_BLACK, bold=true }, self.text_pen) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 338a66925e..c5451ce806 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -54,6 +54,8 @@ distribution. #include "df/renderer.h" #include "df/plant.h" +#include +#include #include #include #include @@ -209,14 +211,15 @@ static bool doSetTile_char(const Pen &pen, int x, int y, bool use_graphics) *texpos_lower = df::global::init->texpos_border_interior; // basic black background } - auto rgb_fg = &gps->uccolor[fg][0]; - auto rgb_bg = &gps->uccolor[bg][0]; - screen[1] = rgb_fg[0]; - screen[2] = rgb_fg[1]; - screen[3] = rgb_fg[2]; - screen[4] = rgb_bg[0]; - screen[5] = rgb_bg[1]; - screen[6] = rgb_bg[2]; + if (fg >= 0 && fg <= COLOR_MAX) + std::ranges::copy(gps->uccolor[fg], &screen[1]); + else + WARN(screen).print("in doSetTile_char, fg {} out of range\n", fg); + + if (bg >= 0 && bg <= COLOR_MAX) + std::ranges::copy(gps->uccolor[bg], &screen[4]); + else + WARN(screen).print("in doSetTile_char, bg {} out of range\n", bg); return true; } From a33cad6400b4074a248aafcbe094ede011dc6751 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 07:30:51 -0500 Subject: [PATCH 117/128] revise changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 49518fdcb4..9ad9dd18dd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: ## Fixes - a safety check was added to ``Screen::doSetTile_char`` fur out of bound pen color values +- ``TextArea`` widget corrected to use ``COLOR_BLACK`` instead of ``COLOR_RESET`` as default background color ## Misc Improvements From 9b3bfcb8a41fa23a3312f7c89626944070d62017 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:29:57 +0000 Subject: [PATCH 118/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index eb0d5d633d..7549711a99 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit eb0d5d633d74b77ab6287393d61ea866b190b814 +Subproject commit 7549711a993e03bef19e90b27427096c1099853e From 7f26bd9568687e6bedf9b425363686d6741c3983 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:37:19 +0000 Subject: [PATCH 119/128] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 47bacb367d..a79647e433 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 47bacb367d26cadca1a99a1ecd74fde88e4ed1a4 +Subproject commit a79647e4338976351ef469bef30a6af04b3d1502 From d89463c5ef24d6fb7013c83e5cc78ce0fecf967d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 11:46:57 -0500 Subject: [PATCH 120/128] add `std::filesystem::file_time_type` as an opaque type --- library/DataIdentity.cpp | 1 + library/include/DataIdentity.h | 1 + 2 files changed, 2 insertions(+) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index 0a797c2069..111166d90e 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -65,6 +65,7 @@ namespace df { OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(std::filesystem::file_time_type); OPAQUE_IDENTITY_TRAITS(wchar_t*); const buffer_container_identity buffer_container_identity::base_instance; diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index cf486e6188..c48dd299fd 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -632,6 +632,7 @@ namespace df OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(std::filesystem::file_time_type); #ifdef BUILD_DFHACK_LIB template From c3de892042c639d1dc7aa82e8fb9d3b47758c459 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:06:18 +0000 Subject: [PATCH 121/128] Auto-update structures ref for 53.16 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a79647e433..f89b44a8c7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a79647e4338976351ef469bef30a6af04b3d1502 +Subproject commit f89b44a8c76f556060ddbec1238a81fa2db3b1e2 From 9aa84eae2886788fccefdfe296e1a7a05e33f3da Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 12:14:03 -0500 Subject: [PATCH 122/128] Update version number and changelog for 53.16-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 852d21f07d..1d9b17943e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. -set(DF_VERSION "53.15") -set(DFHACK_RELEASE "r3") +set(DF_VERSION "53.16") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ad9dd18dd..a290eee5b5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,10 +59,28 @@ Template for new versions: ## New Features ## Fixes -- a safety check was added to ``Screen::doSetTile_char`` fur out of bound pen color values + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.16-r1 + +## New Tools + +## New Features + +## Fixes - ``TextArea`` widget corrected to use ``COLOR_BLACK`` instead of ``COLOR_RESET`` as default background color ## Misc Improvements +- a safety check was added to ``Screen::doSetTile_char`` for out of bound pen color values ## Documentation From c80747da32aad3bdd5915c60105136d6558aca39 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 12:19:13 -0500 Subject: [PATCH 123/128] update xml for 53.16-r1 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f89b44a8c7..1dd01aad64 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f89b44a8c76f556060ddbec1238a81fa2db3b1e2 +Subproject commit 1dd01aad64219afa0578f1328cf15bd0c6006d5a From 3bde79692a4cd5d8c10ba063ccad0030728538a7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 12:41:23 -0500 Subject: [PATCH 124/128] Update download-df.sh bay12 no longer publishes a "small" download for windows so don't attempt to download it for testing --- ci/download-df.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/download-df.sh b/ci/download-df.sh index ed8b373a25..a07f75f2a1 100755 --- a/ci/download-df.sh +++ b/ci/download-df.sh @@ -14,7 +14,7 @@ fi df_url="https://www.bay12games.com/dwarves/df_${minor}_${patch}" if test "$OS_TARGET" = "windows"; then WGET="C:/msys64/usr/bin/wget.exe" - df_url="${df_url}_win_s.zip" + df_url="${df_url}_win.zip" df_archive_name="df.zip" df_extract_cmd="unzip -d ${DF_FOLDER}" elif test "$OS_TARGET" = "ubuntu"; then From 33a04e695df47181f7dc554ef2225f497fbd097b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 7 Aug 2026 23:18:35 -0500 Subject: [PATCH 125/128] fix incorrect use of `std::extent` in `stockpiles` unintended consequence of #5847 --- docs/changelog.txt | 1 + plugins/stockpiles/StockpileSerializer.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index a290eee5b5..6dbc91f6e2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `stockpiles` will no longer incorrectly size the "rough gem" and "cut gem" vectors, which avoids a crash when viewing stockpile settings. This potentially affects anything that uses blueprints to create a stockpile, including `gui/quantum` ## Misc Improvements diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 6e90cb797e..f1b1a3bd3e 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -1984,24 +1984,24 @@ void StockpileSettingsSerializer::read_gems(color_ostream& out, DeserializeMode std::bind(&StockpileSettings::gems, mBuffer), mSettings->flags.whole, mSettings->flags.mask_gems, - [&]() { + [&] () { pgems.cut_other_mats.clear(); pgems.cut_mats.clear(); pgems.rough_other_mats.clear(); pgems.rough_mats.clear(); }, - [&](bool all, char val) { - auto & bgems = mBuffer.gems(); + [&] (bool all, char val) { + auto& bgems = mBuffer.gems(); unserialize_list_material(out, "mats/rough", all, val, filters, gem_mat_is_allowed, - [&](const size_t& idx) -> const string& { return bgems.rough_mats(idx); }, + [&] (const size_t& idx) -> const string& { return bgems.rough_mats(idx); }, bgems.rough_mats_size(), pgems.rough_mats); unserialize_list_material(out, "mats/cut", all, val, filters, gem_cut_mat_is_allowed, - [&](const size_t& idx) -> const string& { return bgems.cut_mats(idx); }, + [&] (const size_t& idx) -> const string& { return bgems.cut_mats(idx); }, bgems.cut_mats_size(), pgems.cut_mats); - const size_t builtin_size = std::extentraws.mat_table.builtin)>::value; + const size_t builtin_size = world->raws.mat_table.builtin.size(); pgems.rough_other_mats.resize(builtin_size, '\0'); pgems.cut_other_mats.resize(builtin_size, '\0'); if (all) { From b638b59d0876d9bdf8b5f97e52714206ab7f3266 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 7 Aug 2026 23:37:03 -0500 Subject: [PATCH 126/128] CMakeLists and changelog for 53.16-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d9b17943e..1495bea2b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. set(DF_VERSION "53.16") -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 6dbc91f6e2..904fcc86fb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.16-r1.1 + +## New Tools + +## New Features + ## Fixes - `stockpiles` will no longer incorrectly size the "rough gem" and "cut gem" vectors, which avoids a crash when viewing stockpile settings. This potentially affects anything that uses blueprints to create a stockpile, including `gui/quantum` From 6516229e6e0f23e433dac1a08319b809c4fe660b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 18 Aug 2026 07:38:27 +0000 Subject: [PATCH 127/128] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 7549711a99..4ac3e364e7 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 7549711a993e03bef19e90b27427096c1099853e +Subproject commit 4ac3e364e7f0a0b1e23df7c4bb50998d172e61c6 From 6327518b43a89e20783a54da0c168529f30e9cce Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:45:05 +0000 Subject: [PATCH 128/128] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 1dd01aad64..86441613f9 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1dd01aad64219afa0578f1328cf15bd0c6006d5a +Subproject commit 86441613f95908af833a8416ba80371314d749c2