From 12ae6dd2b0f292831ef95d9bdb956b31f0c25baf Mon Sep 17 00:00:00 2001 From: Alex Noir Date: Fri, 14 Jun 2024 00:09:27 +0300 Subject: [PATCH 001/333] 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/333] 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/333] 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 71a1a32972a92fd97e7487cf40f18f07d15a43e6 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 2 Dec 2025 20:55:00 +0100 Subject: [PATCH 004/333] orders: add search overlay for manager orders Adds search overlay to find and navigate manager orders with arrow indicators showing current search result. Search uses Alt+S to focus, Alt+P/N for prev/next navigation. Overlays are disabled by default. --- docs/changelog.txt | 1 + plugins/lua/orders.lua | 398 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 398 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a..54e33d712d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators ## Fixes diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 2774bd80ee..c99e24fd21 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -6,6 +6,11 @@ local overlay = require('plugins.overlay') local textures = require('gui.textures') local utils = require('utils') local widgets = require('gui.widgets') +local stockflow = reqscript('internal/quickfort/stockflow') + +-- Shared state for search cursor visibility +local search_cursor_visible = false +local search_last_scroll_position = -1 -- -- OrdersOverlay @@ -74,7 +79,7 @@ local mi = df.global.game.main_interface OrdersOverlay = defclass(OrdersOverlay, overlay.OverlayWidget) OrdersOverlay.ATTRS{ desc='Adds import, export, and other functions to the manager orders screen.', - default_pos={x=53,y=-6}, + default_pos={x=41,y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=43, h=4}, @@ -709,11 +714,401 @@ function QuantityRightClickOverlay:onInput(keys) end end +-- +-- OrdersSearchOverlay +-- + +local search_cursor_visible = false +local search_last_scroll_position = -1 + +local function make_order_key(order) + local mat_cat_str = '' + if order.material_category then + local keys = {} + for k in pairs(order.material_category) do + if type(k) == 'string' then + table.insert(keys, k) + end + end + table.sort(keys) + for _, k in ipairs(keys) do + mat_cat_str = mat_cat_str .. k .. '=' .. tostring(order.material_category[k]) .. ';' + end + end + + local encrust_str = '' + if order.specflag and order.specflag.encrust_flags then + local flags = {'finished_goods', 'furniture', 'ammo'} + for _, flag in ipairs(flags) do + if order.specflag.encrust_flags[flag] then + encrust_str = encrust_str .. flag .. ';' + end + end + end + + return string.format('%d:%d:%d:%d:%d:%s:%s:%s', + order.job_type, + order.item_type, + order.item_subtype, + order.mat_type, + order.mat_index, + order.reaction_name or '', + mat_cat_str, + encrust_str) +end + +local function build_reaction_map() + local map = {} + local reactions = stockflow.collect_reactions() + + for _, reaction in ipairs(reactions) do + local key = make_order_key(reaction.order) + map[key] = reaction.name:lower() + end + + return map +end + +local reaction_map_cache = nil + +local function get_cached_reaction_map() + if not reaction_map_cache then + reaction_map_cache = build_reaction_map() + end + return reaction_map_cache +end + +local function get_order_search_key(order) + local reaction_map = get_cached_reaction_map() + local key = make_order_key(order) + if reaction_map[key] then + return reaction_map[key] + end + return "" +end + +local function matches_all_search_words(search_key, filter_text) + local search_words = {} + for word in filter_text:gmatch('%S+') do + table.insert(search_words, word) + end + + -- Check if all search words are found in search_key (order-independent) + for _, search_word in ipairs(search_words) do + if not search_key:find(search_word, 1, true) then + return false + end + end + return true +end + +OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) +OrdersSearchOverlay.ATTRS{ + desc='Adds a search box to find and navigate to matching manager orders.', + default_pos={x=85, y=-6}, + default_enabled=false, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', + frame={w=34, h=4}, +} + +function OrdersSearchOverlay:init() + local main_panel = widgets.Panel{ + view_id='main_panel', + frame={t=0, l=0, r=0, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + frame_title='Search', + visible=function() return not self.minimized end, + subviews={ + widgets.EditField{ + view_id='filter', + frame={t=0, l=0}, + key='CUSTOM_ALT_S', + on_change=self:callback('update_filter'), + }, + widgets.HotkeyLabel{ + view_id='prev_match', + frame={t=1, l=0}, + label='prev', + key='CUSTOM_ALT_P', + auto_width=true, + on_activate=self:callback('jump_to_previous_match'), + enabled=function() return self:has_matches() end, + }, + widgets.HotkeyLabel{ + view_id='next_match', + frame={t=1, l=17}, + label='next', + key='CUSTOM_ALT_N', + auto_width=true, + on_activate=self:callback('jump_to_next_match'), + enabled=function() return self:has_matches() end, + }, + }, + } + + local minimized_panel = widgets.Panel{ + frame={t=0, r=0, w=3, h=1}, + subviews={ + widgets.Label{ + frame={t=0, l=0, w=1, h=1}, + text='[', + text_pen=COLOR_RED, + visible=function() return self.minimized end, + }, + widgets.Label{ + frame={t=0, l=1, w=1, h=1}, + text={{text=function() return self.minimized and string.char(31) or string.char(30) end}}, + text_pen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_GREY}, + text_hpen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_WHITE}, + on_click=function() self.minimized = not self.minimized end, + }, + widgets.Label{ + frame={t=0, r=0, w=1, h=1}, + text=']', + text_pen=COLOR_RED, + visible=function() return self.minimized end, + }, + }, + } + + self:addviews{ + main_panel, + minimized_panel, + } + + -- Initialize search state + self.filter_text = '' + self.matched_indices = {} + self.current_match_idx = 0 + self.minimized = false +end + +function OrdersSearchOverlay:update_filter(text) + self.filter_text = text:lower() + self.matched_indices = {} + self.current_match_idx = 0 + + if self.filter_text == '' then + self.subviews.main_panel.frame_title = 'Search' + return + end + + local orders = df.global.world.manager_orders.all + for i = 0, #orders - 1 do + local order = orders[i] + local search_key = get_order_search_key(order) + if matches_all_search_words(search_key, self.filter_text) then + table.insert(self.matched_indices, i) + end + end + + self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() +end + +function OrdersSearchOverlay:jump_to_next_match() + if #self.matched_indices == 0 then return end + + self.current_match_idx = self.current_match_idx + 1 + if self.current_match_idx > #self.matched_indices then + self.current_match_idx = 1 + end + + local order_idx = self.matched_indices[self.current_match_idx] + mi.info.work_orders.scroll_position_work_orders = order_idx + search_last_scroll_position = order_idx + search_cursor_visible = true + + self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() +end + +function OrdersSearchOverlay:jump_to_previous_match() + if #self.matched_indices == 0 then return end + + self.current_match_idx = self.current_match_idx - 1 + if self.current_match_idx < 1 then + self.current_match_idx = #self.matched_indices + end + + local order_idx = self.matched_indices[self.current_match_idx] + mi.info.work_orders.scroll_position_work_orders = order_idx + search_last_scroll_position = order_idx + search_cursor_visible = true + + self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() +end + +function OrdersSearchOverlay:get_match_text() + if self.filter_text == '' then + return '' + end + + local total_matches = #self.matched_indices + + if self.current_match_idx == 0 then + return string.format('%d matches', total_matches) + end + + return string.format('%d of %d', self.current_match_idx, total_matches) +end + +function OrdersSearchOverlay:has_matches() + return #self.matched_indices > 0 +end + +local function is_mouse_key(keys) + return keys._MOUSE_L + or keys._MOUSE_R + or keys._MOUSE_M + or keys.CONTEXT_SCROLL_UP + or keys.CONTEXT_SCROLL_DOWN + or keys.CONTEXT_SCROLL_PAGEUP + or keys.CONTEXT_SCROLL_PAGEDOWN +end + +function OrdersSearchOverlay:onInput(keys) + local filter_field = self.subviews.filter + if not filter_field then return false end + + -- Unfocus search on right-click + if keys._MOUSE_R and filter_field.focus then + filter_field:setFocus(false) + return true + end + + -- Let parent handle input first (for HotkeyLabel clicks and widget interactions) + if OrdersSearchOverlay.super.onInput(self, keys) then + return true + end + + -- Unfocus search on left-click when focused (for workshop and number of times changes) + -- And let the click pass through + if keys._MOUSE_L and filter_field.focus then + filter_field:setFocus(false) + return false + end + + -- Only consume input if search field has focus and it's not a mouse key + -- This allows scrolling, navigation, and mouse interaction in the orders list + if filter_field.focus and not is_mouse_key(keys) then + return true + end + + return false +end + +-- ------------------- +-- OrderHighlightOverlay +-- ------------------- + +OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) +OrderHighlightOverlay.ATTRS{ + desc='Shows arrows next to the work order found by orders.search', + default_enabled=false, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', + frame={w=80, h=3}, +} + +function OrderHighlightOverlay:init() + self.ORDER_HEIGHT = 3 + self.TABS_WIDTH_THRESHOLD = 155 + self.LIST_START_Y_ONE_TABS_ROW = 8 + self.LIST_START_Y_TWO_TABS_ROWS = 10 + self.BOTTOM_MARGIN = 9 + self.ARROW_X_FIRST = 5 + self.ARROW_X_SECOND = 6 + self.ARROW_CHAR = '>' + + self.cached_list_start_y = nil + self.cached_viewport_size = nil + self.cached_screen_width = nil + self.cached_screen_height = nil +end + +function OrderHighlightOverlay:getListStartY() + local rect = gui.get_interface_rect() + + if rect.width >= self.TABS_WIDTH_THRESHOLD then + return self.LIST_START_Y_ONE_TABS_ROW + else + return self.LIST_START_Y_TWO_TABS_ROWS + end +end + +function OrderHighlightOverlay:getViewportSize() + local rect = gui.get_interface_rect() + local list_start_y = self:getListStartY() + + local available_height = rect.height - list_start_y - self.BOTTOM_MARGIN + return math.floor(available_height / self.ORDER_HEIGHT) +end + +function OrderHighlightOverlay:calculateSelectedOrderY() + local orders = df.global.world.manager_orders.all + local scroll_pos = mi.info.work_orders.scroll_position_work_orders + + if #orders == 0 or scroll_pos < 0 or scroll_pos >= #orders then + return nil + end + + local list_start_y = self:getListStartY() + local viewport_size = self:getViewportSize() + + local viewport_start = scroll_pos + local viewport_end = scroll_pos + viewport_size - 1 + + -- Selected order tries to be at the top unless we're at the end of the list + if viewport_end >= #orders then + viewport_end = #orders - 1 + viewport_start = math.max(0, viewport_end - viewport_size + 1) + end + + local pos_in_viewport = scroll_pos - viewport_start + + local selected_y = list_start_y + (pos_in_viewport * self.ORDER_HEIGHT) + + return selected_y +end + +function OrderHighlightOverlay:render(dc) + if search_cursor_visible then + local current_scroll = mi.info.work_orders.scroll_position_work_orders + + -- Hide cursor when user manually scrolls + if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then + search_cursor_visible = false + end + search_last_scroll_position = current_scroll + end + + OrderHighlightOverlay.super.render(self, dc) +end + +function OrderHighlightOverlay:onRenderFrame(dc, rect) + OrderHighlightOverlay.super.onRenderFrame(self, dc, rect) + + if not search_cursor_visible then return end + + local selected_y = self:calculateSelectedOrderY() + if not selected_y then return end + + local highlight_pen = dfhack.pen.parse{ + fg=COLOR_LIGHTGREEN, + bold=true, + } + + local y = selected_y + 1 -- Middle line of the 3-line order + dc:seek(self.ARROW_X_FIRST, y):string(self.ARROW_CHAR, highlight_pen) + dc:seek(self.ARROW_X_SECOND, y):string(self.ARROW_CHAR, highlight_pen) +end + -- ------------------- OVERLAY_WIDGETS = { recheck=RecheckOverlay, importexport=OrdersOverlay, + search=OrdersSearchOverlay, + highlight=OrderHighlightOverlay, skillrestrictions=SkillRestrictionOverlay, laborrestrictions=LaborRestrictionsOverlay, conditionsrightclick=ConditionsRightClickOverlay, @@ -722,3 +1117,4 @@ OVERLAY_WIDGETS = { } return _ENV + From cfaa14062102150eb59e6be6d5b338c3941896db Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 2 Dec 2025 21:39:56 +0100 Subject: [PATCH 005/333] Fix trailing whitespace --- plugins/lua/orders.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index c99e24fd21..0095fe8c27 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1056,7 +1056,7 @@ function OrderHighlightOverlay:calculateSelectedOrderY() local viewport_start = scroll_pos local viewport_end = scroll_pos + viewport_size - 1 - + -- Selected order tries to be at the top unless we're at the end of the list if viewport_end >= #orders then viewport_end = #orders - 1 @@ -1116,5 +1116,4 @@ OVERLAY_WIDGETS = { quantityrightclick=QuantityRightClickOverlay, } -return _ENV - +return _ENV \ No newline at end of file From 16ab556bb04bfb1558a70ad48d16e069b4f31a35 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 2 Dec 2025 21:45:39 +0100 Subject: [PATCH 006/333] Newline at the end --- 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 0095fe8c27..22236e4769 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1116,4 +1116,4 @@ OVERLAY_WIDGETS = { quantityrightclick=QuantityRightClickOverlay, } -return _ENV \ No newline at end of file +return _ENV From e36860793be04b18d5612cbdff1b9d495b34cacf Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 17:49:00 +0100 Subject: [PATCH 007/333] orders: load cache reaction map on init --- plugins/lua/orders.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 22236e4769..ef78d5b303 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -758,6 +758,11 @@ local function make_order_key(order) end local function build_reaction_map() + local can_read_stockflow = dfhack.isWorldLoaded() and dfhack.isMapLoaded() + if not can_read_stockflow then + return nil + end + local map = {} local reactions = stockflow.collect_reactions() @@ -812,6 +817,8 @@ OrdersSearchOverlay.ATTRS{ } function OrdersSearchOverlay:init() + get_cached_reaction_map() + local main_panel = widgets.Panel{ view_id='main_panel', frame={t=0, l=0, r=0, h=4}, From b46eb7fa34925f3691df03faa26fefbf3e25485d Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:05:07 +0100 Subject: [PATCH 008/333] Add Enter/Shift+Enter navigation and refactor jump to match --- plugins/lua/orders.lua | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index ef78d5b303..31b9f76cbb 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -832,23 +832,23 @@ function OrdersSearchOverlay:init() frame={t=0, l=0}, key='CUSTOM_ALT_S', on_change=self:callback('update_filter'), + on_submit=self:callback('on_submit'), + on_submit2=self:callback('on_submit2'), }, widgets.HotkeyLabel{ - view_id='prev_match', frame={t=1, l=0}, label='prev', key='CUSTOM_ALT_P', auto_width=true, - on_activate=self:callback('jump_to_previous_match'), + on_activate=self:callback('cycle_match', -1), enabled=function() return self:has_matches() end, }, widgets.HotkeyLabel{ - view_id='next_match', frame={t=1, l=17}, label='next', key='CUSTOM_ALT_N', auto_width=true, - on_activate=self:callback('jump_to_next_match'), + on_activate=self:callback('cycle_match', 1), enabled=function() return self:has_matches() end, }, }, @@ -913,27 +913,23 @@ function OrdersSearchOverlay:update_filter(text) self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() end -function OrdersSearchOverlay:jump_to_next_match() - if #self.matched_indices == 0 then return end - - self.current_match_idx = self.current_match_idx + 1 - if self.current_match_idx > #self.matched_indices then - self.current_match_idx = 1 - end - - local order_idx = self.matched_indices[self.current_match_idx] - mi.info.work_orders.scroll_position_work_orders = order_idx - search_last_scroll_position = order_idx - search_cursor_visible = true +function OrdersSearchOverlay:on_submit() + self:cycle_match(1) + self.subviews.filter:setFocus(true) +end - self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() +function OrdersSearchOverlay:on_submit2() + self:cycle_match(-1) + self.subviews.filter:setFocus(true) end -function OrdersSearchOverlay:jump_to_previous_match() +function OrdersSearchOverlay:cycle_match(direction) if #self.matched_indices == 0 then return end - self.current_match_idx = self.current_match_idx - 1 - if self.current_match_idx < 1 then + self.current_match_idx = self.current_match_idx + direction + if direction > 0 and self.current_match_idx > #self.matched_indices then + self.current_match_idx = 1 + elseif direction < 0 and self.current_match_idx < 1 then self.current_match_idx = #self.matched_indices end From cde147713f745c1130f2eb135b9a23e60422dfd5 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:09:30 +0100 Subject: [PATCH 009/333] Hide search highlight when filter text changes --- plugins/lua/orders.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 31b9f76cbb..a4672eeeda 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -895,6 +895,7 @@ function OrdersSearchOverlay:update_filter(text) self.filter_text = text:lower() self.matched_indices = {} self.current_match_idx = 0 + search_cursor_visible = false if self.filter_text == '' then self.subviews.main_panel.frame_title = 'Search' From 7fa03fb4c200ded3cee46089e8cbf2bee3945bdc Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:18:06 +0100 Subject: [PATCH 010/333] Use full_interface for OrderHighlightOverlay to prevent repositioning --- 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 a4672eeeda..e4cb70e0f6 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1010,7 +1010,7 @@ OrderHighlightOverlay.ATTRS{ desc='Shows arrows next to the work order found by orders.search', default_enabled=false, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - frame={w=80, h=3}, + full_interface=true, } function OrderHighlightOverlay:init() From c163d3c159137f8584355e0dbc13cbc7aeec392c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:35:18 +0100 Subject: [PATCH 011/333] Hide highlight when order list changes --- plugins/lua/orders.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index e4cb70e0f6..7692529440 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -11,6 +11,7 @@ local stockflow = reqscript('internal/quickfort/stockflow') -- Shared state for search cursor visibility local search_cursor_visible = false local search_last_scroll_position = -1 +local order_count_at_highlight = 0 -- -- OrdersOverlay @@ -938,6 +939,7 @@ function OrdersSearchOverlay:cycle_match(direction) mi.info.work_orders.scroll_position_work_orders = order_idx search_last_scroll_position = order_idx search_cursor_visible = true + order_count_at_highlight = #df.global.world.manager_orders.all self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() end @@ -1077,12 +1079,18 @@ end function OrderHighlightOverlay:render(dc) if search_cursor_visible then local current_scroll = mi.info.work_orders.scroll_position_work_orders + local current_order_count = #df.global.world.manager_orders.all -- Hide cursor when user manually scrolls if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then search_cursor_visible = false end search_last_scroll_position = current_scroll + + -- Hide cursor when order list changes (orders added or removed) + if order_count_at_highlight ~= current_order_count then + search_cursor_visible = false + end end OrderHighlightOverlay.super.render(self, dc) From f37e46adec9964b48bc8ada72e298c538b90be5a Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:47:35 +0100 Subject: [PATCH 012/333] Consolidate onRenderFrame into render method --- plugins/lua/orders.lua | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 7692529440..eb2b4207b5 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1085,33 +1085,25 @@ function OrderHighlightOverlay:render(dc) if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then search_cursor_visible = false end - search_last_scroll_position = current_scroll -- Hide cursor when order list changes (orders added or removed) if order_count_at_highlight ~= current_order_count then search_cursor_visible = false end - end - - OrderHighlightOverlay.super.render(self, dc) -end - -function OrderHighlightOverlay:onRenderFrame(dc, rect) - OrderHighlightOverlay.super.onRenderFrame(self, dc, rect) - - if not search_cursor_visible then return end - local selected_y = self:calculateSelectedOrderY() - if not selected_y then return end - - local highlight_pen = dfhack.pen.parse{ - fg=COLOR_LIGHTGREEN, - bold=true, - } + -- Draw highlight arrows + local selected_y = self:calculateSelectedOrderY() + if selected_y then + local highlight_pen = dfhack.pen.parse{ + fg=COLOR_LIGHTGREEN, + bold=true, + } - local y = selected_y + 1 -- Middle line of the 3-line order - dc:seek(self.ARROW_X_FIRST, y):string(self.ARROW_CHAR, highlight_pen) - dc:seek(self.ARROW_X_SECOND, y):string(self.ARROW_CHAR, highlight_pen) + local y = selected_y + 1 -- Middle line of the 3-line order + dc:seek(self.ARROW_X_FIRST, y):string(self.ARROW_CHAR, highlight_pen) + dc:seek(self.ARROW_X_SECOND, y):string(self.ARROW_CHAR, highlight_pen) + end + end end -- ------------------- From 506ca40b1166657cff24c07e46305bdf198ece40 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 20:09:09 +0100 Subject: [PATCH 013/333] Use utils.search_text instead of custom search --- plugins/lua/orders.lua | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index eb2b4207b5..28d16068d8 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -793,21 +793,6 @@ local function get_order_search_key(order) return "" end -local function matches_all_search_words(search_key, filter_text) - local search_words = {} - for word in filter_text:gmatch('%S+') do - table.insert(search_words, word) - end - - -- Check if all search words are found in search_key (order-independent) - for _, search_word in ipairs(search_words) do - if not search_key:find(search_word, 1, true) then - return false - end - end - return true -end - OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', @@ -886,19 +871,17 @@ function OrdersSearchOverlay:init() } -- Initialize search state - self.filter_text = '' self.matched_indices = {} self.current_match_idx = 0 self.minimized = false end function OrdersSearchOverlay:update_filter(text) - self.filter_text = text:lower() self.matched_indices = {} self.current_match_idx = 0 search_cursor_visible = false - if self.filter_text == '' then + if text == '' then self.subviews.main_panel.frame_title = 'Search' return end @@ -907,7 +890,7 @@ function OrdersSearchOverlay:update_filter(text) for i = 0, #orders - 1 do local order = orders[i] local search_key = get_order_search_key(order) - if matches_all_search_words(search_key, self.filter_text) then + if search_key and utils.search_text(search_key, text) then table.insert(self.matched_indices, i) end end @@ -945,12 +928,12 @@ function OrdersSearchOverlay:cycle_match(direction) end function OrdersSearchOverlay:get_match_text() - if self.filter_text == '' then + local total_matches = #self.matched_indices + + if total_matches == 0 then return '' end - local total_matches = #self.matched_indices - if self.current_match_idx == 0 then return string.format('%d matches', total_matches) end From 7fabf2b8b887d795ca2f1c6d3c3e5d057e29e325 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 20:13:46 +0100 Subject: [PATCH 014/333] Force new version of overlay position --- plugins/lua/orders.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 28d16068d8..db2a187f42 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -84,6 +84,7 @@ OrdersOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=43, h=4}, + version=1, } function OrdersOverlay:init() From 2cbf2d7860b09b730f9853c88c50ec55e18452db Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 20:24:51 +0100 Subject: [PATCH 015/333] Narrow search overlay and adjust button positions Now there is 16 visible chars in search --- plugins/lua/orders.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index db2a187f42..c2600ce5b0 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -800,7 +800,7 @@ OrdersSearchOverlay.ATTRS{ default_pos={x=85, y=-6}, default_enabled=false, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - frame={w=34, h=4}, + frame={w=26, h=4}, } function OrdersSearchOverlay:init() @@ -831,7 +831,7 @@ function OrdersSearchOverlay:init() enabled=function() return self:has_matches() end, }, widgets.HotkeyLabel{ - frame={t=1, l=17}, + frame={t=1, l=12}, label='next', key='CUSTOM_ALT_N', auto_width=true, From 1f2705d500ba045cc2aef8d3f3080e81bd8b540b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 21:31:49 +0100 Subject: [PATCH 016/333] Reshape arrow and contrasting colors --- plugins/lua/orders.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index c2600ce5b0..0df4e06e98 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1005,9 +1005,9 @@ function OrderHighlightOverlay:init() self.LIST_START_Y_ONE_TABS_ROW = 8 self.LIST_START_Y_TWO_TABS_ROWS = 10 self.BOTTOM_MARGIN = 9 - self.ARROW_X_FIRST = 5 - self.ARROW_X_SECOND = 6 - self.ARROW_CHAR = '>' + self.ARROW_X = 10 + self.ARROW_FG = COLOR_BLACK + self.ARROW_BG = COLOR_WHITE self.cached_list_start_y = nil self.cached_viewport_size = nil @@ -1079,13 +1079,14 @@ function OrderHighlightOverlay:render(dc) local selected_y = self:calculateSelectedOrderY() if selected_y then local highlight_pen = dfhack.pen.parse{ - fg=COLOR_LIGHTGREEN, + fg=self.ARROW_FG, + bg=self.ARROW_BG, bold=true, } - local y = selected_y + 1 -- Middle line of the 3-line order - dc:seek(self.ARROW_X_FIRST, y):string(self.ARROW_CHAR, highlight_pen) - dc:seek(self.ARROW_X_SECOND, y):string(self.ARROW_CHAR, highlight_pen) + dc:seek(self.ARROW_X, selected_y):string('|', highlight_pen) + dc:seek(self.ARROW_X, selected_y + 1):string('>', highlight_pen) + dc:seek(self.ARROW_X, selected_y + 2):string('|', highlight_pen) end end end From 93f6b1c28d51be3d6b9858367e1bd1b46312c69f Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 7 Dec 2025 10:27:47 +0100 Subject: [PATCH 017/333] Hide overlay when job_details is open. Add author --- docs/about/Authors.rst | 1 + plugins/lua/orders.lua | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 8743b75a19..a154d314b6 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -163,6 +163,7 @@ Omniclasm Ong Ying Gao ong-yinggao98 oorzkws oorzkws OwnageIsMagic OwnageIsMagic +pajawojciech pajawojciech palenerd dlmarquis PassionateAngler PassionateAngler Patrik Lundell PatrikLundell diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 0df4e06e98..8cde607826 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -957,6 +957,8 @@ local function is_mouse_key(keys) end function OrdersSearchOverlay:onInput(keys) + if mi.job_details.open then return end + local filter_field = self.subviews.filter if not filter_field then return false end @@ -987,6 +989,11 @@ function OrdersSearchOverlay:onInput(keys) return false end +function OrdersSearchOverlay:render(dc) + if mi.job_details.open then return end + OrdersSearchOverlay.super.render(self, dc) +end + -- ------------------- -- OrderHighlightOverlay -- ------------------- @@ -1061,6 +1068,8 @@ function OrderHighlightOverlay:calculateSelectedOrderY() end function OrderHighlightOverlay:render(dc) + if mi.job_details.open then return end + if search_cursor_visible then local current_scroll = mi.info.work_orders.scroll_position_work_orders local current_order_count = #df.global.world.manager_orders.all From 6bd16adfcff2d9b064e4d901de5d3151fb173205 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 7 Dec 2025 10:31:48 +0100 Subject: [PATCH 018/333] Remove trailing spaces --- 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 8cde607826..0960bfd83d 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -958,7 +958,7 @@ end function OrdersSearchOverlay:onInput(keys) if mi.job_details.open then return end - + local filter_field = self.subviews.filter if not filter_field then return false end From d8eb61e1b8eb0401f00c5a64428137d70867432d Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:19:38 +0100 Subject: [PATCH 019/333] Rebuild manager order search results on every navigation to fix stale results after sorting/clearing/importing orders --- plugins/lua/orders.lua | 58 +++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 0960bfd83d..8641757de6 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -877,14 +877,11 @@ function OrdersSearchOverlay:init() self.minimized = false end -function OrdersSearchOverlay:update_filter(text) - self.matched_indices = {} - self.current_match_idx = 0 - search_cursor_visible = false +function OrdersSearchOverlay:perform_search(text) + local matches = {} if text == '' then - self.subviews.main_panel.frame_title = 'Search' - return + return matches end local orders = df.global.world.manager_orders.all @@ -892,11 +889,23 @@ function OrdersSearchOverlay:update_filter(text) local order = orders[i] local search_key = get_order_search_key(order) if search_key and utils.search_text(search_key, text) then - table.insert(self.matched_indices, i) + table.insert(matches, i) end end - self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() + return matches +end + +function OrdersSearchOverlay:update_filter(text) + self.matched_indices = self:perform_search(text) + self.current_match_idx = 0 + search_cursor_visible = false + + if text == '' then + self.subviews.main_panel.frame_title = 'Search' + else + self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() + end end function OrdersSearchOverlay:on_submit() @@ -910,22 +919,37 @@ function OrdersSearchOverlay:on_submit2() end function OrdersSearchOverlay:cycle_match(direction) - if #self.matched_indices == 0 then return end + local search_text = self.subviews.filter.text + + local new_matches = self:perform_search(search_text) - self.current_match_idx = self.current_match_idx + direction - if direction > 0 and self.current_match_idx > #self.matched_indices then - self.current_match_idx = 1 - elseif direction < 0 and self.current_match_idx < 1 then - self.current_match_idx = #self.matched_indices + if #new_matches == 0 then + self.matched_indices = {} + self.current_match_idx = 0 + search_cursor_visible = false + self.subviews.main_panel.frame_title = 'Search' + return end + local new_match_idx = self.current_match_idx + direction + + if new_match_idx > #new_matches then + new_match_idx = 1 + elseif new_match_idx < 1 then + new_match_idx = #new_matches + end + + self.matched_indices = new_matches + self.current_match_idx = new_match_idx + + -- Scroll to the selected match local order_idx = self.matched_indices[self.current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx search_last_scroll_position = order_idx search_cursor_visible = true order_count_at_highlight = #df.global.world.manager_orders.all - self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() + self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end function OrdersSearchOverlay:get_match_text() @@ -936,10 +960,10 @@ function OrdersSearchOverlay:get_match_text() end if self.current_match_idx == 0 then - return string.format('%d matches', total_matches) + return string.format(': %d matches', total_matches) end - return string.format('%d of %d', self.current_match_idx, total_matches) + return string.format(': %d of %d', self.current_match_idx, total_matches) end function OrdersSearchOverlay:has_matches() From 860a2a1c9d5b50e85d02d6791bfd43f82bb3ab22 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:23:36 +0100 Subject: [PATCH 020/333] Consolidate search state variables in OrdersSearchOverlay section to remove shadowing --- plugins/lua/orders.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 8641757de6..1d9e8659d0 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -8,11 +8,6 @@ local utils = require('utils') local widgets = require('gui.widgets') local stockflow = reqscript('internal/quickfort/stockflow') --- Shared state for search cursor visibility -local search_cursor_visible = false -local search_last_scroll_position = -1 -local order_count_at_highlight = 0 - -- -- OrdersOverlay -- @@ -722,6 +717,7 @@ end local search_cursor_visible = false local search_last_scroll_position = -1 +local order_count_at_highlight = 0 local function make_order_key(order) local mat_cat_str = '' From ab257aa54b8424bb288c153e5b78ba0e89eb581e Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:27:26 +0100 Subject: [PATCH 021/333] Guard get_order_search_key against nil reaction map and return nil for missing entries --- plugins/lua/orders.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 1d9e8659d0..a47574c96b 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -783,11 +783,11 @@ end local function get_order_search_key(order) local reaction_map = get_cached_reaction_map() - local key = make_order_key(order) - if reaction_map[key] then - return reaction_map[key] + if not reaction_map then + return nil end - return "" + local key = make_order_key(order) + return reaction_map[key] end OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) From 3a727a097a97294cbf1301059946fabaf7bf3a3f Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:29:37 +0100 Subject: [PATCH 022/333] Remove unused cached variables from OrderHighlightOverlay init --- plugins/lua/orders.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index a47574c96b..f8da92677e 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1035,11 +1035,6 @@ function OrderHighlightOverlay:init() self.ARROW_X = 10 self.ARROW_FG = COLOR_BLACK self.ARROW_BG = COLOR_WHITE - - self.cached_list_start_y = nil - self.cached_viewport_size = nil - self.cached_screen_width = nil - self.cached_screen_height = nil end function OrderHighlightOverlay:getListStartY() From d3d7067d1378e0e924849d7c8678c2fdcf5a95f4 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:33:42 +0100 Subject: [PATCH 023/333] Convert OrderHighlightOverlay constants from instance fields to module-locals and inline arrow colors --- plugins/lua/orders.lua | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index f8da92677e..dcfd92d4e5 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1018,6 +1018,13 @@ end -- OrderHighlightOverlay -- ------------------- +local ORDER_HEIGHT = 3 +local TABS_WIDTH_THRESHOLD = 155 +local LIST_START_Y_ONE_TABS_ROW = 8 +local LIST_START_Y_TWO_TABS_ROWS = 10 +local BOTTOM_MARGIN = 9 +local ARROW_X = 10 + OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) OrderHighlightOverlay.ATTRS{ desc='Shows arrows next to the work order found by orders.search', @@ -1026,24 +1033,13 @@ OrderHighlightOverlay.ATTRS{ full_interface=true, } -function OrderHighlightOverlay:init() - self.ORDER_HEIGHT = 3 - self.TABS_WIDTH_THRESHOLD = 155 - self.LIST_START_Y_ONE_TABS_ROW = 8 - self.LIST_START_Y_TWO_TABS_ROWS = 10 - self.BOTTOM_MARGIN = 9 - self.ARROW_X = 10 - self.ARROW_FG = COLOR_BLACK - self.ARROW_BG = COLOR_WHITE -end - function OrderHighlightOverlay:getListStartY() local rect = gui.get_interface_rect() - if rect.width >= self.TABS_WIDTH_THRESHOLD then - return self.LIST_START_Y_ONE_TABS_ROW + if rect.width >= TABS_WIDTH_THRESHOLD then + return LIST_START_Y_ONE_TABS_ROW else - return self.LIST_START_Y_TWO_TABS_ROWS + return LIST_START_Y_TWO_TABS_ROWS end end @@ -1051,8 +1047,8 @@ function OrderHighlightOverlay:getViewportSize() local rect = gui.get_interface_rect() local list_start_y = self:getListStartY() - local available_height = rect.height - list_start_y - self.BOTTOM_MARGIN - return math.floor(available_height / self.ORDER_HEIGHT) + local available_height = rect.height - list_start_y - BOTTOM_MARGIN + return math.floor(available_height / ORDER_HEIGHT) end function OrderHighlightOverlay:calculateSelectedOrderY() @@ -1077,7 +1073,7 @@ function OrderHighlightOverlay:calculateSelectedOrderY() local pos_in_viewport = scroll_pos - viewport_start - local selected_y = list_start_y + (pos_in_viewport * self.ORDER_HEIGHT) + local selected_y = list_start_y + (pos_in_viewport * ORDER_HEIGHT) return selected_y end @@ -1103,14 +1099,14 @@ function OrderHighlightOverlay:render(dc) local selected_y = self:calculateSelectedOrderY() if selected_y then local highlight_pen = dfhack.pen.parse{ - fg=self.ARROW_FG, - bg=self.ARROW_BG, + fg=COLOR_BLACK, + bg=COLOR_WHITE, bold=true, } - dc:seek(self.ARROW_X, selected_y):string('|', highlight_pen) - dc:seek(self.ARROW_X, selected_y + 1):string('>', highlight_pen) - dc:seek(self.ARROW_X, selected_y + 2):string('|', highlight_pen) + dc:seek(ARROW_X, selected_y):string('|', highlight_pen) + dc:seek(ARROW_X, selected_y + 1):string('>', highlight_pen) + dc:seek(ARROW_X, selected_y + 2):string('|', highlight_pen) end end end From 2cd1c6efa7986852aa5e760dbc82283221955493 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:42:01 +0100 Subject: [PATCH 024/333] Use early return guard for search cursor visibility in OrderHighlightOverlay render. Return when disable cursor --- plugins/lua/orders.lua | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index dcfd92d4e5..e11d11c9ef 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1079,35 +1079,35 @@ function OrderHighlightOverlay:calculateSelectedOrderY() end function OrderHighlightOverlay:render(dc) - if mi.job_details.open then return end - - if search_cursor_visible then - local current_scroll = mi.info.work_orders.scroll_position_work_orders - local current_order_count = #df.global.world.manager_orders.all + if mi.job_details.open or not search_cursor_visible then return end - -- Hide cursor when user manually scrolls - if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then - search_cursor_visible = false - end + local current_scroll = mi.info.work_orders.scroll_position_work_orders + local current_order_count = #df.global.world.manager_orders.all - -- Hide cursor when order list changes (orders added or removed) - if order_count_at_highlight ~= current_order_count then - search_cursor_visible = false - end + -- Hide cursor when user manually scrolls + if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then + search_cursor_visible = false + return + end - -- Draw highlight arrows - local selected_y = self:calculateSelectedOrderY() - if selected_y then - local highlight_pen = dfhack.pen.parse{ - fg=COLOR_BLACK, - bg=COLOR_WHITE, - bold=true, - } + -- Hide cursor when order list changes (orders added or removed) + if order_count_at_highlight ~= current_order_count then + search_cursor_visible = false + return + end - dc:seek(ARROW_X, selected_y):string('|', highlight_pen) - dc:seek(ARROW_X, selected_y + 1):string('>', highlight_pen) - dc:seek(ARROW_X, selected_y + 2):string('|', highlight_pen) - end + -- Draw highlight arrows + local selected_y = self:calculateSelectedOrderY() + if selected_y then + local highlight_pen = dfhack.pen.parse{ + fg=COLOR_BLACK, + bg=COLOR_WHITE, + bold=true, + } + + dc:seek(ARROW_X, selected_y):string('|', highlight_pen) + dc:seek(ARROW_X, selected_y + 1):string('>', highlight_pen) + dc:seek(ARROW_X, selected_y + 2):string('|', highlight_pen) end end From d605165a83d8de5253e48655fac480ee59cfa70c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:46:02 +0100 Subject: [PATCH 025/333] Add unconditional super render call to OrderHighlightOverlay for future compatibility --- plugins/lua/orders.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index e11d11c9ef..67ea8ab8b0 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1079,6 +1079,8 @@ function OrderHighlightOverlay:calculateSelectedOrderY() end function OrderHighlightOverlay:render(dc) + OrderHighlightOverlay.super.render(self, dc) + if mi.job_details.open or not search_cursor_visible then return end local current_scroll = mi.info.work_orders.scroll_position_work_orders From 14f46c897bf3a9c7b8acb2f2a6986bc2af78a59b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:48:54 +0100 Subject: [PATCH 026/333] Inline build_reaction_map into get_cached_reaction_map with early return pattern --- plugins/lua/orders.lua | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 67ea8ab8b0..14e79d9f25 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -755,7 +755,13 @@ local function make_order_key(order) encrust_str) end -local function build_reaction_map() +local reaction_map_cache = nil + +local function get_cached_reaction_map() + if reaction_map_cache then + return reaction_map_cache + end + local can_read_stockflow = dfhack.isWorldLoaded() and dfhack.isMapLoaded() if not can_read_stockflow then return nil @@ -769,15 +775,7 @@ local function build_reaction_map() map[key] = reaction.name:lower() end - return map -end - -local reaction_map_cache = nil - -local function get_cached_reaction_map() - if not reaction_map_cache then - reaction_map_cache = build_reaction_map() - end + reaction_map_cache = map return reaction_map_cache end From 1bdd533b3038fa3fbd189b711b18e840f6e5d8db Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 10:00:37 +0100 Subject: [PATCH 027/333] Free C++ manager_order objects allocated by collect_reactions to prevent memory leak --- plugins/lua/orders.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 14e79d9f25..f0e7bc502e 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -773,7 +773,9 @@ local function get_cached_reaction_map() for _, reaction in ipairs(reactions) do local key = make_order_key(reaction.order) map[key] = reaction.name:lower() + df.delete(reaction.order) end + reactions = nil reaction_map_cache = map return reaction_map_cache From 0645b1241d12bdee3265a6357c3f0388f265824e Mon Sep 17 00:00:00 2001 From: Quietust Date: Wed, 17 Dec 2025 09:27:20 -0600 Subject: [PATCH 028/333] Add drawbridge-tiles tweak for ASCII mode --- docs/changelog.txt | 1 + docs/plugins/tweak.rst | 3 ++ plugins/tweak/tweak.cpp | 3 ++ plugins/tweak/tweaks/drawbridge-tiles.h | 60 +++++++++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 plugins/tweak/tweaks/drawbridge-tiles.h diff --git a/docs/changelog.txt b/docs/changelog.txt index baf28404be..c71c0115c0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: ## New Features - `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status +- `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index ae42aecac7..c369960344 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -45,6 +45,9 @@ Commands Fixes crafted items not wearing out over time (:bug:`6003`). With this tweak, items made from cloth and leather will gain a level of wear every 20 in-game years. +``drawbridge-tiles`` + Makes raising bridges in ASCII mode render with different tiles when they + are raised. ``eggs-fertile`` Displays an indicator on fertile eggs. ``fast-heat`` diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 0e69da9df6..eeed5027a8 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -16,6 +16,7 @@ using namespace DFHack; #include "tweaks/adamantine-cloth-wear.h" #include "tweaks/animaltrap-reuse.h" #include "tweaks/craft-age-wear.h" +#include "tweaks/drawbridge-tiles.h" #include "tweaks/eggs-fertile.h" #include "tweaks/fast-heat.h" #include "tweaks/flask-contents.h" @@ -58,6 +59,8 @@ DFhackCExport command_result plugin_init(color_ostream &out, vectory1; p2 = buf->y2; iy = true; + break; + case 2: // Up + case 3: // Down + p1 = buf->x1; p2 = buf->x2; + break; + default: + // Already ignoring retracting above + return; + } + + int x = 0, y = 0; + if (p1 == p2) + buf->tile[0][0] = tiles[direction][1]; + else for (int p = p1; p <= p2; p++) + { + if (p == p1) + buf->tile[x][y] = tiles[direction][0]; + else if (p == p2) + buf->tile[x][y] = tiles[direction][2]; + else + buf->tile[x][y] = tiles[direction][1]; + if (iy) + y++; + else + x++; + } + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(drawbridge_tiles_hook, drawBuilding); From c4e84d744181f78cc12a6e176888eab8451a5adf Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Wed, 17 Dec 2025 19:23:51 +0100 Subject: [PATCH 029/333] Refactor overlay helper functions to local scope in orders.lua --- plugins/lua/orders.lua | 72 +++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index f0e7bc502e..5d96dcfc35 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -790,6 +790,25 @@ local function get_order_search_key(order) return reaction_map[key] end +local function perform_search(text) + local matches = {} + + if text == '' then + return matches + end + + local orders = df.global.world.manager_orders.all + for i = 0, #orders - 1 do + local order = orders[i] + local search_key = get_order_search_key(order) + if search_key and utils.search_text(search_key, text) then + table.insert(matches, i) + end + end + + return matches +end + OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', @@ -873,27 +892,8 @@ function OrdersSearchOverlay:init() self.minimized = false end -function OrdersSearchOverlay:perform_search(text) - local matches = {} - - if text == '' then - return matches - end - - local orders = df.global.world.manager_orders.all - for i = 0, #orders - 1 do - local order = orders[i] - local search_key = get_order_search_key(order) - if search_key and utils.search_text(search_key, text) then - table.insert(matches, i) - end - end - - return matches -end - function OrdersSearchOverlay:update_filter(text) - self.matched_indices = self:perform_search(text) + self.matched_indices = perform_search(text) self.current_match_idx = 0 search_cursor_visible = false @@ -917,7 +917,7 @@ end function OrdersSearchOverlay:cycle_match(direction) local search_text = self.subviews.filter.text - local new_matches = self:perform_search(search_text) + local new_matches = perform_search(search_text) if #new_matches == 0 then self.matched_indices = {} @@ -1025,15 +1025,7 @@ local LIST_START_Y_TWO_TABS_ROWS = 10 local BOTTOM_MARGIN = 9 local ARROW_X = 10 -OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) -OrderHighlightOverlay.ATTRS{ - desc='Shows arrows next to the work order found by orders.search', - default_enabled=false, - viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - full_interface=true, -} - -function OrderHighlightOverlay:getListStartY() +local function getListStartY() local rect = gui.get_interface_rect() if rect.width >= TABS_WIDTH_THRESHOLD then @@ -1043,15 +1035,15 @@ function OrderHighlightOverlay:getListStartY() end end -function OrderHighlightOverlay:getViewportSize() +local function getViewportSize() local rect = gui.get_interface_rect() - local list_start_y = self:getListStartY() + local list_start_y = getListStartY() local available_height = rect.height - list_start_y - BOTTOM_MARGIN return math.floor(available_height / ORDER_HEIGHT) end -function OrderHighlightOverlay:calculateSelectedOrderY() +local function calculateSelectedOrderY() local orders = df.global.world.manager_orders.all local scroll_pos = mi.info.work_orders.scroll_position_work_orders @@ -1059,8 +1051,8 @@ function OrderHighlightOverlay:calculateSelectedOrderY() return nil end - local list_start_y = self:getListStartY() - local viewport_size = self:getViewportSize() + local list_start_y = getListStartY() + local viewport_size = getViewportSize() local viewport_start = scroll_pos local viewport_end = scroll_pos + viewport_size - 1 @@ -1078,6 +1070,14 @@ function OrderHighlightOverlay:calculateSelectedOrderY() return selected_y end +OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) +OrderHighlightOverlay.ATTRS{ + desc='Shows arrows next to the work order found by orders.search', + default_enabled=false, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', + full_interface=true, +} + function OrderHighlightOverlay:render(dc) OrderHighlightOverlay.super.render(self, dc) @@ -1099,7 +1099,7 @@ function OrderHighlightOverlay:render(dc) end -- Draw highlight arrows - local selected_y = self:calculateSelectedOrderY() + local selected_y = calculateSelectedOrderY() if selected_y then local highlight_pen = dfhack.pen.parse{ fg=COLOR_BLACK, From c474f7555a9afff7a23e627e40385b3069011440 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 30 Dec 2025 11:19:02 +0100 Subject: [PATCH 030/333] Remove order name generation from Lua and use DF button trick to get names --- library/LuaApi.cpp | 2 + library/include/modules/Job.h | 2 + library/modules/Job.cpp | 27 +++++++++++++ plugins/lua/orders.lua | 76 +---------------------------------- 4 files changed, 32 insertions(+), 75 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4875b934c6..d46da3fc91 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -92,6 +92,7 @@ distribution. #include "df/job_item.h" #include "df/job_material_category.h" #include "df/language_word_table.h" +#include "df/manager_order.h" #include "df/material.h" #include "df/map_block.h" #include "df/nemesis_record.h" @@ -2018,6 +2019,7 @@ static const LuaWrapper::FunctionReg dfhack_job_module[] = { WRAPM(Job,isSuitableItem), WRAPM(Job,isSuitableMaterial), WRAPM(Job,getName), + WRAPM(Job,getManagerOrderName), WRAPM(Job,linkIntoWorld), WRAPM(Job,removePostings), WRAPM(Job,disconnectJobItem), diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index 25c357bec7..acb37169c3 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -41,6 +41,7 @@ namespace df struct job_item_filter; struct building; struct unit; + struct manager_order; } namespace DFHack @@ -117,6 +118,7 @@ namespace DFHack int mat_index, df::item_type itype); DFHACK_EXPORT std::string getName(df::job *job); + DFHACK_EXPORT std::string getManagerOrderName(df::manager_order *order); struct JobDeleter { void operator()(df::job *ptr) const { diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 3c70807325..91c16dd37a 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -49,6 +49,7 @@ distribution. #include "df/job_list_link.h" #include "df/job_postingst.h" #include "df/job_restrictionst.h" +#include "df/manager_order.h" #include "df/plotinfost.h" #include "df/specific_ref.h" #include "df/unit.h" @@ -686,3 +687,29 @@ std::string Job::getName(df::job *job) return desc; } + +std::string Job::getManagerOrderName(df::manager_order *order) +{ + CHECK_NULL_POINTER(order); + + std::string desc; + auto button = df::allocate(); + button->mstring = order->reaction_name; + button->specdata.hist_figure_id = order->specdata.hist_figure_id; + button->jobtype = order->job_type; + button->itemtype = order->item_type; + button->subtype = order->item_subtype; + button->material = order->mat_type; + button->matgloss = order->mat_index; + button->specflag = order->specflag; + button->job_item_flag = order->material_category; + button->specdata = order->specdata; + button->art_specifier = order->art_spec.type; + button->art_specifier_id1 = order->art_spec.id; + button->art_specifier_id2 = order->art_spec.subid; + + button->text(&desc); + delete button; + + return desc; +} diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 5d96dcfc35..b78096d32e 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -6,7 +6,6 @@ local overlay = require('plugins.overlay') local textures = require('gui.textures') local utils = require('utils') local widgets = require('gui.widgets') -local stockflow = reqscript('internal/quickfort/stockflow') -- -- OrdersOverlay @@ -719,77 +718,6 @@ local search_cursor_visible = false local search_last_scroll_position = -1 local order_count_at_highlight = 0 -local function make_order_key(order) - local mat_cat_str = '' - if order.material_category then - local keys = {} - for k in pairs(order.material_category) do - if type(k) == 'string' then - table.insert(keys, k) - end - end - table.sort(keys) - for _, k in ipairs(keys) do - mat_cat_str = mat_cat_str .. k .. '=' .. tostring(order.material_category[k]) .. ';' - end - end - - local encrust_str = '' - if order.specflag and order.specflag.encrust_flags then - local flags = {'finished_goods', 'furniture', 'ammo'} - for _, flag in ipairs(flags) do - if order.specflag.encrust_flags[flag] then - encrust_str = encrust_str .. flag .. ';' - end - end - end - - return string.format('%d:%d:%d:%d:%d:%s:%s:%s', - order.job_type, - order.item_type, - order.item_subtype, - order.mat_type, - order.mat_index, - order.reaction_name or '', - mat_cat_str, - encrust_str) -end - -local reaction_map_cache = nil - -local function get_cached_reaction_map() - if reaction_map_cache then - return reaction_map_cache - end - - local can_read_stockflow = dfhack.isWorldLoaded() and dfhack.isMapLoaded() - if not can_read_stockflow then - return nil - end - - local map = {} - local reactions = stockflow.collect_reactions() - - for _, reaction in ipairs(reactions) do - local key = make_order_key(reaction.order) - map[key] = reaction.name:lower() - df.delete(reaction.order) - end - reactions = nil - - reaction_map_cache = map - return reaction_map_cache -end - -local function get_order_search_key(order) - local reaction_map = get_cached_reaction_map() - if not reaction_map then - return nil - end - local key = make_order_key(order) - return reaction_map[key] -end - local function perform_search(text) local matches = {} @@ -800,7 +728,7 @@ local function perform_search(text) local orders = df.global.world.manager_orders.all for i = 0, #orders - 1 do local order = orders[i] - local search_key = get_order_search_key(order) + local search_key = dfhack.job.getManagerOrderName(order) if search_key and utils.search_text(search_key, text) then table.insert(matches, i) end @@ -819,8 +747,6 @@ OrdersSearchOverlay.ATTRS{ } function OrdersSearchOverlay:init() - get_cached_reaction_map() - local main_panel = widgets.Panel{ view_id='main_panel', frame={t=0, l=0, r=0, h=4}, From 99d01265965de91713e23719c7a038ecf35f0f0b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 30 Dec 2025 12:19:50 +0100 Subject: [PATCH 031/333] Enable orders search overlay by default --- plugins/lua/orders.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index b78096d32e..846027ffd5 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -741,7 +741,7 @@ OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', default_pos={x=85, y=-6}, - default_enabled=false, + default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=26, h=4}, } @@ -999,7 +999,7 @@ end OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) OrderHighlightOverlay.ATTRS{ desc='Shows arrows next to the work order found by orders.search', - default_enabled=false, + default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', full_interface=true, } From b9c82f68881e55758ea3ce8e9f9eb750da57fc54 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 30 Dec 2025 13:00:20 +0100 Subject: [PATCH 032/333] Move change in changelog to future --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 67890c55d0..9ca880c308 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators ## Fixes @@ -96,7 +97,6 @@ Template for new versions: - `infinite-sky`: Re-enabled with compatibility with new siege map data. ## New Features -- `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators - `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status ## Fixes From 4b5be796af479619eb13419c18c9f760076bf2cc Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 17:55:41 +0100 Subject: [PATCH 033/333] Highlight all --- plugins/lua/orders.lua | 104 ++++++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 37 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 846027ffd5..d6b7850154 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -717,6 +717,8 @@ end local search_cursor_visible = false local search_last_scroll_position = -1 local order_count_at_highlight = 0 +local search_matched_indices = {} +local search_current_match_idx = 0 local function perform_search(text) local matches = {} @@ -812,15 +814,12 @@ function OrdersSearchOverlay:init() minimized_panel, } - -- Initialize search state - self.matched_indices = {} - self.current_match_idx = 0 self.minimized = false end function OrdersSearchOverlay:update_filter(text) - self.matched_indices = perform_search(text) - self.current_match_idx = 0 + search_matched_indices = perform_search(text) + search_current_match_idx = 0 search_cursor_visible = false if text == '' then @@ -846,14 +845,14 @@ function OrdersSearchOverlay:cycle_match(direction) local new_matches = perform_search(search_text) if #new_matches == 0 then - self.matched_indices = {} - self.current_match_idx = 0 + search_matched_indices = {} + search_current_match_idx = 0 search_cursor_visible = false self.subviews.main_panel.frame_title = 'Search' return end - local new_match_idx = self.current_match_idx + direction + local new_match_idx = search_current_match_idx + direction if new_match_idx > #new_matches then new_match_idx = 1 @@ -861,11 +860,11 @@ function OrdersSearchOverlay:cycle_match(direction) new_match_idx = #new_matches end - self.matched_indices = new_matches - self.current_match_idx = new_match_idx + search_matched_indices = new_matches + search_current_match_idx = new_match_idx -- Scroll to the selected match - local order_idx = self.matched_indices[self.current_match_idx] + local order_idx = search_matched_indices[search_current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx search_last_scroll_position = order_idx search_cursor_visible = true @@ -875,21 +874,21 @@ function OrdersSearchOverlay:cycle_match(direction) end function OrdersSearchOverlay:get_match_text() - local total_matches = #self.matched_indices + local total_matches = #search_matched_indices if total_matches == 0 then return '' end - if self.current_match_idx == 0 then + if search_current_match_idx == 0 then return string.format(': %d matches', total_matches) end - return string.format(': %d of %d', self.current_match_idx, total_matches) + return string.format(': %d of %d', search_current_match_idx, total_matches) end function OrdersSearchOverlay:has_matches() - return #self.matched_indices > 0 + return #search_matched_indices > 0 end local function is_mouse_key(keys) @@ -969,31 +968,43 @@ local function getViewportSize() return math.floor(available_height / ORDER_HEIGHT) end -local function calculateSelectedOrderY() +local function getVisibleOrderIndices() local orders = df.global.world.manager_orders.all local scroll_pos = mi.info.work_orders.scroll_position_work_orders - if #orders == 0 or scroll_pos < 0 or scroll_pos >= #orders then - return nil - end + if #orders == 0 then return 0, -1 end - local list_start_y = getListStartY() local viewport_size = getViewportSize() - local viewport_start = scroll_pos local viewport_end = scroll_pos + viewport_size - 1 - -- Selected order tries to be at the top unless we're at the end of the list + -- Handle end-of-list case if viewport_end >= #orders then viewport_end = #orders - 1 viewport_start = math.max(0, viewport_end - viewport_size + 1) end - local pos_in_viewport = scroll_pos - viewport_start + return viewport_start, viewport_end +end + +local function calculateOrderY(order_idx) + local orders = df.global.world.manager_orders.all + + if #orders == 0 or order_idx < 0 or order_idx >= #orders then + return nil + end + + local viewport_start, viewport_end = getVisibleOrderIndices() + + -- Check if order is in viewport + if order_idx < viewport_start or order_idx > viewport_end then + return nil + end - local selected_y = list_start_y + (pos_in_viewport * ORDER_HEIGHT) + local list_start_y = getListStartY() + local pos_in_viewport = order_idx - viewport_start - return selected_y + return list_start_y + (pos_in_viewport * ORDER_HEIGHT) end OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) @@ -1024,18 +1035,37 @@ function OrderHighlightOverlay:render(dc) return end - -- Draw highlight arrows - local selected_y = calculateSelectedOrderY() - if selected_y then - local highlight_pen = dfhack.pen.parse{ - fg=COLOR_BLACK, - bg=COLOR_WHITE, - bold=true, - } - - dc:seek(ARROW_X, selected_y):string('|', highlight_pen) - dc:seek(ARROW_X, selected_y + 1):string('>', highlight_pen) - dc:seek(ARROW_X, selected_y + 2):string('|', highlight_pen) + -- Draw highlight arrows for all matches in viewport + if #search_matched_indices == 0 then return end + + local selected_pen = dfhack.pen.parse{ + fg=COLOR_BLACK, + bg=COLOR_RED, + bold=true, + } + + local match_pen = dfhack.pen.parse{ + fg=COLOR_BLACK, + bg=COLOR_WHITE, + bold=true, + } + + -- Get the order index of the currently selected match + local selected_order_idx = search_current_match_idx > 0 and + search_matched_indices[search_current_match_idx] or nil + + -- Draw highlights for all matching orders in viewport + for _, match_order_idx in ipairs(search_matched_indices) do + local match_y = calculateOrderY(match_order_idx) + + if match_y then + -- Use red pen for selected match, white for others + local pen = (match_order_idx == selected_order_idx) and selected_pen or match_pen + + dc:seek(ARROW_X, match_y):string('|', pen) + dc:seek(ARROW_X, match_y + 1):string('>', pen) + dc:seek(ARROW_X, match_y + 2):string('|', pen) + end end end From 5cfb0c7a6b4fce18d9bdea8aa75a9303d5706ac0 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 18:14:03 +0100 Subject: [PATCH 034/333] Stop hiding on scroll --- plugins/lua/orders.lua | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index d6b7850154..135043562c 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -715,7 +715,6 @@ end -- local search_cursor_visible = false -local search_last_scroll_position = -1 local order_count_at_highlight = 0 local search_matched_indices = {} local search_current_match_idx = 0 @@ -820,7 +819,13 @@ end function OrdersSearchOverlay:update_filter(text) search_matched_indices = perform_search(text) search_current_match_idx = 0 - search_cursor_visible = false + + if #search_matched_indices > 0 then + search_cursor_visible = true + order_count_at_highlight = #df.global.world.manager_orders.all + else + search_cursor_visible = false + end if text == '' then self.subviews.main_panel.frame_title = 'Search' @@ -866,7 +871,6 @@ function OrdersSearchOverlay:cycle_match(direction) -- Scroll to the selected match local order_idx = search_matched_indices[search_current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx - search_last_scroll_position = order_idx search_cursor_visible = true order_count_at_highlight = #df.global.world.manager_orders.all @@ -1020,16 +1024,8 @@ function OrderHighlightOverlay:render(dc) if mi.job_details.open or not search_cursor_visible then return end - local current_scroll = mi.info.work_orders.scroll_position_work_orders - local current_order_count = #df.global.world.manager_orders.all - - -- Hide cursor when user manually scrolls - if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then - search_cursor_visible = false - return - end - -- Hide cursor when order list changes (orders added or removed) + local current_order_count = #df.global.world.manager_orders.all if order_count_at_highlight ~= current_order_count then search_cursor_visible = false return @@ -1040,13 +1036,13 @@ function OrderHighlightOverlay:render(dc) local selected_pen = dfhack.pen.parse{ fg=COLOR_BLACK, - bg=COLOR_RED, + bg=COLOR_WHITE, bold=true, } local match_pen = dfhack.pen.parse{ - fg=COLOR_BLACK, - bg=COLOR_WHITE, + fg=COLOR_WHITE, + bg=COLOR_BLACK, bold=true, } From 0a6378da7542d36c7559d58438cee63682b3ad93 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 19:08:33 +0100 Subject: [PATCH 035/333] Periodic orders change detection --- plugins/lua/orders.lua | 43 +++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 135043562c..fe3f13736b 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -715,9 +715,10 @@ end -- local search_cursor_visible = false -local order_count_at_highlight = 0 local search_matched_indices = {} local search_current_match_idx = 0 +local order_names_checksum = nil +local search_overlay_instance = nil local function perform_search(text) local matches = {} @@ -738,6 +739,19 @@ local function perform_search(text) return matches end +local function calculate_order_names_checksum() + local orders = df.global.world.manager_orders.all + if #orders == 0 then return "" end + + local names = {} + for i = 0, #orders - 1 do + local name = dfhack.job.getManagerOrderName(orders[i]) + table.insert(names, name or "") + end + + return table.concat(names, "|") +end + OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', @@ -814,15 +828,16 @@ function OrdersSearchOverlay:init() } self.minimized = false + search_overlay_instance = self end -function OrdersSearchOverlay:update_filter(text) +function OrdersSearchOverlay:update_filter() + local text = self.subviews.filter.text search_matched_indices = perform_search(text) search_current_match_idx = 0 if #search_matched_indices > 0 then search_cursor_visible = true - order_count_at_highlight = #df.global.world.manager_orders.all else search_cursor_visible = false end @@ -872,7 +887,6 @@ function OrdersSearchOverlay:cycle_match(direction) local order_idx = search_matched_indices[search_current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx search_cursor_visible = true - order_count_at_highlight = #df.global.world.manager_orders.all self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end @@ -953,6 +967,8 @@ local LIST_START_Y_ONE_TABS_ROW = 8 local LIST_START_Y_TWO_TABS_ROWS = 10 local BOTTOM_MARGIN = 9 local ARROW_X = 10 +local CHECK_FRAME_INTERVAL = 50 +local check_frame_counter = 0 local function getListStartY() local rect = gui.get_interface_rect() @@ -1024,11 +1040,20 @@ function OrderHighlightOverlay:render(dc) if mi.job_details.open or not search_cursor_visible then return end - -- Hide cursor when order list changes (orders added or removed) - local current_order_count = #df.global.world.manager_orders.all - if order_count_at_highlight ~= current_order_count then - search_cursor_visible = false - return + -- Periodic check for order name changes + check_frame_counter = check_frame_counter + 1 + if check_frame_counter >= CHECK_FRAME_INTERVAL then + check_frame_counter = 0 + + local new_checksum = calculate_order_names_checksum() + if new_checksum ~= order_names_checksum then + order_names_checksum = new_checksum + + -- Auto re-run search if active + if search_overlay_instance and not search_overlay_instance.minimized then + search_overlay_instance:update_filter() + end + end end -- Draw highlight arrows for all matches in viewport From 9b1becbd70a5a3e63c22ebf423ea2b991efa751b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 20:06:52 +0100 Subject: [PATCH 036/333] Remove unnecessary field assignments --- library/modules/Job.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 91c16dd37a..cbcfebcc23 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -695,7 +695,6 @@ std::string Job::getManagerOrderName(df::manager_order *order) std::string desc; auto button = df::allocate(); button->mstring = order->reaction_name; - button->specdata.hist_figure_id = order->specdata.hist_figure_id; button->jobtype = order->job_type; button->itemtype = order->item_type; button->subtype = order->item_subtype; @@ -704,7 +703,6 @@ std::string Job::getManagerOrderName(df::manager_order *order) button->specflag = order->specflag; button->job_item_flag = order->material_category; button->specdata = order->specdata; - button->art_specifier = order->art_spec.type; button->art_specifier_id1 = order->art_spec.id; button->art_specifier_id2 = order->art_spec.subid; From 2f1f78a15ad04b8a9c2aa004d68410d13f174d15 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 20:10:17 +0100 Subject: [PATCH 037/333] Add documentation entry to lua api --- docs/dev/Lua API.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 3ba6389088..bb6f82748f 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1430,6 +1430,10 @@ Job module Returns the job's description, as seen in the Units and Jobs screens. +* ``dfhack.job.getManagerOrderName(manager_order)`` + + Returns the manager order's description, as seen in the Work orders screen. + Hotkey module ------------- From 8c8d3fcbfa26a846852140bb2c47273c9ee2a674 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 13:38:03 +0100 Subject: [PATCH 038/333] Remove redundant search_cursor_visible variable --- plugins/lua/orders.lua | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index fe3f13736b..061e421287 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -714,7 +714,6 @@ end -- OrdersSearchOverlay -- -local search_cursor_visible = false local search_matched_indices = {} local search_current_match_idx = 0 local order_names_checksum = nil @@ -836,12 +835,6 @@ function OrdersSearchOverlay:update_filter() search_matched_indices = perform_search(text) search_current_match_idx = 0 - if #search_matched_indices > 0 then - search_cursor_visible = true - else - search_cursor_visible = false - end - if text == '' then self.subviews.main_panel.frame_title = 'Search' else @@ -867,7 +860,6 @@ function OrdersSearchOverlay:cycle_match(direction) if #new_matches == 0 then search_matched_indices = {} search_current_match_idx = 0 - search_cursor_visible = false self.subviews.main_panel.frame_title = 'Search' return end @@ -886,7 +878,6 @@ function OrdersSearchOverlay:cycle_match(direction) -- Scroll to the selected match local order_idx = search_matched_indices[search_current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx - search_cursor_visible = true self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end @@ -1038,7 +1029,7 @@ OrderHighlightOverlay.ATTRS{ function OrderHighlightOverlay:render(dc) OrderHighlightOverlay.super.render(self, dc) - if mi.job_details.open or not search_cursor_visible then return end + if mi.job_details.open or #search_matched_indices == 0 then return end -- Periodic check for order name changes check_frame_counter = check_frame_counter + 1 From f8e9af512676ebcab21b6d8f0e39cd3c9a6cc6bb Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 13:55:52 +0100 Subject: [PATCH 039/333] Move periodic order change detection to SearchOverlay using overlay_onupdate --- plugins/lua/orders.lua | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 061e421287..05c47e1979 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -717,7 +717,6 @@ end local search_matched_indices = {} local search_current_match_idx = 0 local order_names_checksum = nil -local search_overlay_instance = nil local function perform_search(text) local matches = {} @@ -758,6 +757,7 @@ OrdersSearchOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=26, h=4}, + overlay_onupdate_max_freq_seconds=1, } function OrdersSearchOverlay:init() @@ -827,7 +827,16 @@ function OrdersSearchOverlay:init() } self.minimized = false - search_overlay_instance = self +end + +function OrdersSearchOverlay:overlay_onupdate() + if self.minimized then return end + + local new_checksum = calculate_order_names_checksum() + if new_checksum ~= order_names_checksum then + order_names_checksum = new_checksum + self:update_filter() + end end function OrdersSearchOverlay:update_filter() @@ -958,8 +967,6 @@ local LIST_START_Y_ONE_TABS_ROW = 8 local LIST_START_Y_TWO_TABS_ROWS = 10 local BOTTOM_MARGIN = 9 local ARROW_X = 10 -local CHECK_FRAME_INTERVAL = 50 -local check_frame_counter = 0 local function getListStartY() local rect = gui.get_interface_rect() @@ -1031,25 +1038,6 @@ function OrderHighlightOverlay:render(dc) if mi.job_details.open or #search_matched_indices == 0 then return end - -- Periodic check for order name changes - check_frame_counter = check_frame_counter + 1 - if check_frame_counter >= CHECK_FRAME_INTERVAL then - check_frame_counter = 0 - - local new_checksum = calculate_order_names_checksum() - if new_checksum ~= order_names_checksum then - order_names_checksum = new_checksum - - -- Auto re-run search if active - if search_overlay_instance and not search_overlay_instance.minimized then - search_overlay_instance:update_filter() - end - end - end - - -- Draw highlight arrows for all matches in viewport - if #search_matched_indices == 0 then return end - local selected_pen = dfhack.pen.parse{ fg=COLOR_BLACK, bg=COLOR_WHITE, From 3e2b7e2665d6302fc43215e8fc69b30071dcc925 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 14:16:08 +0100 Subject: [PATCH 040/333] Consolidate search and highlight into single overlay --- plugins/lua/orders.lua | 190 +++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 104 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 05c47e1979..19295f5aef 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -714,9 +714,12 @@ end -- OrdersSearchOverlay -- -local search_matched_indices = {} -local search_current_match_idx = 0 -local order_names_checksum = nil +local ORDER_HEIGHT = 3 +local TABS_WIDTH_THRESHOLD = 155 +local LIST_START_Y_ONE_TABS_ROW = 8 +local LIST_START_Y_TWO_TABS_ROWS = 10 +local BOTTOM_MARGIN = 9 +local ARROW_X = 10 local function perform_search(text) local matches = {} @@ -750,6 +753,63 @@ local function calculate_order_names_checksum() return table.concat(names, "|") end +local function getListStartY() + local rect = gui.get_interface_rect() + + if rect.width >= TABS_WIDTH_THRESHOLD then + return LIST_START_Y_ONE_TABS_ROW + else + return LIST_START_Y_TWO_TABS_ROWS + end +end + +local function getViewportSize() + local rect = gui.get_interface_rect() + local list_start_y = getListStartY() + + local available_height = rect.height - list_start_y - BOTTOM_MARGIN + return math.floor(available_height / ORDER_HEIGHT) +end + +local function getVisibleOrderIndices() + local orders = df.global.world.manager_orders.all + local scroll_pos = mi.info.work_orders.scroll_position_work_orders + + if #orders == 0 then return 0, -1 end + + local viewport_size = getViewportSize() + local viewport_start = scroll_pos + local viewport_end = scroll_pos + viewport_size - 1 + + -- Handle end-of-list case + if viewport_end >= #orders then + viewport_end = #orders - 1 + viewport_start = math.max(0, viewport_end - viewport_size + 1) + end + + return viewport_start, viewport_end +end + +local function calculateOrderY(order_idx) + local orders = df.global.world.manager_orders.all + + if #orders == 0 or order_idx < 0 or order_idx >= #orders then + return nil + end + + local viewport_start, viewport_end = getVisibleOrderIndices() + + -- Check if order is in viewport + if order_idx < viewport_start or order_idx > viewport_end then + return nil + end + + local list_start_y = getListStartY() + local pos_in_viewport = order_idx - viewport_start + + return list_start_y + (pos_in_viewport * ORDER_HEIGHT) +end + OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', @@ -827,22 +887,25 @@ function OrdersSearchOverlay:init() } self.minimized = false + self.matched_indices = {} + self.current_match_idx = 0 + self.order_names_checksum = nil end function OrdersSearchOverlay:overlay_onupdate() if self.minimized then return end local new_checksum = calculate_order_names_checksum() - if new_checksum ~= order_names_checksum then - order_names_checksum = new_checksum + if new_checksum ~= self.order_names_checksum then + self.order_names_checksum = new_checksum self:update_filter() end end function OrdersSearchOverlay:update_filter() local text = self.subviews.filter.text - search_matched_indices = perform_search(text) - search_current_match_idx = 0 + self.matched_indices = perform_search(text) + self.current_match_idx = 0 if text == '' then self.subviews.main_panel.frame_title = 'Search' @@ -867,13 +930,13 @@ function OrdersSearchOverlay:cycle_match(direction) local new_matches = perform_search(search_text) if #new_matches == 0 then - search_matched_indices = {} - search_current_match_idx = 0 + self.matched_indices = {} + self.current_match_idx = 0 self.subviews.main_panel.frame_title = 'Search' return end - local new_match_idx = search_current_match_idx + direction + local new_match_idx = self.current_match_idx + direction if new_match_idx > #new_matches then new_match_idx = 1 @@ -881,32 +944,32 @@ function OrdersSearchOverlay:cycle_match(direction) new_match_idx = #new_matches end - search_matched_indices = new_matches - search_current_match_idx = new_match_idx + self.matched_indices = new_matches + self.current_match_idx = new_match_idx -- Scroll to the selected match - local order_idx = search_matched_indices[search_current_match_idx] + local order_idx = self.matched_indices[self.current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end function OrdersSearchOverlay:get_match_text() - local total_matches = #search_matched_indices + local total_matches = #self.matched_indices if total_matches == 0 then return '' end - if search_current_match_idx == 0 then + if self.current_match_idx == 0 then return string.format(': %d matches', total_matches) end - return string.format(': %d of %d', search_current_match_idx, total_matches) + return string.format(': %d of %d', self.current_match_idx, total_matches) end function OrdersSearchOverlay:has_matches() - return #search_matched_indices > 0 + return #self.matched_indices > 0 end local function is_mouse_key(keys) @@ -955,88 +1018,11 @@ end function OrdersSearchOverlay:render(dc) if mi.job_details.open then return end OrdersSearchOverlay.super.render(self, dc) + self:render_highlights(dc) end --- ------------------- --- OrderHighlightOverlay --- ------------------- - -local ORDER_HEIGHT = 3 -local TABS_WIDTH_THRESHOLD = 155 -local LIST_START_Y_ONE_TABS_ROW = 8 -local LIST_START_Y_TWO_TABS_ROWS = 10 -local BOTTOM_MARGIN = 9 -local ARROW_X = 10 - -local function getListStartY() - local rect = gui.get_interface_rect() - - if rect.width >= TABS_WIDTH_THRESHOLD then - return LIST_START_Y_ONE_TABS_ROW - else - return LIST_START_Y_TWO_TABS_ROWS - end -end - -local function getViewportSize() - local rect = gui.get_interface_rect() - local list_start_y = getListStartY() - - local available_height = rect.height - list_start_y - BOTTOM_MARGIN - return math.floor(available_height / ORDER_HEIGHT) -end - -local function getVisibleOrderIndices() - local orders = df.global.world.manager_orders.all - local scroll_pos = mi.info.work_orders.scroll_position_work_orders - - if #orders == 0 then return 0, -1 end - - local viewport_size = getViewportSize() - local viewport_start = scroll_pos - local viewport_end = scroll_pos + viewport_size - 1 - - -- Handle end-of-list case - if viewport_end >= #orders then - viewport_end = #orders - 1 - viewport_start = math.max(0, viewport_end - viewport_size + 1) - end - - return viewport_start, viewport_end -end - -local function calculateOrderY(order_idx) - local orders = df.global.world.manager_orders.all - - if #orders == 0 or order_idx < 0 or order_idx >= #orders then - return nil - end - - local viewport_start, viewport_end = getVisibleOrderIndices() - - -- Check if order is in viewport - if order_idx < viewport_start or order_idx > viewport_end then - return nil - end - - local list_start_y = getListStartY() - local pos_in_viewport = order_idx - viewport_start - - return list_start_y + (pos_in_viewport * ORDER_HEIGHT) -end - -OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) -OrderHighlightOverlay.ATTRS{ - desc='Shows arrows next to the work order found by orders.search', - default_enabled=true, - viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - full_interface=true, -} - -function OrderHighlightOverlay:render(dc) - OrderHighlightOverlay.super.render(self, dc) - - if mi.job_details.open or #search_matched_indices == 0 then return end +function OrdersSearchOverlay:render_highlights(dc) + if #self.matched_indices == 0 then return end local selected_pen = dfhack.pen.parse{ fg=COLOR_BLACK, @@ -1050,16 +1036,13 @@ function OrderHighlightOverlay:render(dc) bold=true, } - -- Get the order index of the currently selected match - local selected_order_idx = search_current_match_idx > 0 and - search_matched_indices[search_current_match_idx] or nil + local selected_order_idx = self.current_match_idx > 0 and + self.matched_indices[self.current_match_idx] or nil - -- Draw highlights for all matching orders in viewport - for _, match_order_idx in ipairs(search_matched_indices) do + for _, match_order_idx in ipairs(self.matched_indices) do local match_y = calculateOrderY(match_order_idx) if match_y then - -- Use red pen for selected match, white for others local pen = (match_order_idx == selected_order_idx) and selected_pen or match_pen dc:seek(ARROW_X, match_y):string('|', pen) @@ -1075,7 +1058,6 @@ OVERLAY_WIDGETS = { recheck=RecheckOverlay, importexport=OrdersOverlay, search=OrdersSearchOverlay, - highlight=OrderHighlightOverlay, skillrestrictions=SkillRestrictionOverlay, laborrestrictions=LaborRestrictionsOverlay, conditionsrightclick=ConditionsRightClickOverlay, From e6cd89514c37832b2a4c95e4e884aa383ddd5df1 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 14:22:23 +0100 Subject: [PATCH 041/333] Rename checksum to concat_order_names and cached_order_names --- plugins/lua/orders.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 19295f5aef..a64ce81684 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -740,7 +740,7 @@ local function perform_search(text) return matches end -local function calculate_order_names_checksum() +local function concat_order_names() local orders = df.global.world.manager_orders.all if #orders == 0 then return "" end @@ -889,15 +889,15 @@ function OrdersSearchOverlay:init() self.minimized = false self.matched_indices = {} self.current_match_idx = 0 - self.order_names_checksum = nil + self.cached_order_names = nil end function OrdersSearchOverlay:overlay_onupdate() if self.minimized then return end - local new_checksum = calculate_order_names_checksum() - if new_checksum ~= self.order_names_checksum then - self.order_names_checksum = new_checksum + local current_order_names = concat_order_names() + if current_order_names ~= self.cached_order_names then + self.cached_order_names = current_order_names self:update_filter() end end From 0f7174dcdac9931cecf85dcf554b67ff9992787e Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 14:37:45 +0100 Subject: [PATCH 042/333] Hoist pens to module-level constants and inline has_matches --- plugins/lua/orders.lua | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index a64ce81684..19e6e0d9bb 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -721,6 +721,9 @@ local LIST_START_Y_TWO_TABS_ROWS = 10 local BOTTOM_MARGIN = 9 local ARROW_X = 10 +local SELECTED_PEN = dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_WHITE, bold=true} +local MATCH_PEN = dfhack.pen.parse{fg=COLOR_WHITE, bg=COLOR_BLACK, bold=true} + local function perform_search(text) local matches = {} @@ -843,7 +846,7 @@ function OrdersSearchOverlay:init() key='CUSTOM_ALT_P', auto_width=true, on_activate=self:callback('cycle_match', -1), - enabled=function() return self:has_matches() end, + enabled=function() return #self.matched_indices > 0 end, }, widgets.HotkeyLabel{ frame={t=1, l=12}, @@ -851,7 +854,7 @@ function OrdersSearchOverlay:init() key='CUSTOM_ALT_N', auto_width=true, on_activate=self:callback('cycle_match', 1), - enabled=function() return self:has_matches() end, + enabled=function() return #self.matched_indices > 0 end, }, }, } @@ -968,10 +971,6 @@ function OrdersSearchOverlay:get_match_text() return string.format(': %d of %d', self.current_match_idx, total_matches) end -function OrdersSearchOverlay:has_matches() - return #self.matched_indices > 0 -end - local function is_mouse_key(keys) return keys._MOUSE_L or keys._MOUSE_R @@ -1024,18 +1023,6 @@ end function OrdersSearchOverlay:render_highlights(dc) if #self.matched_indices == 0 then return end - local selected_pen = dfhack.pen.parse{ - fg=COLOR_BLACK, - bg=COLOR_WHITE, - bold=true, - } - - local match_pen = dfhack.pen.parse{ - fg=COLOR_WHITE, - bg=COLOR_BLACK, - bold=true, - } - local selected_order_idx = self.current_match_idx > 0 and self.matched_indices[self.current_match_idx] or nil @@ -1043,7 +1030,7 @@ function OrdersSearchOverlay:render_highlights(dc) local match_y = calculateOrderY(match_order_idx) if match_y then - local pen = (match_order_idx == selected_order_idx) and selected_pen or match_pen + local pen = (match_order_idx == selected_order_idx) and SELECTED_PEN or MATCH_PEN dc:seek(ARROW_X, match_y):string('|', pen) dc:seek(ARROW_X, match_y + 1):string('>', pen) From 806ff7b1f3d35ba8ab8d0d9221f134509f444c5c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 14:48:56 +0100 Subject: [PATCH 043/333] Only scroll to selected match when outside visible viewport --- plugins/lua/orders.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 19e6e0d9bb..80a6196e13 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -950,9 +950,12 @@ function OrdersSearchOverlay:cycle_match(direction) self.matched_indices = new_matches self.current_match_idx = new_match_idx - -- Scroll to the selected match + -- Scroll to the selected match only if not already visible local order_idx = self.matched_indices[self.current_match_idx] - mi.info.work_orders.scroll_position_work_orders = order_idx + local viewport_start, viewport_end = getVisibleOrderIndices() + if order_idx < viewport_start or order_idx > viewport_end then + mi.info.work_orders.scroll_position_work_orders = order_idx + end self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end From 9560cef37f664929ea1fc5f486e78f16b1bd4311 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:38:55 +0100 Subject: [PATCH 044/333] Fix the suspendmanager overlay appearing for followed units --- docs/changelog.txt | 1 + plugins/lua/suspendmanager.lua | 32 +++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2eb584644f..cc0acf0217 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `suspendmanager`: Fix the overlay appearing where it should not when following a unit ## Misc Improvements diff --git a/plugins/lua/suspendmanager.lua b/plugins/lua/suspendmanager.lua index c15be19402..a7de42e5a0 100644 --- a/plugins/lua/suspendmanager.lua +++ b/plugins/lua/suspendmanager.lua @@ -26,6 +26,25 @@ function isBuildingPlanJob(job) return suspendmanager_isBuildingPlanJob(job) end +--- Return the selected construction job +local function getSelectedBuildingJob() + -- This is not relying on dfhack.gui.getSelectedJob() because we don't want + -- the job of a selected or followed unit, only of a selected building + local building = dfhack.gui.getSelectedBuilding(true) + if not building then + return nil + end + + -- Find if the building is being constructed + for _, job in ipairs(building.jobs) do + if job.job_type == df.job_type.ConstructBuilding then + return job + end + end + + return nil +end + function runOnce(prevent_blocking, quiet, unsuspend_everything) suspendmanager_runOnce(prevent_blocking, unsuspend_everything) if (not quiet) then @@ -69,7 +88,7 @@ function StatusOverlay:init() end function StatusOverlay:get_status_string() - local job = dfhack.gui.getSelectedJob(true) + local job = getSelectedBuildingJob() if job and job.flags.suspend then return "Suspended because: " .. suspendmanager_suspensionDescription(job) .. "." end @@ -77,8 +96,11 @@ function StatusOverlay:get_status_string() end function StatusOverlay:render(dc) - local job = dfhack.gui.getSelectedJob(true) - if not job or job.job_type ~= df.job_type.ConstructBuilding or not isEnabled() or isBuildingPlanJob(job) then + if not isEnabled() then + return + end + local job = getSelectedBuildingJob() + if not job or isBuildingPlanJob(job) then return end StatusOverlay.super.render(self, dc) @@ -110,8 +132,8 @@ function ToggleOverlay:init() end function ToggleOverlay:shouldRender() - local job = dfhack.gui.getSelectedJob(true) - return job and job.job_type == df.job_type.ConstructBuilding and not isBuildingPlanJob(job) + local job = getSelectedBuildingJob() + return job and not isBuildingPlanJob(job) end function ToggleOverlay:render(dc) From ec0440ff8dac686e16bbe14fa9af23e72a702aa1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:58:32 +0000 Subject: [PATCH 045/333] [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.35.0 → 0.36.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.35.0...0.36.0) --- .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 cc38b7e9ab..5d77a667a4 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.35.0 + rev: 0.36.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From fb0e17caa52098e980a93868a041cc7077962243 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 5 Jan 2026 14:10:22 -0600 Subject: [PATCH 046/333] change `Filesystem::as_string` to always use utf-8 our policy is that strings are to be encoded in utf-8 as much as possible. however, this function was using the system locale encoding, which was causing a transcoding error when a pathname was round-tripped through lua fixes #5688 --- docs/changelog.txt | 1 + library/include/modules/Filesystem.h | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2eb584644f..1f4d389c73 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +# ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding ## Misc Improvements diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 407579926f..68da9ec7f6 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -77,12 +77,13 @@ namespace DFHack { DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; inline std::string as_string(const std::filesystem::path path) noexcept { - auto pStr = path.string(); - if constexpr (std::filesystem::path::preferred_separator != '/') - { - std::ranges::replace(pStr, std::filesystem::path::preferred_separator, '/'); - } - return pStr; + // this just mashes the utf-8 into a std::string without any conversion + // this is largely because we use utf-8 everywhere internally as much as we can + // but we should ultimately convert to using u8strings for strings that are utf-8 + // and use a different string type for strings encoded in cp437 or in the locale codepage + std::u8string pstr = path.generic_u8string(); + return std::string((char*)pstr.c_str()); + } DFHACK_EXPORT std::filesystem::path getInstallDir() noexcept; DFHACK_EXPORT std::filesystem::path getBaseDir() noexcept; From a0f37eef886569ecba9eca8a15b4ff534bf825c2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:27:04 +0000 Subject: [PATCH 047/333] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index da0b52eb3a..c9c7f4e528 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit da0b52eb3ad79866b1228a880be3b734cfac7b55 +Subproject commit c9c7f4e52866c528f63a82c774aa602e48cf1dc7 diff --git a/scripts b/scripts index 00d826f0eb..8359ac6759 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 00d826f0eb81e837912c841438eba522149d93fc +Subproject commit 8359ac67595ebdb2b83ba27225856834732b43f3 From d04b116626177a9d7ae50f6894e24abd7f16a9c9 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 6 Jan 2026 20:35:23 +0100 Subject: [PATCH 048/333] Add death cause button to dead/missing tab --- docs/changelog.txt | 1 + plugins/lua/sort.lua | 1 + plugins/lua/sort/deathcause_button.lua | 78 ++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 plugins/lua/sort/deathcause_button.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index 2eb584644f..4dfb1419fe 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `sort`: Add death cause button to dead/missing tab in the creatures screen ## Fixes diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index bdad4ae90e..a81bff5beb 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1294,6 +1294,7 @@ OVERLAY_WIDGETS = { candidates=require('plugins.sort.info').CandidatesOverlay, interrogation=require('plugins.sort.info').InterrogationOverlay, conviction=require('plugins.sort.info').ConvictionOverlay, + deathcause_button=require('plugins.sort.deathcause_button').DeathCauseOverlay, location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, -- TODO: maybe rewrite for 50.12 -- burrow_assignment=require('plugins.sort.unitselector').BurrowAssignmentOverlay, diff --git a/plugins/lua/sort/deathcause_button.lua b/plugins/lua/sort/deathcause_button.lua new file mode 100644 index 0000000000..e5f424c2a9 --- /dev/null +++ b/plugins/lua/sort/deathcause_button.lua @@ -0,0 +1,78 @@ +local _ENV = mkmodule('plugins.sort.deathcause_button') + +local dialogs = require('gui.dialogs') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +DeathCauseOverlay = defclass(DeathCauseOverlay, overlay.OverlayWidget) +DeathCauseOverlay.ATTRS{ + desc='Adds a button to view death cause on the dead/missing tab.', + default_pos={x=50, y=-7}, + default_enabled=true, + viewscreens='dwarfmode/Info/CREATURES/DECEASED', + frame={w=21, h=1}, +} + +function DeathCauseOverlay:init() + local deathcause = reqscript('deathcause') + + local function get_selected_unit() + -- Navigate to the creatures/deceased widget hierarchy: + -- list_widget - the main deceased list + -- ├─ children[0]: scrollbar widget + -- └─ children[1]: container widget (list_container) + -- ├─ grandchildren[0]: header + -- ├─ grandchildren[1]: header or other UI + -- └─ grandchildren[2]: scrollable rows container (scrollable_list) + -- └─ rows: row widgets (each row = one unit in the list) + -- └─ row.children[x]: unit widget + -- └─ unit_widget.u: pointer to the df.unit object + + local creatures = df.global.game.main_interface.info.creatures + local list_widget = dfhack.gui.getWidget(creatures, 'Tabs', 'Dead/Missing') + if not list_widget then return nil end + + local children = dfhack.gui.getWidgetChildren(list_widget) + local list_container = children[1] + local grandchildren = dfhack.gui.getWidgetChildren(list_container) + + local scrollable_list = grandchildren[2] + if not scrollable_list then + return nil + end + + local rows = dfhack.gui.getWidgetChildren(scrollable_list) + + local cursor_idx = list_widget.cursor_idx or 0 + + if cursor_idx >= 0 and cursor_idx < #rows then + local row = rows[cursor_idx + 1] + + local ok, unit = pcall(function() return dfhack.gui.getWidget(row, 0).u end) + if ok and unit then + return unit + end + end + + return nil + end + + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='Show death cause', + key='CUSTOM_D', + on_activate=function() + local unit = get_selected_unit() + if not unit then + dialogs.showMessage('Death Cause', 'No unit selected.') + return + end + local cause = deathcause.getDeathCause(unit) + dialogs.showMessage('Death Cause', dfhack.df2console(cause)) + end, + }, + } +end + +return _ENV From 9e916367761ca33cc6a1eabe08997710888a949c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 6 Jan 2026 20:54:12 +0100 Subject: [PATCH 049/333] Trim trailing whitespace --- plugins/lua/sort/deathcause_button.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/lua/sort/deathcause_button.lua b/plugins/lua/sort/deathcause_button.lua index e5f424c2a9..b149601a7a 100644 --- a/plugins/lua/sort/deathcause_button.lua +++ b/plugins/lua/sort/deathcause_button.lua @@ -31,20 +31,20 @@ function DeathCauseOverlay:init() local creatures = df.global.game.main_interface.info.creatures local list_widget = dfhack.gui.getWidget(creatures, 'Tabs', 'Dead/Missing') if not list_widget then return nil end - + local children = dfhack.gui.getWidgetChildren(list_widget) local list_container = children[1] local grandchildren = dfhack.gui.getWidgetChildren(list_container) - + local scrollable_list = grandchildren[2] if not scrollable_list then return nil end - + local rows = dfhack.gui.getWidgetChildren(scrollable_list) - + local cursor_idx = list_widget.cursor_idx or 0 - + if cursor_idx >= 0 and cursor_idx < #rows then local row = rows[cursor_idx + 1] From 563262ae8b8b828f5094a7b318086eaa90f02935 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 10:59:57 -0600 Subject: [PATCH 050/333] Update build-windows.yml add jinja2 dependency for windows builds --- .github/workflows/build-windows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 0864b716f9..4ad823e696 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -71,6 +71,7 @@ jobs: - name: Install build dependencies run: | choco install sccache + pip install Jinja2 - name: Install doc dependencies if: inputs.docs run: | From 47eab6c1ad4013d6c1637f29cf4c22c8495a6dce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 11:16:52 -0600 Subject: [PATCH 051/333] update version to 53.09 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6aafbe107f..cd9d70f567 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.08") +set(DF_VERSION "53.09") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 11f8580a07c0cddf74420035f82aa8122e7f154c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:18:00 +0000 Subject: [PATCH 052/333] 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 c9c7f4e528..cffd2fa02c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit c9c7f4e52866c528f63a82c774aa602e48cf1dc7 +Subproject commit cffd2fa02cdd753843e5336ac140f9753a3fc9ce From 99a69c81770064baee1e19502f35cf735b5abe61 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:27:04 +0000 Subject: [PATCH 053/333] Auto-update structures ref for 53.09 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index cffd2fa02c..2b9e59ea39 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit cffd2fa02cdd753843e5336ac140f9753a3fc9ce +Subproject commit 2b9e59ea390c9e6ee234c7da7fc7832f8ccb161b From 6e0ae2dfde9bc901f5ae4d08234087d083a87af4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 11:41:19 -0600 Subject: [PATCH 054/333] changelog for 53.09 --- docs/changelog.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index e63253551f..907e037428 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.09-r1 + +## New Tools + +## New Features + ## Fixes # ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding From 587a85c149dbc753a516888831da7f9a8f0f9216 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 11:57:14 -0600 Subject: [PATCH 055/333] FIx changelog.txt move one entry to the correct release --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 907e037428..742fd54ecc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -75,6 +75,7 @@ Template for new versions: ## New Tools ## New Features +- `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) ## Fixes # ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding @@ -116,7 +117,6 @@ Template for new versions: ## New Features - `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status -- `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit From 652b79254729a670ef7816751b2ca0e202a084e4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 12:39:44 -0600 Subject: [PATCH 056/333] Update changelog.txt fix markup error --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 742fd54ecc..da2aa1ef4e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -78,7 +78,7 @@ Template for new versions: - `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) ## Fixes -# ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding +- ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding ## Misc Improvements From 5e1fb2d5515bd52e20d63d8d7acbd2240bb927a5 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Wed, 7 Jan 2026 22:10:05 +0100 Subject: [PATCH 057/333] Add Uniformed filter to squad selection screen --- plugins/lua/sort.lua | 73 +++++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index bdad4ae90e..19413be269 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1029,12 +1029,29 @@ function SquadFilterOverlay:init() local left_panel = widgets.Panel{ view_id='left_panel', - frame={t=1, b=0, l=0, w=NARROW_WIDTH-4}, + frame={t=0, b=0, l=0, w=NARROW_WIDTH-4}, visible=true, subviews={ + widgets.HotkeyLabel{ + view_id='toggle_all', + frame={t=0, l=0}, + key='CUSTOM_SHIFT_A', + label='Toggle all', + on_activate=function() + local target = self.subviews.military:getOptionValue() == 'exclude' and 'include' or 'exclude' + self.subviews.military:setOption(target) + self.subviews.officials:setOption(target) + self.subviews.nobles:setOption(target) + self.subviews.infant:setOption(target) + self.subviews.unstable:setOption(target) + self.subviews.maimed:setOption(target) + self.subviews.labor_conflict:setOption(target) + poke_list() + end, + }, widgets.CycleHotkeyLabel{ view_id='military', - frame={t=0, l=0}, + frame={t=1, l=0}, key='CUSTOM_SHIFT_Q', label='Other squads:', options={ @@ -1047,7 +1064,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='officials', - frame={t=1, l=0}, + frame={t=2, l=0}, key='CUSTOM_SHIFT_O', label=' Officials:', options={ @@ -1060,7 +1077,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='nobles', - frame={t=2, l=0}, + frame={t=3, l=0}, key='CUSTOM_SHIFT_N', label=' Nobility:', options={ @@ -1076,12 +1093,25 @@ function SquadFilterOverlay:init() local right_panel = widgets.Panel{ view_id='right_panel', - frame={t=1, b=0, r=2, w=NARROW_WIDTH-4}, + frame={t=0, b=0, r=2, w=NARROW_WIDTH-4}, visible=false, subviews={ widgets.CycleHotkeyLabel{ - view_id='infant', + view_id='labor_conflict', frame={t=0, l=0}, + key='CUSTOM_SHIFT_U', + label=' Uniformed:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_LIGHTRED}, + }, + initial_option='include', + on_change=poke_list, + }, + widgets.CycleHotkeyLabel{ + view_id='infant', + frame={t=1, l=0}, key='CUSTOM_SHIFT_M', label='With infants:', options={ @@ -1094,7 +1124,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='unstable', - frame={t=1, l=0}, + frame={t=2, l=0}, key='CUSTOM_SHIFT_D', label='Hates combat:', options={ @@ -1107,7 +1137,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='maimed', - frame={t=2, l=0}, + frame={t=3, l=0}, key='CUSTOM_SHIFT_I', label=' Maimed:', options={ @@ -1125,21 +1155,6 @@ function SquadFilterOverlay:init() frame_style=gui.FRAME_MEDIUM, frame_background=gui.CLEAR_PEN, subviews={ - widgets.HotkeyLabel{ - frame={t=0, w=NARROW_WIDTH-3}, - key='CUSTOM_SHIFT_A', - label='Toggle all filters', - on_activate=function() - local target = self.subviews.military:getOptionValue() == 'exclude' and 'include' or 'exclude' - self.subviews.military:setOption(target) - self.subviews.officials:setOption(target) - self.subviews.nobles:setOption(target) - self.subviews.infant:setOption(target) - self.subviews.unstable:setOption(target) - self.subviews.maimed:setOption(target) - poke_list() - end, - }, left_panel, widgets.Label{ view_id='shifter', @@ -1167,9 +1182,8 @@ function SquadFilterOverlay:init() main_panel, widgets.Divider{ view_id='divider', - frame={l=NARROW_WIDTH-1, w=1, t=2}, + frame={l=NARROW_WIDTH-1, w=1, t=0}, frame_style=gui.FRAME_MEDIUM, - frame_style_t=false, visible=false, }, widgets.HelpButton{ @@ -1253,6 +1267,12 @@ local function is_maimed(unit) unit.status2.limbs_stand_count == 0 end +local function has_labor_conflict(unit) + return unit.status.labors[df.unit_labor.MINE] or + unit.status.labors[df.unit_labor.CUTWOOD] or + unit.status.labors[df.unit_labor.HUNT] +end + local function filter_matches(unit, filter) if filter.military == 'only' and not is_in_military(unit) then return false end if filter.military == 'exclude' and is_in_military(unit) then return false end @@ -1266,6 +1286,8 @@ local function filter_matches(unit, filter) if filter.unstable == 'exclude' and is_unstable(unit) then return false end if filter.maimed == 'only' and not is_maimed(unit) then return false end if filter.maimed == 'exclude' and is_maimed(unit) then return false end + if filter.labor_conflict == 'only' and not has_labor_conflict(unit) then return false end + if filter.labor_conflict == 'exclude' and has_labor_conflict(unit) then return false end return true end @@ -1281,6 +1303,7 @@ function do_squad_filter(unit) infant=self.subviews.infant:getOptionValue(), unstable=self.subviews.unstable:getOptionValue(), maimed=self.subviews.maimed:getOptionValue(), + labor_conflict=self.subviews.labor_conflict:getOptionValue(), } return filter_matches(unit, filter) end From 7daf2e7fa66d6ee9679b3a7ca1afdc0f8272da59 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Wed, 7 Jan 2026 22:12:44 +0100 Subject: [PATCH 058/333] Changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index da2aa1ef4e..0786cbaa0d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `sort`: added ``Uniformed`` filter to squad assignment screen to filter dwarves with mining, woodcutting, or hunting labors ## Fixes From 821123c558cca83d71b347cac147e96b319f95f7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 8 Jan 2026 20:10:15 -0600 Subject: [PATCH 059/333] fix autochop report bug fix format string specification in `autochop` fixes #5701 --- docs/changelog.txt | 3 +++ docs/dev/Lua API.rst | 5 +++++ library/include/modules/Burrows.h | 2 ++ library/modules/Burrows.cpp | 7 +++++++ plugins/autochop.cpp | 5 ++--- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index da2aa1ef4e..63ed45501e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,14 +59,17 @@ Template for new versions: ## New Features ## Fixes +- `autochop`: the report will no longer throw a C++ exception when burrows are defined. ## Misc Improvements ## Documentation ## API +- Added ``Burrows::getName``: obtains the name of a burrow, or the same placeholder name that DF would show if the burrow is unnamed. ## Lua +- Added ``Burrows::getName`` as ``dfhack.burrows.getName``. ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 3ba6389088..52fb7c778f 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2489,6 +2489,11 @@ Maps module Burrows module -------------- +* ``dfhack.burrows.getName(burrow)`` + + Returns the name of the burrow. + If the burrow has no set name, returns the same placeholder name that DF would show in the UI. + * ``dfhack.burrows.findByName(name[, ignore_final_plus])`` Returns the burrow pointer or *nil*. if ``ignore_final_plus`` is ``true``, diff --git a/library/include/modules/Burrows.h b/library/include/modules/Burrows.h index b2e3e56b4e..dbd2e51dd6 100644 --- a/library/include/modules/Burrows.h +++ b/library/include/modules/Burrows.h @@ -45,6 +45,8 @@ namespace DFHack { namespace Burrows { + DFHACK_EXPORT std::string getName(df::burrow* burrow); + DFHACK_EXPORT df::burrow *findByName(std::string name, bool ignore_final_plus = false); // Units diff --git a/library/modules/Burrows.cpp b/library/modules/Burrows.cpp index 6c8802469c..17ab0389d4 100644 --- a/library/modules/Burrows.cpp +++ b/library/modules/Burrows.cpp @@ -52,6 +52,13 @@ using namespace df::enums; using df::global::world; using df::global::plotinfo; +std::string Burrows::getName(df::burrow* burrow) +{ + CHECK_NULL_POINTER(burrow); + return burrow->name.empty() ? fmt::format("Burrow {}", burrow->id + 1) : burrow->name; +} + + df::burrow *Burrows::findByName(std::string name, bool ignore_final_plus) { auto &vec = df::burrow::get_vector(); diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index 6b98d0347a..811a3d1cb0 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -669,9 +669,8 @@ static void autochop_printStatus(color_ostream &out) { for (auto &burrow : plotinfo->burrows.list) { name_width = std::max(name_width, (int)burrow->name.size()); } - name_width = -name_width; // left justify - constexpr auto fmt = "{:{}} {:4} {:4} {:8} {:5} {:6} {:7}\n"; + constexpr auto fmt = "{:<{}} {:4} {:4} {:8} {:5} {:6} {:7}\n"; out.print(fmt, "burrow name", name_width, " id ", "chop", "clearcut", "trees", "marked", "protect"); out.print(fmt, "-----------", name_width, "----", "----", "--------", "-----", "------", "-------"); @@ -689,7 +688,7 @@ static void autochop_printStatus(color_ostream &out) { protect_edible = c.get_bool(BURROW_CONFIG_PROTECT_EDIBLE); protect_cookable = c.get_bool(BURROW_CONFIG_PROTECT_COOKABLE); } - out.print(fmt, burrow->name, name_width, burrow->id, + out.print(fmt, Burrows::getName(burrow), name_width, burrow->id, chop ? "[x]" : "[ ]", clearcut ? "[x]" : "[ ]", tree_counts[burrow->id], designated_tree_counts[burrow->id], From c050add7bc472ec9e6daa8b0e5752120c20dc03d Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 10 Jan 2026 12:57:04 +0100 Subject: [PATCH 060/333] Create plugin for clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. --- docs/changelog.txt | 1 + docs/plugins/logcleaner.rst | 62 ++++++++ plugins/CMakeLists.txt | 1 + plugins/logcleaner/logcleaner.cpp | 239 ++++++++++++++++++++++++++++++ plugins/lua/logcleaner.lua | 66 +++++++++ 5 files changed, 369 insertions(+) create mode 100644 docs/plugins/logcleaner.rst create mode 100644 plugins/logcleaner/logcleaner.cpp create mode 100644 plugins/lua/logcleaner.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index da2aa1ef4e..377577da84 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ Template for new versions: # Future ## New Tools +- ``logcleaner``: New plugin for time-triggered clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. ## New Features diff --git a/docs/plugins/logcleaner.rst b/docs/plugins/logcleaner.rst new file mode 100644 index 0000000000..d9d0930d82 --- /dev/null +++ b/docs/plugins/logcleaner.rst @@ -0,0 +1,62 @@ +logcleaner +========== +.. dfhack-tool:: + :summary: Automatically clear combat, sparring, and hunting reports. + :tags: fort auto units + +This plugin prevents spam from cluttering your announcement history and filling +the 3000-item reports buffer. It runs every 100 ticks and clears selected report +types from both the global reports buffer and per-unit logs. + +Usage +----- + +Basic commands +~~~~~~~~~~~~~~ + +``logcleaner`` + Show the current status of the plugin. +``logcleaner enable`` + Enable the plugin (persists per save). +``logcleaner disable`` + Disable the plugin. + +Configuring filters +~~~~~~~~~~~~~~~~~~~ + +``logcleaner combat`` + Clear combat reports (also enables the plugin if disabled). +``logcleaner sparring`` + Clear sparring reports. +``logcleaner hunting`` + Clear hunting reports. +``logcleaner combat,sparring`` + Clear multiple report types (comma-separated). +``logcleaner all`` + Enable all three filter types. +``logcleaner none`` + Disable all filter types. + +Examples +~~~~~~~~ + +Clear only sparring reports:: + + logcleaner sparring + +Clear combat and hunting, but not sparring:: + + logcleaner combat,hunting + +Overlay UI +---------- + +Run ``gui/logcleaner`` to open the settings overlay, or access it from the +control panel under the Gameplay tab. + +The overlay provides: + +- **Enable toggle**: Turn the plugin on or off (``Shift+E``) +- **Combat toggle**: Clear combat reports (``Shift+C``) +- **Sparring toggle**: Clear sparring reports (``Shift+S``) +- **Hunting toggle**: Clear hunting reports (``Shift+H``) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 355cc0ac4c..4a4423f48f 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -66,6 +66,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(createitem createitem.cpp) dfhack_plugin(cursecheck cursecheck.cpp) dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) + dfhack_plugin(logcleaner logcleaner/logcleaner.cpp LINK_LIBRARIES lua) dfhack_plugin(deramp deramp.cpp) dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(dig dig.cpp LINK_LIBRARIES lua) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp new file mode 100644 index 0000000000..b6be95a70e --- /dev/null +++ b/plugins/logcleaner/logcleaner.cpp @@ -0,0 +1,239 @@ +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "PluginLua.h" + +#include "modules/Persistence.h" +#include "modules/World.h" + +#include +#include +#include +#include + +using namespace DFHack; + +DFHACK_PLUGIN("logcleaner"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + +static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; +static PersistentDataItem config; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_CLEAR_COMBAT = 1, + CONFIG_CLEAR_SPARING = 2, + CONFIG_CLEAR_HUNTING = 3, +}; + +static bool clear_combat = false; +static bool clear_sparring = true; +static bool clear_hunting = false; + +namespace DFHack { + DBG_DECLARE(logcleaner, control, DebugCategory::LINFO); + DBG_DECLARE(logcleaner, cleanup, DebugCategory::LINFO); +} + +static void cleanupLogs(color_ostream& out); +static command_result do_command(color_ostream& out, std::vector& params); +static void do_enable(); +static void do_disable(); + +// Getter functions for Lua +static bool logcleaner_getCombat() { return clear_combat; } +static bool logcleaner_getSparring() { return clear_sparring; } +static bool logcleaner_getHunting() { return clear_hunting; } + +// Setter functions for Lua (also persist to config) +static void logcleaner_setCombat(color_ostream& out, bool val) { + clear_combat = val; + config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); +} + +static void logcleaner_setSparring(color_ostream& out, bool val) { + clear_sparring = val; + config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); +} + +static void logcleaner_setHunting(color_ostream& out, bool val) { + clear_hunting = val; + config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); +} + +DFhackCExport command_result plugin_init(color_ostream& out, std::vector& commands) { + commands.push_back(PluginCommand( + plugin_name, + "Prevent report buffer from filling up by clearing selected report types (combat, sparring, hunting).", + do_command)); + + return CR_OK; +} + +static command_result do_command(color_ostream& out, std::vector& params) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot use {} without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + bool show_help = false; + if (!Lua::CallLuaModuleFunction(out, "plugins.logcleaner", "parse_commandline", params, + 1, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +static void do_enable() { +} + +static void do_disable() { +} + +DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(control, out).print("{} from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + config.set_bool(CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_enable(); + else + do_disable(); + } else { + DEBUG(control, out).print("{} from the API, but already {}; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream& out) { + DEBUG(control, out).print("shutting down {}\n", plugin_name); + return CR_OK; +} + +DFhackCExport command_result plugin_load_site_data(color_ostream& out) { + config = World::GetPersistentSiteData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(control, out).print("no config found in this save; initializing\n"); + config = World::AddPersistentSiteData(CONFIG_KEY); + config.set_bool(CONFIG_IS_ENABLED, is_enabled); + config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); + config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); + config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); + } + + is_enabled = config.get_bool(CONFIG_IS_ENABLED); + clear_combat = config.get_bool(CONFIG_CLEAR_COMBAT); + clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); + clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); + + DEBUG(control, out).print("loading persisted enabled state: {}\n", + is_enabled ? "true" : "false"); + if (is_enabled) + do_enable(); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { + DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); + is_enabled = false; + do_disable(); + } + return CR_OK; +} + +static void cleanupLogs(color_ostream& out) { + if (!is_enabled || !world) + return; + + // Collect all report IDs from unit combat/sparring/hunting logs + std::unordered_set report_ids_to_remove; + + for (auto unit : world->units.all) { + // Combat logs (index 0) + if (clear_combat) { + auto& log = unit->reports.log[0]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + // Sparring logs (index 1) + if (clear_sparring) { + auto& log = unit->reports.log[1]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + // Hunting logs (index 2) + if (clear_hunting) { + auto& log = unit->reports.log[2]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + } + + if (report_ids_to_remove.empty()) + return; + + // Remove collected reports from global buffers + auto& reports = world->status.reports; + + int reports_erased = 0; + + for (auto report_id : report_ids_to_remove) { + df::report* report = df::report::find(report_id); + if (!report) + continue; + + auto it = std::find(reports.begin(), reports.end(), report); + if (it != reports.end()) { + delete report; + reports.erase(it); + reports_erased++; + } + } +} + +DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { + static int32_t tick_counter = 0; + + if (!is_enabled || !world) + return CR_OK; + + tick_counter++; + if (tick_counter >= 100) { + tick_counter = 0; + cleanupLogs(out); + } + + return CR_OK; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(logcleaner_getCombat), + DFHACK_LUA_FUNCTION(logcleaner_getSparring), + DFHACK_LUA_FUNCTION(logcleaner_getHunting), + DFHACK_LUA_FUNCTION(logcleaner_setCombat), + DFHACK_LUA_FUNCTION(logcleaner_setSparring), + DFHACK_LUA_FUNCTION(logcleaner_setHunting), + DFHACK_LUA_END +}; diff --git a/plugins/lua/logcleaner.lua b/plugins/lua/logcleaner.lua new file mode 100644 index 0000000000..f7fa328ea6 --- /dev/null +++ b/plugins/lua/logcleaner.lua @@ -0,0 +1,66 @@ +local _ENV = mkmodule('plugins.logcleaner') + +local function print_status() + print(('logcleaner is %s'):format(isEnabled() and "enabled" or "disabled")) + print(' Combat: ' .. (logcleaner_getCombat() and 'enabled' or 'disabled')) + print(' Sparring: ' .. (logcleaner_getSparring() and 'enabled' or 'disabled')) + print(' Hunting: ' .. (logcleaner_getHunting() and 'enabled' or 'disabled')) +end + +function parse_commandline(...) + local args = {...} + local command = args[1] + + -- Show status if no command or "status" + if not command or command == 'status' then + print_status() + return true + end + + -- Start with all disabled, enable only what's specified + local new_combat, new_sparring, new_hunting = false, false, false + local has_filter = false + + for _, param in ipairs(args) do + if param == 'all' then + new_combat, new_sparring, new_hunting = true, true, true + has_filter = true + elseif param == 'none' then + new_combat, new_sparring, new_hunting = false, false, false + else + -- Split by comma for multiple options in one parameter + for token in param:gmatch('([^,]+)') do + if token == 'combat' then + new_combat = true + has_filter = true + elseif token == 'sparring' then + new_sparring = true + has_filter = true + elseif token == 'hunting' then + new_hunting = true + has_filter = true + else + dfhack.printerr('Unknown option: ' .. token) + return false + end + end + end + end + + -- Auto-enable plugin when filters are being configured + if has_filter and not isEnabled() then + dfhack.run_command('enable', 'logcleaner') + print('logcleaner enabled') + end + + logcleaner_setCombat(new_combat) + logcleaner_setSparring(new_sparring) + logcleaner_setHunting(new_hunting) + + print('Log cleaning config updated:') + print_status() + + return true +end + +return _ENV From 67b26213c5c7db21db12f93914c87e53b8150700 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 10 Jan 2026 15:08:31 +0100 Subject: [PATCH 061/333] Remove debugs, add enable / disable, refactor --- plugins/logcleaner/logcleaner.cpp | 72 +++++-------------------------- plugins/lua/logcleaner.lua | 63 +++++++++++++++++---------- 2 files changed, 52 insertions(+), 83 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index b6be95a70e..56804613ad 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -1,4 +1,3 @@ -#include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" #include "PluginLua.h" @@ -32,15 +31,8 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -namespace DFHack { - DBG_DECLARE(logcleaner, control, DebugCategory::LINFO); - DBG_DECLARE(logcleaner, cleanup, DebugCategory::LINFO); -} - static void cleanupLogs(color_ostream& out); static command_result do_command(color_ostream& out, std::vector& params); -static void do_enable(); -static void do_disable(); // Getter functions for Lua static bool logcleaner_getCombat() { return clear_combat; } @@ -48,17 +40,17 @@ static bool logcleaner_getSparring() { return clear_sparring; } static bool logcleaner_getHunting() { return clear_hunting; } // Setter functions for Lua (also persist to config) -static void logcleaner_setCombat(color_ostream& out, bool val) { +static void logcleaner_setCombat(bool val) { clear_combat = val; config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); } -static void logcleaner_setSparring(color_ostream& out, bool val) { +static void logcleaner_setSparring(bool val) { clear_sparring = val; config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); } -static void logcleaner_setHunting(color_ostream& out, bool val) { +static void logcleaner_setHunting(bool val) { clear_hunting = val; config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); } @@ -89,12 +81,6 @@ static command_result do_command(color_ostream& out, std::vector& p return show_help ? CR_WRONG_USAGE : CR_OK; } -static void do_enable() { -} - -static void do_disable() { -} - DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); @@ -103,23 +89,12 @@ DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(control, out).print("{} from the API; persisting\n", - is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); - if (enable) - do_enable(); - else - do_disable(); - } else { - DEBUG(control, out).print("{} from the API, but already {}; no action\n", - is_enabled ? "enabled" : "disabled", - is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream& out) { - DEBUG(control, out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -127,7 +102,6 @@ DFhackCExport command_result plugin_load_site_data(color_ostream& out) { config = World::GetPersistentSiteData(CONFIG_KEY); if (!config.isValid()) { - DEBUG(control, out).print("no config found in this save; initializing\n"); config = World::AddPersistentSiteData(CONFIG_KEY); config.set_bool(CONFIG_IS_ENABLED, is_enabled); config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); @@ -140,19 +114,12 @@ DFhackCExport command_result plugin_load_site_data(color_ostream& out) { clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); - DEBUG(control, out).print("loading persisted enabled state: {}\n", - is_enabled ? "true" : "false"); - if (is_enabled) - do_enable(); - return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { - DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; - do_disable(); } return CR_OK; } @@ -163,31 +130,17 @@ static void cleanupLogs(color_ostream& out) { // Collect all report IDs from unit combat/sparring/hunting logs std::unordered_set report_ids_to_remove; + bool log_types[] = {clear_combat, clear_sparring, clear_hunting}; for (auto unit : world->units.all) { - // Combat logs (index 0) - if (clear_combat) { - auto& log = unit->reports.log[0]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); - } - log.clear(); - } - // Sparring logs (index 1) - if (clear_sparring) { - auto& log = unit->reports.log[1]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); + for (int log_idx = 0; log_idx < 3; log_idx++) { + if (log_types[log_idx]) { + auto& log = unit->reports.log[log_idx]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); } - log.clear(); - } - // Hunting logs (index 2) - if (clear_hunting) { - auto& log = unit->reports.log[2]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); - } - log.clear(); } } @@ -197,8 +150,6 @@ static void cleanupLogs(color_ostream& out) { // Remove collected reports from global buffers auto& reports = world->status.reports; - int reports_erased = 0; - for (auto report_id : report_ids_to_remove) { df::report* report = df::report::find(report_id); if (!report) @@ -208,7 +159,6 @@ static void cleanupLogs(color_ostream& out) { if (it != reports.end()) { delete report; reports.erase(it); - reports_erased++; } } } diff --git a/plugins/lua/logcleaner.lua b/plugins/lua/logcleaner.lua index f7fa328ea6..104c23ab88 100644 --- a/plugins/lua/logcleaner.lua +++ b/plugins/lua/logcleaner.lua @@ -17,32 +17,51 @@ function parse_commandline(...) return true end + -- Handle enable/disable commands + if command == 'enable' then + if isEnabled() then + print('logcleaner is already enabled') + else + dfhack.run_command('enable', 'logcleaner') + print('logcleaner enabled') + end + return true + end + + if command == 'disable' then + if not isEnabled() then + print('logcleaner is already disabled') + else + dfhack.run_command('disable', 'logcleaner') + print('logcleaner disabled') + end + return true + end + -- Start with all disabled, enable only what's specified local new_combat, new_sparring, new_hunting = false, false, false local has_filter = false - for _, param in ipairs(args) do - if param == 'all' then - new_combat, new_sparring, new_hunting = true, true, true - has_filter = true - elseif param == 'none' then - new_combat, new_sparring, new_hunting = false, false, false - else - -- Split by comma for multiple options in one parameter - for token in param:gmatch('([^,]+)') do - if token == 'combat' then - new_combat = true - has_filter = true - elseif token == 'sparring' then - new_sparring = true - has_filter = true - elseif token == 'hunting' then - new_hunting = true - has_filter = true - else - dfhack.printerr('Unknown option: ' .. token) - return false - end + if command == 'all' then + new_combat, new_sparring, new_hunting = true, true, true + has_filter = true + elseif command == 'none' then + new_combat, new_sparring, new_hunting = false, false, false + else + -- Split by comma for multiple options in one parameter + for token in command:gmatch('([^,]+)') do + if token == 'combat' then + new_combat = true + has_filter = true + elseif token == 'sparring' then + new_sparring = true + has_filter = true + elseif token == 'hunting' then + new_hunting = true + has_filter = true + else + dfhack.printerr('Unknown option: ' .. token) + return false end end end From 47e9d9b956facc5529cc50de245190b4e9cb2d73 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:52:55 +0000 Subject: [PATCH 062/333] Auto-update structures ref for 53.10 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 2b9e59ea39..3d1ee6456c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2b9e59ea390c9e6ee234c7da7fc7832f8ccb161b +Subproject commit 3d1ee6456c7739612c6f920f4e165866a299275b From b1185f65255f9b0b764b45a298c1c9a3bf5d24c2 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:55:40 +0000 Subject: [PATCH 063/333] Auto-update structures ref for 53.10 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 3d1ee6456c..56511218a5 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3d1ee6456c7739612c6f920f4e165866a299275b +Subproject commit 56511218a5b1eb1cb494a22a7f5e9753cbf7cd87 From 281cfefd6436cebf8d02adf0e0ebaba7f52d811b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:59:53 +0000 Subject: [PATCH 064/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8359ac6759..0b7816249f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8359ac67595ebdb2b83ba27225856834732b43f3 +Subproject commit 0b7816249ff0461c7d171e0cece44311c3015f61 From a62e75b4c99f03e5b34b6a52427300192a4f5afc Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:15:29 +0000 Subject: [PATCH 065/333] Auto-update structures ref for 53.10 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 56511218a5..3826f45ef0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 56511218a5b1eb1cb494a22a7f5e9753cbf7cd87 +Subproject commit 3826f45ef0fad7bd3357a6d55d5c9d28b56614c2 From 20c4278faf98f2ec2a1b8c1959ca95f4d4819452 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 12 Jan 2026 09:33:38 -0600 Subject: [PATCH 066/333] 53.10-r1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cd9d70f567..ea1e613556 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.09") +set(DF_VERSION "53.10") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index 77478e3f32..9481bbbbd8 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.10-r1 + +## New Tools + +## New Features + ## Fixes - `autochop`: the report will no longer throw a C++ exception when burrows are defined. - `suspendmanager`: Fix the overlay appearing where it should not when following a unit From 28129e3a23697f679d55bd7e6bd353c2e01279a9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 12 Jan 2026 09:35:07 -0600 Subject: [PATCH 067/333] Update scripts --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 0b7816249f..ff1b95a7b4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 0b7816249ff0461c7d171e0cece44311c3015f61 +Subproject commit ff1b95a7b4e97be9a94218162c099dd95eaf4680 From 9c28ca8a5e6314f1dfda36a4a5d41b1960f4a75c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Mon, 12 Jan 2026 17:46:06 +0100 Subject: [PATCH 068/333] Add constant for frequency. Remove unused variables --- plugins/logcleaner/logcleaner.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index 56804613ad..fc00a9cb13 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -31,7 +31,9 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -static void cleanupLogs(color_ostream& out); +static const int32_t CLEANUP_TICK_INTERVAL = 97; + +static void cleanupLogs(); static command_result do_command(color_ostream& out, std::vector& params); // Getter functions for Lua @@ -124,7 +126,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_chan return CR_OK; } -static void cleanupLogs(color_ostream& out) { +static void cleanupLogs() { if (!is_enabled || !world) return; @@ -170,9 +172,9 @@ DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_ev return CR_OK; tick_counter++; - if (tick_counter >= 100) { + if (tick_counter >= CLEANUP_TICK_INTERVAL) { tick_counter = 0; - cleanupLogs(out); + cleanupLogs(); } return CR_OK; From e2cc7351f6288190a8136667a5f6e329f3c22062 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 16 Jan 2026 14:10:07 -0600 Subject: [PATCH 069/333] add vtable validation doReadClassName will now first validate that the vtable pointer points to mapped memory before attempting to read it, and throws an exception if it does not --- docs/changelog.txt | 1 + library/DataDefs.cpp | 2 +- library/Process.cpp | 17 +++++++++++++++++ library/include/MemAccess.h | 5 ++++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9481bbbbd8..e1b0e51eec 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- Core: DFHack now validates vtable pointers in objects read from memory and will throw an exception instead of crashing when an invalid vtable pointer is encountered. This makes it easier to identify which DF data structure contains corrupted data when this manifests in the form of a bad vtable pointer, and shifts blame for such crashes from DFHack to DF. ## Documentation diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 54cdfff982..880a8b93a5 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -370,7 +370,7 @@ const virtual_identity *virtual_identity::find(void *vtable) // If using a reader/writer lock, re-grab as write here, and recheck Core &core = Core::getInstance(); - std::string name = core.p->doReadClassName(vtable); + std::string name = core.p->readClassName(vtable); auto name_it = (*name_lookup).find(name); if (name_it != (*name_lookup).end()) { diff --git a/library/Process.cpp b/library/Process.cpp index 3e8ae8de8d..c60f3c8038 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -236,6 +236,9 @@ Process::~Process() string Process::doReadClassName (void * vptr) { + if (!checkValidAddress(vptr)) + throw std::runtime_error(std::format("invalid vtable ptr {}", vptr)); + char* rtti = Process::readPtr(((char*)vptr - sizeof(void*))); #ifndef WIN32 char* typestring = Process::readPtr(rtti + sizeof(void*)); @@ -591,6 +594,20 @@ void Process::getMemRanges(vector& ranges) } #endif +bool Process::checkValidAddress(void* ptr) +{ + uintptr_t addr = reinterpret_cast(ptr); + auto validate = [&] (t_memrange& r) { + uintptr_t lo = reinterpret_cast(r.start); + uintptr_t hi = reinterpret_cast(r.end); + return addr >= lo && addr < hi; + }; + std::vector mr; + getMemRanges(mr); + bool valid = std::any_of(mr.begin(), mr.end(), validate); + return valid; +} + uintptr_t Process::getBase() { #if WIN32 diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 19d65468ed..5115348ba2 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -221,7 +221,7 @@ namespace DFHack std::string readClassName(void* vptr) { - std::map::iterator it = classNameCache.find(vptr); + auto it = classNameCache.find(vptr); if (it != classNameCache.end()) return it->second; return classNameCache[vptr] = doReadClassName(vptr); @@ -247,6 +247,9 @@ namespace DFHack /// get virtual memory ranges of the process (what is mapped where) static void getMemRanges(std::vector& ranges); + /// check if an address has a mapping + bool checkValidAddress(void* ptr); + /// get the symbol table extension of this process std::shared_ptr getDescriptor() { From 9020877ab253642727e10c018668e536c4686af0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 16 Jan 2026 14:19:28 -0600 Subject: [PATCH 070/333] duh forgot to shift brain into dfhack gear --- library/Process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Process.cpp b/library/Process.cpp index c60f3c8038..3e3ba6db80 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -237,7 +237,7 @@ Process::~Process() string Process::doReadClassName (void * vptr) { if (!checkValidAddress(vptr)) - throw std::runtime_error(std::format("invalid vtable ptr {}", vptr)); + throw std::runtime_error(fmt::format("invalid vtable ptr {}", vptr)); char* rtti = Process::readPtr(((char*)vptr - sizeof(void*))); #ifndef WIN32 From 09d3789689c92830c88a1cbbe4ee507f90e935e5 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 20 Jan 2026 07:29:23 +0000 Subject: [PATCH 071/333] 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 50190a34de..7f8db9d9c1 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 50190a34de760732a4d8f926f28df9048fd0a5e2 +Subproject commit 7f8db9d9c1225a396678ca4057e9caaae267666f From 58af0b9d4cf064043d3ff2d8ae29508f5d007b42 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 27 Jan 2026 07:29:27 +0000 Subject: [PATCH 072/333] 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 7f8db9d9c1..efe712b62d 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 7f8db9d9c1225a396678ca4057e9caaae267666f +Subproject commit efe712b62d50b1f537558168701af8e6ed2bdece From 153da7d9a783728f9e2f71e50bdfd24807a3cb7b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:39:49 +0000 Subject: [PATCH 073/333] [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.36.0 → 0.36.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.0...0.36.1) - [github.com/Lucas-C/pre-commit-hooks: v1.5.5 → v1.5.6](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.5.5...v1.5.6) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d77a667a4..c47c9ccfdf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.0 + rev: 0.36.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 + rev: v1.5.6 hooks: - id: forbid-tabs exclude_types: From edda03064e9769fd92c96d2595fdc98e2d9ef9b5 Mon Sep 17 00:00:00 2001 From: Quietust Date: Wed, 14 Jan 2026 17:11:20 -0600 Subject: [PATCH 074/333] Add library functions for placing spatters Required for #5703 --- docs/changelog.txt | 6 + docs/dev/Lua API.rst | 16 +++ library/LuaApi.cpp | 36 +++++ library/include/modules/Items.h | 6 + library/include/modules/Maps.h | 5 + library/modules/Items.cpp | 87 ++++++----- library/modules/Maps.cpp | 246 ++++++++++++++++++++++++++++++++ 7 files changed, 363 insertions(+), 39 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9481bbbbd8..b568d027cf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,8 +65,14 @@ Template for new versions: ## Documentation ## API +- Added ``Maps::addMaterialSpatter``: add a spatter of the specified material + state to the indicated tile, returning whatever amount wouldn't fit in the tile. +- Added ``Maps::addItemSpatter``: add a spatter of the specified item + material + growth print to the indicated tile, returning whatever amount wouldn't fit in the tile. +- Added ``Items::pickGrowthPrint``: given a plant material and a growth index, returns the print variant corresponding to the current in-game time. +- Added ``Items::useStandardMaterial``: given an item type, returns true if the item is made of a specific material and false if it has a race and caste instead. ## Lua +- Added ``Maps::addMaterialSpatter`` as ``dfhack.maps.addMaterialSpatter``. +- Added ``Maps::addItemSpatter`` as ``dfhack.maps.addItemSpatter``. ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 52fb7c778f..4995e420c7 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2486,6 +2486,22 @@ Maps module Removes an aquifer from the given tile position. Returns *true* or *false* depending on success. +* ``dfhack.maps.addMaterialSpatter(pos, mat, matg, state, amount)`` + + Adds a material spatter to the specified map tile. If the tile is already + full of that spatter, returns the amount left over. + + Specifying a state of -1 (None) will automatically choose either Solid, + Liquid, or Gas based on the material properties and the tile temperature. + +* ``dfhack.maps.addItemSpatter(pos, i_type, i_subtype, subcat1, subcat2, print_variant, amount)`` + + Adds an item spatter to the specified map tile. If the tile is already + full of that spatter, returns the amount left over. + + For plant growths, specifying a print_variant of -1 will automatically + choose an appropriate value. For other item types, this field is ignored. + Burrows module -------------- diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4875b934c6..ce2a8d2481 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2781,6 +2781,40 @@ static int maps_removeTileAquifer(lua_State* L) return 1; } +static int maps_addMaterialSpatter(lua_State *L) +{ + int32_t rv; + df::coord pos; + + Lua::CheckDFAssign(L, &pos, 1); + int16_t mat = lua_tointeger(L, 2); + int32_t matg = lua_tointeger(L, 3); + df::matter_state state = (df::matter_state)lua_tointeger(L, 4); + int32_t amount = lua_tointeger(L, 5); + rv = Maps::addMaterialSpatter(pos, mat, matg, state, amount); + + lua_pushinteger(L, rv); + return 1; +} + +static int maps_addItemSpatter(lua_State *L) +{ + int32_t rv; + df::coord pos; + + Lua::CheckDFAssign(L, &pos, 1); + df::item_type i_type = (df::item_type)lua_tointeger(L, 2); + int16_t i_subtype = lua_tointeger(L, 3); + int16_t i_subcat1 = lua_tointeger(L, 4); + int32_t i_subcat2 = lua_tointeger(L, 5); + int32_t print_variant = lua_tointeger(L, 6); + int32_t amount = lua_tointeger(L, 7); + rv = Maps::addItemSpatter(pos, i_type, i_subtype, i_subcat1, i_subcat2, print_variant, amount); + + lua_pushinteger(L, rv); + return 1; +} + static const luaL_Reg dfhack_maps_funcs[] = { { "isValidTilePos", maps_isValidTilePos }, { "isTileVisible", maps_isTileVisible }, @@ -2796,6 +2830,8 @@ static const luaL_Reg dfhack_maps_funcs[] = { { "isTileHeavyAquifer", maps_isTileHeavyAquifer }, { "setTileAquifer", maps_setTileAquifer }, { "removeTileAquifer", maps_removeTileAquifer }, + { "addMaterialSpatter", maps_addMaterialSpatter }, + { "addItemSpatter", maps_addItemSpatter }, { NULL, NULL } }; diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index b4235df11d..52af19e680 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -170,6 +170,9 @@ DFHACK_EXPORT int getItemBaseValue(int16_t item_type, int16_t item_subtype, int1 // Gets the value of a specific item, taking into account civ values and trade agreements if a caravan is given. DFHACK_EXPORT int getValue(df::item *item, df::caravan_state *caravan = NULL); +// Automatically choose a growth print variant for the specified plant growth subtype+material +DFHACK_EXPORT int32_t pickGrowthPrint(int16_t subtype, int16_t mat, int32_t matg); + DFHACK_EXPORT bool createItem(std::vector &out_items, df::unit *creator, df::item_type type, int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor = false, int32_t count = 1); @@ -186,6 +189,9 @@ DFHACK_EXPORT bool markForTrade(df::item *item, df::building_tradedepotst *depot // Returns true if an active caravan will pay extra for the given item. DFHACK_EXPORT bool isRequestedTradeGood(df::item *item, df::caravan_state *caravan = NULL); +// DF standard_material_itemtype - returns true if item has material/matgloss, false if race+caste +DFHACK_EXPORT bool usesStandardMaterial(df::item_type item_type); + // Returns true if the item can currently be melted. If game_ui, then able to be marked is enough. DFHACK_EXPORT bool canMelt(df::item *item, bool game_ui = false); // Marks the item for melting. diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index 052dbe3aab..1e7eac89e1 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -38,6 +38,7 @@ distribution. #include "df/block_flags.h" #include "df/feature_type.h" #include "df/flow_type.h" +#include "df/matter_state.h" #include "df/tile_dig_designation.h" #include "df/tiletype.h" @@ -372,6 +373,10 @@ extern DFHACK_EXPORT bool SortBlockEvents(df::map_block *block, std::vector *priorities = 0 ); +// Add spatters at the specified location, returning the amount that couldn't be placed (e.g. due to overflow) +extern DFHACK_EXPORT int32_t addMaterialSpatter (df::coord pos, int16_t mat, int32_t matg, df::matter_state state, int32_t amount); +extern DFHACK_EXPORT int32_t addItemSpatter (df::coord pos, df::item_type i_type, int16_t i_subtype, int16_t i_subcat1, int32_t i_subcat2, int32_t print_variant, int32_t amount); + // Remove a block event from the block by address. extern DFHACK_EXPORT bool RemoveBlockEvent(int32_t x, int32_t y, int32_t z, df::block_square_event *which ); extern DFHACK_EXPORT bool RemoveBlockEvent(uint32_t x, uint32_t y, uint32_t z, df::block_square_event *which ); // TODO: deprecate me diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 6acd3b9401..adce8ba8a6 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1752,6 +1752,37 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { return value; } +// Automatically choose a growth print variant for the specified plant growth subtype+material +int32_t Items::pickGrowthPrint(int16_t subtype, int16_t mat, int32_t matg) +{ + int growth_print = -1; + // Make sure it's made of a valid plant material, then grab its definition + if (mat >= 419 && mat <= 618 && matg >= 0 && (unsigned)matg < world->raws.plants.all.size()) + { + auto plant_def = world->raws.plants.all[matg]; + // Make sure it subtype is also valid + if (subtype >= 0 && (unsigned)subtype < plant_def->growths.size()) + { + auto growth_def = plant_def->growths[subtype]; + // Try and find a growth print matching the current time + // (in practice, only tree leaves use this for autumn color changes) + for (size_t i = 0; i < growth_def->prints.size(); i++) + { + auto print_def = growth_def->prints[i]; + if (print_def->timing_start <= *df::global::cur_year_tick && *df::global::cur_year_tick <= print_def->timing_end) + { + growth_print = i; + break; + } + } + // If we didn't find one, then pick the first one (if it exists) + if (growth_print == -1 && !growth_def->prints.empty()) + growth_print = 0; + } + } + return growth_print; +} + bool Items::createItem(vector &out_items, df::unit *unit, df::item_type item_type, int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor, int32_t count) { // Based on Quietust's plugins/createitem.cpp @@ -1802,34 +1833,7 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t for (auto out_item : out_items) { // Plant growths need a valid "growth print", otherwise they behave oddly if (auto growth = virtual_cast(out_item)) - { - int growth_print = -1; - // Make sure it's made of a valid plant material, then grab its definition - if (growth->mat_type >= 419 && growth->mat_type <= 618 && growth->mat_index >= 0 && (unsigned)growth->mat_index < world->raws.plants.all.size()) - { - auto plant_def = world->raws.plants.all[growth->mat_index]; - // Make sure it subtype is also valid - if (growth->subtype >= 0 && (unsigned)growth->subtype < plant_def->growths.size()) - { - auto growth_def = plant_def->growths[growth->subtype]; - // Try and find a growth print matching the current time - // (in practice, only tree leaves use this for autumn color changes) - for (size_t i = 0; i < growth_def->prints.size(); i++) - { - auto print_def = growth_def->prints[i]; - if (print_def->timing_start <= *df::global::cur_year_tick && *df::global::cur_year_tick <= print_def->timing_end) - { - growth_print = i; - break; - } - } - // If we didn't find one, then pick the first one (if it exists) - if (growth_print == -1 && !growth_def->prints.empty()) - growth_print = 0; - } - } - growth->growth_print = growth_print; - } + growth->growth_print = pickGrowthPrint(growth->subtype, growth->mat_type, growth->mat_index); if (!no_floor) out_item->moveToGround(pos.x, pos.y, pos.z); } @@ -1962,17 +1966,10 @@ bool Items::isRequestedTradeGood(df::item *item, df::caravan_state *caravan) { return false; } -/// When called with game_ui = true, this is equivalent to Bay12's itemst::meltable() -/// (i.e., returning true if and only if the item has a "designate for melting" button in game) -bool Items::canMelt(df::item *item, bool game_ui) { - CHECK_NULL_POINTER(item); - MaterialInfo mat(item); - if (mat.getCraftClass() != craft_material_class::Metal) - return false; - - switch(item->getType()) +bool Items::usesStandardMaterial(df::item_type item_type) +{ + switch(item_type) { using namespace df::enums::item_type; - // These are not meltable, even if made from metal case CORPSE: case CORPSEPIECE: case REMAINS: @@ -1984,8 +1981,20 @@ bool Items::canMelt(df::item *item, bool game_ui) { case EGG: return false; default: - break; + return true; } +} + +/// When called with game_ui = true, this is equivalent to Bay12's itemst::meltable() +/// (i.e., returning true if and only if the item has a "designate for melting" button in game) +bool Items::canMelt(df::item *item, bool game_ui) { + CHECK_NULL_POINTER(item); + MaterialInfo mat(item); + if (mat.getCraftClass() != craft_material_class::Metal) + return false; + + if (!usesStandardMaterial(item->getType())) + return false; if (item->flags.bits.artifact) return false; diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 7e5707fccb..6f449dc2fc 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -42,6 +42,9 @@ distribution. #include "df/block_burrow.h" #include "df/block_burrow_link.h" #include "df/block_square_event_grassst.h" +#include "df/block_square_event_item_spatterst.h" +#include "df/block_square_event_material_spatterst.h" +#include "df/block_square_event_spoorst.h" #include "df/building.h" #include "df/building_type.h" #include "df/builtin_mats.h" @@ -52,6 +55,7 @@ distribution. #include "df/flow_info.h" #include "df/map_block.h" #include "df/map_block_column.h" +#include "df/material.h" #include "df/plant.h" #include "df/plant_root_tile.h" #include "df/plant_tree_info.h" @@ -656,6 +660,248 @@ bool Maps::SortBlockEvents(df::map_block *block, return true; } +// Based on worldst::add_material_spatter_tile_capped +int32_t Maps::addMaterialSpatter (df::coord pos, int16_t mat, int32_t matg, df::matter_state state, int32_t amount) +{ + // Hardcoded maximum + int32_t cap = 255; + + // Sanity checks + if (amount > cap) + amount = cap; + // DF doesn't handle negative numbers, so disallow them + if (amount < 0) + amount = 0; + + // DF rejects materials of NONE:* + if (mat == -1) + return amount; + + // Extra check: make sure the material correctly exists + MaterialInfo matinfo(mat, matg); + if (!matinfo.isValid()) + return amount; + + df::map_block *block = Maps::getTileBlock(pos); + if (!block) + return amount; + + int16_t bx = pos.x & 0xF, by = pos.y & 0xF; + + // Extra check: specify state == NONE to auto-pick based on tile temperature + // Note that this won't choose POWDER/PASTE/PRESSED + if (state == df::matter_state::None) + { + uint16_t tile = block->temperature_1[bx][by]; + uint16_t melt = matinfo.material->heat.melting_point; + uint16_t boil = matinfo.material->heat.boiling_point; + if (boil != 60001 && tile >= boil) + state = df::matter_state::Gas; + else if (melt != 60001 && tile >= melt) + state = df::matter_state::Liquid; + else + state = df::matter_state::Solid; + } + + if (amount > 0) + { + // scan all SPOOR events and clear the PRESENT flag if the type is HFID_COMBINEDCASTE_BP, ITEMT_ITEMST_ORIENT, or MESS + for (size_t i = 0; i < block->block_events.size(); i++) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::spoor) + continue; + auto spoor = (df::block_square_event_spoorst *)evt; + if (!spoor->info.flags[bx][by].bits.present) + continue; + if (spoor->info.type[bx][by] == df::spoor_type::HFID_COMBINEDCASTE_BP || + spoor->info.type[bx][by] == df::spoor_type::ITEMT_ITEMST_ORIENT || + spoor->info.type[bx][by] == df::spoor_type::MESS) + spoor->info.flags[bx][by].bits.present = false; + } + } + + // Find existing matching material spatter + df::block_square_event_material_spatterst *spatter = nullptr; + // DF: get_material_spatter_event_even_if_empty(...) + for (int i = block->block_events.size() - 1; i >= 0; i--) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::material_spatter) + continue; + auto spt = (df::block_square_event_material_spatterst *)evt; + if (spt->mat_type == mat && spt->mat_index == matg && + spt->mat_state == state) + { + spatter = spt; + break; + } + } + + // If we didn't find one, make a new one + if (!spatter) + { + spatter = df::allocate(); + spatter->mat_type = mat; + spatter->mat_index = matg; + spatter->mat_state = state; + memset(spatter->amount, 0, sizeof(spatter->amount)); + spatter->min_temperature = spatter->max_temperature = 60001; + + uint16_t melt = matinfo.material->heat.melting_point; + uint16_t boil = matinfo.material->heat.boiling_point; + + switch (state) + { using namespace df::enums::matter_state; + case Solid: + case Powder: + case Paste: + case Pressed: + if (melt != 60001) + boil = melt; + spatter->max_temperature = boil; + break; + case Liquid: + if (melt != 60001 && melt != 0) + spatter->min_temperature = melt - 1; + spatter->max_temperature = boil; + break; + // Can't really have gas spatters, but DF has this check + // presumably, DF could convert this into a flow + case Gas: + if (boil != 60001 && boil != 0) + spatter->min_temperature = boil - 1; + else if (melt != 60001 && melt != 0) + spatter->min_temperature = melt - 1; + break; + case None: + // impossible + break; + } + // DF doesn't check heatdam/colddam/ignite points here + block->block_events.push_back(spatter); + } + + int32_t newamount = spatter->amount[bx][by] + amount; + if (newamount > cap) + { + amount = newamount - cap; + newamount = cap; + } + else + amount = 0; + + spatter->amount[bx][by] = (uint8_t)newamount; + block->flags.bits.may_have_material_spatter = 1; + + return amount; +} + +// Based on worldst::add_item_spatter_tile_capped +int32_t Maps::addItemSpatter (df::coord pos, df::item_type i_type, int16_t i_subtype, int16_t i_subcat1, int32_t i_subcat2, int32_t print_variant, int32_t amount) +{ + // DF passes this as a parameter, but it's always the same + int32_t cap = 10000; + + // Sanity checks + if (amount > cap) + amount = cap; + // DF doesn't handle negative numbers, so disallow them + if (amount < 0) + amount = 0; + + df::map_block *block = Maps::getTileBlock(pos); + if (!block) + return amount; + + int16_t bx = pos.x & 0xF, by = pos.y & 0xF; + + if (amount > 0) + { + // scan all SPOOR events and clear the PRESENT flag if the type is HFID_COMBINEDCASTE_BP, ITEMT_ITEMST_ORIENT, or MESS + for (size_t i = 0; i < block->block_events.size(); i++) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::spoor) + continue; + auto spoor = (df::block_square_event_spoorst *)evt; + if (!spoor->info.flags[bx][by].bits.present) + continue; + if (spoor->info.type[bx][by] == df::spoor_type::HFID_COMBINEDCASTE_BP || + spoor->info.type[bx][by] == df::spoor_type::ITEMT_ITEMST_ORIENT || + spoor->info.type[bx][by] == df::spoor_type::MESS) + spoor->info.flags[bx][by].bits.present = false; + } + } + + // Allow auto-selecting growth print for plant growths + if (i_type == df::item_type::PLANT_GROWTH && print_variant == -1) + print_variant = Items::pickGrowthPrint(i_subtype, i_subcat1, i_subcat2); + + // Find existing matching item spatter + df::block_square_event_item_spatterst *spatter = nullptr; + // DF: get_item_spatter_event_even_if_empty(...) + for (int i = block->block_events.size() - 1; i >= 0; i--) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::item_spatter) + continue; + auto spt = (df::block_square_event_item_spatterst *)evt; + if (spt->item_type == i_type && spt->item_subtype == i_subtype && + spt->mattype == i_subcat1 && spt->matindex == i_subcat2 && + spt->print_variant == print_variant) + { + spatter = spt; + break; + } + } + + // If we didn't find one, make a new one + if (!spatter) + { + spatter = df::allocate(); + spatter->item_type = i_type; + spatter->item_subtype = i_subtype; + spatter->mattype = i_subcat1; + spatter->matindex = i_subcat2; + spatter->print_variant = print_variant; + memset(spatter->amount, 0, sizeof(spatter->amount)); + memset(spatter->flag, 0, sizeof(spatter->flag)); + spatter->min_temperature = spatter->max_temperature = 60001; + + if (Items::usesStandardMaterial(i_type)) + { + MaterialInfo info(i_subcat1, i_subcat2); + if (info.isValid()) + { + uint16_t melt = info.material->heat.melting_point; + uint16_t boil = info.material->heat.melting_point; + if (melt != 60001) + spatter->max_temperature = melt; + else + spatter->max_temperature = boil; + // DF doesn't look at the heatdam/colddam/ignite temperatures + } + } + block->block_events.push_back(spatter); + } + + int32_t newamount = spatter->amount[bx][by] + amount; + if (newamount > cap) + { + amount = newamount - cap; + newamount = cap; + } + else + amount = 0; + + spatter->amount[bx][by] = newamount; + spatter->flag[bx][by].bits.season_full_timer = 7; + block->flags.bits.may_have_item_spatter = 1; + + return amount; +} + inline bool RemoveBlockEventInline(int32_t x, int32_t y, int32_t z, df::block_square_event * which) { df::map_block *block = Maps::getBlock(x, y, z); From a2e058bd0f4905ff321777cf0cbcc5af52094c03 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 5 Feb 2026 00:59:29 -0600 Subject: [PATCH 075/333] add python setup to build-windows.yml so that jinja2 has somewhere to install to (may be necessary in the windows runner, unclear?) --- .github/workflows/build-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 4ad823e696..25e42808dc 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -68,6 +68,8 @@ jobs: name: Build win64 runs-on: windows-2022 steps: + - name: Set up Python + uses: actions/setup-python@v5 - name: Install build dependencies run: | choco install sccache From ef54ef2cc8d744125fcfe69c6d03d0eecf5fd2c4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 5 Feb 2026 01:17:48 -0600 Subject: [PATCH 076/333] python: explicitly require 3.x (we're not that picky) --- .github/workflows/build-windows.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 25e42808dc..1dec211117 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -69,7 +69,9 @@ jobs: runs-on: windows-2022 steps: - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 + with: + python-version: '3.x' - name: Install build dependencies run: | choco install sccache From 92bd3642c51b3fe015fd2167530af12b6149728f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 6 Feb 2026 07:48:34 +0000 Subject: [PATCH 077/333] 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 3826f45ef0..8042f0a357 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3826f45ef0fad7bd3357a6d55d5c9d28b56614c2 +Subproject commit 8042f0a357469d247d515893c8a01483c6c385c7 From 20240e17b4c208afad7d98d9b49b9f63ebc5cea0 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:57:25 +0000 Subject: [PATCH 078/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ff1b95a7b4..ca71e41da4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ff1b95a7b4e97be9a94218162c099dd95eaf4680 +Subproject commit ca71e41da49d6b7dbc767fab262a939b4172bfb6 From 59c900e5ae1a1cd3fa750e00d3ff627ba27bce75 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Mon, 9 Feb 2026 22:49:13 +0100 Subject: [PATCH 079/333] Add changelog entry in api section --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ca880c308..fe7bd82320 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## Documentation ## API +- `dfhack.job.getManagerOrderName()`: New function to get the display name of a manager order ## Lua From aa4d3962cdcf3e3b4e86bab107a6c7477dee8de3 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Mon, 9 Feb 2026 22:59:36 +0100 Subject: [PATCH 080/333] Fix changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7257bd62a1..04321422d8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -116,7 +116,7 @@ Template for new versions: ## Documentation ## API -- `dfhack.job.getManagerOrderName()`: New function to get the display name of a manager order +- ``dfhack.job.getManagerOrderName``: New function to get the display name of a manager order ## Lua From 7b2090017d9b338fb28efa737afecd57a86d2cee Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 10 Feb 2026 07:58:45 +0000 Subject: [PATCH 081/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ca71e41da4..1b9f8c61f7 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ca71e41da49d6b7dbc767fab262a939b4172bfb6 +Subproject commit 1b9f8c61f7615bfa361da83ed49288b305b9d3a6 From c011d31bfafd565c03d9769516755e6e28c9b4af Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 10 Feb 2026 21:13:46 +0100 Subject: [PATCH 082/333] constExpr. erase_if --- plugins/logcleaner/logcleaner.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index fc00a9cb13..770bb2f26f 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -31,7 +31,7 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -static const int32_t CLEANUP_TICK_INTERVAL = 97; +static constexpr int32_t CLEANUP_TICK_INTERVAL = 97; static void cleanupLogs(); static command_result do_command(color_ostream& out, std::vector& params); @@ -130,6 +130,9 @@ static void cleanupLogs() { if (!is_enabled || !world) return; + if (!clear_combat && !clear_sparring && !clear_hunting) + return; + // Collect all report IDs from unit combat/sparring/hunting logs std::unordered_set report_ids_to_remove; bool log_types[] = {clear_combat, clear_sparring, clear_hunting}; @@ -152,17 +155,12 @@ static void cleanupLogs() { // Remove collected reports from global buffers auto& reports = world->status.reports; - for (auto report_id : report_ids_to_remove) { - df::report* report = df::report::find(report_id); - if (!report) - continue; - - auto it = std::find(reports.begin(), reports.end(), report); - if (it != reports.end()) { - delete report; - reports.erase(it); - } - } + std::erase_if(reports, [&](df::report* report) { + if (!report || !report_ids_to_remove.contains(report->id)) + return false; + delete report; + return true; + }); } DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { From bbfabe6ab00d8736bb97d349ac2f1951a6b8a329 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 11 Feb 2026 07:55:28 +0000 Subject: [PATCH 083/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 1b9f8c61f7..103e1b394f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1b9f8c61f7615bfa361da83ed49288b305b9d3a6 +Subproject commit 103e1b394f9ba889b7909aeef45fa8281f59950b From 97e2fc5fcc6a1aae3f8a2e43c9f3c4baf6d4153a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:51:07 +0000 Subject: [PATCH 084/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 103e1b394f..16bd197d6c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 103e1b394f9ba889b7909aeef45fa8281f59950b +Subproject commit 16bd197d6ca5ca9adbe2f8cf0856448f3f5ac86b From 38ed2723d04baeb1b9f6b132a35c6bac77c88967 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 17 Feb 2026 08:51:28 -0600 Subject: [PATCH 085/333] add covering default case to `Maps::SortBlockEvents` temporary - replace with `case block_square_event_type::NONE` after dfhack/df-structures#2293 is merged --- library/modules/Maps.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 6f449dc2fc..11499a4270 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -655,6 +655,9 @@ bool Maps::SortBlockEvents(df::map_block *block, if (priorities) priorities->push_back((df::block_square_event_designation_priorityst *)evt); break; + default: + assert("Unhandled block event type" && false); // FIXME temporary - replace with NONE case after structure are updated + break; } } return true; From 36d0ace6ba809902d43293fe72d6612b5a4fe85b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:51:15 +0000 Subject: [PATCH 086/333] 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 efe712b62d..e86849a674 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit efe712b62d50b1f537558168701af8e6ed2bdece +Subproject commit e86849a674a83ff7169330e65d18857c5dbe11cb From 380c0498fc452a47da975e8d0fd810ace83bb1f6 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:07:57 +0000 Subject: [PATCH 087/333] 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 e86849a674..2667acf1a8 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit e86849a674a83ff7169330e65d18857c5dbe11cb +Subproject commit 2667acf1a8c7b2be8a37e2ecd68545f733094305 From c3ccb430c2cb7dd8e0e3f8ba546c029d3fbadb51 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:32:20 +0000 Subject: [PATCH 088/333] 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 2667acf1a8..f393d6bfc1 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 2667acf1a8c7b2be8a37e2ecd68545f733094305 +Subproject commit f393d6bfc1d30aec6beae543d3cf6a04e700e821 From 3468c748624fe369d70f931c882fa4aa73691046 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:33:51 +0000 Subject: [PATCH 089/333] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 8042f0a357..3100ca32f1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 8042f0a357469d247d515893c8a01483c6c385c7 +Subproject commit 3100ca32f12313bcd74437f381b72a14bf291b8d diff --git a/scripts b/scripts index 16bd197d6c..e88bc68286 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 16bd197d6ca5ca9adbe2f8cf0856448f3f5ac86b +Subproject commit e88bc68286d6a15b86594822d114e21d197542d8 From 0d84266e5b34375eb9ec5be31fdeb12d5882a15b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 22 Feb 2026 11:32:45 +0100 Subject: [PATCH 090/333] 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 6d2b752f6b69780261bc88741a87ff5ef25d8aa2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:20:55 +0000 Subject: [PATCH 091/333] 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 f393d6bfc1..a951081b3f 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f393d6bfc1d30aec6beae543d3cf6a04e700e821 +Subproject commit a951081b3f9edce7704841a91f1b14bb13dc27a8 diff --git a/scripts b/scripts index e88bc68286..32739c4c76 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e88bc68286d6a15b86594822d114e21d197542d8 +Subproject commit 32739c4c7607b75e6ef2fb18ddefec34b4191740 From 465833f0365ceb0e034a9b16e945ef0f72180e8a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 24 Feb 2026 11:17:40 -0600 Subject: [PATCH 092/333] up version to 53.10-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ea1e613556..f0cbce0acb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.10") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 0f0fa85c2642c0889a58dd731a6f5e3b113313a7 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 24 Feb 2026 21:01:25 +0100 Subject: [PATCH 093/333] 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 8a682c84d246683338dfba6f38f257e38fedec15 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 28 Feb 2026 15:21:46 -0800 Subject: [PATCH 094/333] Properly delete announcements --- plugins/logcleaner/logcleaner.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index 770bb2f26f..d9c0b8dce3 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -158,6 +158,8 @@ static void cleanupLogs() { std::erase_if(reports, [&](df::report* report) { if (!report || !report_ids_to_remove.contains(report->id)) return false; + if (report->flags.bits.announcement) + erase_from_vector(world->status.announcements, &df::report::id, report->id); delete report; return true; }); From aee495573408594857201a27d51ef112828d50ef Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 1 Mar 2026 23:43:37 -0800 Subject: [PATCH 095/333] Fix logcleaner to use ticks --- plugins/logcleaner/logcleaner.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index d9c0b8dce3..4b59a88d1e 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -31,7 +31,8 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -static constexpr int32_t CLEANUP_TICK_INTERVAL = 97; +static constexpr int32_t CYCLE_TICKS = 97; +static int32_t cycle_timestamp = 0; static void cleanupLogs(); static command_result do_command(color_ostream& out, std::vector& params); @@ -116,6 +117,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream& out) { clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); + cycle_timestamp = 0; return CR_OK; } @@ -166,16 +168,13 @@ static void cleanupLogs() { } DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { - static int32_t tick_counter = 0; - if (!is_enabled || !world) return CR_OK; + else if (world->frame_counter - cycle_timestamp <= CYCLE_TICKS) + return CR_OK; - tick_counter++; - if (tick_counter >= CLEANUP_TICK_INTERVAL) { - tick_counter = 0; - cleanupLogs(); - } + cycle_timestamp = world->frame_counter; + cleanupLogs(); return CR_OK; } From a12d60c432a2bbcc047577cfc6bbc5f8aa835679 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 1 Mar 2026 23:47:01 -0800 Subject: [PATCH 096/333] Update logcleaner.rst - not exactly 100 ticks --- docs/plugins/logcleaner.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/logcleaner.rst b/docs/plugins/logcleaner.rst index d9d0930d82..6e4cba07d6 100644 --- a/docs/plugins/logcleaner.rst +++ b/docs/plugins/logcleaner.rst @@ -5,8 +5,8 @@ logcleaner :tags: fort auto units This plugin prevents spam from cluttering your announcement history and filling -the 3000-item reports buffer. It runs every 100 ticks and clears selected report -types from both the global reports buffer and per-unit logs. +the 3000-item reports buffer. It runs approximately every 100 ticks and clears +selected report types from both the global reports buffer and per-unit logs. Usage ----- From 3d808ccf0e3214fb937390ca298cf251afdf251d Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 Mar 2026 00:21:07 -0800 Subject: [PATCH 097/333] Fix logcleaner and autolabor ticks offset by +1 * Update logcleaner.cpp * Update autolabor.cpp --- plugins/autolabor/autolabor.cpp | 2 +- plugins/logcleaner/logcleaner.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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; diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index 4b59a88d1e..89f84cf32f 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -170,7 +170,7 @@ static void cleanupLogs() { DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { if (!is_enabled || !world) return CR_OK; - else if (world->frame_counter - cycle_timestamp <= CYCLE_TICKS) + else if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) return CR_OK; cycle_timestamp = world->frame_counter; From a5c5a87ae40620cdad9d686002acb7a11c31c87d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 2 Mar 2026 06:00:02 -0600 Subject: [PATCH 098/333] revert change to autolabor out of scope for this PR --- plugins/autolabor/autolabor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index 4fd73ce181..a95d6636d4 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -1,4 +1,4 @@ -#include "laborstatemap.h" +g#include "laborstatemap.h" #include "Debug.h" #include "PluginManager.h" @@ -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 80b4446849f1eed4452087200b05d16e466e19bd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 2 Mar 2026 06:03:27 -0600 Subject: [PATCH 099/333] gah remove line noise --- 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 a95d6636d4..d0cec796fb 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -1,4 +1,4 @@ -g#include "laborstatemap.h" +#include "laborstatemap.h" #include "Debug.h" #include "PluginManager.h" From 7c9ec563663fbdae3c4ddabb318736abe095ae13 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 2 Mar 2026 07:03:25 -0600 Subject: [PATCH 100/333] cmakelist/changelog/submodules for 53.10-r2 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 24 +++++++++++++++++++++--- library/xml | 2 +- scripts | 2 +- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f0cbce0acb..36e18160c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.10") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DFHACK_RELEASE "r2") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3d78dd8335..23204c7218 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,24 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.10-r2 + ## New Tools - ``logcleaner``: New plugin for time-triggered clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. @@ -70,14 +88,14 @@ Template for new versions: ## Documentation ## API -- Added ``Maps::addMaterialSpatter``: add a spatter of the specified material + state to the indicated tile, returning whatever amount wouldn't fit in the tile. -- Added ``Maps::addItemSpatter``: add a spatter of the specified item + material + growth print to the indicated tile, returning whatever amount wouldn't fit in the tile. - Added ``Items::pickGrowthPrint``: given a plant material and a growth index, returns the print variant corresponding to the current in-game time. - Added ``Items::useStandardMaterial``: given an item type, returns true if the item is made of a specific material and false if it has a race and caste instead. +- Added ``Maps::addItemSpatter``: add a spatter of the specified item + material + growth print to the indicated tile, returning whatever amount wouldn't fit in the tile. +- Added ``Maps::addMaterialSpatter``: add a spatter of the specified material + state to the indicated tile, returning whatever amount wouldn't fit in the tile. ## Lua -- Added ``Maps::addMaterialSpatter`` as ``dfhack.maps.addMaterialSpatter``. - Added ``Maps::addItemSpatter`` as ``dfhack.maps.addItemSpatter``. +- Added ``Maps::addMaterialSpatter`` as ``dfhack.maps.addMaterialSpatter``. ## Removed diff --git a/library/xml b/library/xml index 3100ca32f1..b3364bb752 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3100ca32f12313bcd74437f381b72a14bf291b8d +Subproject commit b3364bb752fce026a1e217cd1871125731d6224e diff --git a/scripts b/scripts index 32739c4c76..edf2d1f92f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 32739c4c7607b75e6ef2fb18ddefec34b4191740 +Subproject commit edf2d1f92f9b68cdef590d299a3a47690ce80442 From 3213f9d9f457f495200134d9a1055018b8cff1a8 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 Mar 2026 06:17:07 -0800 Subject: [PATCH 101/333] 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 102/333] 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 23d425376a7b92f091370488c3f4c2882bc950b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:18:22 +0000 Subject: [PATCH 103/333] [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.36.1 → 0.37.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.1...0.37.0) --- .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 c47c9ccfdf..90b37493a7 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.36.1 + rev: 0.37.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 191e25296621271e9eb23bed3961dd68b8c52881 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 2 Mar 2026 18:49:39 -0600 Subject: [PATCH 104/333] add type identity support for `static-wstring` as an opaque type only --- library/DataIdentity.cpp | 2 ++ library/include/DataIdentity.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index 1bbeb64e77..346565c62f 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -33,6 +33,7 @@ namespace df { INTEGER_IDENTITY_TRAITS(unsigned long, "unsigned long"); INTEGER_IDENTITY_TRAITS(long long, "int64_t"); INTEGER_IDENTITY_TRAITS(unsigned long long, "uint64_t"); + INTEGER_IDENTITY_TRAITS(wchar_t, "wchar_t"); FLOAT_IDENTITY_TRAITS(float); FLOAT_IDENTITY_TRAITS(double); @@ -57,6 +58,7 @@ namespace df { OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(wchar_t*); const buffer_container_identity buffer_container_identity::base_instance; diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index f8fd3c6fd7..e58247fdaa 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -617,8 +617,10 @@ namespace df INTEGER_IDENTITY_TRAITS(unsigned long); INTEGER_IDENTITY_TRAITS(long long); INTEGER_IDENTITY_TRAITS(unsigned long long); + INTEGER_IDENTITY_TRAITS(wchar_t); FLOAT_IDENTITY_TRAITS(float); FLOAT_IDENTITY_TRAITS(double); + OPAQUE_IDENTITY_TRAITS(wchar_t*); OPAQUE_IDENTITY_TRAITS(std::condition_variable); OPAQUE_IDENTITY_TRAITS(std::fstream); OPAQUE_IDENTITY_TRAITS(std::mutex); From 9a81dc37f87ecfd2985e63461f4969b14b8beb69 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:43:40 +0000 Subject: [PATCH 105/333] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index b3364bb752..228ee8d136 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b3364bb752fce026a1e217cd1871125731d6224e +Subproject commit 228ee8d13642ac65f737837e31ed6b2142cb188c diff --git a/plugins/stonesense b/plugins/stonesense index a951081b3f..4760027eee 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit a951081b3f9edce7704841a91f1b14bb13dc27a8 +Subproject commit 4760027eee745d8c35cca843a2fcc46c21be326a diff --git a/scripts b/scripts index edf2d1f92f..46da47f64e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit edf2d1f92f9b68cdef590d299a3a47690ce80442 +Subproject commit 46da47f64ef1c8e7d49e257b36c59963770b6d53 From 6f5af720424dece4d7da4abe10684250488f11cb Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Wed, 4 Mar 2026 04:57:32 +0100 Subject: [PATCH 106/333] Fix spelling of 'perseverance' in sort.lua changed e to a --- plugins/lua/sort.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 978e165354..af066fb175 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -293,7 +293,7 @@ local function get_mental_stability(unit) local emotionally_obsessive = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE local humor = unit.status.current_soul.personality.traits.HUMOR local love_propensity = unit.status.current_soul.personality.traits.LOVE_PROPENSITY - local perseverence = unit.status.current_soul.personality.traits.PERSEVERENCE + local perseverance = unit.status.current_soul.personality.traits.PERSEVERANCE local politeness = unit.status.current_soul.personality.traits.POLITENESS local privacy = unit.status.current_soul.personality.traits.PRIVACY local stress_vulnerability = unit.status.current_soul.personality.traits.STRESS_VULNERABILITY @@ -315,7 +315,7 @@ local function get_mental_stability(unit) + (anxiety_propensity * -0.06) + (bravery * 0.06) + (cheer_propensity * 0.41) + (curious * -0.06) + (discord * 0.14) + (dutifulness * -0.03) + (emotionally_obsessive * -0.13) - + (humor * -0.05) + (love_propensity * 0.15) + (perseverence * -0.07) + + (humor * -0.05) + (love_propensity * 0.15) + (perseverance * -0.07) + (politeness * -0.14) + (privacy * 0.03) + (stress_vulnerability * -0.20) + (tolerant * -0.11) From 3a2dfaa09e859ee6957ba162f4db64c3252163aa Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 3 Mar 2026 22:19:42 -0600 Subject: [PATCH 107/333] add changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 23204c7218..9620704520 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## New Features ## Fixes +- `sort`: correct misspelling of ``PERSEVERENCE``; fixes "hates combat" filter in squad selection screen ## Misc Improvements From 5cd74c711bc5190653cd521c332279acba0596a5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 4 Mar 2026 10:16:17 -0600 Subject: [PATCH 108/333] Update version to 53.11 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 36e18160c7..9d6e55bd12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.10") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "53.11") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 85cdf29e27ff0b0660fc52a4eb4c3b33109c864f Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:30:02 +0000 Subject: [PATCH 109/333] Auto-update structures ref for 53.11 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 228ee8d136..491bec7440 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 228ee8d13642ac65f737837e31ed6b2142cb188c +Subproject commit 491bec744029a33abc021e893ffceac541fc89e2 From 012306d028582e01774f8529fb1fc887ff37979a Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:34:33 +0000 Subject: [PATCH 110/333] Auto-update structures ref for 53.11 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 491bec7440..228549676e 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 491bec744029a33abc021e893ffceac541fc89e2 +Subproject commit 228549676e0cc03746d6f23da66562a94f3bf59d From 7ab06c4ef523ff2322bb7ceab246036a8417a146 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:47:05 +0000 Subject: [PATCH 111/333] Auto-update structures ref for 53.11 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 228549676e..435e433fd6 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 228549676e0cc03746d6f23da66562a94f3bf59d +Subproject commit 435e433fd686c842fd53868c1648938280afeea3 From ce08f11bd689454bb32aeba16adbb8a963e3f849 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 4 Mar 2026 11:02:25 -0600 Subject: [PATCH 112/333] changelog for 53.11-r1 --- docs/changelog.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9620704520..79db327313 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,7 +33,6 @@ Template for new versions: ## New Features ## Fixes -- `sort`: correct misspelling of ``PERSEVERENCE``; fixes "hates combat" filter in squad selection screen ## Misc Improvements @@ -71,6 +70,25 @@ Template for new versions: ## Removed +# 53.11-r1 + +## New Tools + +## New Features + +## Fixes +- `sort`: correct misspelling of ``PERSEVERENCE``; fixes "hates combat" filter in squad selection screen + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 53.10-r2 ## New Tools From 64587d365681afedc01a046f93b8eaa77ea14dd1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 4 Mar 2026 11:02:39 -0600 Subject: [PATCH 113/333] update xml --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 435e433fd6..d792883748 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 435e433fd686c842fd53868c1648938280afeea3 +Subproject commit d79288374802cc63b1507900030b32231ffd244b From f766031a2bd8d33815e991e98641fc430acbf662 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 5 Mar 2026 18:59:10 -0600 Subject: [PATCH 114/333] fix windows console (#5737) always use utf-8 when writing to the Windows console --- docs/changelog.txt | 1 + library/MiscUtils.cpp | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 79db327313..09878b5110 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- General: DFHack will unconditionally use UTF-8 for the console on Windows, now that DF forces the process effective system code page to 65001 during startup ## Documentation diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index 082657ea34..dc1d4950ac 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -653,20 +653,31 @@ std::string UTF2DF(const std::string &in) out.resize(pos); return out; } - -DFHACK_EXPORT std::string DF2CONSOLE(const std::string &in) +static bool console_is_utf8() { - bool is_utf = false; #ifdef LINUX_BUILD + static bool checked = false; + static bool is_utf = false; + if (checked) + return is_utf; + std::string locale = ""; if (getenv("LANG")) locale += getenv("LANG"); if (getenv("LC_CTYPE")) locale += getenv("LC_CTYPE"); locale = toUpper_cp437(locale); - is_utf = (locale.find("UTF-8") != std::string::npos) || - (locale.find("UTF8") != std::string::npos); + is_utf = (locale.find("UTF-8") != std::string::npos) || (locale.find("UTF8") != std::string::npos); + checked = true; + return is_utf; +#else + return true; // Since DF 53.11, Windows console is always UTF-8 #endif +} + +DFHACK_EXPORT std::string DF2CONSOLE(const std::string &in) +{ + bool is_utf = console_is_utf8(); return is_utf ? DF2UTF(in) : in; } From d1db2f79e6fce3e4eb8f180ae0a4415e827d6c8c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 5 Mar 2026 23:19:42 -0600 Subject: [PATCH 115/333] update for changed `manager_order` default constructor default-initialized value for `frequency` is now `NONE`, was previously `OneTime` --- docs/changelog.txt | 1 + plugins/autoclothing.cpp | 1 + plugins/autoslab.cpp | 1 + plugins/tailor.cpp | 1 + 4 files changed, 4 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 79db327313..7174ed1649 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `autoclothing`, `autoslab`, `tailor`: orders will no longer be created with a repetition frequency of ``NONE`` ## Misc Improvements diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index 89a45ee15f..076892f9bd 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -613,6 +613,7 @@ static void add_clothing_orders() { newOrder->material_category = clothingOrder.material_category; newOrder->amount_left = amount; newOrder->amount_total = amount; + newOrder->frequency = df::workquota_frequency_type::OneTime; world->manager_orders.all.push_back(newOrder); } } diff --git a/plugins/autoslab.cpp b/plugins/autoslab.cpp index 08c4c837d1..e8c06d575d 100644 --- a/plugins/autoslab.cpp +++ b/plugins/autoslab.cpp @@ -141,6 +141,7 @@ static void createSlabJob(df::unit *unit) order->specdata.hist_figure_id = unit->hist_figure_id; order->amount_left = 1; order->amount_total = 1; + order->frequency = df::workquota_frequency_type::OneTime; world->manager_orders.all.push_back(order); } diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index efd9044024..1018999ec4 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -663,6 +663,7 @@ class Tailor { order->amount_total = c; order->status.bits.validated = false; order->status.bits.active = false; + order->frequency = df::workquota_frequency_type::OneTime; order->id = world->manager_orders.manager_order_next_id++; world->manager_orders.all.push_back(order); From 1ba56e61ea46186c4c719498de58fd4bb5d5ab1d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:47:57 +0000 Subject: [PATCH 116/333] 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 d792883748..0f9e05ac64 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d79288374802cc63b1507900030b32231ffd244b +Subproject commit 0f9e05ac64a2ac15714172361c86f28ae9ebf821 From 5b0049a53a1ddcd8905cbd5ed2adc51745d30d8e Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Tue, 10 Mar 2026 20:56:28 +0100 Subject: [PATCH 117/333] 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 118/333] 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 119/333] 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 120/333] 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 850b8ac51d8faa72a501ec06ed70293ecda498f9 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:44:05 +0000 Subject: [PATCH 121/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 46da47f64e..eb772d3b49 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 46da47f64ef1c8e7d49e257b36c59963770b6d53 +Subproject commit eb772d3b49f2019cf27ba5d6af5a3917dc285712 From 5f71a9102008294eb233dfe6b23f94f517bc0c8b Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Wed, 11 Mar 2026 11:27:53 +0100 Subject: [PATCH 122/333] 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 123/333] 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 124/333] 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 125/333] 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 126/333] 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 c92ca2af021a752752bb3527660892aafc38df78 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 13 Mar 2026 09:10:26 -0500 Subject: [PATCH 127/333] Bump version to 53.11-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ library/xml | 2 +- scripts | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d6e55bd12..93df1f3576 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.11") -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 fdde2f1639..bf2ca7d998 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.11-r2 + +## New Tools + +## New Features + ## Fixes - `autoclothing`, `autoslab`, `tailor`: orders will no longer be created with a repetition frequency of ``NONE`` diff --git a/library/xml b/library/xml index 0f9e05ac64..114321c480 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 0f9e05ac64a2ac15714172361c86f28ae9ebf821 +Subproject commit 114321c4802fb7e16e9d1092c8f2c057422a1c82 diff --git a/scripts b/scripts index eb772d3b49..70460d491c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit eb772d3b49f2019cf27ba5d6af5a3917dc285712 +Subproject commit 70460d491c0fc3e3cb03bc0907bf2fda381eaf42 From 929399a601b6b64f008ef641203c5f4fa94bfa3c Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Fri, 13 Mar 2026 15:29:19 +0100 Subject: [PATCH 128/333] 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 129/333] 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 130/333] 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 131/333] 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 132/333] 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 133/333] 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 134/333] [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 c2e2cae4c5e64751a29780ba7ed654c646b0ba92 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 15 Mar 2026 07:46:07 +0000 Subject: [PATCH 135/333] Auto-update submodules depends/dfhooks: main --- depends/dfhooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/dfhooks b/depends/dfhooks index 481dc1a12b..4c48e25a2a 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 481dc1a12b1264ef06ce95e331ef35cbfa0e6ace +Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 From 0b56dc8a8f1905965172b0a13b02ff3f0f690f56 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 22:26:23 -0500 Subject: [PATCH 136/333] replace hardcoded `hack` path with `getHackPath()` removes dependency on `hack` being in the DF CWD at runtime also use `std::filesystem::path` in `Textures` instead of `string` for pathnames --- library/include/modules/Textures.h | 2 +- library/lua/gui/textures.lua | 20 ++++++++++---------- library/modules/Textures.cpp | 6 +++--- plugins/dig.cpp | 2 +- plugins/lua/dig.lua | 2 +- plugins/lua/hotkeys.lua | 2 +- plugins/lua/suspendmanager.lua | 2 +- plugins/pathable.cpp | 2 +- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h index b820c3332d..c2038c20b3 100644 --- a/library/include/modules/Textures.h +++ b/library/include/modules/Textures.h @@ -32,7 +32,7 @@ DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface, bool reserved = fal * Load tileset from image file. * Return vector of handles to obtain valid texposes. */ -DFHACK_EXPORT std::vector loadTileset(const std::string& file, +DFHACK_EXPORT std::vector loadTileset(const std::filesystem::path file, int tile_px_w = TILE_WIDTH_PX, int tile_px_h = TILE_HEIGHT_PX, bool reserved = false); diff --git a/library/lua/gui/textures.lua b/library/lua/gui/textures.lua index 44d4f0de30..b97a10e439 100644 --- a/library/lua/gui/textures.lua +++ b/library/lua/gui/textures.lua @@ -8,16 +8,16 @@ local _ENV = mkmodule('gui.textures') -- Use these handles if you need to get dfhack standard textures. ---@type table local texpos_handles = { - green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12, true), - red_pin = dfhack.textures.loadTileset('hack/data/art/red-pin.png', 8, 12, true), - icons = dfhack.textures.loadTileset('hack/data/art/icons.png', 8, 12, true), - on_off = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true), - control_panel = dfhack.textures.loadTileset('hack/data/art/control-panel.png', 8, 12, true), - border_thin = dfhack.textures.loadTileset('hack/data/art/border-thin.png', 8, 12, true), - border_medium = dfhack.textures.loadTileset('hack/data/art/border-medium.png', 8, 12, true), - border_bold = dfhack.textures.loadTileset('hack/data/art/border-bold.png', 8, 12, true), - border_panel = dfhack.textures.loadTileset('hack/data/art/border-panel.png', 8, 12, true), - border_window = dfhack.textures.loadTileset('hack/data/art/border-window.png', 8, 12, true), + green_pin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/green-pin.png', 8, 12, true), + red_pin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/red-pin.png', 8, 12, true), + icons = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/icons.png', 8, 12, true), + on_off = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/on-off.png', 8, 12, true), + control_panel = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/control-panel.png', 8, 12, true), + border_thin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-thin.png', 8, 12, true), + border_medium = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-medium.png', 8, 12, true), + border_bold = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-bold.png', 8, 12, true), + border_panel = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-panel.png', 8, 12, true), + border_window = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-window.png', 8, 12, true), } -- Get valid texpos for preloaded texture in tileset diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index 21642a9bbe..4a1e29aeab 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -54,7 +54,7 @@ static ReservedRange reserved_range{}; static std::unordered_map g_handle_to_texpos; static std::unordered_map g_handle_to_reserved_texpos; static std::unordered_map g_handle_to_surface; -static std::unordered_map> g_tileset_to_handles; +static std::unordered_map> g_tileset_to_handles; static std::vector g_delayed_regs; static std::mutex g_adding_mutex; static std::atomic loading_state = false; @@ -195,14 +195,14 @@ TexposHandle Textures::loadTexture(SDL_Surface* surface, bool reserved) { return handle; } -std::vector Textures::loadTileset(const std::string& file, int tile_px_w, +std::vector Textures::loadTileset(const std::filesystem::path file, int tile_px_w, int tile_px_h, bool reserved) { if (g_tileset_to_handles.contains(file)) return g_tileset_to_handles[file]; if (!enabler) return std::vector{}; - SDL_Surface* surface = DFIMG_Load(file.c_str()); + SDL_Surface* surface = DFIMG_Load(file.string().c_str()); if (!surface) { ERR(textures).printerr("unable to load textures from '{}'\n", file); return std::vector{}; diff --git a/plugins/dig.cpp b/plugins/dig.cpp index 1be2a9a116..5fbf23b251 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -68,7 +68,7 @@ static bool is_painting_warm = false; static bool is_painting_damp = false; DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - textures = Textures::loadTileset("hack/data/art/damp_dig_map.png", 32, 32, true); + textures = Textures::loadTileset(Core::getInstance().getHackPath() / "data" / "art" / "damp_dig_map.png", 32, 32, true); commands.push_back(PluginCommand( "digv", diff --git a/plugins/lua/dig.lua b/plugins/lua/dig.lua index babdb3b7ab..192989d8a1 100644 --- a/plugins/lua/dig.lua +++ b/plugins/lua/dig.lua @@ -4,7 +4,7 @@ local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local toolbar_textures = dfhack.textures.loadTileset('hack/data/art/damp_dig_toolbar.png', 8, 12, true) +local toolbar_textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/damp_dig_toolbar.png', 8, 12, true) local main_if = df.global.game.main_interface local selection_rect = df.global.selection_rect diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index d0cecb5ba0..5c04fba2df 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -5,7 +5,7 @@ local helpdb = require('helpdb') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12, true) +local logo_textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/logo.png', 8, 12, true) local function get_command(cmdline) local first_word = cmdline:trim():split(' +')[1] diff --git a/plugins/lua/suspendmanager.lua b/plugins/lua/suspendmanager.lua index a7de42e5a0..86f0504ed3 100644 --- a/plugins/lua/suspendmanager.lua +++ b/plugins/lua/suspendmanager.lua @@ -155,7 +155,7 @@ end -- suspend overlay (formerly in unsuspend.lua) -local textures = dfhack.textures.loadTileset('hack/data/art/unsuspend.png', 32, 32, true) +local textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/unsuspend.png', 32, 32, true) local ok, buildingplan = pcall(require, 'plugins.buildingplan') if not ok then diff --git a/plugins/pathable.cpp b/plugins/pathable.cpp index d5983bb019..3fd1eaaad1 100644 --- a/plugins/pathable.cpp +++ b/plugins/pathable.cpp @@ -42,7 +42,7 @@ namespace DFHack { static std::vector textures; DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - textures = Textures::loadTileset("hack/data/art/pathable.png", 32, 32, true); + textures = Textures::loadTileset(Core::getInstance().getHackPath() / "data" / "art" / "pathable.png", 32, 32, true); return CR_OK; } From 5af6036e0804b535bb60061c629bf40ce5b11147 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:48:08 +0000 Subject: [PATCH 137/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 70460d491c..56c934eb5d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 70460d491c0fc3e3cb03bc0907bf2fda381eaf42 +Subproject commit 56c934eb5d4c957ca731705783a0a43397a9ba2c From 056dd45c1603da99e163bff64845a26370b3986e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 17 Mar 2026 15:09:36 -0500 Subject: [PATCH 138/333] fix more instances of `hack` hardcoding also in some places switch to using pathnames instead of strings for paths --- library/Core.cpp | 14 +++++--------- library/Process.cpp | 2 +- library/VersionInfoFactory.cpp | 5 +++-- library/include/VersionInfoFactory.h | 3 ++- library/lua/helpdb.lua | 4 ++-- library/modules/DFSteam.cpp | 6 ++++-- plugins/orders.cpp | 9 ++++----- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index a5cd996667..8979cb3eee 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -124,7 +124,7 @@ namespace DFHack { static const std::filesystem::path getConfigDefaultsPath() { - return Filesystem::getInstallDir() / "hack" / "data" / "dfhack-config-defaults"; + return Core::getInstance().getHackPath() / "data" / "dfhack-config-defaults"; }; class MainThread { @@ -492,7 +492,7 @@ void Core::getScriptPaths(std::vector *dest) if (save.size()) dest->emplace_back(df_pref_path / "save" / save / "scripts"); } - dest->emplace_back(df_install_path / "hack" / "scripts"); + dest->emplace_back(getHackPath() / "scripts"); for (auto & path : script_paths[2]) dest->emplace_back(path); for (auto & path : script_paths[1]) @@ -1054,7 +1054,7 @@ void Core::fatal (std::string output, const char * title) std::filesystem::path Core::getHackPath() { - return p->getPath() / "hack"; + return Filesystem::get_initial_cwd() / "hack"; } df::viewscreen * Core::getTopViewscreen() { @@ -1099,16 +1099,12 @@ bool Core::InitMainThread() { } // find out what we are... - #ifdef LINUX_BUILD - const char * path = "hack/symbols.xml"; - #else - const char * path = "hack\\symbols.xml"; - #endif + std::filesystem::path symbols_path = getHackPath() / "symbols.xml"; auto local_vif = std::make_unique(); std::cerr << "Identifying DF version.\n"; try { - local_vif->loadFile(path); + local_vif->loadFile(symbols_path); } catch(Error::All & err) { diff --git a/library/Process.cpp b/library/Process.cpp index 3e3ba6db80..c45e5aea45 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -664,7 +664,7 @@ uint32_t Process::getTickCount() #endif /* WIN32 */ } -std::filesystem::path Process::getPath() +[[deprecated]] std::filesystem::path Process::getPath() { #if defined(WIN32) || !defined(_DARWIN) return Filesystem::get_initial_cwd(); diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index b4f6eefc78..94e2560e37 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -29,6 +29,7 @@ distribution. #include #include #include +#include #include "VersionInfoFactory.h" #include "VersionInfo.h" @@ -226,9 +227,9 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) } // method // load the XML file with offsets -bool VersionInfoFactory::loadFile(string path_to_xml) +bool VersionInfoFactory::loadFile(std::filesystem::path path_to_xml) { - TiXmlDocument doc( path_to_xml.c_str() ); + TiXmlDocument doc( path_to_xml.string().c_str() ); std::cerr << "Loading " << path_to_xml << " ... "; //bool loadOkay = doc.LoadFile(); if (!doc.LoadFile()) diff --git a/library/include/VersionInfoFactory.h b/library/include/VersionInfoFactory.h index 060d622ecd..92a5f94e10 100644 --- a/library/include/VersionInfoFactory.h +++ b/library/include/VersionInfoFactory.h @@ -26,6 +26,7 @@ distribution. #pragma once #include +#include #include "Export.h" @@ -38,7 +39,7 @@ namespace DFHack public: VersionInfoFactory(); ~VersionInfoFactory(); - bool loadFile( std::string path_to_xml); + bool loadFile( std::filesystem::path path_to_xml); bool isInErrorState() const {return error;}; std::shared_ptr getVersionInfoByMD5(std::string md5string) const; std::shared_ptr getVersionInfoByPETimestamp(uintptr_t timestamp) const; diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index 0bd64d29cc..5af55f5697 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -5,8 +5,8 @@ local _ENV = mkmodule('helpdb') local argparse = require('argparse') -- paths -local RENDERED_PATH = 'hack/docs/docs/tools/' -local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt' +local RENDERED_PATH = dfhack.getHackPath() .. '/docs/docs/tools/' +local TAG_DEFINITIONS = dfhack.getHackPath() .. '/docs/docs/Tags.txt' -- used when reading help text embedded in script sources local SCRIPT_DOC_BEGIN = '[====[' diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 10074eef29..b7453a0db5 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -157,7 +157,8 @@ static bool launchDFHack(color_ostream& out) { si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - static LPCWSTR procname = L"hack/launchdf.exe"; + auto procpath = Core.getInstance().getHackPath() / "launchdf.exe"; + LPCWSTR procname = procpath.wstring().c_str(); static const char * env = "\0"; // note that the environment must be explicitly zeroed out and not NULL, @@ -208,7 +209,8 @@ static bool launchDFHack(color_ostream& out) { return false; } else if (pid == 0) { // child process - static const char * command = "hack/launchdf"; + auto procpath = Core.getInstance().getHackPath() / "launchdf.exe"; + char * command = procpath.string().c_str(); unsetenv("SteamAppId"); execl(command, command, NULL); _exit(EXIT_FAILURE); diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 4bdff21fe2..95932acb52 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -44,8 +44,8 @@ DFHACK_PLUGIN("orders"); REQUIRE_GLOBAL(world); -static const std::string ORDERS_DIR = "dfhack-config/orders"; -static const std::string ORDERS_LIBRARY_DIR = "hack/data/orders"; +static std::filesystem::path ORDERS_DIR = std::filesystem::path("dfhack-config") / "orders"; +static std::filesystem::path ORDERS_LIBRARY_DIR = Core::getInstance().getHackPath() / "data" / "orders"; static command_result orders_command(color_ostream & out, std::vector & parameters); @@ -506,7 +506,7 @@ static command_result orders_export_command(color_ostream & out, const std::stri Filesystem::mkdir(ORDERS_DIR); - std::ofstream file(ORDERS_DIR + "/" + name + ".json"); + std::ofstream file(ORDERS_DIR / ( name + ".json")); file << orders << std::endl; @@ -924,8 +924,7 @@ static command_result orders_import_command(color_ostream & out, const std::stri return CR_WRONG_USAGE; } - const std::string filename((is_library ? ORDERS_LIBRARY_DIR : ORDERS_DIR) + - "/" + fname + ".json"); + auto filename((is_library ? ORDERS_LIBRARY_DIR : ORDERS_DIR) / (fname + ".json")); Json::Value orders; { From d959609a64e5d5c281dd956cc6e53add461f4a14 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 17 Mar 2026 15:15:02 -0500 Subject: [PATCH 139/333] fix typo --- library/modules/DFSteam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index b7453a0db5..7e42414c72 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -209,7 +209,7 @@ static bool launchDFHack(color_ostream& out) { return false; } else if (pid == 0) { // child process - auto procpath = Core.getInstance().getHackPath() / "launchdf.exe"; + auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; char * command = procpath.string().c_str(); unsetenv("SteamAppId"); execl(command, command, NULL); From 8afcf1b5141333c41b95ada60d3dc9314b4d8f2b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 17 Mar 2026 15:22:52 -0500 Subject: [PATCH 140/333] dangle me not --- library/modules/DFSteam.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 7e42414c72..400c489232 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -157,14 +157,13 @@ static bool launchDFHack(color_ostream& out) { si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - auto procpath = Core.getInstance().getHackPath() / "launchdf.exe"; - LPCWSTR procname = procpath.wstring().c_str(); + auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; static const char * env = "\0"; // note that the environment must be explicitly zeroed out and not NULL, // otherwise the launched process will inherit this process's environment, // and the Steam API in the launchdf process will think it is in DF's context. - BOOL res = CreateProcessW(procname, + BOOL res = CreateProcessW(procpath.wstring().c_str(), NULL, NULL, NULL, FALSE, 0, (LPVOID)env, NULL, &si, &pi); return !!res; @@ -210,9 +209,9 @@ static bool launchDFHack(color_ostream& out) { } else if (pid == 0) { // child process auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; - char * command = procpath.string().c_str(); + auto command = procpath.string(); unsetenv("SteamAppId"); - execl(command, command, NULL); + execl(command.c_str(), command.c_str(), NULL); _exit(EXIT_FAILURE); } From 6efba41ebbc6af5e840d85b561ac3cd0a4bc854d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 24 Mar 2026 08:45:59 -0500 Subject: [PATCH 141/333] correct executable name on linux --- library/modules/DFSteam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 400c489232..61e50a61b8 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -208,7 +208,7 @@ static bool launchDFHack(color_ostream& out) { return false; } else if (pid == 0) { // child process - auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; + auto procpath = Core::getInstance().getHackPath() / "launchdf"; auto command = procpath.string(); unsetenv("SteamAppId"); execl(command.c_str(), command.c_str(), NULL); From 93c91007da87dc9433f733b9080b7541be6b6e73 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:44:01 +0000 Subject: [PATCH 142/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 56c934eb5d..b672e4afc6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 56c934eb5d4c957ca731705783a0a43397a9ba2c +Subproject commit b672e4afc6225d4e5e414beeb13f530b8cf66b39 From 97c7eecb272aa8a625aef81d741b6cc8f1e3681c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 14 Mar 2026 08:16:18 -0500 Subject: [PATCH 143/333] split dfhack.dll out of dfhooks_dfhack.dll removes disparallelism with linux, and makes dealing with cross-folder hooking mechanics easier --- library/CMakeLists.txt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index b405469b0e..3982a1a37c 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -318,8 +318,6 @@ endif() # Compilation -add_definitions(-DBUILD_DFHACK_LIB) - if(UNIX) if(CONSOLE_NO_CATCH) add_definitions(-DCONSOLE_NO_CATCH) @@ -373,6 +371,7 @@ if(EXISTS ${dfhack_SOURCE_DIR}/.git/index AND EXISTS ${dfhack_SOURCE_DIR}/.git/m endif() add_library(dfhack SHARED ${PROJECT_SOURCES}) +target_compile_definitions(dfhack PRIVATE BUILD_DFHACK_LIB) target_include_directories(dfhack PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/proto) get_target_property(xlsxio_INCLUDES xlsxio_read_STATIC INTERFACE_INCLUDE_DIRECTORIES) @@ -381,6 +380,7 @@ add_dependencies(dfhack generate_proto_core) add_dependencies(dfhack generate_headers) add_library(dfhack-client SHARED RemoteClient.cpp ColorText.cpp MiscUtils.cpp Error.cpp ${PROJECT_PROTO_SRCS} ${CONSOLE_SOURCES}) +target_compile_definitions(dfhack-client PRIVATE BUILD_DFHACK_LIB) target_include_directories(dfhack-client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/proto) add_dependencies(dfhack-client dfhack) @@ -391,16 +391,16 @@ add_executable(binpatch binpatch.cpp) target_link_libraries(binpatch dfhack-md5) if(WIN32) - set_target_properties(dfhack PROPERTIES OUTPUT_NAME "dfhooks_dfhack" ) set_target_properties(dfhack PROPERTIES COMPILE_FLAGS "/FI\"Export.h\"" ) set_target_properties(dfhack-client PROPERTIES COMPILE_FLAGS "/FI\"Export.h\"" ) else() set_target_properties(dfhack PROPERTIES COMPILE_FLAGS "-include Export.h" ) set_target_properties(dfhack-client PROPERTIES COMPILE_FLAGS "-include Export.h" ) - add_library(dfhooks_dfhack SHARED Hooks.cpp) - target_link_libraries(dfhooks_dfhack dfhack ${FMTLIB}) endif() +add_library(dfhooks_dfhack SHARED Hooks.cpp) +target_link_libraries(dfhooks_dfhack PUBLIC dfhack ${FMTLIB}) + # effectively disables debug builds... set_target_properties(dfhack PROPERTIES DEBUG_POSTFIX "-debug" ) @@ -450,10 +450,11 @@ if(UNIX) install(PROGRAMS ${dfhack_SOURCE_DIR}/package/linux/dfhack-run DESTINATION .) endif() - install(TARGETS dfhooks_dfhack +endif() + +install(TARGETS dfhooks_dfhack LIBRARY DESTINATION . RUNTIME DESTINATION .) -endif() # install the main lib install(TARGETS dfhack From a3a26bda97160b1809afa213070533cb9ceb943c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 14 Mar 2026 11:07:24 -0500 Subject: [PATCH 144/333] update dfhooks to resteam branch --- depends/dfhooks | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 4c48e25a2a..2d84a5826c 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 +Subproject commit 2d84a5826c51e99a6ff2c7d4c530680b366044c1 diff --git a/scripts b/scripts index b672e4afc6..56c934eb5d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b672e4afc6225d4e5e414beeb13f530b8cf66b39 +Subproject commit 56c934eb5d4c957ca731705783a0a43397a9ba2c From e2bbfc9fe8a987035622503b9d8c476b8c50d164 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 14:46:29 -0500 Subject: [PATCH 145/333] Further work on Steam compatibility 1. Adopt changes to dfhooks 2. Install dfhooks_dfhack the `hack` folder, and generate and install an appropriate `dfhooks_dfhack.ini` into the DF folder 3. Adjust RPATH (on Linux) so dfhooks_dfhack.dll can find dfhack.dll 4. Use dfhooks preinit to get the hack base path. Use this path when initializing Core 5. in `getHackPath()`, use the collected hack base path instead of hardcoding `./hack`. 6. Fix at one instance where `hack` was hardcoded outside of `getHackPath` (note: there are others this commit does not fix) 7. Update VersionInfoFactory to use a fs `path` instead of a `string` as its argument --- CMakeLists.txt | 13 +++++-------- library/CMakeLists.txt | 12 +++++++++--- library/Core.cpp | 5 +++-- library/Hooks.cpp | 10 +++++++++- library/include/Core.h | 4 +++- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 93df1f3576..5694d4bd9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -226,13 +226,10 @@ set(DFHACK_DATA_DESTINATION hack) ## where to install things (after the build is done, classic 'make install' or package structure) # the dfhack libraries will be installed here: -if(UNIX) - # put the lib into DF/hack - set(DFHACK_LIBRARY_DESTINATION ${DFHACK_DATA_DESTINATION}) -else() - # windows is crap, therefore we can't do nice things with it. leave the libs on a nasty pile... - set(DFHACK_LIBRARY_DESTINATION .) -endif() + +# put the lib into DF/hack +# windows will find it because dfhooks will `AddDllDirectory` the hack folder at runtime +set(DFHACK_LIBRARY_DESTINATION ${DFHACK_DATA_DESTINATION}) # external tools will be installed here: set(DFHACK_BINARY_DESTINATION .) @@ -267,7 +264,7 @@ if(UNIX) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32 -march=i686") endif() string(REPLACE "-DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") - set(CMAKE_INSTALL_RPATH ${DFHACK_LIBRARY_DESTINATION}) + set(CMAKE_INSTALL_RPATH "$ORIGIN/${DFHACK_LIBRARY_DESTINATION}") elseif(MSVC) # for msvc, tell it to always use 8-byte pointers to member functions to avoid confusion set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm /MP") diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 3982a1a37c..f91afef2e2 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -131,7 +131,6 @@ endif() set(MAIN_SOURCES_WINDOWS ${CONSOLE_SOURCES} - Hooks.cpp ) if(WIN32) @@ -453,8 +452,9 @@ if(UNIX) endif() install(TARGETS dfhooks_dfhack - LIBRARY DESTINATION . - RUNTIME DESTINATION .) + LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} + RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) + # install the main lib install(TARGETS dfhack @@ -465,6 +465,12 @@ install(TARGETS dfhack-run dfhack-client binpatch LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/dfhooks_dfhack.ini + CONTENT "${DFHACK_DATA_DESTINATION}/$") + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/dfhooks_dfhack.ini + DESTINATION .) + endif(BUILD_LIBRARY) # install the offset file diff --git a/library/Core.cpp b/library/Core.cpp index 8979cb3eee..9fe3cf9bc6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1054,16 +1054,17 @@ void Core::fatal (std::string output, const char * title) std::filesystem::path Core::getHackPath() { - return Filesystem::get_initial_cwd() / "hack"; + return hack_path; } df::viewscreen * Core::getTopViewscreen() { return getInstance().top_viewscreen; } -bool Core::InitMainThread() { +bool Core::InitMainThread(std::filesystem::path path) { // this hook is always called from DF's main (render) thread, so capture this thread id df_render_thread = std::this_thread::get_id(); + hack_path = path; Filesystem::init(); diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 0f957fea06..31d711056b 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -7,6 +7,14 @@ static bool disabled = false; DFhackCExport const int32_t dfhooks_priority = 100; +static std::filesystem::path basepath{"./hack"}; + +// called by the chainloader before the main thread is initialized and before any other hooks are called. +DFhackCExport void dfhooks_preinit(std::filesystem::path dllpath) +{ + basepath = dllpath.parent_path(); +} + // called from the main thread before the simulation thread is started // and the main event loop is initiated DFhackCExport void dfhooks_init() { @@ -17,7 +25,7 @@ DFhackCExport void dfhooks_init() { } // we need to init DF globals before we can check the commandline - if (!DFHack::Core::getInstance().InitMainThread() || !df::global::game) { + if (!DFHack::Core::getInstance().InitMainThread(basepath) || !df::global::game) { // we don't set disabled to true here so symbol generation can work return; } diff --git a/library/include/Core.h b/library/include/Core.h index 1f3cea5837..9ccf0ca871 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -268,7 +268,7 @@ namespace DFHack struct Private; std::unique_ptr d; - bool InitMainThread(); + bool InitMainThread(std::filesystem::path path); bool InitSimulationThread(); int Update (void); int Shutdown (void); @@ -353,6 +353,8 @@ namespace DFHack uint32_t unpaused_ms; // reset to 0 on map load + std::filesystem::path hack_path; + friend class CoreService; friend class ServerConnection; friend class CoreSuspender; From 8e20fbd4eb3ffe9bbaacc4d7429fcc8ebdc4c40d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 15:23:06 -0500 Subject: [PATCH 146/333] canonicalize hack path at init --- library/Hooks.cpp | 2 +- library/include/Hooks.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 31d711056b..3f5ca36c32 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -25,7 +25,7 @@ DFhackCExport void dfhooks_init() { } // we need to init DF globals before we can check the commandline - if (!DFHack::Core::getInstance().InitMainThread(basepath) || !df::global::game) { + if (!DFHack::Core::getInstance().InitMainThread(std::filesystem::canonical(basepath)) || !df::global::game) { // we don't set disabled to true here so symbol generation can work return; } diff --git a/library/include/Hooks.h b/library/include/Hooks.h index 6945de2ea6..5f49d8daba 100644 --- a/library/include/Hooks.h +++ b/library/include/Hooks.h @@ -26,6 +26,7 @@ distribution. union SDL_Event; +DFhackCExport void dfhooks_preinit(std::filesystem::path dllpath); DFhackCExport void dfhooks_init(); DFhackCExport void dfhooks_shutdown(); DFhackCExport void dfhooks_update(); From a546399f150bb352fb57f39adbb0dcc44f3a6b31 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 15:54:22 -0500 Subject: [PATCH 147/333] test: use RPATH `$ORIGIN` on Linux --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5694d4bd9f..9be9193210 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -264,7 +264,7 @@ if(UNIX) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32 -march=i686") endif() string(REPLACE "-DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") - set(CMAKE_INSTALL_RPATH "$ORIGIN/${DFHACK_LIBRARY_DESTINATION}") + set(CMAKE_INSTALL_RPATH "$ORIGIN") elseif(MSVC) # for msvc, tell it to always use 8-byte pointers to member functions to avoid confusion set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm /MP") From 02f64aa48c7712f54e03e8e575f671836680c0be Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 21:34:16 -0500 Subject: [PATCH 148/333] add path autodetection --- library/Hooks.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 3f5ca36c32..9865ca3276 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -3,11 +3,36 @@ #include "df/gamest.h" +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +# include +#else +# include +#endif + static bool disabled = false; DFhackCExport const int32_t dfhooks_priority = 100; -static std::filesystem::path basepath{"./hack"}; +static std::filesystem::path getModulePath() +{ +#ifdef _WIN32 + HMODULE module = nullptr; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)getModulePath, &module); + if (!module) return std::filesystem::path(); // should never happen, but just in case, return an empty path instead of crashing + + wchar_t path[MAX_PATH]; + GetModuleFileNameW(module, path, MAX_PATH); + return std::filesystem::path(path); +#else + DL_info info; + dladdr(getModulePath, &info); + return std::filesystem::path(info.dli_fname); +#endif +} + +static std::filesystem::path basepath{getModulePath()}; // called by the chainloader before the main thread is initialized and before any other hooks are called. DFhackCExport void dfhooks_preinit(std::filesystem::path dllpath) From 0413b712ca06206943e0013a84363b07927b9cff Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 21:34:38 -0500 Subject: [PATCH 149/333] fix lua default path --- library/Core.cpp | 50 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 9fe3cf9bc6..d7fc3f0d7b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1092,6 +1092,7 @@ bool Core::InitMainThread(std::filesystem::path path) { std::cerr << "Build url: " << Version::dfhack_run_url() << std::endl; } std::cerr << "Starting with working directory: " << Filesystem::getcwd() << std::endl; + std::cerr << "Hack path: " << getHackPath() << std::endl; std::cerr << "Binding to SDL.\n"; if (!DFSDL::init(con)) { @@ -1233,9 +1234,9 @@ bool Core::InitSimulationThread() { // the update hook is only called from the simulation thread, so capture this thread id df_simulation_thread = std::this_thread::get_id(); - if(started) + if (started) return true; - if(errorstate) + if (errorstate) return false; // Lock the CoreSuspendMutex until the thread exits or call Core::Shutdown @@ -1277,20 +1278,20 @@ bool Core::InitSimulationThread() std::cout << "Console disabled.\n"; } } - else if(con.init(false)) + else if (con.init(false)) std::cerr << "Console is running.\n"; else std::cerr << "Console has failed to initialize!\n"; -/* - // dump offsets to a file - std::ofstream dump("offsets.log"); - if(!dump.fail()) - { - //dump << vinfo->PrintOffsets(); - dump.close(); - } - */ - // initialize data defs + /* + // dump offsets to a file + std::ofstream dump("offsets.log"); + if(!dump.fail()) + { + //dump << vinfo->PrintOffsets(); + dump.close(); + } + */ + // initialize data defs virtual_identity::Init(this); // create config directory if it doesn't already exist @@ -1307,7 +1308,8 @@ bool Core::InitSimulationThread() else { // ensure all config file directories exist before we start copying files - for (auto &entry : default_config_files) { + for (auto& entry : default_config_files) + { // skip over files if (!entry.second) continue; @@ -1317,19 +1319,22 @@ bool Core::InitSimulationThread() } // copy files from the default tree that don't already exist in the config tree - for (auto &entry : default_config_files) { + for (auto& entry : default_config_files) + { // skip over directories if (entry.second) continue; std::filesystem::path filename = entry.first; - if (!config_files.contains(filename)) { + if (!config_files.contains(filename)) + { std::filesystem::path src_file = getConfigDefaultsPath() / filename; if (!Filesystem::isfile(src_file)) continue; std::filesystem::path dest_file = getConfigPath() / filename; std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); - if (!src.good() || !dest.good()) { + if (!src.good() || !dest.good()) + { con.printerr("Copy failed: '{}'\n", filename); continue; } @@ -1340,6 +1345,17 @@ bool Core::InitSimulationThread() } } + // set lua default path if not already set + if (std::getenv("DFHACK_LUA_PATH") == nullptr) + { + std::filesystem::path lua_path = getHackPath() / "lua" / "?.lua"; +#ifdef WIN32 + _putenv_s("DFHACK_LUA_PATH", lua_path.string().c_str()); +#else + setenv("DFHACK_LUA_PATH", lua_path.string().c_str(), 1); +#endif + } + loadScriptPaths(con); // initialize common lua context From 0b1ea135a6bdecc8ab4f9735b3ddfda8a6b904f7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 22:51:37 -0500 Subject: [PATCH 150/333] `script`: try multiple locatons specifically, try in the hack path, in the hack path parent directory, and in the CWD, in that order --- library/Core.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index d7fc3f0d7b..2deba390ff 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -859,7 +859,22 @@ bool Core::loadScriptFile(color_ostream &out, std::filesystem::path fname, bool INFO(script,out) << "Running script: " << fname << std::endl; std::cerr << "Running script: " << fname << std::endl; } - std::ifstream script{ fname.c_str() }; + + auto pathlist = {getHackPath(), getHackPath().parent_path(), std::filesystem::current_path()}; + + std::filesystem::path path; + + for (auto& p : pathlist) + { + auto candidate = fname.is_relative() ? p / fname : fname; + if (std::filesystem::exists(candidate)) + { + path = candidate; + break; + } + } + + std::ifstream script{ path }; if ( !script ) { if(!silent) From 0e25a2fd28d93988c46e619244b635eba1e8cd60 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 23:04:09 -0500 Subject: [PATCH 151/333] fix typo --- library/Hooks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 9865ca3276..d37f9fad08 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -26,7 +26,7 @@ static std::filesystem::path getModulePath() GetModuleFileNameW(module, path, MAX_PATH); return std::filesystem::path(path); #else - DL_info info; + Dl_info info; dladdr(getModulePath, &info); return std::filesystem::path(info.dli_fname); #endif From 56462222b00ca499d4881b9a03652a7799b8d908 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 23:10:24 -0500 Subject: [PATCH 152/333] gcc wants an explicit cast here --- library/Hooks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index d37f9fad08..42e9f859af 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -27,7 +27,7 @@ static std::filesystem::path getModulePath() return std::filesystem::path(path); #else Dl_info info; - dladdr(getModulePath, &info); + dladdr((const void*)getModulePath, &info); return std::filesystem::path(info.dli_fname); #endif } From 1ec0bc211a9ad372c8149a5114ca0f7aee70ec07 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 26 Mar 2026 11:47:07 -0500 Subject: [PATCH 153/333] Make stonesense loadable in a relocated installation --- library/PlugLoad.cpp | 3 ++- plugins/stonesense | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 336e7f50e7..fa58d39514 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -15,10 +15,11 @@ #ifdef WIN32 #define NOMINMAX #include +#include #define global_search_handle() GetModuleHandle(nullptr) #define get_function_address(plugin, function) GetProcAddress((HMODULE)plugin, function) #define clear_error() -#define load_library(fn) LoadLibraryW(fn.c_str()) +#define load_library(fn) LoadLibraryExW(fn.wstring().c_str(), NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); #define close_library(handle) (!(FreeLibrary((HMODULE)handle))) #else #include diff --git a/plugins/stonesense b/plugins/stonesense index 4760027eee..581f8ff731 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 4760027eee745d8c35cca843a2fcc46c21be326a +Subproject commit 581f8ff7317fd15723fb31632650746afede7b6c From 2fee311c0c1f2142bb9c07cf1e21a73599b61290 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 26 Mar 2026 13:57:23 -0500 Subject: [PATCH 154/333] incorporate stonesense updates --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 581f8ff731..64cc02b123 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 581f8ff7317fd15723fb31632650746afede7b6c +Subproject commit 64cc02b12321abf9b99e41c4e5f931f71bc81a45 From 8efc6726301f084c70e66b3d702945257f04150d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 26 Mar 2026 14:48:54 -0500 Subject: [PATCH 155/333] sync stonesense --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 64cc02b123..b1b676bcf1 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 64cc02b12321abf9b99e41c4e5f931f71bc81a45 +Subproject commit b1b676bcf1dc810e7210aceef89c1626069136f5 From 1ba2130b4c9a94930989ea82094f047e87efe054 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:59:50 +0000 Subject: [PATCH 156/333] 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 4760027eee..9dfa9d731f 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 4760027eee745d8c35cca843a2fcc46c21be326a +Subproject commit 9dfa9d731f6b84b6deaba42364168b7374157e6e diff --git a/scripts b/scripts index b672e4afc6..af457f9b5c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b672e4afc6225d4e5e414beeb13f530b8cf66b39 +Subproject commit af457f9b5c86fc041a064ef5bdfbcab38f38a50f From 2d6d34ef1c7b47cd8a2915e700fe0f10c73cc79b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 27 Mar 2026 15:26:00 -0500 Subject: [PATCH 157/333] on linux, use proper rpath for plugins Co-Authored-By: Christian Doczkal <20443222+chdoc@users.noreply.github.com> --- plugins/Plugins.cmake | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 82439f69a5..192662bccc 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -141,6 +141,10 @@ macro(dfhack_plugin) set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.dll) endif() + if (UNIX) + set_target_properties(${PLUGIN_NAME} PROPERTIES INSTALL_RPATH "$ORIGIN/..") + endif() + install(TARGETS ${PLUGIN_NAME} LIBRARY DESTINATION ${DFHACK_PLUGIN_DESTINATION} RUNTIME DESTINATION ${DFHACK_PLUGIN_DESTINATION}) From bcc0d46dda602cdfbe9054aad3f185e3b2036bdd Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 31 Mar 2026 04:10:46 -0700 Subject: [PATCH 158/333] 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 159/333] 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 160/333] 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 161/333] 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 162/333] 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 4166d211fb21db8f16c40c66e859269d4640912b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 1 Apr 2026 11:36:15 -0500 Subject: [PATCH 163/333] changes to launchdf to handle relocatable install instead of assuming colocation, this version will (when both DF and DFHack are installed in Steam) automatically inject DFHack into DF environments where not both apps are installed in Steam are (hopefully) unaffected --- package/launchdf.cpp | 74 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 5a850c6cf9..c7f9b58c89 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -10,6 +10,8 @@ #include "steam_api.h" #include +#include +#include #define xstr(s) str(s) #define str(s) #s @@ -245,14 +247,19 @@ int main(int argc, char* argv[]) { } bool nowait = false; + bool installmode = false; #ifdef WIN32 std::wstring cmdline(lpCmdLine); if (cmdline.find(L"--nowait") != std::wstring::npos) nowait = true; + if (cmdline.find(L"--install") != std::wstring::npos) + installmode = true; #else for (int idx = 0; idx < argc; ++idx) { if (strcmp(argv[idx], "--nowait") == 0) nowait = true; + if (strcmp(argv[idx], "--install") == 0) + installmode = true; } #endif @@ -294,22 +301,75 @@ int main(int argc, char* argv[]) { char buf[2048] = ""; int b1 = SteamApps()->GetAppInstallDir(DFHACK_STEAM_APPID, (char*)&buf, 2048); - std::string dfhack_install_folder = (b1 != -1) ? std::string(buf) : ""; + std::filesystem::path dfhack_install_folder = (b1 != -1) ? std::string(buf) : ""; int b2 = SteamApps()->GetAppInstallDir(DF_STEAM_APPID, (char*)&buf, 2048); - std::string df_install_folder = (b2 != -1) ? std::string(buf) : ""; + std::filesystem::path df_install_folder = (b2 != -1) ? std::string(buf) : ""; + if (df_install_folder != dfhack_install_folder) + { +#ifdef WIN32 + constexpr auto dfhooks_dll_name = "dfhooks.dll"; + constexpr auto dfhook_dfhack_dll_name = "dfhooks_dfhack.dll"; +#else + constexpr auto dfhooks_dll_name = "libdfhooks.so"; + constexpr auto dfhook_dfhack_dll_name = "libdfhooks_dfhack.so"; +#endif + // DF and DFHack are not co-installed (modern case) + // inject dfhooks.dll and dfhooks_dfhack.ini into DF install folder + std::filesystem::path dfhooks_dll_src = dfhack_install_folder / dfhooks_dll_name; + std::filesystem::path dfhooks_dll_dst = df_install_folder / dfhooks_dll_name; + std::filesystem::path dfhooks_ini_dst = df_install_folder / "dfhooks_dfhack.ini"; + std::filesystem::path dfhooks_dfhack_dll_src = dfhack_install_folder / "hack" / dfhook_dfhack_dll_name; - if (df_install_folder != dfhack_install_folder) { - // DF and DFHack are not installed in the same library + std::error_code ec; + + std::filesystem::copy(dfhooks_dll_src, dfhooks_dll_dst, std::filesystem::copy_options::update_existing, ec); + if (!ec) + { + std::string indirection; + if (std::filesystem::exists(dfhooks_ini_dst)) + { + std::ifstream ini(dfhooks_ini_dst); + std::getline(ini, indirection); + } + + if (indirection != dfhooks_dfhack_dll_src.string()) + { + std::ofstream ini(dfhooks_ini_dst); + ini << dfhooks_dfhack_dll_src.string() << std::endl; + } + } + else + { #ifdef WIN32 - MessageBoxW(NULL, L"DFHack and Dwarf Fortress must be installed in the same Steam library.\nAborting.", NULL, 0); + std::wstring message{ + L"Failed to inject DFHack into Dwarf Fortress\n\n" + L"Details:\n" + std::filesystem::relative(dfhooks_dll_src).wstring() + + L" -> " + std::filesystem::relative(dfhooks_dll_dst).wstring() + + L"\n\nError code: " + std::to_wstring(ec.value()) + + L"\nError message: " + std::filesystem::relative(ec.message()).wstring() + }; + + MessageBoxW(NULL, message.c_str(), NULL, 0); #else - notify("DFHack and Dwarf Fortress must be installed in the same Steam library.\nAborting."); + std::string message{ + "Failed to inject DFHack into Dwarf Fortress\n\n" + "Details:\n" + std::filesystem::relative(dfhooks_dll_src).string() + + " -> " + std::filesystem::relative(dfhooks_dll_dst).string() + + "\n\nError code: " + std::to_string(ec.value()) + + "\nError message: " + std::filesystem::relative(ec.message()).string() + }; + + notify(message.c_str()); #endif - exit(1); + exit(1); + } } + if (installmode) + exit(0); + if (!wrap_launch(launch_via_steam)) exit(1); From e718a021e349db0f55f3b79bd31d990888361aad Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 12:33:46 -0500 Subject: [PATCH 164/333] add code to detect/clean legacy install on windows, will prompt user on linux, will advise user on how to manually clean also removed install mode - can't seem to make work with steam API rn --- package/launchdf.cpp | 106 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index c7f9b58c89..03085ed6d5 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -236,6 +236,80 @@ bool waitForDF(bool nowait) { #endif +constexpr const char* old_filelist[] { + "hack", + "stonesense", +#ifdef WIN32 + "binpatch.exe", + "dfhack-run.exe", + "allegro-5.2.dll", + "allegro_color-5.2.dll", + "allegro_font-5.2.dll", + "allegro_image-5.2.dll", + "allegro_primitives-5.2.dll", + "allegro_ttf-5.2.dll", + "allegro-5.2.dll", + "dfhack-client.dll", + "dfhooks_dfhack.dll", + "lua53.dll", + "protobuf-lite.dll" +#else + "binpatch", + "dfhack-run", + "liballegro-5.2.so", + "liballegro_color-5.2.so", + "liballegro_font-5.2.so", + "liballegro_image-5.2.so", + "liballegro_primitives-5.2.so", + "liballegro_ttf-5.2.so", + "liballegro-5.2.so", + "libdfhack-client.so", + "libdfhooks_dfhack.so", + "liblua53.so", + "libprotobuf-lite.so" +#endif +}; + +bool check_for_old_install(std::filesystem::path df_path) +{ + for (auto file : old_filelist) + { + std::filesystem::path p = df_path / file; + bool exists = std::filesystem::exists(p); +// std::wstring message = L"Checking for legacy files:\n" + p.wstring() + L": " + (exists ? L"found" : L"not found"); +// MessageBoxW(NULL, message.c_str(), L"Checking for legacy files", 0); + if (exists) + return true; + } + return false; +} + +void remove_old_install(std::filesystem::path df_path) +{ + std::string message{ + "Removing legacy files:" + }; + + for (auto file : old_filelist) + { + std::error_code ec; + + std::filesystem::path p = df_path / file; + + if (std::filesystem::is_directory(p)) + std::filesystem::remove_all(p, ec); + else if (std::filesystem::is_regular_file(p)) + std::filesystem::remove(p, ec); + else + continue; + + message += "\n" + p.string() + ": " + (ec ? "failed to remove - " + ec.message() : "removed successfully"); + } +#ifdef WIN32 + MessageBoxW(NULL, std::wstring(message.begin(), message.end()).c_str(), L"Legacy Install Cleanup", 0); +#endif +} + #ifdef WIN32 int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) { #else @@ -247,19 +321,14 @@ int main(int argc, char* argv[]) { } bool nowait = false; - bool installmode = false; #ifdef WIN32 std::wstring cmdline(lpCmdLine); if (cmdline.find(L"--nowait") != std::wstring::npos) nowait = true; - if (cmdline.find(L"--install") != std::wstring::npos) - installmode = true; #else for (int idx = 0; idx < argc; ++idx) { if (strcmp(argv[idx], "--nowait") == 0) nowait = true; - if (strcmp(argv[idx], "--install") == 0) - installmode = true; } #endif @@ -365,10 +434,30 @@ int main(int argc, char* argv[]) { #endif exit(1); } - } + bool dirty = check_for_old_install(df_install_folder); + if (dirty) + { +#ifdef WIN32 + int ok = MessageBoxW(NULL, L"A legacy install of DFHack has been detected in the Dwarf Fortress folder. This likely means that you have installed DFHack with the old Steam client (or manually). This legacy installation will almost certainly interfere with using DFHack. Do you want to remove the old files now? (recommended)", L"Legacy DFHack Install Detected", MB_OKCANCEL); - if (installmode) - exit(0); + if (ok == IDOK) + remove_old_install(df_install_folder); +#else + int response = 0; + std::string filelist; + for (auto file : old_filelist) + if (std::filesystem::exists(df_install_folder / file)) + filelist += (filelist.empty() ? "" : std::string(",")) + file; + + std::string message{ + "A legacy install of DFHack has been detected in the Dwarf Fortress directory.This likely means that you have installed DFHack with the old Steam client (or manually).This installation will almost certainly interfere with using DFHack. \n\n" + "To remove these files, run the following command: rm -r " + df_install_folder.string() + "/{ " + filelist + "}\n\n" + }; + + notify(message.c_str()); +#endif + } + } if (!wrap_launch(launch_via_steam)) exit(1); @@ -389,6 +478,5 @@ int main(int argc, char* argv[]) { usleep(1000000); #endif } - exit(0); } From 5ecdca159200723f9455d10cf62ce93125db0633 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 13:34:44 -0500 Subject: [PATCH 165/333] changes to hopefully make injection work in wine changed injection/legacy check code to before instead of after wine-specific launch code also made path detection hopefully more robust (and less wet) --- package/launchdf.cpp | 79 ++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 03085ed6d5..a2e3cbaf3e 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #define xstr(s) str(s) #define str(s) #s @@ -275,10 +276,7 @@ bool check_for_old_install(std::filesystem::path df_path) for (auto file : old_filelist) { std::filesystem::path p = df_path / file; - bool exists = std::filesystem::exists(p); -// std::wstring message = L"Checking for legacy files:\n" + p.wstring() + L": " + (exists ? L"found" : L"not found"); -// MessageBoxW(NULL, message.c_str(), L"Checking for legacy files", 0); - if (exists) + if (std::filesystem::exists(p)) return true; } return false; @@ -341,42 +339,38 @@ int main(int argc, char* argv[]) { } #ifdef WIN32 - if (is_running_on_wine()) { - // attempt launch via steam client - LPCWSTR err = launch_via_steam_posix(); - - if (err != NULL) - // steam client launch failed, attempt fallback launch - err = launch_direct(); - - if (err != NULL) - { - MessageBoxW(NULL, err, NULL, 0); - exit(1); - } - exit(0); - } + bool wine_detected = is_running_on_wine(); +#else + bool wine_detected = false; #endif - // steam detected and not running in wine + bool df_detected = SteamApps()->BIsAppInstalled(DF_STEAM_APPID); - if (!SteamApps()->BIsAppInstalled(DF_STEAM_APPID)) { + if (!df_detected) { // Steam DF is not installed. Assume DF is installed in same directory as DFHack and do a fallback launch exit(wrap_launch(launch_direct) ? 0 : 1); } - // obtain DF app path - - char buf[2048] = ""; + // obtain DF and DFHack app paths - int b1 = SteamApps()->GetAppInstallDir(DFHACK_STEAM_APPID, (char*)&buf, 2048); - std::filesystem::path dfhack_install_folder = (b1 != -1) ? std::string(buf) : ""; + auto get_app_path_from_steam = [] (AppId_t appid) -> std::optional { + char buf[2048] = ""; + int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, 2048); + if (bytes == -1) + return std::nullopt; + // steam API counts the null terminator in the byte count returned + if (buf[bytes] == '\0') bytes--; + return std::string(buf, bytes); + }; - int b2 = SteamApps()->GetAppInstallDir(DF_STEAM_APPID, (char*)&buf, 2048); - std::filesystem::path df_install_folder = (b2 != -1) ? std::string(buf) : ""; + auto opt_dfhack_install_folder = get_app_path_from_steam(DFHACK_STEAM_APPID); + auto opt_df_install_folder = get_app_path_from_steam(DF_STEAM_APPID); - if (df_install_folder != dfhack_install_folder) + if (opt_dfhack_install_folder && opt_df_install_folder && (*opt_df_install_folder != *opt_dfhack_install_folder)) { + auto& dfhack_install_folder = *opt_dfhack_install_folder; + auto& df_install_folder = *opt_df_install_folder; + #ifdef WIN32 constexpr auto dfhooks_dll_name = "dfhooks.dll"; constexpr auto dfhook_dfhack_dll_name = "dfhooks_dfhack.dll"; @@ -414,8 +408,8 @@ int main(int argc, char* argv[]) { #ifdef WIN32 std::wstring message{ L"Failed to inject DFHack into Dwarf Fortress\n\n" - L"Details:\n" + std::filesystem::relative(dfhooks_dll_src).wstring() + - L" -> " + std::filesystem::relative(dfhooks_dll_dst).wstring() + + L"Details:\n" + dfhooks_dll_src.wstring() + + L" -> " + dfhooks_dll_dst.wstring() + L"\n\nError code: " + std::to_wstring(ec.value()) + L"\nError message: " + std::filesystem::relative(ec.message()).wstring() }; @@ -424,8 +418,8 @@ int main(int argc, char* argv[]) { #else std::string message{ "Failed to inject DFHack into Dwarf Fortress\n\n" - "Details:\n" + std::filesystem::relative(dfhooks_dll_src).string() + - " -> " + std::filesystem::relative(dfhooks_dll_dst).string() + + "Details:\n" + dfhooks_dll_src.string() + + " -> " + dfhooks_dll_dst.string() + "\n\nError code: " + std::to_string(ec.value()) + "\nError message: " + std::filesystem::relative(ec.message()).string() }; @@ -459,6 +453,25 @@ int main(int argc, char* argv[]) { } } +#ifdef WIN32 + if (wine_detected) + { + // attempt launch via steam client + LPCWSTR err = launch_via_steam_posix(); + + if (err != NULL) + // steam client launch failed, attempt fallback launch + err = launch_direct(); + + if (err != NULL) + { + MessageBoxW(NULL, err, NULL, 0); + exit(1); + } + exit(0); + } +#endif + if (!wrap_launch(launch_via_steam)) exit(1); From 7afeddc100614dec6adfab8048c400260f4ccc88 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 13:36:27 -0500 Subject: [PATCH 166/333] slightly safer return check for `GetAppInstallDir` --- package/launchdf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index a2e3cbaf3e..96875e3b68 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -356,7 +356,7 @@ int main(int argc, char* argv[]) { auto get_app_path_from_steam = [] (AppId_t appid) -> std::optional { char buf[2048] = ""; int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, 2048); - if (bytes == -1) + if (bytes <= 0) return std::nullopt; // steam API counts the null terminator in the byte count returned if (buf[bytes] == '\0') bytes--; From 3483d778db665fabb764264375f03ba5763d0fba Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 13:41:13 -0500 Subject: [PATCH 167/333] move toward release candidate status --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9be9193210..083ea5bc38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r2") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r3rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From cef0742f571de6bd66d711db56c52a15488cc05c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 14:02:15 -0500 Subject: [PATCH 168/333] make gcc happy --- package/launchdf.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 96875e3b68..5ffc939e15 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -340,8 +340,6 @@ int main(int argc, char* argv[]) { #ifdef WIN32 bool wine_detected = is_running_on_wine(); -#else - bool wine_detected = false; #endif bool df_detected = SteamApps()->BIsAppInstalled(DF_STEAM_APPID); @@ -437,7 +435,6 @@ int main(int argc, char* argv[]) { if (ok == IDOK) remove_old_install(df_install_folder); #else - int response = 0; std::string filelist; for (auto file : old_filelist) if (std::filesystem::exists(df_install_folder / file)) From 6a0f9c89d5a928b930d370aaf5c2fa90fb732aa7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:59:45 +0000 Subject: [PATCH 169/333] Auto-update submodules scripts: master plugins/stonesense: master depends/dfhooks: main --- depends/dfhooks | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 2d84a5826c..4c48e25a2a 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 2d84a5826c51e99a6ff2c7d4c530680b366044c1 +Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 diff --git a/plugins/stonesense b/plugins/stonesense index b1b676bcf1..9dfa9d731f 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit b1b676bcf1dc810e7210aceef89c1626069136f5 +Subproject commit 9dfa9d731f6b84b6deaba42364168b7374157e6e diff --git a/scripts b/scripts index 56c934eb5d..af457f9b5c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 56c934eb5d4c957ca731705783a0a43397a9ba2c +Subproject commit af457f9b5c86fc041a064ef5bdfbcab38f38a50f From e88c8c16d747fa5c413be8f008d43b3e3de79ff4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 4 Apr 2026 12:24:36 -0500 Subject: [PATCH 170/333] revert inadvertent dfhooks submodule change in ff1b068 --- depends/dfhooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/dfhooks b/depends/dfhooks index 4c48e25a2a..2d84a5826c 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 +Subproject commit 2d84a5826c51e99a6ff2c7d4c530680b366044c1 From 95e89e9156ed9a1fb16233bfe6c7a0efc1e4e191 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 4 Apr 2026 12:21:20 -0500 Subject: [PATCH 171/333] use utf8 in windows console been wanting to do this for a very long time ref #1474 --- docs/changelog.txt | 1 + library/Console-windows.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf2ca7d998..36aba6f46e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## New Features ## Fixes +- Core: Windows console will always use UTF-8 regardless of system code page settings ## Misc Improvements diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 12a3e0c2eb..058cedebe0 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -474,6 +474,10 @@ bool Console::init(bool) HMENU hm = GetSystemMenu(d->ConsoleWindow,false); DeleteMenu(hm, SC_CLOSE, MF_BYCOMMAND); + // force console code pages to utf-8 + SetConsoleCP(CP_UTF8); + SetConsoleOutputCP(CP_UTF8); + // set the screen buffer to be big enough to let us scroll text GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); d->default_attributes = coninfo.wAttributes; From 731662185e022b8abbbf7d20b3beb4ab3bc01cc6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 6 Apr 2026 08:17:51 -0500 Subject: [PATCH 172/333] Update changelog.txt --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf2ca7d998..8feb15325f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,8 +59,10 @@ Template for new versions: ## New Features ## Fixes +- Steam launcher: Switch to injection strategy, allowing Dwarf Fortress and DFHack to be installed in disparate locations ## Misc Improvements +- Make DFHack relocatable so that it doesn't depend on being fully co-installed with Dwarf Fortress ## Documentation From 35735053c3ae1b19e7c2a9f1856700ab52e131c0 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:55:10 +0000 Subject: [PATCH 173/333] Auto-update submodules depends/dfhooks: main --- depends/dfhooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/dfhooks b/depends/dfhooks index 2d84a5826c..4c48e25a2a 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 2d84a5826c51e99a6ff2c7d4c530680b366044c1 +Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 From 688200286b9bb7f72f7b6454fc7fe506a53b706d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:01:17 +0000 Subject: [PATCH 174/333] Auto-update submodules depends/dfhooks: main --- depends/dfhooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/dfhooks b/depends/dfhooks index 4c48e25a2a..8a578206fb 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 +Subproject commit 8a578206fb9b1dd32b04c8c7c35217e2b83e369e From 1680442d630365d9f3c99effd9f26ae46d6412e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:17:08 +0000 Subject: [PATCH 175/333] [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.0 → 0.37.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.0...0.37.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90b37493a7..b991bfbb74 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.0 + rev: 0.37.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From bafc832f218e706bc33bb986f783ab303fe685a1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 01:16:36 -0500 Subject: [PATCH 176/333] more aggressive traiiling null trimming seems to be needed on Steam Deck --- package/launchdf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 5ffc939e15..58906af30d 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -357,7 +357,7 @@ int main(int argc, char* argv[]) { if (bytes <= 0) return std::nullopt; // steam API counts the null terminator in the byte count returned - if (buf[bytes] == '\0') bytes--; + while (bytes && buf[bytes] == '\0') bytes--; return std::string(buf, bytes); }; From 4981e7ee09d885ed6876d75e7637865c200f981e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 01:54:23 -0500 Subject: [PATCH 177/333] improve `get_app_path_from_steam`; force UTF-8 on windows --- package/launchdf.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 58906af30d..e2596331f0 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -312,6 +312,10 @@ void remove_old_install(std::filesystem::path df_path) int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) { #else int main(int argc, char* argv[]) { +#endif +#ifdef WIN32 + // force UTF-8 + std::setlocale(LC_ALL, ".utf8"); #endif // initialize steam context if (SteamAPI_RestartAppIfNecessary(DFHACK_STEAM_APPID)) { @@ -352,11 +356,12 @@ int main(int argc, char* argv[]) { // obtain DF and DFHack app paths auto get_app_path_from_steam = [] (AppId_t appid) -> std::optional { - char buf[2048] = ""; - int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, 2048); + constexpr auto BUFSIZE = 2048; + char buf[BUFSIZE] = ""; + int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, BUFSIZE); if (bytes <= 0) return std::nullopt; - // steam API counts the null terminator in the byte count returned + // steam API includes one or more null terminators after the path, so trim those off while (bytes && buf[bytes] == '\0') bytes--; return std::string(buf, bytes); }; From ba555de43675f4ec48768275c8aef0a325cc4539 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:48:10 +0000 Subject: [PATCH 178/333] 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 9dfa9d731f..f910ace3ab 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9dfa9d731f6b84b6deaba42364168b7374157e6e +Subproject commit f910ace3abea09feef79df5c211c62c91146aa37 From 174ebd48826e3bad73336cd7c24e89807d67cd00 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 20:50:44 -0500 Subject: [PATCH 179/333] fix changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 039b2acbb5..88c7546a61 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,7 +33,6 @@ Template for new versions: ## New Features ## Fixes -- Core: Windows console will always use UTF-8 regardless of system code page settings ## Misc Improvements @@ -60,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- Core: Windows console will always use UTF-8 regardless of system code page settings - Steam launcher: Switch to injection strategy, allowing Dwarf Fortress and DFHack to be installed in disparate locations ## Misc Improvements From de7fa493073d025776c2536c39b1342f96bf15c1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 21:23:51 -0500 Subject: [PATCH 180/333] Update stonesense --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index f910ace3ab..963dfd7c73 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f910ace3abea09feef79df5c211c62c91146aa37 +Subproject commit 963dfd7c73c78be921c15d86e80fea9c7910faf9 From 5d4e181ba737dcb4235beff17592d82ef1736281 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 22:04:51 -0500 Subject: [PATCH 181/333] fix stupid in launchdf --- package/launchdf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index e2596331f0..3b07abf99a 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -362,7 +362,7 @@ int main(int argc, char* argv[]) { if (bytes <= 0) return std::nullopt; // steam API includes one or more null terminators after the path, so trim those off - while (bytes && buf[bytes] == '\0') bytes--; + for (; bytes > 0 && buf[bytes-1] == '\0'; bytes--); return std::string(buf, bytes); }; From 3f6aaeb123d24472288716b755a9cb81126ec50f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 22:06:03 -0500 Subject: [PATCH 182/333] Update stonesense --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 963dfd7c73..c8ddd2c523 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 963dfd7c73c78be921c15d86e80fea9c7910faf9 +Subproject commit c8ddd2c52387d32f06d7c99d83b9303e3038b47b From 07e81d05751787c05ca1cab02a143016498096e6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Apr 2026 08:46:11 -0500 Subject: [PATCH 183/333] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 083ea5bc38..b40fd0a451 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r3rc1") +set(DFHACK_RELEASE "r3rc2") set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From ccb0aa186fe8737800b64ef1a86eef19809bb622 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Apr 2026 11:22:16 -0500 Subject: [PATCH 184/333] make `stockpiles.lua` relocatable --- 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 ac48ed6fc9..1b2955459d 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -8,7 +8,7 @@ local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local STOCKPILES_DIR = 'dfhack-config/stockpiles' -local STOCKPILES_LIBRARY_DIR = 'hack/data/stockpiles' +local STOCKPILES_LIBRARY_DIR = dfhack.getHackPath() .. 'data/stockpiles' local BAD_FILENAME_REGEX = '[^%w._]' From 8d602bc1eeada61a8b43b4a55c348aafe81c7869 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 13 Apr 2026 09:42:29 -0500 Subject: [PATCH 185/333] Change build version to 53.11-r3 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b40fd0a451..307208ddf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r3rc2") -set(DFHACK_PRERELEASE TRUE) +set(DFHACK_RELEASE "r3") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From eacfddfb2185e5ee90dd7950ef7f686973d67d64 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 13 Apr 2026 09:42:43 -0500 Subject: [PATCH 186/333] Update changelogs --- docs/changelog.txt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 88c7546a61..7fb648d8db 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,7 +52,23 @@ Template for new versions: ======== were in submodules with their own changelogs! ======== ================================================================================ -# Future +# Future## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.11-r3 ## New Tools From a835c5baa9019b62236c6b695a3aecc1a0a16ad6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 14 Apr 2026 15:44:49 -0500 Subject: [PATCH 187/333] Update CMakeLists.txt for 53.12-r1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 307208ddf2..ebc0ffe310 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r3") +set(DF_VERSION "53.12") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 848dbe8f7dbe747e7a0304d84696832ae0da3a9c Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:54:31 +0000 Subject: [PATCH 188/333] Auto-update structures ref for 53.12 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 114321c480..f1cc38c163 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 114321c4802fb7e16e9d1092c8f2c057422a1c82 +Subproject commit f1cc38c163d90cafc414f9dffd120d19d45da113 From 424140be9da9102759d0ecd27dafe6ab7d25525c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:31:26 +0000 Subject: [PATCH 189/333] 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 f1cc38c163..5c432a10a9 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f1cc38c163d90cafc414f9dffd120d19d45da113 +Subproject commit 5c432a10a9e50ee7c95bee61bee0564149b1d2bc From e96c220eb9e93a8a94f5bc76318e8909889e02d8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 14 Apr 2026 17:15:28 -0500 Subject: [PATCH 190/333] fix missing `/` in stockpiles --- 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 1b2955459d..33f6e375ad 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -8,7 +8,7 @@ local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local STOCKPILES_DIR = 'dfhack-config/stockpiles' -local STOCKPILES_LIBRARY_DIR = dfhack.getHackPath() .. 'data/stockpiles' +local STOCKPILES_LIBRARY_DIR = dfhack.getHackPath() .. '/data/stockpiles' local BAD_FILENAME_REGEX = '[^%w._]' From 7e7886e698847195b8c03c539537c3b31d155d8e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 14 Apr 2026 17:15:43 -0500 Subject: [PATCH 191/333] Update changelog for 53.12-r1 --- docs/changelog.txt | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7fb648d8db..495a5ecc92 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,11 +52,33 @@ Template for new versions: ======== were in submodules with their own changelogs! ======== ================================================================================ -# Future## New Tools +# Future + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.12-r1 + +## New Tools ## New Features +- Compatibility with Dwarf Fortress 53.12 ## Fixes +- Stockpile definitions in the default library will be correctly found and used (fixed missing path separator) ## Misc Improvements From ab5b43742761a2daa65011633e66ee7a63ed24a6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 15 Apr 2026 12:24:42 -0500 Subject: [PATCH 192/333] improve `enum_field` with more flexible casting mostly so `RemoteFortressReader` will behave with updated structures --- docs/changelog.txt | 1 + library/include/DataDefs.h | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 495a5ecc92..cfff16e5a4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,6 +65,7 @@ Template for new versions: ## Documentation ## API +- add flexible casting to ``enum_field`` to enable explicit casting to more types ## Lua diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 08d8d02097..88c4a93651 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -632,6 +632,10 @@ namespace df enum_field &operator=(EnumType ev) { value = IntType(ev); return *this; } + explicit operator IntType () const { return IntType(value); } + template + explicit operator T () const { return static_cast(IntType(value)); } + }; template From 6cb697cc66dd46d07582faa8691074a7c8cf7002 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:19:18 +0000 Subject: [PATCH 193/333] 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 5c432a10a9..cab90ab30c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 5c432a10a9e50ee7c95bee61bee0564149b1d2bc +Subproject commit cab90ab30c4f52796aec9f83ce21c2fa5974494b From 97e7aa75a399f62f45ac9e6554059e0a1fd13cda Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 00:34:24 -0500 Subject: [PATCH 194/333] `ColorText.cpp`: remove unneeded headers --- library/ColorText.cpp | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/library/ColorText.cpp b/library/ColorText.cpp index bdd3e98f44..a101d795a9 100644 --- a/library/ColorText.cpp +++ b/library/ColorText.cpp @@ -35,24 +35,12 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -#include -#include -#include -#include -#include -#include #include +#include +#include #include "ColorText.h" -#include "MiscUtils.h" - -#include -#include -#include -#include -using namespace std; using namespace DFHack; bool color_ostream::log_errors_to_stderr = false; @@ -81,7 +69,7 @@ void color_ostream::end_batch() flush_proxy(); } -color_ostream::color_ostream() : ostream(new buffer(this)), cur_color(COLOR_RESET) +color_ostream::color_ostream() : std::ostream(new buffer(this)), cur_color(COLOR_RESET) { // } From d5b1fcb4d6a0e673c8e8aaa4fdbb0fb437d136a1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:21:29 -0500 Subject: [PATCH 195/333] dedup/sort/clean `Core.cpp` includes --- library/Core.cpp | 84 ++++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 2deba390ff..d7824165c2 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -26,40 +26,43 @@ distribution. #include "Internal.h" -#include "Error.h" -#include "MemAccess.h" +#include "ColorText.h" +#include "Commands.h" +#include "Console.h" +#include "CoreDefs.h" #include "DataDefs.h" #include "Debug.h" -#include "Console.h" +#include "DFHackVersion.h" +#include "Error.h" +#include "Format.h" +#include "LuaTools.h" +#include "MemAccess.h" #include "MemoryPatcher.h" #include "MiscUtils.h" +#include "MiscUtils.h" #include "Module.h" -#include "VersionInfoFactory.h" -#include "VersionInfo.h" -#include "PluginManager.h" #include "ModuleFactory.h" +#include "PluginManager.h" #include "RemoteServer.h" #include "RemoteTools.h" -#include "LuaTools.h" -#include "DFHackVersion.h" -#include "md5wrapper.h" -#include "Format.h" - -#include "Commands.h" +#include "VersionInfo.h" +#include "VersionInfoFactory.h" #include "modules/DFSDL.h" #include "modules/DFSteam.h" #include "modules/EventManager.h" #include "modules/Filesystem.h" +#include "modules/Graphic.h" #include "modules/Gui.h" #include "modules/Hotkey.h" +#include "modules/Persistence.h" #include "modules/Textures.h" #include "modules/World.h" -#include "modules/Persistence.h" -#include "df/init.h" #include "df/gamest.h" +#include "df/global_objects.h" #include "df/graphic.h" +#include "df/init.h" #include "df/interfacest.h" #include "df/plotinfost.h" #include "df/viewscreen_dwarfmodest.h" @@ -68,35 +71,51 @@ distribution. #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_new_regionst.h" #include "df/viewscreen_savegamest.h" -#include "df/world.h" #include "df/world_data.h" +#include "df/world.h" -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include #include -#include -#include -#include +#include #include -#include -#include #include -#include -#include +#include #include -#include +#include +#include +#include +#include #include -#include -#include +#include +#include + +#include "md5wrapper.h" + +#include + #include +#include +#include +#include -#ifdef _WIN32 -#define NOMINMAX -#include +#ifdef WIN32 +#include #endif #ifdef LINUX_BUILD @@ -105,6 +124,7 @@ distribution. using namespace DFHack; using namespace df::enums; + using df::global::init; using df::global::world; using std::string; From 3d80246bc94cf2929766dbd73560144c1c9d7907 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:25:03 -0500 Subject: [PATCH 196/333] update includes in `DataIdentity.cpp` --- library/DataIdentity.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index 346565c62f..0a797c2069 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -1,14 +1,21 @@ -#include +#include "DataIdentity.h" + +#include "BitArray.h" +#include "DataDefs.h" #include +#include +#include #include +#include +#include +#include #include +#include #include -#include #include +#include -#include "DataFuncs.h" -#include "DataIdentity.h" // the space after the uses of "type" in OPAQUE_IDENTITY_TRAITS_NAME is _required_ // without it the macro generates a syntax error when type is a template specification From 7278c55a47c9b567316c42d162909d45161d5a01 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:37:39 -0500 Subject: [PATCH 197/333] update includes in `MiscUtils.cpp` --- library/MiscUtils.cpp | 64 +++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index dc1d4950ac..44f93524ee 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -22,39 +22,49 @@ must not be misrepresented as being the original software. distribution. */ -#include "Internal.h" #include "Export.h" #include "MiscUtils.h" #include "ColorText.h" -#include "modules/DFSDL.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#ifndef LINUX_BUILD -// We don't want min and max macros +#ifdef WIN32 +// Suppress warning which occurs in header on some WinSDK versions +// See dfhack/dfhack#5147 for more information #define NOMINMAX - #include - // Suppress warning which occurs in header on some WinSDK versions - // See dfhack/dfhack#5147 for more information - #pragma warning(push) - #pragma warning(disable:4091) - #include - #pragma warning(pop) -#else - #include - #include - #include +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma warning(push) +#pragma warning(disable:4091) +#include +#pragma warning(pop) #endif -#include -#include -#include -#include -#include - -#include -#include -#include -#include +#ifdef LINUX_BUILD +#include +#include +#include +#endif NumberFormatType preferred_number_format_type = NumberFormatType::DEFAULT; @@ -499,8 +509,8 @@ uint64_t GetTimeMs64() /* Character decoding */ // See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. -#define UTF8_ACCEPT 0 -#define UTF8_REJECT 12 +constexpr auto UTF8_ACCEPT = 0; +constexpr auto UTF8_REJECT = 12; static const uint8_t utf8d[] = { // The first part of the table maps bytes to character classes that From fcb02402c711c1edf442fe2be9f0cbb5aa2a6022 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:50:03 -0500 Subject: [PATCH 198/333] correction on `Core.cpp` --- library/Core.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/Core.cpp b/library/Core.cpp index d7824165c2..793824ac8e 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -115,6 +115,9 @@ distribution. #include #ifdef WIN32 +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include #include #endif From 208d550ab7385a085df291c91bb4e26985a210b7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:50:32 -0500 Subject: [PATCH 199/333] add missing include in LuaWrapper.h --- library/include/LuaWrapper.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 7576be7a11..70f36d3f95 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -25,7 +25,8 @@ distribution. #pragma once #include -#include + +#include "DataDefs.h" /** * Internal header file of the lua wrapper. From 5be4f9549252db21cb936a6ae0da7ca472c53060 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:50:53 -0500 Subject: [PATCH 200/333] remove C include from `Graphic.h` --- library/include/modules/Graphic.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/modules/Graphic.h b/library/include/modules/Graphic.h index 9fa498a7cf..5e30d89805 100644 --- a/library/include/modules/Graphic.h +++ b/library/include/modules/Graphic.h @@ -30,9 +30,9 @@ distribution. #ifndef CL_MOD_GRAPHIC #define CL_MOD_GRAPHIC -#include #include "Export.h" #include "Module.h" + #include namespace DFHack From 371be6a8addd444b893a4813158ecda225d26dd8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:51:55 -0500 Subject: [PATCH 201/333] update includes in `PluginManager.cpp` --- library/PluginManager.cpp | 50 ++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index b7e0b2c3c4..05486e885d 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -22,36 +22,48 @@ must not be misrepresented as being the original software. distribution. */ -#include "modules/EventManager.h" -#include "modules/Filesystem.h" -#include "modules/Screen.h" -#include "modules/World.h" -#include "Internal.h" +#include "PluginManager.h" + +#include "ColorText.h" #include "Core.h" +#include "CoreDefs.h" +#include "LuaWrapper.h" +#include "LuaTools.h" #include "MemAccess.h" -#include "PluginManager.h" +#include "MiscUtils.h" #include "RemoteServer.h" -#include "Console.h" #include "Types.h" #include "VersionInfo.h" -#include "DataDefs.h" -#include "MiscUtils.h" -#include "DFHackVersion.h" - -#include "LuaWrapper.h" -#include "LuaTools.h" - -using namespace DFHack; +#include "modules/EventManager.h" +#include "modules/Filesystem.h" +#include "modules/Screen.h" +#include "modules/World.h" +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include -using std::string; +#include +#include + +#include "df/viewscreen.h" -#include +#include +#include + +using namespace DFHack; +using std::string; #if defined(_LINUX) static const string plugin_suffix = ".plug.so"; @@ -871,7 +883,7 @@ void PluginManager::init() loadAll(); bool any_loaded = false; - for (auto p : all_plugins) + for (auto& p : all_plugins) { if (p.second->getState() == Plugin::PS_LOADED) { From 21d865acb19b3f2b2dd2bed168ac118788455530 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:53:47 -0500 Subject: [PATCH 202/333] clean headers in `PlugLoad.cpp` --- library/PlugLoad.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index fa58d39514..6b4dc9d451 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -1,19 +1,15 @@ -#include "Core.h" #include "Debug.h" -#include "Export.h" #include "PluginManager.h" -#include "Hooks.h" #include #include #include #include -#include -#include #ifdef WIN32 #define NOMINMAX +#define WIN32_LEAN_AND_MEAN #include #include #define global_search_handle() GetModuleHandle(nullptr) From e363b443701886b1e89a6c0af9ddfc23d1a8a520 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:05:31 -0500 Subject: [PATCH 203/333] adjust includes in `PlugLoad.cpp` --- library/PlugLoad.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 6b4dc9d451..d3b7560773 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -2,10 +2,7 @@ #include "PluginManager.h" #include -#include #include -#include - #ifdef WIN32 #define NOMINMAX From b14b82dc0e0824e175b7fc16167fd0b7f52b0bfc Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:06:22 -0500 Subject: [PATCH 204/333] includes in `Process.cpp` --- library/Process.cpp | 56 ++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/library/Process.cpp b/library/Process.cpp index c45e5aea45..1ff63622f0 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -22,22 +22,30 @@ must not be misrepresented as being the original software. distribution. */ -#ifndef WIN32 -#ifndef _DARWIN -#include -#endif /* ! _DARWIN */ -#endif /* ! WIN32 */ +#include "Format.h" +#include "MemAccess.h" +#include "Memory.h" +#include "MemoryPatcher.h" +#include "MiscUtils.h" +#include "VersionInfo.h" +#include "VersionInfoFactory.h" + +#include "modules/Filesystem.h" + +#include +#include +#include #include -#include +#include +#include +#include #include -#include +#include +#include #include #include -#include - -#include "Format.h" -#ifndef WIN32 +#ifdef LINUX_BUILD #include #include #include @@ -53,28 +61,24 @@ distribution. #include #include #endif /* _DARWIN */ -#endif /* ! WIN32 */ -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MemoryPatcher.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "modules/Filesystem.h" - -#ifndef WIN32 #include "md5wrapper.h" -#else /* WIN32 */ +#endif /* LINUX_BUILD */ + +#ifdef WIN32 #define _WIN32_WINNT 0x0600 #define WINVER 0x0600 + +#define NOMINMAX #define WIN32_LEAN_AND_MEAN #include #include +#include +#include +#include + +#include -#include #endif /* WIN32 */ using namespace DFHack; @@ -151,7 +155,7 @@ Process::Process(const VersionInfoFactory& known_versions) : identified(false) uint32_t pe_offset = readDWord(d->base + 0x3C); read(d->base + pe_offset, sizeof(d->pe_header), (uint8_t*)&(d->pe_header)); const size_t sectionsSize = sizeof(IMAGE_SECTION_HEADER) * d->pe_header.FileHeader.NumberOfSections; - d->sections = (IMAGE_SECTION_HEADER*)malloc(sectionsSize); + d->sections = (IMAGE_SECTION_HEADER*)std::malloc(sectionsSize); read(d->base + pe_offset + sizeof(d->pe_header), sectionsSize, (uint8_t*)(d->sections)); } catch (std::exception&) From e3605d6834c8e001c3db7976a6505c10cdaa7112 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:11:21 -0500 Subject: [PATCH 205/333] headers in `RemoteClient.cpp` --- library/RemoteClient.cpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp index b159843761..c917a3232a 100644 --- a/library/RemoteClient.cpp +++ b/library/RemoteClient.cpp @@ -36,24 +36,25 @@ POSSIBILITY OF SUCH DAMAGE. */ -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include -#include #include -#include +#include +#include "ColorText.h" +#include "CoreDefs.h" #include "RemoteClient.h" -#include -#include "MiscUtils.h" -#include -#include -#include +#include "ActiveSocket.h" +#include "Host.h" +#include "SimpleSocket.h" -#include #include "json/json.h" From 6a641dbef43b3181bec0681e795c6dd85d41f662 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:17:24 -0500 Subject: [PATCH 206/333] includes in `RemoteServer.cpp` --- library/RemoteServer.cpp | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 9557a15862..0e6129b926 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -34,28 +34,33 @@ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - - -#include -#include -#include +#include +#include +#include +#include +#include +#include #include +#include #include -#include #include -#include +#include +#include + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "Debug.h" +#include "MiscUtils.h" +#include "PluginManager.h" +#include "ActiveSocket.h" +#include "Host.h" +#include "RemoteClient.h" #include "RemoteServer.h" #include "RemoteTools.h" - #include "PassiveSocket.h" -#include "PluginManager.h" -#include "MiscUtils.h" -#include "Debug.h" - -#include -#include -#include +#include "SimpleSocket.h" #include #include From 9b15e57e6dfe0c0211e6c7d7bdd8eb501a72ffec Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:19:14 -0500 Subject: [PATCH 207/333] includes in `Types.cpp` --- library/Types.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/library/Types.cpp b/library/Types.cpp index 1dab657c1c..21437723f1 100644 --- a/library/Types.cpp +++ b/library/Types.cpp @@ -22,24 +22,20 @@ must not be misrepresented as being the original software. distribution. */ -#include "Internal.h" -#include "Export.h" #include "MiscUtils.h" -#include "Error.h" #include "Types.h" #include "modules/Filesystem.h" #include "df/general_ref.h" +#include "df/general_ref_type.h" +#include "df/global_objects.h" #include "df/specific_ref.h" +#include "df/specific_ref_type.h" -#include -#include - -#include -#include -#include #include +#include +#include int DFHack::getdir(std::filesystem::path dir, std::vector &files) From d223a385486ed7b7c4b91612110e58cb03d2637a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:25:33 -0500 Subject: [PATCH 208/333] Includes in `RemoteTools.cpp` --- library/RemoteTools.cpp | 63 +++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index dbef7a9906..5dab76e8cb 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -35,57 +35,58 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -#include -#include -#include -#include -#include -#include -#include -#include - #include "RemoteTools.h" -#include "PluginManager.h" + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "DataDefs.h" +#include "DFHackVersion.h" +#include "LuaTools.h" #include "MiscUtils.h" +#include "PluginManager.h" #include "VersionInfo.h" -#include "DFHackVersion.h" #include "modules/Materials.h" #include "modules/Translation.h" #include "modules/Units.h" #include "modules/World.h" -#include "LuaTools.h" - -#include "DataDefs.h" -#include "df/plotinfost.h" #include "df/adventurest.h" -#include "df/world.h" -#include "df/world_data.h" -#include "df/unit.h" -#include "df/unit_misc_trait.h" -#include "df/unit_soul.h" -#include "df/unit_skill.h" +#include "df/creature_raw.h" +#include "df/global_objects.h" +#include "df/historical_entity.h" +#include "df/historical_figure.h" +#include "df/incident.h" +#include "df/inorganic_raw.h" +#include "df/language_name.h" #include "df/material.h" #include "df/matter_state.h" -#include "df/inorganic_raw.h" -#include "df/creature_raw.h" -#include "df/plant_raw.h" #include "df/nemesis_record.h" -#include "df/historical_figure.h" -#include "df/historical_entity.h" -#include "df/squad.h" +#include "df/plant_raw.h" +#include "df/plotinfost.h" +#include "df/profession.h" #include "df/squad_position.h" -#include "df/incident.h" +#include "df/squad.h" +#include "df/unit_misc_trait.h" +#include "df/unit_skill.h" +#include "df/unit_soul.h" +#include "df/unit.h" +#include "df/world_data.h" +#include "df/world.h" #include "BasicApi.pb.h" +#include #include #include -#include - +#include +#include +#include #include +#include +#include +#include using namespace DFHack; using namespace df::enums; From 7c6a6c114465809ab31199d922669d16b7abb67f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:36:00 -0500 Subject: [PATCH 209/333] constrain winsock leakage --- library/RemoteServer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 0e6129b926..aa3deb201d 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -34,6 +34,10 @@ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#ifdef WIN32 +#define NOMINMAX +#endif #include #include #include From a1a4ab5b6af94585c4f8266b506b7456c4ad8831 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:36:18 -0500 Subject: [PATCH 210/333] BitArray.h includes --- library/include/BitArray.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 360281c6b1..7658e33102 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -24,14 +24,14 @@ distribution. #pragma once #include "Error.h" -#include -#include -#include #include +#include #include -#include #include +#include +#include +#include namespace DFHack { From 01b36494cb9f5e140175a1ef7ae7cc3c1ba169ab Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:37:16 -0500 Subject: [PATCH 211/333] includes in `DFSDL.h` --- library/include/modules/DFSDL.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 393877e095..953686deed 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -1,12 +1,12 @@ #pragma once -#include "Export.h" #include "ColorText.h" +#include "Export.h" #include #include +#include #include -#include struct SDL_Surface; struct SDL_Rect; From ec58ce6a8bc4e51ed8d8549c3fd1092308783952 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:37:28 -0500 Subject: [PATCH 212/333] includes in `DFSteam.cpp` --- library/modules/DFSteam.cpp | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 61e50a61b8..31cacfea63 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -1,11 +1,30 @@ -#include "Internal.h" - #include "modules/DFSteam.h" #include "Debug.h" #include "PluginManager.h" +#include "ColorText.h" +#include "Core.h" + +#include +#include +#include +#include +#include + #include "df/gamest.h" +#include + +#ifdef WIN32 +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#endif namespace DFHack { @@ -100,9 +119,6 @@ void DFSteam::cleanup(color_ostream& out) { #ifdef WIN32 -#include -#include -#include static bool is_running_on_wine() { typedef const char* (CDECL wine_get_version)(void); From 39cc838655f3a97c74cb6f4cc57d17c9f59cec8d Mon Sep 17 00:00:00 2001 From: sizzlins Date: Wed, 22 Apr 2026 12:28:01 +0700 Subject: [PATCH 213/333] 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 214/333] 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 215/333] 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 7a4c94417b2f12fe2a106d9805989e5bfc7959a5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 1 May 2026 13:50:18 -0500 Subject: [PATCH 216/333] a few minor adjustments --- library/Core.cpp | 2 -- library/MiscUtils.cpp | 4 ++-- library/PluginManager.cpp | 6 ++---- library/Process.cpp | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 793824ac8e..c1ab8812b3 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -106,8 +106,6 @@ distribution. #include "md5wrapper.h" -#include - #include #include #include diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index 44f93524ee..d91471803b 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -48,12 +48,12 @@ distribution. #include #ifdef WIN32 -// Suppress warning which occurs in header on some WinSDK versions -// See dfhack/dfhack#5147 for more information #define NOMINMAX #define WIN32_LEAN_AND_MEAN #include #include +// Suppress warning which occurs in header on some WinSDK versions +// See dfhack/dfhack#5147 for more information #pragma warning(push) #pragma warning(disable:4091) #include diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 05486e885d..34095898db 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -27,6 +27,7 @@ distribution. #include "ColorText.h" #include "Core.h" #include "CoreDefs.h" +#include "Format.h" #include "LuaWrapper.h" #include "LuaTools.h" #include "MemAccess.h" @@ -54,13 +55,10 @@ distribution. #include #include -#include -#include - #include "df/viewscreen.h" -#include #include +#include using namespace DFHack; using std::string; diff --git a/library/Process.cpp b/library/Process.cpp index 1ff63622f0..e6b82ccabd 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -30,7 +30,7 @@ distribution. #include "VersionInfo.h" #include "VersionInfoFactory.h" -#include "modules/Filesystem.h" +#include "Modules/Filesystem.h" #include #include From 397d95ade374048c0bbaba0df82cde7f0923e5d4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 1 May 2026 13:54:49 -0500 Subject: [PATCH 217/333] case dependent filesystems --- library/Process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Process.cpp b/library/Process.cpp index e6b82ccabd..1ff63622f0 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -30,7 +30,7 @@ distribution. #include "VersionInfo.h" #include "VersionInfoFactory.h" -#include "Modules/Filesystem.h" +#include "modules/Filesystem.h" #include #include From 3c2c40db54703d5cd58310c011c25887f9797327 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 19:45:32 +0000 Subject: [PATCH 218/333] [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.1 → 0.37.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.1...0.37.2) --- .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 b991bfbb74..91a34bdee2 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.1 + rev: 0.37.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From ebb18ee3afe1ea81325b3490ef389a46eeab3b79 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 10 May 2026 09:54:51 +0200 Subject: [PATCH 219/333] Handle units without current soul in `Units::getFocusPenalty` --- docs/changelog.txt | 1 + library/modules/Units.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index cfff16e5a4..84b897421d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## API - add flexible casting to ``enum_field`` to enable explicit casting to more types +- Handle units without current soul in ``Units::getFocusPenalty`` ## Lua diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index d8bfebdc97..707437ea34 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2027,6 +2027,9 @@ int32_t Units::getFocusPenalty(df::unit* unit, need_type_set need_types) { CHECK_NULL_POINTER(unit); int max_penalty = INT_MAX; + if (!unit->status.current_soul) { + return max_penalty; + } auto& needs = unit->status.current_soul->personality.needs; for (auto const need : needs) { if (need_types.test(need->id)) { From 6b65dce6a7e26d7cef2bad497945190a85694563 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 10 May 2026 11:58:30 +0200 Subject: [PATCH 220/333] exclude `fmtlib` build artifacts --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 60a0e1b6b2..8ee4eb5ff1 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ build/compile_commands.json build/dfhack_setarch.txt build/ImportExecutables.cmake build/Testing +build/_deps # Python binding binaries *.pyc From a35a0c333262868144b35f6d0f23baa1f24029c4 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 10 May 2026 15:09:57 +0200 Subject: [PATCH 221/333] improve `autofarm` documentation --- docs/plugins/autofarm.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/plugins/autofarm.rst b/docs/plugins/autofarm.rst index 8896c3117c..0fe98f2b8b 100644 --- a/docs/plugins/autofarm.rst +++ b/docs/plugins/autofarm.rst @@ -24,9 +24,7 @@ Usage Sets thresholds of individual plant types. You can find the identifiers for the crop types in your world by running the -following command:: - - lua "for _,plant in ipairs(df.global.world.raws.plants.all) do if plant.flags.SEED then print(plant.id) end end" +following command: ``getplants -f`` Examples -------- From 9a1d5b86fdcaf329dbc5be81356e15eb419cd968 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 13 May 2026 16:07:06 +0000 Subject: [PATCH 222/333] Auto-update structures ref for 53.13 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index cab90ab30c..ab0b764886 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit cab90ab30c4f52796aec9f83ce21c2fa5974494b +Subproject commit ab0b7648862b4718de07bc1e30a5ac0d9d1b1c89 From eec022051369f45e227c371bed9ac8ea2ea8c6af Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 13 May 2026 11:19:18 -0500 Subject: [PATCH 223/333] update CMakeLists for 53.13 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ebc0ffe310..815a2df89a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.12") +set(DF_VERSION "53.13") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 3c9e10d46e4738e26068c4a74a70fd67924a3470 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 13 May 2026 17:09:09 +0000 Subject: [PATCH 224/333] 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 ab0b764886..392d6522cd 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ab0b7648862b4718de07bc1e30a5ac0d9d1b1c89 +Subproject commit 392d6522cd2f88fdc36c978944e508f17cb43ad3 From 5611803aaf10b32fdd13187026ea994d863c9f68 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 13 May 2026 17:50:45 +0000 Subject: [PATCH 225/333] 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 392d6522cd..ee2b7c7380 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 392d6522cd2f88fdc36c978944e508f17cb43ad3 +Subproject commit ee2b7c73806f9652d6524d3324673600c04f6481 From 66dddaaea5832149bc3f4a21ba7b00ecc2dc352e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 13 May 2026 13:55:17 -0500 Subject: [PATCH 226/333] Changelog and structures for 53.13-r1 --- docs/changelog.txt | 19 +++++++++++++++++++ library/xml | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 84b897421d..6b87efe40d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,25 @@ Template for new versions: ## Documentation +## API + +## Lua + +## Removed + +# 53.13-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation +- updated documentation for ``autofarm`` for more clarity + ## API - add flexible casting to ``enum_field`` to enable explicit casting to more types - Handle units without current soul in ``Units::getFocusPenalty`` diff --git a/library/xml b/library/xml index ee2b7c7380..82e62e90f0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ee2b7c73806f9652d6524d3324673600c04f6481 +Subproject commit 82e62e90f0f50f143dcb3eca1698344ff39c45dd From 4a48d49429941ece57085a814ac9b202554f1429 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 14 May 2026 19:08:10 -0500 Subject: [PATCH 227/333] remove announcement culling from `Gui` module --- docs/changelog.txt | 1 + library/modules/Gui.cpp | 39 +++------------------------------------ 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6b87efe40d..fa85b98865 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- ``Gui::makeAnnoucement``, ``Gui::showPopupAnnouncement`, and ``Gui::autoDFAnnouncement`` will no longer attempt to cull the DF announcement vector ## Misc Improvements diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 68d63e68de..06de785a39 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -105,10 +105,9 @@ using std::string; using std::vector; using namespace DFHack; -const size_t MAX_REPORTS_SIZE = 3000; // DF clears old reports to maintain this vector size -const int32_t RECENT_REPORT_TICKS = 500; // used by UNIT_COMBAT_REPORT_ALL_ACTIVE -const int32_t ANNOUNCE_LINE_DURATION = 100; // time to display each line in announcement bar; 2 sec at 50 GFPS -const int16_t ANNOUNCE_DISPLAY_TIME = 2000; // DF uses this value for most announcements; 40 sec at 50 GFPS +static constexpr int32_t RECENT_REPORT_TICKS = 500; // used by UNIT_COMBAT_REPORT_ALL_ACTIVE +static constexpr int32_t ANNOUNCE_LINE_DURATION = 100; // time to display each line in announcement bar; 2 sec at 50 GFPS +static constexpr int16_t ANNOUNCE_DISPLAY_TIME = 2000; // DF uses this value for most announcements; 40 sec at 50 GFPS namespace DFHack { @@ -1928,18 +1927,6 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce world->status.display_timer = ANNOUNCE_DISPLAY_TIME; } - // Delete excess reports - while (reports.size() > MAX_REPORTS_SIZE) - { // Report destructor - if (reports[0] != NULL) - { - if (reports[0]->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); - delete reports[0]; - } - reports.erase(reports.begin()); - } - return world->status.reports.size() - 1; } @@ -2032,14 +2019,6 @@ void Gui::showPopupAnnouncement(std::string message, int color, bool bright) auto &popups = world->status.popups; popups.push_back(popup); - // Delete excess popups - while (popups.size() > MAX_REPORTS_SIZE) - { - if (popups[0] != NULL) - delete popups[0]; - popups.erase(popups.begin()); - } - Gui::MTB_clean(&world->status.mega_text); Gui::MTB_parse(&world->status.mega_text, popups[0]->text); Gui::MTB_set_width(&world->status.mega_text); @@ -2268,18 +2247,6 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) world->status.display_timer = info.display_timer; } - // Delete excess reports - while (reports.size() > MAX_REPORTS_SIZE) - { // Report destructor - if (reports[0] != NULL) - { - if (reports[0]->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); - delete reports[0]; - } - reports.erase(reports.begin()); - } - if (*gamemode == game_mode::DWARF || // Did dwarf announcement or UCR (*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || // Did adventure announcement (a_flags.bits.DO_MEGA && !adv_unconscious)) // Did popup From 2d5ae9a7c012d5c66b60ae55bce3877be37bcc9b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 15 May 2026 09:48:30 +0000 Subject: [PATCH 228/333] 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 82e62e90f0..79749f5bb1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 82e62e90f0f50f143dcb3eca1698344ff39c45dd +Subproject commit 79749f5bb15a50f7b4a065aae05a1fc53a6ab462 From e4dc45e36ebc6de37dbed02b40a7ed1e26f5b96a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 16 May 2026 07:54:48 -0500 Subject: [PATCH 229/333] CMakelist & changelog for 53.13-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 815a2df89a..6120219fae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.13") -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 fa85b98865..bf0171b318 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,25 @@ Template for new versions: # Future + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.13-r2 + ## New Tools ## New Features From e53187c8b975f71c5c356d9a587563009684bc89 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 16 May 2026 07:57:00 -0500 Subject: [PATCH 230/333] structures for 53.13-r2 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 79749f5bb1..5ac7a80129 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 79749f5bb15a50f7b4a065aae05a1fc53a6ab462 +Subproject commit 5ac7a801297a604ff2a58d7235291b33535a64a0 From 6c7a24c910fea5220ee462a238040a06c8822bd7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 20 May 2026 10:24:50 +0000 Subject: [PATCH 231/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index af457f9b5c..e320f138ff 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit af457f9b5c86fc041a064ef5bdfbcab38f38a50f +Subproject commit e320f138ffa762f6f7533451d8adcdfb769cf1a1 From fa1165bce8c0c5630d93a32efea5f2fb29c903ad Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 20 May 2026 13:18:43 +0000 Subject: [PATCH 232/333] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 5ac7a80129..37bd2498d4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 5ac7a801297a604ff2a58d7235291b33535a64a0 +Subproject commit 37bd2498d42f26ba904abe04b38341c625f5f3b3 diff --git a/plugins/stonesense b/plugins/stonesense index c8ddd2c523..32471bc1e7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit c8ddd2c52387d32f06d7c99d83b9303e3038b47b +Subproject commit 32471bc1e76d29ca0051d61a1fba4fc104f240e9 diff --git a/scripts b/scripts index e320f138ff..66c1ff627b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e320f138ffa762f6f7533451d8adcdfb769cf1a1 +Subproject commit 66c1ff627b4bf195e0576eaa2e4dbe2146ba6fb8 From 108dd8c1e842f1c3e97f9d1ab9a1d37fe38ebdc9 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 20 May 2026 16:07:56 +0000 Subject: [PATCH 233/333] Auto-update structures ref for 53.14 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 37bd2498d4..01aae95cac 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 37bd2498d42f26ba904abe04b38341c625f5f3b3 +Subproject commit 01aae95cacd98850e4f477c45a4b75f800bacecc From ac13e275a2b7da55566118972762798dcae64d79 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 20 May 2026 11:16:18 -0500 Subject: [PATCH 234/333] Update version to 53.14-r1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6120219fae..62f55d0b4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.13") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "53.14") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From a58f695ff3b521b94fba64ddd6fdeddd60542e32 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 20 May 2026 11:22:14 -0500 Subject: [PATCH 235/333] Changelog for 53.14 --- docs/changelog.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf0171b318..113fc16ba9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,10 +54,28 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.14-r1 ## New Tools ## New Features +- Compatibility with Dwarf Fortress 53.14 ## Fixes From 128014165b230b7b7e437d5e3435fbf6c6cc9137 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 20 May 2026 17:03:39 +0000 Subject: [PATCH 236/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 66c1ff627b..3ce62a5ade 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 66c1ff627b4bf195e0576eaa2e4dbe2146ba6fb8 +Subproject commit 3ce62a5ade38c3ddb266294017e9a151772a09c9 From c2f6e92abc1a18b9edf377a180b768d0d4eff76c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 21 May 2026 11:26:37 -0500 Subject: [PATCH 237/333] protect DF pooled objects from being deleted --- docs/changelog.txt | 1 + library/include/DataDefs.h | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 113fc16ba9..735ddddf4e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- Core: attempts to delete a pool-allocated DF object will now throw an exception instead of corrupting the heap ## Documentation diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 88c4a93651..aabcfdd9db 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -24,14 +24,15 @@ distribution. #pragma once +#include #include #include #include +#include #include #include #include #include -#include #include "BitArray.h" #include "Export.h" @@ -572,15 +573,21 @@ namespace df * */ + template concept pooled_object = requires () { { T::pool_id } -> std::convertible_to; }; + template concept copy_assignable = std::assignable_from && std::assignable_from; template void *allocator_fn(void *out, const void *in) { - if (out) + // unerase type + T* _out = out ? reinterpret_cast(out) : nullptr; + const T* _in = in ? reinterpret_cast(in) : nullptr; + + if (_out) { if constexpr (copy_assignable) { - *(T*)out = *(const T*)in; + *_out = *_in; return out; } else @@ -588,13 +595,20 @@ namespace df return nullptr; } } - else if (in) + else if (_in) { + if constexpr (pooled_object) + { + if (_in->pool_id != -1) + { + throw std::runtime_error("Pool-allocated type cannot be deallocated with allocator_fn"); + } + } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" - delete (T*)in; + delete _in; #pragma GCC diagnostic pop - return (T*)in; + return const_cast(in); } else return new T(); From ab59c374923cba0ccba5c0a32154d37cf49f6c4f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 21 May 2026 11:39:03 -0500 Subject: [PATCH 238/333] `pool_id`s are `size_t` --- library/include/DataDefs.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index aabcfdd9db..37892391a4 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -573,12 +573,14 @@ namespace df * */ - template concept pooled_object = requires () { { T::pool_id } -> std::convertible_to; }; + using df_pool_id_t = size_t; + template concept pooled_object = requires () { { T::pool_id } -> std::convertible_to; }; template concept copy_assignable = std::assignable_from && std::assignable_from; template void *allocator_fn(void *out, const void *in) { + constexpr df_pool_id_t invalid_pool_id = static_cast(-1); // unerase type T* _out = out ? reinterpret_cast(out) : nullptr; const T* _in = in ? reinterpret_cast(in) : nullptr; @@ -599,7 +601,7 @@ namespace df { if constexpr (pooled_object) { - if (_in->pool_id != -1) + if (_in->pool_id != invalid_pool_id) { throw std::runtime_error("Pool-allocated type cannot be deallocated with allocator_fn"); } From d86cf1928ae2a3c4574eb50531ca939dfc102bb8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 10:51:08 -0500 Subject: [PATCH 239/333] Remove ``logcleaner`` --- docs/changelog.txt | 1 + docs/plugins/logcleaner.rst | 62 ---------- plugins/CMakeLists.txt | 1 - plugins/logcleaner/logcleaner.cpp | 190 ------------------------------ plugins/lua/logcleaner.lua | 85 ------------- 5 files changed, 1 insertion(+), 338 deletions(-) delete mode 100644 docs/plugins/logcleaner.rst delete mode 100644 plugins/logcleaner/logcleaner.cpp delete mode 100644 plugins/lua/logcleaner.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index 735ddddf4e..8d723a4c01 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,7 @@ Template for new versions: ## Lua ## Removed +- ``logcleaner``: Removed (cannot be safely implemented at this time) # 53.14-r1 diff --git a/docs/plugins/logcleaner.rst b/docs/plugins/logcleaner.rst deleted file mode 100644 index 6e4cba07d6..0000000000 --- a/docs/plugins/logcleaner.rst +++ /dev/null @@ -1,62 +0,0 @@ -logcleaner -========== -.. dfhack-tool:: - :summary: Automatically clear combat, sparring, and hunting reports. - :tags: fort auto units - -This plugin prevents spam from cluttering your announcement history and filling -the 3000-item reports buffer. It runs approximately every 100 ticks and clears -selected report types from both the global reports buffer and per-unit logs. - -Usage ------ - -Basic commands -~~~~~~~~~~~~~~ - -``logcleaner`` - Show the current status of the plugin. -``logcleaner enable`` - Enable the plugin (persists per save). -``logcleaner disable`` - Disable the plugin. - -Configuring filters -~~~~~~~~~~~~~~~~~~~ - -``logcleaner combat`` - Clear combat reports (also enables the plugin if disabled). -``logcleaner sparring`` - Clear sparring reports. -``logcleaner hunting`` - Clear hunting reports. -``logcleaner combat,sparring`` - Clear multiple report types (comma-separated). -``logcleaner all`` - Enable all three filter types. -``logcleaner none`` - Disable all filter types. - -Examples -~~~~~~~~ - -Clear only sparring reports:: - - logcleaner sparring - -Clear combat and hunting, but not sparring:: - - logcleaner combat,hunting - -Overlay UI ----------- - -Run ``gui/logcleaner`` to open the settings overlay, or access it from the -control panel under the Gameplay tab. - -The overlay provides: - -- **Enable toggle**: Turn the plugin on or off (``Shift+E``) -- **Combat toggle**: Clear combat reports (``Shift+C``) -- **Sparring toggle**: Clear sparring reports (``Shift+S``) -- **Hunting toggle**: Clear hunting reports (``Shift+H``) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4a4423f48f..355cc0ac4c 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -66,7 +66,6 @@ if(BUILD_SUPPORTED) dfhack_plugin(createitem createitem.cpp) dfhack_plugin(cursecheck cursecheck.cpp) dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) - dfhack_plugin(logcleaner logcleaner/logcleaner.cpp LINK_LIBRARIES lua) dfhack_plugin(deramp deramp.cpp) dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(dig dig.cpp LINK_LIBRARIES lua) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp deleted file mode 100644 index 89f84cf32f..0000000000 --- a/plugins/logcleaner/logcleaner.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#include "LuaTools.h" -#include "PluginManager.h" -#include "PluginLua.h" - -#include "modules/Persistence.h" -#include "modules/World.h" - -#include -#include -#include -#include - -using namespace DFHack; - -DFHACK_PLUGIN("logcleaner"); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -REQUIRE_GLOBAL(world); - -static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; -static PersistentDataItem config; - -enum ConfigValues { - CONFIG_IS_ENABLED = 0, - CONFIG_CLEAR_COMBAT = 1, - CONFIG_CLEAR_SPARING = 2, - CONFIG_CLEAR_HUNTING = 3, -}; - -static bool clear_combat = false; -static bool clear_sparring = true; -static bool clear_hunting = false; - -static constexpr int32_t CYCLE_TICKS = 97; -static int32_t cycle_timestamp = 0; - -static void cleanupLogs(); -static command_result do_command(color_ostream& out, std::vector& params); - -// Getter functions for Lua -static bool logcleaner_getCombat() { return clear_combat; } -static bool logcleaner_getSparring() { return clear_sparring; } -static bool logcleaner_getHunting() { return clear_hunting; } - -// Setter functions for Lua (also persist to config) -static void logcleaner_setCombat(bool val) { - clear_combat = val; - config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); -} - -static void logcleaner_setSparring(bool val) { - clear_sparring = val; - config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); -} - -static void logcleaner_setHunting(bool val) { - clear_hunting = val; - config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); -} - -DFhackCExport command_result plugin_init(color_ostream& out, std::vector& commands) { - commands.push_back(PluginCommand( - plugin_name, - "Prevent report buffer from filling up by clearing selected report types (combat, sparring, hunting).", - do_command)); - - return CR_OK; -} - -static command_result do_command(color_ostream& out, std::vector& params) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot use {} without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - bool show_help = false; - if (!Lua::CallLuaModuleFunction(out, "plugins.logcleaner", "parse_commandline", params, - 1, [&](lua_State *L) { - show_help = !lua_toboolean(L, -1); - })) { - return CR_FAILURE; - } - - return show_help ? CR_WRONG_USAGE : CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - if (enable != is_enabled) { - is_enabled = enable; - config.set_bool(CONFIG_IS_ENABLED, is_enabled); - } - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown(color_ostream& out) { - return CR_OK; -} - -DFhackCExport command_result plugin_load_site_data(color_ostream& out) { - config = World::GetPersistentSiteData(CONFIG_KEY); - - if (!config.isValid()) { - config = World::AddPersistentSiteData(CONFIG_KEY); - config.set_bool(CONFIG_IS_ENABLED, is_enabled); - config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); - config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); - config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); - } - - is_enabled = config.get_bool(CONFIG_IS_ENABLED); - clear_combat = config.get_bool(CONFIG_CLEAR_COMBAT); - clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); - clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); - - cycle_timestamp = 0; - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) { - if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { - is_enabled = false; - } - return CR_OK; -} - -static void cleanupLogs() { - if (!is_enabled || !world) - return; - - if (!clear_combat && !clear_sparring && !clear_hunting) - return; - - // Collect all report IDs from unit combat/sparring/hunting logs - std::unordered_set report_ids_to_remove; - bool log_types[] = {clear_combat, clear_sparring, clear_hunting}; - - for (auto unit : world->units.all) { - for (int log_idx = 0; log_idx < 3; log_idx++) { - if (log_types[log_idx]) { - auto& log = unit->reports.log[log_idx]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); - } - log.clear(); - } - } - } - - if (report_ids_to_remove.empty()) - return; - - // Remove collected reports from global buffers - auto& reports = world->status.reports; - - std::erase_if(reports, [&](df::report* report) { - if (!report || !report_ids_to_remove.contains(report->id)) - return false; - if (report->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, report->id); - delete report; - return true; - }); -} - -DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { - if (!is_enabled || !world) - return CR_OK; - else if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) - return CR_OK; - - cycle_timestamp = world->frame_counter; - cleanupLogs(); - - return CR_OK; -} - -DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(logcleaner_getCombat), - DFHACK_LUA_FUNCTION(logcleaner_getSparring), - DFHACK_LUA_FUNCTION(logcleaner_getHunting), - DFHACK_LUA_FUNCTION(logcleaner_setCombat), - DFHACK_LUA_FUNCTION(logcleaner_setSparring), - DFHACK_LUA_FUNCTION(logcleaner_setHunting), - DFHACK_LUA_END -}; diff --git a/plugins/lua/logcleaner.lua b/plugins/lua/logcleaner.lua deleted file mode 100644 index 104c23ab88..0000000000 --- a/plugins/lua/logcleaner.lua +++ /dev/null @@ -1,85 +0,0 @@ -local _ENV = mkmodule('plugins.logcleaner') - -local function print_status() - print(('logcleaner is %s'):format(isEnabled() and "enabled" or "disabled")) - print(' Combat: ' .. (logcleaner_getCombat() and 'enabled' or 'disabled')) - print(' Sparring: ' .. (logcleaner_getSparring() and 'enabled' or 'disabled')) - print(' Hunting: ' .. (logcleaner_getHunting() and 'enabled' or 'disabled')) -end - -function parse_commandline(...) - local args = {...} - local command = args[1] - - -- Show status if no command or "status" - if not command or command == 'status' then - print_status() - return true - end - - -- Handle enable/disable commands - if command == 'enable' then - if isEnabled() then - print('logcleaner is already enabled') - else - dfhack.run_command('enable', 'logcleaner') - print('logcleaner enabled') - end - return true - end - - if command == 'disable' then - if not isEnabled() then - print('logcleaner is already disabled') - else - dfhack.run_command('disable', 'logcleaner') - print('logcleaner disabled') - end - return true - end - - -- Start with all disabled, enable only what's specified - local new_combat, new_sparring, new_hunting = false, false, false - local has_filter = false - - if command == 'all' then - new_combat, new_sparring, new_hunting = true, true, true - has_filter = true - elseif command == 'none' then - new_combat, new_sparring, new_hunting = false, false, false - else - -- Split by comma for multiple options in one parameter - for token in command:gmatch('([^,]+)') do - if token == 'combat' then - new_combat = true - has_filter = true - elseif token == 'sparring' then - new_sparring = true - has_filter = true - elseif token == 'hunting' then - new_hunting = true - has_filter = true - else - dfhack.printerr('Unknown option: ' .. token) - return false - end - end - end - - -- Auto-enable plugin when filters are being configured - if has_filter and not isEnabled() then - dfhack.run_command('enable', 'logcleaner') - print('logcleaner enabled') - end - - logcleaner_setCombat(new_combat) - logcleaner_setSparring(new_sparring) - logcleaner_setHunting(new_hunting) - - print('Log cleaning config updated:') - print_status() - - return true -end - -return _ENV From bdbb52f45f88f0895b55f72de677216114260869 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 11:14:21 -0500 Subject: [PATCH 240/333] tombstone `logcleaner` documentation --- docs/about/Removed.rst | 12 ++++++++++++ docs/changelog.txt | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 5f73b86be3..9ef7781ebf 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,6 +258,11 @@ gui/hack-wish ============= Replaced by `gui/create-item`. +gui/log-cleaner +=============== +Removed because changes to Dwarf Fortress internals made the functionality +impossible to implement safely. + .. _gui/manager-quantity: gui/manager-quantity @@ -278,6 +283,13 @@ Tool that warned the user when the ``dfhack.init`` file did not exist. Now that ``dfhack.init`` is autogenerated in ``dfhack-config/init``, this warning is no longer necessary. +.. _logcleaner: + +logcleaner +=============== +Removed because changes to Dwarf Fortress internals made the functionality +impossible to implement safely. + .. _masspit: masspit diff --git a/docs/changelog.txt b/docs/changelog.txt index 8d723a4c01..cddef2d75c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,7 +70,7 @@ Template for new versions: ## Lua ## Removed -- ``logcleaner``: Removed (cannot be safely implemented at this time) +- `logcleaner`: Removed (cannot be safely implemented at this time) # 53.14-r1 From 4f31fff8290487c3f5bbcf93c8d741ea83f1587d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 11:53:38 -0500 Subject: [PATCH 241/333] Update 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 9ef7781ebf..b0a7f469a1 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,7 +258,7 @@ gui/hack-wish ============= Replaced by `gui/create-item`. -gui/log-cleaner +gui/logcleaner =============== Removed because changes to Dwarf Fortress internals made the functionality impossible to implement safely. From 6e85d8551316c8bd826b1665c2d8afb8f8d32120 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 12:23:59 -0500 Subject: [PATCH 242/333] Update Removed.rst --- docs/about/Removed.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index b0a7f469a1..cf9bf95b09 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,6 +258,7 @@ gui/hack-wish ============= Replaced by `gui/create-item`. +.. _gui/logcleaner gui/logcleaner =============== Removed because changes to Dwarf Fortress internals made the functionality From 360d0f76d79b111bc755c27a784e75c3018adaf6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 12:39:10 -0500 Subject: [PATCH 243/333] Update Removed.rst sigh --- docs/about/Removed.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index cf9bf95b09..96eaa3c142 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,7 +258,8 @@ gui/hack-wish ============= Replaced by `gui/create-item`. -.. _gui/logcleaner +.. _gui/logcleaner: + gui/logcleaner =============== Removed because changes to Dwarf Fortress internals made the functionality From 7db509ea6529b078b3615d58f8f31050896703df Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 22 May 2026 18:17:30 +0000 Subject: [PATCH 244/333] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 3ce62a5ade..364dd46ab0 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3ce62a5ade38c3ddb266294017e9a151772a09c9 +Subproject commit 364dd46ab037521a34ade84bb7bf3ce3638723f4 From 2e2b92130d9f0c1bddbdd20cec262ae5cdb050e7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 13:58:23 -0500 Subject: [PATCH 245/333] update version, changelog and scripts for 53.14-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ scripts | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 62f55d0b4f..dff1b7fac3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.14") -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 cddef2d75c..70e55aff58 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,24 @@ Template for new versions: ## Fixes +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.14-r2 + +## New Tools + +## New Features + +## Fixes + ## Misc Improvements - Core: attempts to delete a pool-allocated DF object will now throw an exception instead of corrupting the heap diff --git a/scripts b/scripts index 364dd46ab0..92aec15dd7 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 364dd46ab037521a34ade84bb7bf3ce3638723f4 +Subproject commit 92aec15dd7eff76b76ff74efb3164e31b7f0e5bd From 91458a222a7adee32901f05422535c6733f85f26 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 23 May 2026 00:16:26 -0500 Subject: [PATCH 246/333] remove dead `Graphic` module This module has been without purpose for at least a decade; nothing uses it I don't think it's had a purpose since DFHack became an in-process toolset --- library/CMakeLists.txt | 2 - library/Core.cpp | 2 - library/include/Core.h | 5 -- library/include/modules/DFSDL.h | 11 ---- library/include/modules/Graphic.h | 59 -------------------- library/modules/Graphic.cpp | 93 ------------------------------- 6 files changed, 172 deletions(-) delete mode 100644 library/include/modules/Graphic.h delete mode 100644 library/modules/Graphic.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index f91afef2e2..6da4909968 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -157,7 +157,6 @@ set(MODULE_HEADERS include/modules/Designations.h include/modules/EventManager.h include/modules/Filesystem.h - include/modules/Graphic.h include/modules/Gui.h include/modules/GuiHooks.h include/modules/Hotkey.h @@ -189,7 +188,6 @@ set(MODULE_SOURCES modules/Designations.cpp modules/EventManager.cpp modules/Filesystem.cpp - modules/Graphic.cpp modules/Gui.cpp modules/Hotkey.cpp modules/Items.cpp diff --git a/library/Core.cpp b/library/Core.cpp index c1ab8812b3..62a280a187 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -52,7 +52,6 @@ distribution. #include "modules/DFSteam.h" #include "modules/EventManager.h" #include "modules/Filesystem.h" -#include "modules/Graphic.h" #include "modules/Gui.h" #include "modules/Hotkey.h" #include "modules/Persistence.h" @@ -2203,4 +2202,3 @@ TYPE * Core::get##TYPE() \ } MODULE_GETTER(Materials); -MODULE_GETTER(Graphic); diff --git a/library/include/Core.h b/library/include/Core.h index 9ccf0ca871..ee50a83080 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -29,8 +29,6 @@ distribution. #include "Export.h" #include "Hooks.h" -#include "modules/Graphic.h" - #include #include #include @@ -166,8 +164,6 @@ namespace DFHack /// get the materials module Materials * getMaterials(); - /// get the graphic module - Graphic * getGraphic(); command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream& out, const std::string& command); @@ -300,7 +296,6 @@ namespace DFHack struct { Materials * pMaterials; - Graphic * pGraphic; } s_mods; std::vector> allModules; DFHack::PluginManager *plug_mgr; diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 953686deed..7d53242ad0 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -16,17 +16,6 @@ struct SDL_Window; union SDL_Event; using SDL_Keycode = int32_t; -namespace DFHack -{ - struct DFTileSurface - { - bool paintOver; // draw over original tile? - SDL_Surface* surface; // from where it should be drawn - SDL_Rect* rect; // from which coords (NULL to draw whole surface) - SDL_Rect* dstResize; // if not NULL dst rect will be resized (x/y/w/h will be added to original dst) - }; -} - /** * The DFSDL module - provides access to SDL functions without actually * requiring build-time linkage to SDL diff --git a/library/include/modules/Graphic.h b/library/include/modules/Graphic.h deleted file mode 100644 index 5e30d89805..0000000000 --- a/library/include/modules/Graphic.h +++ /dev/null @@ -1,59 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mr�zek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -/******************************************************************************* - GRAPHIC - Changing tile cache -*******************************************************************************/ -#pragma once -#ifndef CL_MOD_GRAPHIC -#define CL_MOD_GRAPHIC - -#include "Export.h" -#include "Module.h" - -#include - -namespace DFHack -{ - // forward declaration used here instead of including DFSDL.h to reduce inclusion loading - struct DFTileSurface; - - class DFHACK_EXPORT Graphic : public Module - { - public: - bool Finish() - { - return true; - } - bool Register(DFTileSurface* (*func)(int,int)); - bool Unregister(DFTileSurface* (*func)(int,int)); - DFTileSurface* Call(int x, int y); - - private: - std::vector funcs; - }; -} - -#endif diff --git a/library/modules/Graphic.cpp b/library/modules/Graphic.cpp deleted file mode 100644 index b55ee83ed4..0000000000 --- a/library/modules/Graphic.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mr�zek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - - -#include "Internal.h" - -#include -#include -#include -#include -#include -using namespace std; - -#include "modules/Graphic.h" -#include "Error.h" -#include "VersionInfo.h" -#include "MemAccess.h" -#include "MiscUtils.h" -#include "ModuleFactory.h" -#include "Core.h" - -using namespace DFHack; - -std::unique_ptr DFHack::createGraphic() -{ - return std::make_unique(); -} - -bool Graphic::Register(DFTileSurface* (*func)(int,int)) -{ - funcs.push_back(func); - return true; -} - -bool Graphic::Unregister(DFTileSurface* (*func)(int,int)) -{ - if ( funcs.empty() ) return false; - - vector::iterator it = funcs.begin(); - while ( it != funcs.end() ) - { - if ( *it == func ) - { - funcs.erase(it); - return true; - } - it++; - } - - return false; -} - -// This will return first DFTileSurface it can get (or NULL if theres none) -DFTileSurface* Graphic::Call(int x, int y) -{ - if ( funcs.empty() ) return NULL; - - DFTileSurface* temp = NULL; - - vector::iterator it = funcs.begin(); - while ( it != funcs.end() ) - { - temp = (*it)(x,y); - if ( temp != NULL ) - { - return temp; - } - it++; - } - - return NULL; -} From b799dd6366a4dc5a61ed3d51a2ce4079c94758de Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 23 May 2026 02:42:46 -0500 Subject: [PATCH 247/333] remove use of `Materials` module from `probe` also remove UB use of a union and clean up headers (at least somewhat) --- plugins/probe.cpp | 96 +++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 53 deletions(-) diff --git a/plugins/probe.cpp b/plugins/probe.cpp index f49e167867..9df2200406 100644 --- a/plugins/probe.cpp +++ b/plugins/probe.cpp @@ -1,11 +1,16 @@ +#include +#include +#include + #include "LuaTools.h" +#include "MiscUtils.h" #include "PluginManager.h" #include "TileTypes.h" #include "modules/Gui.h" -#include "modules/Materials.h" #include "modules/MapCache.h" #include "modules/Maps.h" +#include "modules/Materials.h" #include "df/block_square_event_grassst.h" #include "df/block_square_event_world_constructionst.h" @@ -15,6 +20,8 @@ #include "df/civzone_type.h" #include "df/construction_type.h" #include "df/furnace_type.h" +#include "df/global_objects.h" +#include "df/inorganic_raw.h" #include "df/item.h" #include "df/map_block.h" #include "df/region_map_entry.h" @@ -86,10 +93,8 @@ static void describeTile(color_ostream &out, df::tiletype tiletype) { } static command_result df_probe(color_ostream &out, vector & parameters) { - DFHack::Materials *Materials = Core::getInstance().getMaterials(); - vector inorganic; - bool hasmats = Materials->CopyInorganicMaterials(inorganic); + auto& inorganic = world->raws.inorganics.all; if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); @@ -173,29 +178,25 @@ static command_result df_probe(color_ostream &out, vector & parameters) out << "geolayer: " << des.bits.geolayer_index << std::endl; int16_t base_rock = mc.layerMaterialAt(cursor); - if (base_rock != -1) { + if (base_rock != -1) + { out << "Layer material: " << std::dec << base_rock; - if(hasmats) - out << " / " << inorganic[base_rock].id - << " / " - << inorganic[base_rock].name - << std::endl; - else - out << std::endl; + out << " / " << inorganic[base_rock]->id + << " / " + << inorganic[base_rock]->material.stone_name + << std::endl; } int16_t vein_rock = mc.veinMaterialAt(cursor); - if (vein_rock != -1) { + if (vein_rock != -1) + { out << "Vein material (final): " << std::dec << vein_rock; - if(hasmats) - out << " / " << inorganic[vein_rock].id - << " / " - << inorganic[vein_rock].name - << " (" - << ENUM_KEY_STR(inclusion_type,b->veinTypeAt(cursor)) - << ")" - << std::endl; - else - out << std::endl; + out << " / " << inorganic[vein_rock]->id + << " / " + << inorganic[vein_rock]->material.stone_name + << " (" + << ENUM_KEY_STR(inclusion_type, b->veinTypeAt(cursor)) + << ")" + << std::endl; } MaterialInfo minfo(mc.baseMaterialAt(cursor)); if (minfo.isValid()) @@ -309,17 +310,6 @@ static command_result df_probe(color_ostream &out, vector & parameters) return CR_OK; } -union Subtype { - int16_t subtype; - df::civzone_type civzone_type; - df::furnace_type furnace_type; - df::workshop_type workshop_type; - df::construction_type construction_type; - df::shop_type shop_type; - df::siegeengine_type siegeengine_type; - df::trap_type trap_type; -}; - static command_result df_bprobe(color_ostream &out, vector & parameters) { auto bld = Gui::getSelectedBuilding(out); if (!bld) @@ -329,7 +319,7 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) bld->getName(&name); auto bld_type = bld->getType(); - Subtype subtype{bld->getSubtype()}; + int16_t subtype{bld->getSubtype()}; int32_t custom = bld->getCustomType(); out.print("Building {:<4}, \"{}\", type {} ({})", @@ -342,46 +332,46 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) switch (bld_type) { case df::building_type::Civzone: out.print(", subtype {} ({})", - ENUM_KEY_STR(civzone_type, subtype.civzone_type), - subtype.subtype); + ENUM_KEY_STR(civzone_type, static_cast(subtype)), + subtype); break; case df::building_type::Furnace: out.print(", subtype {} ({})", - ENUM_KEY_STR(furnace_type, subtype.furnace_type), - subtype.subtype); - if (subtype.furnace_type == df::furnace_type::Custom) + ENUM_KEY_STR(furnace_type, static_cast(subtype)), + subtype); + if (static_cast(subtype) == df::furnace_type::Custom) out.print(", custom type {} ({})", world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Workshop: out.print(", subtype {} ({})", - ENUM_KEY_STR(workshop_type, subtype.workshop_type), - subtype.subtype); - if (subtype.workshop_type == df::workshop_type::Custom) + ENUM_KEY_STR(workshop_type, static_cast(subtype)), + subtype); + if (subtype == static_cast(df::workshop_type::Custom)) out.print(", custom type {} ({})", world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Construction: out.print(", subtype {} ({})", - ENUM_KEY_STR(construction_type, subtype.construction_type), - subtype.subtype); + ENUM_KEY_STR(construction_type, static_cast(subtype)), + subtype); break; case df::building_type::Shop: out.print(", subtype {} ({})", - ENUM_KEY_STR(shop_type, subtype.shop_type), - subtype.subtype); + ENUM_KEY_STR(shop_type, static_cast(subtype)), + subtype); break; case df::building_type::SiegeEngine: out.print(", subtype {} ({})", - ENUM_KEY_STR(siegeengine_type, subtype.siegeengine_type), - subtype.subtype); + ENUM_KEY_STR(siegeengine_type, static_cast(subtype)), + subtype); break; case df::building_type::Trap: out.print(", subtype {} ({})", - ENUM_KEY_STR(trap_type, subtype.trap_type), - subtype.subtype); + ENUM_KEY_STR(trap_type, static_cast(subtype)), + subtype); break; case df::building_type::NestBox: { @@ -391,8 +381,8 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) break; } default: - if (subtype.subtype != -1) - out.print(", subtype {}", subtype.subtype); + if (subtype != -1) + out.print(", subtype {}", subtype); break; } From faea23b361913eefc25c10f1bcf7be98768bf575 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 24 May 2026 04:17:39 +0000 Subject: [PATCH 248/333] 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 32471bc1e7..992957670f 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 32471bc1e76d29ca0051d61a1fba4fc104f240e9 +Subproject commit 992957670fd99b55d10196cf2850a4554bb15374 From d29682533d2f7a9d8cebc1d1a8c63a7046542bc1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 23 May 2026 23:37:31 -0500 Subject: [PATCH 249/333] Remove `Materials` module from `prospector` was unused - the plugin only obtained the handle and then disposed it without using it for anything --- plugins/prospector.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index 035d6cdee1..5dd712e872 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -610,9 +610,7 @@ static command_result embark_prospector(color_ostream &out, } if (options.hidden) { - DFHack::Materials *mats = Core::getInstance().getMaterials(); printVeins(out, veinMats, options); - mats->Finish(); } out << "Embark depth: " << (world_bottom.upper_z-world_bottom.lower_z+1) << " "; @@ -635,8 +633,6 @@ static command_result map_prospector(color_ostream &con, Maps::getSize(x_max, y_max, z_max); MapExtras::MapCache map; - DFHack::Materials *mats = Core::getInstance().getMaterials(); - DFHack::t_feature blockFeatureGlobal; DFHack::t_feature blockFeatureLocal; @@ -894,9 +890,6 @@ static command_result map_prospector(color_ostream &con, printMats(con, treeMats, world->raws.plants.all, options); } - // Cleanup - mats->Finish(); - return CR_OK; } From e82a2e1c8f45f027e8b7b77ba35f8a1f652d4bb2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 24 May 2026 00:30:19 -0500 Subject: [PATCH 250/333] Remove `Materials` module --- library/Core.cpp | 2 - library/include/Core.h | 5 - library/include/modules/Materials.h | 134 +++++----- library/modules/Materials.cpp | 383 +++++----------------------- 4 files changed, 131 insertions(+), 393 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 62a280a187..50c6fe5f8b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2200,5 +2200,3 @@ TYPE * Core::get##TYPE() \ }\ return s_mods.p##TYPE;\ } - -MODULE_GETTER(Materials); diff --git a/library/include/Core.h b/library/include/Core.h index ee50a83080..085dd33cf1 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -61,7 +61,6 @@ namespace DFHack class Process; class Module; - class Materials; struct VersionInfo; class VersionInfoFactory; class PluginManager; @@ -162,9 +161,6 @@ namespace DFHack /// Is everything OK? bool isValid(void) { return !errorstate; } - /// get the materials module - Materials * getMaterials(); - command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream& out, const std::string& command); @@ -295,7 +291,6 @@ namespace DFHack // Module storage struct { - Materials * pMaterials; } s_mods; std::vector> allModules; DFHack::PluginManager *plug_mgr; diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 2c3be4e731..9689c7790c 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -35,7 +35,8 @@ distribution. #include "df/craft_material_class.h" -namespace df { +namespace df +{ struct item; struct plant_raw; struct creature_raw; @@ -54,28 +55,32 @@ namespace df { namespace DFHack { - struct t_matpair { + struct t_matpair + { int16_t mat_type; int32_t mat_index; t_matpair(int16_t type = -1, int32_t index = -1) - : mat_type(type), mat_index(index) {} + : mat_type(type), mat_index(index) + {} }; - struct DFHACK_EXPORT MaterialInfo { + struct DFHACK_EXPORT MaterialInfo + { static const int NUM_BUILTIN = 19; static const int GROUP_SIZE = 200; static const int CREATURE_BASE = NUM_BUILTIN; static const int FIGURE_BASE = NUM_BUILTIN + GROUP_SIZE; - static const int PLANT_BASE = NUM_BUILTIN + GROUP_SIZE*2; - static const int END_BASE = NUM_BUILTIN + GROUP_SIZE*3; + static const int PLANT_BASE = NUM_BUILTIN + GROUP_SIZE * 2; + static const int END_BASE = NUM_BUILTIN + GROUP_SIZE * 3; int16_t type; int32_t index; - df::material *material; + df::material* material; - enum Mode { + enum Mode + { None, Builtin, Inorganic, @@ -85,16 +90,16 @@ namespace DFHack Mode mode; int16_t subtype; - df::inorganic_raw *inorganic; - df::creature_raw *creature; - df::plant_raw *plant; + df::inorganic_raw* inorganic; + df::creature_raw* creature; + df::plant_raw* plant; - df::historical_figure *figure; + df::historical_figure* figure; public: MaterialInfo(int16_t type = -1, int32_t index = -1) { decode(type, index); } - MaterialInfo(const t_matpair &mp) { decode(mp.mat_type, mp.mat_index); } - template MaterialInfo(T *ptr) { decode(ptr); } + MaterialInfo(const t_matpair& mp) { decode(mp.mat_type, mp.mat_index); } + template MaterialInfo(T* ptr) { decode(ptr); } bool isValid() const { return material != NULL; } @@ -108,25 +113,27 @@ namespace DFHack bool isInorganicWildcard() const { return isAnyInorganic() && isBuiltin(); } bool decode(int16_t type, int32_t index = -1); - bool decode(df::item *item); - bool decode(const df::material_vec_ref &vr, int idx); - bool decode(const t_matpair &mp) { return decode(mp.mat_type, mp.mat_index); } + bool decode(df::item* item); + bool decode(const df::material_vec_ref& vr, int idx); + bool decode(const t_matpair& mp) { return decode(mp.mat_type, mp.mat_index); } - template bool decode(T *ptr) { + template bool decode(T* ptr) + { // Assume and exploit a certain naming convention return ptr ? decode(ptr->mat_type, ptr->mat_index) : decode(-1); } - bool find(const std::string &token); - bool find(const std::vector &tokens); + bool find(const std::string& token); + bool find(const std::vector& tokens); - bool findBuiltin(const std::string &token); - bool findInorganic(const std::string &token); - bool findPlant(const std::string &token, const std::string &subtoken); - bool findCreature(const std::string &token, const std::string &subtoken); + bool findBuiltin(const std::string& token); + bool findInorganic(const std::string& token); + bool findPlant(const std::string& token, const std::string& subtoken); + bool findCreature(const std::string& token, const std::string& subtoken); - bool findProduct(df::material *material, const std::string &name); - bool findProduct(const MaterialInfo &info, const std::string &name) { + bool findProduct(df::material* material, const std::string& name); + bool findProduct(const MaterialInfo& info, const std::string& name) + { return findProduct(info.material, name); } @@ -135,35 +142,38 @@ namespace DFHack bool isAnyCloth() const; - void getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask) const; - void getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask) const; - void getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask) const; + void getMatchBits(df::job_item_flags1& ok, df::job_item_flags1& mask) const; + void getMatchBits(df::job_item_flags2& ok, df::job_item_flags2& mask) const; + void getMatchBits(df::job_item_flags3& ok, df::job_item_flags3& mask) const; df::craft_material_class getCraftClass(); - bool matches(const MaterialInfo &mat) const + bool matches(const MaterialInfo& mat) const { if (!mat.isValid()) return true; return (type == mat.type) && - (mat.index == -1 || index == mat.index); + (mat.index == -1 || index == mat.index); } - bool matches(const df::job_material_category &cat) const; - bool matches(const df::dfhack_material_category &cat) const; - bool matches(const df::job_item &jitem, - df::item_type itype = df::item_type::NONE) const; + bool matches(const df::job_material_category& cat) const; + bool matches(const df::dfhack_material_category& cat) const; + bool matches(const df::job_item& jitem, + df::item_type itype = df::item_type::NONE) const; }; - DFHACK_EXPORT bool parseJobMaterialCategory(df::job_material_category *cat, const std::string &token); - DFHACK_EXPORT bool parseJobMaterialCategory(df::dfhack_material_category *cat, const std::string &token); + DFHACK_EXPORT bool parseJobMaterialCategory(df::job_material_category* cat, const std::string& token); + DFHACK_EXPORT bool parseJobMaterialCategory(df::dfhack_material_category* cat, const std::string& token); - inline bool operator== (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator== (const MaterialInfo& a, const MaterialInfo& b) + { return a.type == b.type && a.index == b.index; } - inline bool operator!= (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator!= (const MaterialInfo& a, const MaterialInfo& b) + { return a.type != b.type || a.index != b.index; } - inline bool operator< (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator< (const MaterialInfo& a, const MaterialInfo& b) + { return a.type < b.type || (a.type == b.type && a.index < b.index); } @@ -345,38 +355,18 @@ namespace DFHack t_materialIndex mat_index; uint32_t flags; }; - /** - * The Materials module - * \ingroup grp_modules - * \ingroup grp_materials - */ - class DFHACK_EXPORT Materials : public Module + + + namespace Materials { - public: - Materials(); - ~Materials(); - bool Finish(); - - std::vector race; - std::vector raceEx; - std::vector color; - std::vector other; - std::vector alldesc; - - bool CopyInorganicMaterials (std::vector & inorganic); - bool CopyOrganicMaterials (std::vector & organic); - bool CopyWoodMaterials (std::vector & tree); - bool CopyPlantMaterials (std::vector & plant); - - bool ReadCreatureTypes (void); - bool ReadCreatureTypesEx (void); - bool ReadDescriptorColors(void); - bool ReadOthers (void); - - bool ReadAllMaterials(void); - - std::string getType(const t_material & mat); - std::string getDescription(const t_material & mat); - }; + /** + * The Materials module + * \ingroup grp_modules + * \ingroup grp_materials + */ + + std::string getType(const t_material& mat); + std::string getDescription(const t_material& mat); + } } #endif diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index d5358f6139..3351726d57 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -68,7 +68,7 @@ using namespace df::enums; using df::global::world; using df::global::plotinfo; -bool MaterialInfo::decode(df::item *item) +bool MaterialInfo::decode(df::item* item) { if (!item) return decode(-1); @@ -76,7 +76,7 @@ bool MaterialInfo::decode(df::item *item) return decode(item->getActualMaterial(), item->getActualMaterialIndex()); } -bool MaterialInfo::decode(const df::material_vec_ref &vr, int idx) +bool MaterialInfo::decode(const df::material_vec_ref& vr, int idx) { if (size_t(idx) >= vr.mat_type.size() || size_t(idx) >= vr.mat_index.size()) return decode(-1); @@ -94,14 +94,15 @@ bool MaterialInfo::decode(int16_t type, int32_t index) inorganic = NULL; plant = NULL; creature = NULL; figure = NULL; - if (type < 0) { + if (type < 0) + { mode = None; return false; } - auto &raws = world->raws; + auto& raws = world->raws; - if (size_t(type) >= sizeof(raws.mat_table.builtin)/sizeof(void*)) + if (size_t(type) >= sizeof(raws.mat_table.builtin) / sizeof(void*)) return false; if (index < 0) @@ -123,7 +124,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < FIGURE_BASE) { mode = Creature; - subtype = type-CREATURE_BASE; + subtype = type - CREATURE_BASE; creature = df::creature_raw::find(index); if (!creature || size_t(subtype) >= creature->material.size()) return false; @@ -132,7 +133,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < PLANT_BASE) { mode = Creature; - subtype = type-FIGURE_BASE; + subtype = type - FIGURE_BASE; figure = df::historical_figure::find(index); if (!figure) return false; @@ -144,7 +145,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < END_BASE) { mode = Plant; - subtype = type-PLANT_BASE; + subtype = type - PLANT_BASE; plant = df::plant_raw::find(index); if (!plant || size_t(subtype) >= plant->material.size()) return false; @@ -158,24 +159,24 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return (material != NULL); } -bool MaterialInfo::find(const std::string &token) +bool MaterialInfo::find(const std::string& token) { std::vector items; split_string(&items, token, ":"); return find(items); } -bool MaterialInfo::find(const std::vector &items) +bool MaterialInfo::find(const std::vector& items) { if (items.empty()) return false; if (items[0] == "INORGANIC" && items.size() > 1) - return findInorganic(vector_get(items,1)); + return findInorganic(vector_get(items, 1)); if (items[0] == "CREATURE_MAT" || items[0] == "CREATURE") - return findCreature(vector_get(items,1), vector_get(items,2)); + return findCreature(vector_get(items, 1), vector_get(items, 2)); if (items[0] == "PLANT_MAT" || items[0] == "PLANT") - return findPlant(vector_get(items,1), vector_get(items,2)); + return findPlant(vector_get(items, 1), vector_get(items, 2)); if (items.size() == 1) { @@ -188,7 +189,8 @@ bool MaterialInfo::find(const std::vector &items) } else if (items.size() == 2) { - if (items[0] == "COAL" && findBuiltin(items[0])) { + if (items[0] == "COAL" && findBuiltin(items[0])) + { if (items[1] == "COKE") this->index = 0; else if (items[1] == "CHARCOAL") @@ -206,17 +208,18 @@ bool MaterialInfo::find(const std::vector &items) return false; } -bool MaterialInfo::findBuiltin(const std::string &token) +bool MaterialInfo::findBuiltin(const std::string& token) { if (token.empty()) return decode(-1); - if (token == "NONE") { + if (token == "NONE") + { decode(-1); return true; } - auto &raws = world->raws; + auto& raws = world->raws; for (int i = 0; i < NUM_BUILTIN; i++) { auto obj = raws.mat_table.builtin[i]; @@ -226,34 +229,35 @@ bool MaterialInfo::findBuiltin(const std::string &token) return decode(-1); } -bool MaterialInfo::findInorganic(const std::string &token) +bool MaterialInfo::findInorganic(const std::string& token) { if (token.empty()) return decode(-1); - if (token == "NONE") { + if (token == "NONE") + { decode(0, -1); return true; } - auto &raws = world->raws; + auto& raws = world->raws; for (size_t i = 0; i < raws.inorganics.all.size(); i++) { - df::inorganic_raw *p = raws.inorganics.all[i]; + df::inorganic_raw* p = raws.inorganics.all[i]; if (p->id == token) return decode(0, i); } return decode(-1); } -bool MaterialInfo::findPlant(const std::string &token, const std::string &subtoken) +bool MaterialInfo::findPlant(const std::string& token, const std::string& subtoken) { if (token.empty()) return decode(-1); - auto &raws = world->raws; + auto& raws = world->raws; for (size_t i = 0; i < raws.plants.all.size(); i++) { - df::plant_raw *p = raws.plants.all[i]; + df::plant_raw* p = raws.plants.all[i]; if (p->id != token) continue; @@ -263,39 +267,39 @@ bool MaterialInfo::findPlant(const std::string &token, const std::string &subtok for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(PLANT_BASE+j, i); + return decode(PLANT_BASE + j, i); break; } return decode(-1); } -bool MaterialInfo::findCreature(const std::string &token, const std::string &subtoken) +bool MaterialInfo::findCreature(const std::string& token, const std::string& subtoken) { if (token.empty() || subtoken.empty()) return decode(-1); - auto &raws = world->raws; + auto& raws = world->raws; for (size_t i = 0; i < raws.creatures.all.size(); i++) { - df::creature_raw *p = raws.creatures.all[i]; + df::creature_raw* p = raws.creatures.all[i]; if (p->creature_id != token) continue; for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(CREATURE_BASE+j, i); + return decode(CREATURE_BASE + j, i); break; } return decode(-1); } -bool MaterialInfo::findProduct(df::material *material, const std::string &name) +bool MaterialInfo::findProduct(df::material* material, const std::string& name) { if (!material || name.empty()) return decode(-1); - auto &pids = material->reaction_product.id; + auto& pids = material->reaction_product.id; for (size_t i = 0; i < pids.size(); i++) if ((*pids[i]) == name) return decode(material->reaction_product.material, i); @@ -311,9 +315,11 @@ std::string MaterialInfo::getToken() const if (!material) return fmt::format("INVALID:{}:{}", type, index); - switch (mode) { + switch (mode) + { case Builtin: - if (material->id == "COAL") { + if (material->id == "COAL") + { if (index == 0) return "COAL:COKE"; else if (index == 1) @@ -382,10 +388,10 @@ bool MaterialInfo::isAnyCloth() const material->flags.is_set(THREAD_PLANT) || material->flags.is_set(SILK) || material->flags.is_set(YARN) - ); + ); } -bool MaterialInfo::matches(const df::job_material_category &cat) const +bool MaterialInfo::matches(const df::job_material_category& cat) const { if (!material) return false; @@ -414,7 +420,7 @@ bool MaterialInfo::matches(const df::job_material_category &cat) const return false; } -bool MaterialInfo::matches(const df::dfhack_material_category &cat) const +bool MaterialInfo::matches(const df::dfhack_material_category& cat) const { if (!material) return false; @@ -443,7 +449,7 @@ bool MaterialInfo::matches(const df::dfhack_material_category &cat) const #undef TEST -bool MaterialInfo::matches(const df::job_item &jitem, df::item_type itype) const +bool MaterialInfo::matches(const df::job_item& jitem, df::item_type itype) const { if (!isValid()) return false; @@ -460,11 +466,11 @@ bool MaterialInfo::matches(const df::job_item &jitem, df::item_type itype) const mask2.whole &= ~xmask2.whole; return bits_match(jitem.flags1.whole, ok1.whole, mask1.whole) && - bits_match(jitem.flags2.whole, ok2.whole, mask2.whole) && - bits_match(jitem.flags3.whole, ok3.whole, mask3.whole); + bits_match(jitem.flags2.whole, ok2.whole, mask2.whole) && + bits_match(jitem.flags3.whole, ok3.whole, mask3.whole); } -void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags1& ok, df::job_item_flags1& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -486,10 +492,10 @@ void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &ma TEST(processable_to_vial, structural && FLAG(plant, plant_raw_flags::EXTRACT_VIAL)); TEST(processable_to_barrel, structural && FLAG(plant, plant_raw_flags::EXTRACT_BARREL)); TEST(solid, !(MAT_FLAG(ALCOHOL_PLANT) || - MAT_FLAG(ALCOHOL_CREATURE) || - MAT_FLAG(LIQUID_MISC_PLANT) || - MAT_FLAG(LIQUID_MISC_CREATURE) || - MAT_FLAG(LIQUID_MISC_OTHER))); + MAT_FLAG(ALCOHOL_CREATURE) || + MAT_FLAG(LIQUID_MISC_PLANT) || + MAT_FLAG(LIQUID_MISC_CREATURE) || + MAT_FLAG(LIQUID_MISC_OTHER))); TEST(tameable_vermin, false); TEST(sharpenable, MAT_FLAG(IS_STONE)); TEST(milk, linear_index(material->reaction_product.id, std::string("CHEESE_MAT")) >= 0); @@ -497,7 +503,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &ma //04000000 - "milkable" - vtable[107],1,1 } -void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags2& ok, df::job_item_flags2& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -511,15 +517,15 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma TEST(glass_making, MAT_FLAG(CRYSTAL_GLASSABLE)); TEST(fire_safe, material->heat.melting_point > 11000 - && material->heat.boiling_point > 11000 - && material->heat.ignite_point > 11000 - && material->heat.heatdam_point > 11000 - && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 11000)); + && material->heat.boiling_point > 11000 + && material->heat.ignite_point > 11000 + && material->heat.heatdam_point > 11000 + && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 11000)); TEST(magma_safe, material->heat.melting_point > 12000 - && material->heat.boiling_point > 12000 - && material->heat.ignite_point > 12000 - && material->heat.heatdam_point > 12000 - && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 12000)); + && material->heat.boiling_point > 12000 + && material->heat.ignite_point > 12000 + && material->heat.heatdam_point > 12000 + && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 12000)); TEST(deep_material, FLAG(inorganic, inorganic_flags::SPECIAL)); TEST(non_economic, !inorganic || !(plotinfo && vector_get(plotinfo->economic_stone, index))); @@ -537,7 +543,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma TEST(yarn, MAT_FLAG(YARN)); } -void MaterialInfo::getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags3& ok, df::job_item_flags3& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -549,7 +555,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &ma #undef FLAG #undef TEST -bool DFHack::parseJobMaterialCategory(df::job_material_category *cat, const std::string &token) +bool DFHack::parseJobMaterialCategory(df::job_material_category* cat, const std::string& token) { cat->whole = 0; @@ -565,7 +571,7 @@ bool DFHack::parseJobMaterialCategory(df::job_material_category *cat, const std: return true; } -bool DFHack::parseJobMaterialCategory(df::dfhack_material_category *cat, const std::string &token) +bool DFHack::parseJobMaterialCategory(df::dfhack_material_category* cat, const std::string& token) { cat->whole = 0; @@ -600,32 +606,14 @@ bool DFHack::isStoneInorganic(int material) return true; } -std::unique_ptr DFHack::createMaterials() -{ - return std::make_unique(); -} - -Materials::Materials() -{ -} - -Materials::~Materials() -{ -} - -bool Materials::Finish() -{ - return true; -} - t_matgloss::t_matgloss() { - fore = 0; - back = 0; - bright = 0; + fore = 0; + back = 0; + bright = 0; - value = 0; - wall_tile = 0; + value = 0; + wall_tile = 0; boulder_tile = 0; } @@ -643,240 +631,7 @@ bool t_matglossInorganic::isGem() return is_gem; } -bool Materials::CopyInorganicMaterials (std::vector & inorganic) -{ - size_t size = world->raws.inorganics.all.size(); - inorganic.clear(); - inorganic.reserve (size); - for (size_t i = 0; i < size;i++) - { - df::inorganic_raw *orig = world->raws.inorganics.all[i]; - t_matglossInorganic mat; - mat.id = orig->id; - mat.name = orig->material.stone_name; - - mat.ore_types = orig->metal_ore.mat_index; - mat.ore_chances = orig->metal_ore.probability; - mat.strand_types = orig->thread_metal.mat_index; - mat.strand_chances = orig->thread_metal.probability; - mat.value = orig->material.material_value; - mat.wall_tile = orig->material.tile; - mat.boulder_tile = orig->material.item_symbol; - mat.fore = orig->material.basic_color[0]; - mat.bright = orig->material.basic_color[1]; - mat.is_gem = orig->material.flags.is_set(material_flags::IS_GEM); - inorganic.push_back(mat); - } - return true; -} - -bool Materials::CopyOrganicMaterials (std::vector & organic) -{ - size_t size = world->raws.plants.all.size(); - organic.clear(); - organic.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.all[i]->id; - organic.push_back(mat); - } - return true; -} - -bool Materials::CopyWoodMaterials (std::vector & tree) -{ - size_t size = world->raws.plants.trees.size(); - tree.clear(); - tree.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.trees[i]->id; - tree.push_back(mat); - } - return true; -} - -bool Materials::CopyPlantMaterials (std::vector & plant) -{ - size_t size = world->raws.plants.bushes.size(); - plant.clear(); - plant.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.bushes[i]->id; - plant.push_back(mat); - } - return true; -} - -bool Materials::ReadCreatureTypes (void) -{ - size_t size = world->raws.creatures.all.size(); - race.clear(); - race.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.creatures.all[i]->creature_id; - race.push_back(mat); - } - return true; -} - -bool Materials::ReadOthers(void) -{ - other.clear(); - FOR_ENUM_ITEMS(builtin_mats, i) - { - t_matglossOther mat; - mat.id = world->raws.mat_table.builtin[i]->id; - other.push_back(mat); - } - return true; -} - -bool Materials::ReadDescriptorColors (void) -{ - size_t size = world->raws.descriptors.colors.size(); - - color.clear(); - if(size == 0) - return false; - color.reserve(size); - for (size_t i = 0; i < size;i++) - { - df::descriptor_color *c = world->raws.descriptors.colors[i]; - t_descriptor_color col; - col.id = c->id; - col.name = c->name; - col.red = c->red; - col.green = c->green; - col.blue = c->blue; - color.push_back(col); - } - - size = world->raws.descriptors.patterns.size(); - alldesc.clear(); - alldesc.reserve(size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.descriptors.patterns[i]->id; - alldesc.push_back(mat); - } - return true; -} - -bool Materials::ReadCreatureTypesEx (void) -{ - size_t size = world->raws.creatures.all.size(); - raceEx.clear(); - raceEx.reserve (size); - for (size_t i = 0; i < size; i++) - { - df::creature_raw *cr = world->raws.creatures.all[i]; - t_creaturetype mat; - mat.id = cr->creature_id; - mat.tile_character = cr->creature_tile; - mat.tilecolor.fore = cr->color[0]; - mat.tilecolor.back = cr->color[1]; - mat.tilecolor.bright = cr->color[2]; - - size_t sizecas = cr->caste.size(); - for (size_t j = 0; j < sizecas;j++) - { - df::caste_raw *ca = cr->caste[j]; - /* caste name */ - t_creaturecaste caste; - caste.id = ca->caste_id; - caste.singular = ca->caste_name[0]; - caste.plural = ca->caste_name[1]; - caste.adjective = ca->caste_name[2]; - - // color mod reading - // Caste + offset > color mod vector - auto & colorings = ca->color_modifiers; - size_t sizecolormod = colorings.size(); - caste.ColorModifier.resize(sizecolormod); - for(size_t k = 0; k < sizecolormod;k++) - { - // color mod [0] -> color list - auto & indexes = colorings[k]->pattern_index; - size_t sizecolorlist = indexes.size(); - caste.ColorModifier[k].colorlist.resize(sizecolorlist); - for(size_t l = 0; l < sizecolorlist; l++) - caste.ColorModifier[k].colorlist[l] = indexes[l]; - // color mod [color_modifier_part_offset] = string part - caste.ColorModifier[k].part = colorings[k]->part; - caste.ColorModifier[k].startdate = colorings[k]->start_date; - caste.ColorModifier[k].enddate = colorings[k]->end_date; - } - - // body parts - caste.bodypart.clear(); - size_t sizebp = ca->body_info.body_parts.size(); - for (size_t k = 0; k < sizebp; k++) - { - df::body_part_raw *bp = ca->body_info.body_parts[k]; - t_bodypart part; - part.id = bp->token; - part.category = bp->category; - caste.bodypart.push_back(part); - } - using namespace df::enums::mental_attribute_type; - using namespace df::enums::physical_attribute_type; - for (int32_t k = 0; k < 7; k++) - { - auto & physical = ca->attributes.phys_att_range; - caste.strength[k] = physical[STRENGTH][k]; - caste.agility[k] = physical[AGILITY][k]; - caste.toughness[k] = physical[TOUGHNESS][k]; - caste.endurance[k] = physical[ENDURANCE][k]; - caste.recuperation[k] = physical[RECUPERATION][k]; - caste.disease_resistance[k] = physical[DISEASE_RESISTANCE][k]; - - auto & mental = ca->attributes.ment_att_range; - caste.analytical_ability[k] = mental[ANALYTICAL_ABILITY][k]; - caste.focus[k] = mental[FOCUS][k]; - caste.willpower[k] = mental[WILLPOWER][k]; - caste.creativity[k] = mental[CREATIVITY][k]; - caste.intuition[k] = mental[INTUITION][k]; - caste.patience[k] = mental[PATIENCE][k]; - caste.memory[k] = mental[MEMORY][k]; - caste.linguistic_ability[k] = mental[LINGUISTIC_ABILITY][k]; - caste.spatial_sense[k] = mental[SPATIAL_SENSE][k]; - caste.musicality[k] = mental[MUSICALITY][k]; - caste.kinesthetic_sense[k] = mental[KINESTHETIC_SENSE][k]; - caste.empathy[k] = mental[EMPATHY][k]; - caste.social_awareness[k] = mental[SOCIAL_AWARENESS][k]; - } - mat.castes.push_back(caste); - } - for (size_t j = 0; j < world->raws.creatures.all[i]->material.size(); j++) - { - t_creatureextract extract; - extract.id = world->raws.creatures.all[i]->material[j]->id; - mat.extract.push_back(extract); - } - raceEx.push_back(mat); - } - return true; -} - -bool Materials::ReadAllMaterials(void) -{ - bool ok = true; - ok &= this->ReadCreatureTypes(); - ok &= this->ReadCreatureTypesEx(); - ok &= this->ReadDescriptorColors(); - ok &= this->ReadOthers(); - return ok; -} - -std::string Materials::getDescription(const t_material & mat) +std::string Materials::getDescription(const t_material& mat) { MaterialInfo mi(mat.mat_type, mat.mat_index); if (mi.creature) @@ -889,7 +644,7 @@ std::string Materials::getDescription(const t_material & mat) // type of material only so we know which vector to retrieve // This is completely worthless now -std::string Materials::getType(const t_material & mat) +std::string Materials::getType(const t_material& mat) { MaterialInfo mi(mat.mat_type, mat.mat_index); switch (mi.mode) From a527e8cdcbd954ff1c66cfd58c9f7f957eefe1a4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 24 May 2026 03:26:59 -0500 Subject: [PATCH 251/333] sunset the old Modules implementation not relevant since we move to in-process --- library/CMakeLists.txt | 2 - library/Core.cpp | 21 ---------- library/include/Core.h | 5 --- library/include/Module.h | 59 ----------------------------- library/include/ModuleFactory.h | 38 ------------------- library/include/modules/Materials.h | 1 - library/modules/Buildings.cpp | 1 - library/modules/Gui.cpp | 1 - library/modules/Items.cpp | 1 - library/modules/Kitchen.cpp | 1 - library/modules/MapCache.cpp | 1 - library/modules/Maps.cpp | 1 - library/modules/Materials.cpp | 1 - library/modules/Random.cpp | 1 - library/modules/Screen.cpp | 1 - library/modules/Units.cpp | 1 - 16 files changed, 136 deletions(-) delete mode 100644 library/include/Module.h delete mode 100644 library/include/ModuleFactory.h diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 6da4909968..d87e20531e 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -66,10 +66,8 @@ set(MAIN_HEADERS include/MemAccess.h include/Memory.h include/MiscUtils.h - include/Module.h include/MemAccess.h include/MemoryPatcher.h - include/ModuleFactory.h include/PluginLua.h include/PluginManager.h include/PluginStatics.h diff --git a/library/Core.cpp b/library/Core.cpp index 50c6fe5f8b..ec105a6c6b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -40,8 +40,6 @@ distribution. #include "MemoryPatcher.h" #include "MiscUtils.h" #include "MiscUtils.h" -#include "Module.h" -#include "ModuleFactory.h" #include "PluginManager.h" #include "RemoteServer.h" #include "RemoteTools.h" @@ -1047,7 +1045,6 @@ Core::Core() : plug_mgr = nullptr; errorstate = false; vinfo = 0; - memset(&(s_mods), 0, sizeof(s_mods)); // set up hotkey capture suppress_duplicate_keyboard_events = true; @@ -1949,11 +1946,9 @@ int Core::Shutdown ( void ) plug_mgr = nullptr; } // invalidate all modules - allModules.clear(); Textures::cleanup(); DFSDL::cleanup(); DFSteam::cleanup(getConsole()); - memset(&(s_mods), 0, sizeof(s_mods)); d.reset(); return -1; } @@ -2184,19 +2179,3 @@ std::string Core::GetAliasCommand(const std::string &name, bool ignore_params) return join_strings(" ", aliases[name]); } -/******************************************************************************* - M O D U L E S -*******************************************************************************/ - -#define MODULE_GETTER(TYPE) \ -TYPE * Core::get##TYPE() \ -{ \ - if(errorstate) return nullptr;\ - if(!s_mods.p##TYPE)\ - {\ - std::unique_ptr mod = create##TYPE();\ - s_mods.p##TYPE = (TYPE *) mod.get();\ - allModules.push_back(std::move(mod));\ - }\ - return s_mods.p##TYPE;\ -} diff --git a/library/include/Core.h b/library/include/Core.h index 085dd33cf1..8b78e58097 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -288,11 +288,6 @@ namespace DFHack // FIXME: shouldn't be kept around like this std::unique_ptr vif; - // Module storage - struct - { - } s_mods; - std::vector> allModules; DFHack::PluginManager *plug_mgr; // Hotkey Manager diff --git a/library/include/Module.h b/library/include/Module.h deleted file mode 100644 index 7770e13970..0000000000 --- a/library/include/Module.h +++ /dev/null @@ -1,59 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#pragma once - -#ifndef MODULE_H_INCLUDED -#define MODULE_H_INCLUDED - -#include "Export.h" -namespace DFHack -{ - /** - * The parent class for all DFHack modules - * \ingroup grp_modules - */ - class DFHACK_EXPORT Module - { - public: - virtual ~Module(){}; - virtual bool Start(){return true;};// default start... - virtual bool Finish() = 0;// everything should have a Finish() - /* - // should Context call Finish when Resume is called? - virtual bool OnResume() - { - Finish(); - return true; - }; - // Finish when map change is detected? - // TODO: implement - virtual bool OnMapChange() - { - return false; - }; - */ - }; -} -#endif //MODULE_H_INCLUDED diff --git a/library/include/ModuleFactory.h b/library/include/ModuleFactory.h deleted file mode 100644 index c99e7b3289..0000000000 --- a/library/include/ModuleFactory.h +++ /dev/null @@ -1,38 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#pragma once - -#ifndef MODULE_FACTORY_H_INCLUDED -#define MODULE_FACTORY_H_INCLUDED - -#include - -namespace DFHack -{ - class Module; - std::unique_ptr createMaterials(); - std::unique_ptr createGraphic(); -} -#endif diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 9689c7790c..1f87f548ff 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -30,7 +30,6 @@ distribution. * @ingroup grp_modules */ #include "Export.h" -#include "Module.h" #include "DataDefs.h" #include "df/craft_material_class.h" diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index e5a0af116b..ddedbbc1b6 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -29,7 +29,6 @@ distribution. #include "MemAccess.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "TileTypes.h" #include "MiscUtils.h" diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 06de785a39..e7c8233ba1 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -30,7 +30,6 @@ distribution. #include "VersionInfo.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "Debug.h" #include "PluginManager.h" diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index adce8ba8a6..6b5b3ee1d2 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -28,7 +28,6 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "Types.h" #include "VersionInfo.h" diff --git a/library/modules/Kitchen.cpp b/library/modules/Kitchen.cpp index be1415d90d..c9d4c7d5c6 100644 --- a/library/modules/Kitchen.cpp +++ b/library/modules/Kitchen.cpp @@ -14,7 +14,6 @@ using namespace std; #include "Types.h" #include "Error.h" #include "modules/Kitchen.h" -#include "ModuleFactory.h" #include "Core.h" using namespace DFHack; diff --git a/library/modules/MapCache.cpp b/library/modules/MapCache.cpp index c01f5bf576..603d1d0da2 100644 --- a/library/modules/MapCache.cpp +++ b/library/modules/MapCache.cpp @@ -30,7 +30,6 @@ distribution. #include "Error.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "VersionInfo.h" #include "modules/Buildings.h" diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 11499a4270..2376936054 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -31,7 +31,6 @@ distribution. #include "Error.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "VersionInfo.h" #include "modules/Buildings.h" diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 3351726d57..dba8a78132 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -28,7 +28,6 @@ distribution. #include "VersionInfo.h" #include "MemAccess.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "MiscUtils.h" diff --git a/library/modules/Random.cpp b/library/modules/Random.cpp index f0d2054c9e..d6d682d921 100644 --- a/library/modules/Random.cpp +++ b/library/modules/Random.cpp @@ -35,7 +35,6 @@ using namespace std; #include "VersionInfo.h" #include "MemAccess.h" #include "Types.h" -#include "ModuleFactory.h" #include "Core.h" #include "Error.h" #include "VTableInterpose.h" diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index d629c4b8cf..b16a3e9b0e 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -28,7 +28,6 @@ distribution. #include "VersionInfo.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "PluginManager.h" #include "LuaTools.h" diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 707437ea34..00821c11c0 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -27,7 +27,6 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "Types.h" #include "VersionInfo.h" From 2c164932af0c2a401e10dec54086db6c13b1a716 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 24 May 2026 03:44:51 -0500 Subject: [PATCH 252/333] missed one --- library/modules/Translation.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Translation.cpp b/library/modules/Translation.cpp index 38ae2c51ba..097de85cc9 100644 --- a/library/modules/Translation.cpp +++ b/library/modules/Translation.cpp @@ -27,7 +27,6 @@ distribution. #include "VersionInfo.h" #include "MemAccess.h" #include "Types.h" -#include "ModuleFactory.h" #include "Core.h" #include "Error.h" #include "DataDefs.h" From 5a8c9f3559122b0719c3d3ecc4ae9b9508a677a8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 24 May 2026 03:45:45 -0500 Subject: [PATCH 253/333] Update Core.cpp --- library/Core.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index ec105a6c6b..00419bd7ae 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2178,4 +2178,3 @@ std::string Core::GetAliasCommand(const std::string &name, bool ignore_params) return aliases[name][0]; return join_strings(" ", aliases[name]); } - From a0e630371e23d97634af231b0a74f00329519e5b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 1 Jun 2026 12:37:07 -0500 Subject: [PATCH 254/333] 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 255/333] 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 256/333] 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 257/333] 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 258/333] 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 259/333] 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 260/333] 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 261/333] 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 262/333] 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 263/333] 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 264/333] `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 265/333] `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 266/333] `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 267/333] `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 268/333] `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 269/333] `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 270/333] `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 271/333] 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 272/333] 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 273/333] 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 274/333] 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 275/333] 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 0aad4ad4778ff3d3b22dce37b92393b2af74e332 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 7 Jun 2026 01:54:56 -0500 Subject: [PATCH 276/333] `timestream`: no skips during flows or caravan load/unload fixes #5811 fixes #5669 --- docs/changelog.txt | 1 + plugins/timestream.cpp | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 70e55aff58..600bfc0706 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `timestream`: will not skip ticks whenever a flow is active or when a caravan is loading or unloading ## Misc Improvements diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index deb6c45e4d..749d51849d 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -33,8 +33,10 @@ #include "df/building_nest_boxst.h" #include "df/building_trapst.h" #include "df/buildingitemst.h" +#include "df/caravan_state.h" #include "df/init.h" #include "df/item_eggst.h" +#include "df/plotinfost.h" #include "df/unit.h" #include "df/world.h" @@ -52,6 +54,8 @@ REQUIRE_GLOBAL(cur_year_tick); REQUIRE_GLOBAL(cur_year_tick_advmode); REQUIRE_GLOBAL(init); REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(flows); +REQUIRE_GLOBAL(plotinfo); namespace DFHack { // for configuration-related logging @@ -314,9 +318,38 @@ static int32_t clamp_coverage(int32_t timeskip) { return timeskip; } +static bool detect_flows() +{ + return df::global::flows->size() > 0; +} + +static bool detect_caravans() +{ + auto& caravans = df::global::plotinfo->caravans; + return std::any_of(caravans.begin(), caravans.end(), [](auto caravan) { + if (caravan->trade_state != df::caravan_state::T_trade_state::AtDepot) + return false; + auto car_civ = caravan->entity; + auto& units = world->units.active; + return std::any_of(units.begin(), units.end(), [car_civ] (auto un) { + return (un->civ_id == car_civ + && DFHack::Units::isMerchant(un) + && std::any_of(un->inventory.begin(), un->inventory.end(), [] (auto inv_item) { + return inv_item->item && inv_item->item->flags.bits.trader; + })); + }); + }); +} + static int32_t clamp_timeskip(int32_t timeskip) { if (timeskip <= 0) return 0; + // timeskip cannot be applied when flows are active, since they are updated every tick and we can't predict them well enough to batch updates + if (detect_flows()) + return 0; + // timeskip cannot be applied when caravans are loading/unloading because we don't know how to jog that timer + if (detect_caravans()) + return 0; int32_t next_tick = *cur_year_tick + 1; timeskip = std::min(timeskip, get_next_trigger_year_tick(next_tick) - next_tick); timeskip = std::min(timeskip, get_next_birthday(next_tick) - next_tick); From c6f61e3880cd567305e1dfdec63ba3900ab74e0e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 7 Jun 2026 14:55:20 -0500 Subject: [PATCH 277/333] better flow tick handling Instead of just giving up when flows are present, figure out when the next flow update will occur and allow skipping up to that tick h/t to @cokernel for help in getting the tick-skipping math right, and @quietust for reverse engineering the flow update code Co-Authored-By: Quietust <1005195+quietust@users.noreply.github.com> Co-Authored-By: MLE Slone --- plugins/timestream.cpp | 64 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index 749d51849d..d7dca161cb 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -34,6 +34,8 @@ #include "df/building_trapst.h" #include "df/buildingitemst.h" #include "df/caravan_state.h" +#include "df/flow_info.h" +#include "df/flow_type.h" #include "df/init.h" #include "df/item_eggst.h" #include "df/plotinfost.h" @@ -41,6 +43,8 @@ #include "df/world.h" #include +#include +#include using std::string; using std::vector; @@ -318,9 +322,61 @@ static int32_t clamp_coverage(int32_t timeskip) { return timeskip; } -static bool detect_flows() +using df_tick_type = std::remove_reference_t; + +static df_tick_type flow_next_required_tick(int flow_index) { - return df::global::flows->size() > 0; + using namespace df::enums::flow_type; + + const auto flow = (*flows)[flow_index]; + + if (flow == nullptr || flow->flags.bits.DEAD) + return std::numeric_limits::max(); + + const auto cur_tick = *cur_year_tick; + + struct update_parameters_t { + int speed; + int cycle; + }; + + const auto [speed, cycle] = [flow] () -> update_parameters_t { + switch (flow->type) + { + case ItemCloud: + case MaterialDust: + case MaterialGas: + case MaterialVapor: + if (flow->flags.bits.CREEPING) + return update_parameters_t{10, 100}; + else + return update_parameters_t{1, 5}; + case Dragonfire: + case Fire: + case Web: + return update_parameters_t{1, 3}; + default: + return update_parameters_t{10, 100}; + } + }(); + + const int stride = cycle / speed; + + const int phase = (flow_index % stride) * speed; + + const int cur_phase = cur_tick % cycle; + + return cur_tick + (phase - cur_phase) + ((cur_phase > phase) ? cycle : 0); +} + +static df_tick_type flows_next_required_tick() { + if (flows == nullptr || flows->empty()) + return std::numeric_limits::max(); + + auto flow_indices = std::views::iota(0, (int)flows->size()); + auto next_flow = std::ranges::min(flow_indices, {}, flow_next_required_tick); + + return flow_next_required_tick(next_flow); } static bool detect_caravans() @@ -344,15 +400,13 @@ static bool detect_caravans() static int32_t clamp_timeskip(int32_t timeskip) { if (timeskip <= 0) return 0; - // timeskip cannot be applied when flows are active, since they are updated every tick and we can't predict them well enough to batch updates - if (detect_flows()) - return 0; // timeskip cannot be applied when caravans are loading/unloading because we don't know how to jog that timer if (detect_caravans()) return 0; int32_t next_tick = *cur_year_tick + 1; timeskip = std::min(timeskip, get_next_trigger_year_tick(next_tick) - next_tick); timeskip = std::min(timeskip, get_next_birthday(next_tick) - next_tick); + timeskip = std::min(timeskip, flows_next_required_tick() - next_tick); return clamp_coverage(timeskip); } From d1f64b37d35e2dee0821571c57011122a1b03f2c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 7 Jun 2026 17:32:50 -0500 Subject: [PATCH 278/333] amend changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 600bfc0706..16fd09ee99 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,7 +59,7 @@ Template for new versions: ## New Features ## Fixes -- `timestream`: will not skip ticks whenever a flow is active or when a caravan is loading or unloading +- `timestream`: do not skip ticks when a caravan is loading or unloading, and be more careful about skipping ticks when flows are active ## Misc Improvements From a31197bcc787d43ab49ec84a7977962320d4a9b4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 8 Jun 2026 19:21:37 -0500 Subject: [PATCH 279/333] `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 ff633ad28f6cfe0b54ff04cc8490b4aa6076f7dd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 10 Jun 2026 11:34:30 -0500 Subject: [PATCH 280/333] `autoclothing`: don't count gloves and pants as helms also mitigate some of the worst code standard violations --- docs/changelog.txt | 1 + plugins/autoclothing.cpp | 168 +++++++++++++++++++++------------------ 2 files changed, 92 insertions(+), 77 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 70e55aff58..47353d9ffe 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `autoclothing`: will no longer count gloves and pants as if they were helms ## Misc Improvements diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index 076892f9bd..76a5452220 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -20,6 +20,7 @@ #include "df/item_helmst.h" #include "df/item_pantsst.h" #include "df/item_shoesst.h" +#include "df/itemdef_handlerst.h" #include "df/itemdef_armorst.h" #include "df/itemdef_glovesst.h" #include "df/itemdef_helmst.h" @@ -64,8 +65,8 @@ enum ConfigValues { struct ClothingRequirement; command_result autoclothing(color_ostream &out, vector ¶meters); static void do_autoclothing(); -static bool validateMaterialCategory(ClothingRequirement *requirement); -static bool setItem(string name, ClothingRequirement *requirement); +static bool validateMaterialCategory(ClothingRequirement& requirement); +static std::optional setItem(string name); static void generate_control(color_ostream &out); static bool isAvailableItem(df::item *item); @@ -127,32 +128,38 @@ struct ClothingRequirement { stream >> needed_per_citizen; } - bool SetFromParameters(color_ostream &out, vector ¶meters) + //FIXME: when C++23, use std::expected here + static std::optional createFromParameters(color_ostream &out, vector ¶meters) { - if (parameters[0] == "clear") { // handle the clear case - if (!set_bitfield_field(&material_category, parameters[1], 1)) - out << "Unrecognized material type: " << parameters[1] << endl; - if (!setItem(parameters[2], this)) { - out << "Unrecognized item name or token: " << parameters[2] << endl; - return false; - } - else if (!validateMaterialCategory(this)) { - out << parameters[1] << " is not a valid material category for " << parameters[2] << endl; - return false; - } - return true; + df::job_material_category material_category; + if (parameters.size() < 1) return std::nullopt; + + size_t idx = 0; + if (parameters[0] == "clear") idx++; + + if (parameters.size() < idx + 1) return std::nullopt; + + if (!set_bitfield_field(&material_category, parameters[idx], 1)) + { + out << "Unrecognized material type: " << parameters[idx] << endl; + return std::nullopt; } - if (!set_bitfield_field(&material_category, parameters[0], 1)) - out << "Unrecognized material type: " << parameters[0] << endl; - if (!setItem(parameters[1], this)) { - out << "Unrecognized item name or token: " << parameters[1] << endl; - return false; + + if (auto req = setItem(parameters[idx+1]); !req) + { + out << "Unrecognized item name or token: " << parameters[idx+1] << endl; + return std::nullopt; } - else if (!validateMaterialCategory(this)) { - out << parameters[0] << " is not a valid material category for " << parameters[1] << endl; - return false; + + else 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; } - return true; } string ToReadableLabel() { @@ -311,94 +318,98 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } -static bool setItemFromName(string name, ClothingRequirement *requirement) +std::optional setItemFromName(string name) { -#define SEARCH_ITEM_RAWS(rawType, job, item) \ -for (auto &itemdef : world->raws.itemdefs.rawType) { \ - string fullName = itemdef->adjective.empty() ? itemdef->name : itemdef->adjective + " " + itemdef->name; \ - if (fullName == name) { \ - requirement->jobType = job_type::job; \ - requirement->itemType = item_type::item; \ - requirement->item_subtype = itemdef->subtype; \ - return true; \ - } \ -} - SEARCH_ITEM_RAWS(armor, MakeArmor, ARMOR); - SEARCH_ITEM_RAWS(gloves, MakeGloves, GLOVES); - SEARCH_ITEM_RAWS(shoes, MakeShoes, SHOES); - SEARCH_ITEM_RAWS(helms, MakeHelm, HELM); - SEARCH_ITEM_RAWS(pants, MakePants, PANTS); - return false; + auto SEARCH_ITEM_RAWS = [&name](FT df::itemdef_handlerst:: * rawType, df::job_type job, df::item_type item) -> std::optional + { + auto& itemdefs = world->raws.itemdefs.*rawType; + auto it = std::find_if(itemdefs.begin(), itemdefs.end(), [&name] (auto& itemdef) { + string fullName = itemdef->adjective.empty() ? itemdef->name : itemdef->adjective + " " + itemdef->name; + return fullName == name; + }); + if (it != itemdefs.end()) + { + auto& itemdef = *it; + return ClothingRequirement{.jobType = job, .itemType = item, .item_subtype = itemdef->subtype}; + } + return std::nullopt; + }; + + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::armor, df::job_type::MakeArmor, df::item_type::ARMOR)) + return v; + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::gloves, df::job_type::MakeGloves, df::item_type::GLOVES)) + return v; + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::shoes, df::job_type::MakeShoes, df::item_type::SHOES)) + return v; + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::helms, df::job_type::MakeHelm, df::item_type::HELM)) + return v; + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::pants, df::job_type::MakePants, df::item_type::PANTS)) + return v; + return std::nullopt; } -static bool setItemFromToken(string token, ClothingRequirement *requirement) { +static std::optional setItemFromToken(string token) { ItemTypeInfo itemInfo; if (!itemInfo.find(token)) - return false; + return std::nullopt; switch (itemInfo.type) { case item_type::ARMOR: - requirement->jobType = job_type::MakeArmor; - break; + return ClothingRequirement{.jobType = job_type::MakeArmor, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; case item_type::GLOVES: - requirement->jobType = job_type::MakeGloves; - break; + return ClothingRequirement{.jobType = job_type::MakeGloves, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; case item_type::SHOES: - requirement->jobType = job_type::MakeShoes; - break; + return ClothingRequirement{.jobType = job_type::MakeShoes, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; case item_type::HELM: - requirement->jobType = job_type::MakeHelm; - break; + return ClothingRequirement{.jobType = job_type::MakeHelm, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; case item_type::PANTS: - requirement->jobType = job_type::MakePants; - break; + return ClothingRequirement{.jobType = job_type::MakePants, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; default: - return false; + return std::nullopt; } - requirement->itemType = itemInfo.type; - requirement->item_subtype = itemInfo.subtype; - return true; } -static bool setItem(string name, ClothingRequirement *requirement) { - return setItemFromName(name, requirement) || setItemFromToken(name, requirement); +static std::optional setItem(string name) +{ + if (auto v = setItemFromName(name)) return v; + return setItemFromToken(name); } -static bool armorFlagsMatch(BitArray *flags, df::job_material_category *category) { - if (flags->is_set(df::armor_general_flags::SOFT) && - (category->bits.cloth || category->bits.yarn || category->bits.silk) +static bool armorFlagsMatch(BitArray& flags, df::job_material_category& category) { + if (flags.is_set(df::armor_general_flags::SOFT) && + (category.bits.cloth || category.bits.yarn || category.bits.silk) ) return true; - else if (flags->is_set(df::armor_general_flags::BARRED) && category->bits.bone) + else if (flags.is_set(df::armor_general_flags::BARRED) && category.bits.bone) return true; - else if (flags->is_set(df::armor_general_flags::SCALED) && category->bits.shell) + else if (flags.is_set(df::armor_general_flags::SCALED) && category.bits.shell) return true; - return flags->is_set(df::armor_general_flags::LEATHER) && category->bits.leather; + return flags.is_set(df::armor_general_flags::LEATHER) && category.bits.leather; } -static bool validateMaterialCategory(ClothingRequirement *requirement) { - auto itemDef = Items::getSubtypeDef(requirement->itemType, requirement->item_subtype); - switch (requirement->itemType) +static bool validateMaterialCategory(ClothingRequirement& requirement) { + auto itemDef = Items::getSubtypeDef(requirement.itemType, requirement.item_subtype); + switch (requirement.itemType) { case item_type::ARMOR: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_armorst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; case item_type::GLOVES: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_glovesst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; case item_type::SHOES: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_shoesst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; case item_type::HELM: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_helmst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; case item_type::PANTS: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_pantsst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; default: break; @@ -448,9 +459,12 @@ command_result autoclothing(color_ostream &out, vector ¶meters) } // Create a new requirement from the available parameters. - ClothingRequirement newRequirement; - if (!newRequirement.SetFromParameters(out, parameters)) + auto newRequirementOpt = ClothingRequirement::createFromParameters(out, parameters); + if (!newRequirementOpt) return CR_WRONG_USAGE; + + auto& newRequirement = *newRequirementOpt; + // All checks are passed. Now we either show or set the amount. bool settingSize = false; bool matchedExisting = false; @@ -787,7 +801,7 @@ static void generate_control(color_ostream &out) { } map availableGloves; - for (auto glove : world->items.other.HELM) { + for (auto glove : world->items.other.GLOVES) { if (!isAvailableItem(glove)) continue; availableGloves[glove->maker_race]++; @@ -798,7 +812,7 @@ static void generate_control(color_ostream &out) { } map availablePants; - for (auto pants : world->items.other.HELM) { + for (auto pants : world->items.other.PANTS) { if (!isAvailableItem(pants)) continue; availablePants[pants->maker_race]++; 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 281/333] 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 282/333] 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 283/333] 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 7bde3476e564a251afe4182d0b027e2a205a7102 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 15 Jun 2026 18:41:55 -0500 Subject: [PATCH 284/333] more cleanups --- plugins/autoclothing.cpp | 46 +++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index 76a5452220..01b6290aff 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -88,11 +88,11 @@ struct ClothingRequirement { int16_t needed_per_citizen = 0; map total_needed_per_race; - bool matches(ClothingRequirement *b) { - return b->jobType == this->jobType - && b->itemType == this->itemType - && b->item_subtype == this->item_subtype - && b->material_category.whole == this->material_category.whole; + bool operator==(ClothingRequirement& b) const { + return b.jobType == this->jobType + && b.itemType == this->itemType + && b.item_subtype == this->item_subtype + && b.material_category.whole == this->material_category.whole; } string Serialize() { @@ -467,7 +467,7 @@ command_result autoclothing(color_ostream &out, vector ¶meters) // All checks are passed. Now we either show or set the amount. bool settingSize = false; - bool matchedExisting = false; + if (parameters.size() > 2) { if (parameters[0] == "clear") { newRequirement.needed_per_citizen = 0; @@ -485,28 +485,30 @@ command_result autoclothing(color_ostream &out, vector ¶meters) } } - for (size_t i = clothingOrders.size(); i-- > 0;) { - if (!clothingOrders[i].matches(&newRequirement)) - continue; - matchedExisting = true; - if (settingSize) { - if (newRequirement.needed_per_citizen == 0) { - clothingOrders.erase(clothingOrders.begin() + i); - if (parameters[0] == "clear") - out << "Unset " << parameters[1] << " " << parameters[2] << endl; - else - out << "Unset " << parameters[0] << " " << parameters[1] << endl; + auto it = std::find(clothingOrders.begin(), clothingOrders.end(), newRequirement); + if (it != clothingOrders.end()) + { + if (settingSize) + { + if (newRequirement.needed_per_citizen == 0) + { + clothingOrders.erase(it); + if (parameters[0] == "clear") + out << "Unset " << parameters[1] << " " << parameters[2] << endl; + else + out << "Unset " << parameters[0] << " " << parameters[1] << endl; } - else { - clothingOrders[i] = newRequirement; + else + { + *it = newRequirement; out << "Set " << parameters[0] << " " << parameters[1] << " to " << parameters[2] << endl; } } else - out << parameters[0] << " " << parameters[1] << " is set to " << clothingOrders[i].needed_per_citizen << endl; - break; + out << parameters[0] << " " << parameters[1] << " is set to " << it->needed_per_citizen << endl; } - if (!matchedExisting) { + else + { if (settingSize) { if (newRequirement.needed_per_citizen == 0) out << parameters[0] << " " << parameters[1] << " already unset." << endl; From 9d86dc2f5a8959fb298cc0f6c80187c6d92596a3 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:08:51 +0000 Subject: [PATCH 285/333] 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 992957670f..8791e2c266 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 992957670fd99b55d10196cf2850a4554bb15374 +Subproject commit 8791e2c26693cea552c42700e38c87503b7ac7da From cba1503196531eae57bf4c28c508766f43eb028c Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 21 Jun 2026 10:38:01 -0700 Subject: [PATCH 286/333] base configuration: tweak .gitignore and CMakeLists.txt files. --- .gitignore | 1 + CMakeLists.txt | 1 + build/.gitignore | 1 + plugins/CMakeLists.txt | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 8ee4eb5ff1..eda2b226d5 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ build/CPack*Config.cmake # VSCode files .vscode +*.code-workspace # ctags file tags diff --git a/CMakeLists.txt b/CMakeLists.txt index dff1b7fac3..837020bc5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ cmake_minimum_required(VERSION 3.18 FATAL_ERROR) cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) +set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. set(DF_VERSION "53.14") diff --git a/build/.gitignore b/build/.gitignore index 11f87d8c3e..fc12899589 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -8,3 +8,4 @@ _CPack_Packages .cmake win64-cross dest +DF \ No newline at end of file diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 355cc0ac4c..c898e5a7b0 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -27,6 +27,10 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) # Plugins # If you are adding a plugin that you do not intend to commit to the DFHack repo, # see instructions for adding "external" plugins at the end of this file. +# +# CAVEAT: currently (June 2026) DFHack's core code will only autoload plugins from +# the hack/plugins directory. They cannot be autoloaded from mod directories. +# This has been explicit policy for several years. # Example plugin that uses protobufs # proto file must be in the proto/ folder From 5b423dd43353aeab30646b2083b34c84d10f7d11 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 21 Jun 2026 13:33:00 -0700 Subject: [PATCH 287/333] fix ci test -- "fix newlines" failed. --- build/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/.gitignore b/build/.gitignore index fc12899589..0675a165cf 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -8,4 +8,4 @@ _CPack_Packages .cmake win64-cross dest -DF \ No newline at end of file +DF 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 288/333] 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 289/333] 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 290/333] 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 291/333] 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 292/333] 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 293/333] 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 294/333] 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 295/333] 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 296/333] 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 297/333] 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 298/333] 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 299/333] 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 300/333] 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 301/333] 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 302/333] 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 303/333] 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 304/333] [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 305/333] 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 306/333] `>=` 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 307/333] 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 308/333] 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 309/333] 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 310/333] 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 311/333] `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 312/333] 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 313/333] 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 314/333] 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 315/333] 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 316/333] 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 317/333] 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 318/333] 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 319/333] 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 320/333] 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 321/333] 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 322/333] 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 323/333] 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 324/333] 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 325/333] 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 326/333] 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 327/333] 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 328/333] 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 329/333] 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 330/333] 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 331/333] 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 332/333] 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 333/333] 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