From 12ae6dd2b0f292831ef95d9bdb956b31f0c25baf Mon Sep 17 00:00:00 2001 From: Alex Noir Date: Fri, 14 Jun 2024 00:09:27 +0300 Subject: [PATCH 0001/1082] 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 0002/1082] 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 0003/1082] 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 3fc7d4ee1f505789622f1453940e74f06e5f31f3 Mon Sep 17 00:00:00 2001 From: Alex Noir Date: Tue, 2 Jul 2024 04:29:11 +0300 Subject: [PATCH 0004/1082] =?UTF-8?q?=F0=9F=92=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/about/Removed.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index f02dc27d18..a80b1e637e 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -10,6 +10,12 @@ work (e.g. links from the `changelog`). :local: :depth: 1 +.. _linger: + +linger +====== +Merged into `bodyswap` as `bodyswap linger`. + .. _adv-rumors: adv-rumors From b97455668d966231c36c48abd9a617a846414620 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 1 Jul 2024 18:43:06 -0700 Subject: [PATCH 0005/1082] Update docs/about/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 a80b1e637e..94cf58e5ba 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -14,7 +14,7 @@ work (e.g. links from the `changelog`). linger ====== -Merged into `bodyswap` as `bodyswap linger`. +Merged into `bodyswap` as ``bodyswap linger``. .. _adv-rumors: From d5b6f2044a90c8a6e648f155c72ff155d15f2d27 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:31:13 -0600 Subject: [PATCH 0006/1082] Update Stonesense docs (see description) Update the Stonesense docs to tell about using OpenGL on Windows for stability and reflect the changes made in https://github.com/DFHack/stonesense/pull/121 Also added a note about our need for some missing art in stonesense (we don't have sprites for bookcases and pedestals for example) --- docs/images/stonesense-yellowcubes.png | Bin 0 -> 178548 bytes docs/plugins/stonesense.rst | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 docs/images/stonesense-yellowcubes.png diff --git a/docs/images/stonesense-yellowcubes.png b/docs/images/stonesense-yellowcubes.png new file mode 100644 index 0000000000000000000000000000000000000000..83ae6ce5077a512ae28286bfa0748c62d86374e8 GIT binary patch literal 178548 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 z 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%0A3LFowg*Ew(zB+&5 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 literal 0 HcmV?d00001 diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 69b305261d..56461f1a95 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -19,7 +19,7 @@ or be moved independently. Configuration for stonesense can be set in the ``dfhack-config/stonesense/init.txt`` file in your DF game directory. If the window refresh rate is too low, change ``SEGMENTSIZE_Z`` to ``2`` in this file, and if you are unable to see the edges of the map with the overlay active, try decreasing the -value for ``SEGMENTSIZE_XY`` -- normal values are ``50`` to ``80``, depending +value for ``SEGMENTSIZE_X`` and ``SEGMENTSIZE_Y`` -- normal values are ``50`` to ``80``, depending on your screen resolution. .. figure:: ../images/stonesense-roadtruss.jpg @@ -52,7 +52,7 @@ See ``dfhack-config/stonesense/keybinds.txt`` to learn or set keybindings, inclu zooming, changing the dimensions of the rendered area, toggling various views, fog, and rotation. Here's the important section: -.. include:: ../../plugins/stonesense/resources/keybinds.txt +.. include:: ../../plugins/stonesense/configs/keybinds.txt :literal: :end-before: VALID ACTIONS: @@ -78,6 +78,20 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. +If you are on Windows, you may find that using OpenGL runs better and more stable. +Simply open :file:`dfhack-config/stonesense/init.txt` and set the renderer to ``[RENDERER:OPENGL]`` + +Yellow Cubes and Missing Sprites +-------------------------------- +If you are seeing yellow cubes in Stonesense, that is simply things we don't have sprites for. + +.. figure:: ../images/stonesense-yellowcubes.jpg + :align: center + + An example of the yellow cubes. + +If you would like to help us in fixing this, please see the wiki page on :wiki:`adding content ` + Useful links ------------ - Report issues on `Github `_ From 8a548e65b930eb708d6069848e4fe74991615710 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:38:02 -0600 Subject: [PATCH 0007/1082] my new pic is a png not a jpg --- docs/plugins/stonesense.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 56461f1a95..efdcb7f230 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -85,7 +85,7 @@ Yellow Cubes and Missing Sprites -------------------------------- If you are seeing yellow cubes in Stonesense, that is simply things we don't have sprites for. -.. figure:: ../images/stonesense-yellowcubes.jpg +.. figure:: ../images/stonesense-yellowcubes.png :align: center An example of the yellow cubes. From a6b97aab627c17d48da3130cab263402a314b4f7 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 19 Jan 2025 17:11:36 -0600 Subject: [PATCH 0008/1082] we fixed the resize crash. removing old info --- docs/plugins/stonesense.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index efdcb7f230..12649c7add 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -78,9 +78,6 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. -If you are on Windows, you may find that using OpenGL runs better and more stable. -Simply open :file:`dfhack-config/stonesense/init.txt` and set the renderer to ``[RENDERER:OPENGL]`` - Yellow Cubes and Missing Sprites -------------------------------- If you are seeing yellow cubes in Stonesense, that is simply things we don't have sprites for. From 46693058d93327f01427fb96ca608cf1b6981f97 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 19 Jan 2025 17:21:07 -0600 Subject: [PATCH 0009/1082] Update stonesense.rst --- docs/plugins/stonesense.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 12649c7add..48f321cbde 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -93,6 +93,7 @@ Useful links ------------ - Report issues on `Github `_ - `support` +- `Stonesense Subreddit `_ - :forums:`Official Stonesense thread <106497>` - :forums:`Screenshots thread <48172>` - :wiki:`Main wiki page ` From 130787901b46458ed8194973489a103215b836d7 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:40:58 -0600 Subject: [PATCH 0010/1082] Update the Yellow cubes blerb --- docs/plugins/stonesense.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 48f321cbde..3864b69df4 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -87,7 +87,9 @@ If you are seeing yellow cubes in Stonesense, that is simply things we don't hav An example of the yellow cubes. -If you would like to help us in fixing this, please see the wiki page on :wiki:`adding content ` +If you would like to help us in fixing this, there are 2 things you can do: +- Make an issue on Github with what item is missing and pictures of what it looks like in DF +- Create the art yourself. For help with this, please see the wiki page on :wiki:`adding content ` Useful links ------------ From 598aa1237f773a31d5243de20d4adb682b9ee45a Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:17:22 -0600 Subject: [PATCH 0011/1082] New Widget: Slider This is just like Range_Slider except that it has only one slider bar. --- library/lua/gui/widgets.lua | 1 + library/lua/gui/widgets/slider.lua | 132 +++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 library/lua/gui/widgets/slider.lua diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 8a5daab7b2..e7870f88e8 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -27,6 +27,7 @@ List = require('gui.widgets.list') FilteredList = require('gui.widgets.filtered_list') TabBar = require('gui.widgets.tab_bar') RangeSlider = require('gui.widgets.range_slider') +Slider = require('gui.widgets.slider') DimensionsTooltip = require('gui.widgets.dimensions_tooltip') TextArea = require('gui.widgets.text_area') diff --git a/library/lua/gui/widgets/slider.lua b/library/lua/gui/widgets/slider.lua new file mode 100644 index 0000000000..92263d2924 --- /dev/null +++ b/library/lua/gui/widgets/slider.lua @@ -0,0 +1,132 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- Slider +-------------------------------- + +---@class widgets.Slider.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field get_idx_fn? function +---@field on_change? fun(index: integer) + +---@class widgets.Slider.attrs.partial: widgets.Slider.attrs + +---@class widgets.Slider.initTable: widgets.Slider.attrs +---@field num_stops integer + +---@class widgets.Slider: widgets.Widget, widgets.Slider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) +---@overload fun(init_table: widgets.Slider.initTable): self +Slider = defclass(Slider, Widget) +Slider.ATTRS{ + num_stops=DEFAULT_NIL, + get_idx_fn=DEFAULT_NIL, + on_change=DEFAULT_NIL, +} + +function Slider:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function Slider:init() + if self.num_stops < 2 then error('too few Slider stops') end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +local function Slider_get_width_per_idx(self) + return math.max(3, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx = self.get_idx_fn() + local width_per_idx = Slider_get_width_per_idx(self) + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(left_idx-1) + 4 + if x < left_pos then + self.on_change(self.get_idx_fn() - 1) + else + self.is_dragging_target = 'both' + self.is_dragging_idx = x - right_pos + end + return true +end + +local function Slider_do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + if self.is_dragging_target == 'both' then + if new_idx > self.num_stops then + return + end + end + if new_idx and new_idx ~= self.get_idx_fn() then + self.on_change(new_idx) + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function Slider:onRenderBody(dc, rect) + local left_idx = self.get_idx_fn() + local width_per_idx = Slider_get_width_per_idx(self) + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx ~= stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif left_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if left_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + -- draw tab + dc:seek(width_per_idx*(left_idx-1)+2) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + -- manage dragging + if self.is_dragging_target then + Slider_do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + +return Slider From 8779a7bd8f314bc07a8d472df9cb38a1799dc614 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:24:38 -0600 Subject: [PATCH 0012/1082] Add Lua API Docs for the new widget --- docs/dev/Lua API.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index bfadfc6899..fbff70120e 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -6440,10 +6440,24 @@ change, the ``RangeSlider`` appearance will adjust automatically. :get_left_idx_fn: The function used by the RangeSlider to get the notch index on which to display the left handle. :get_right_idx_fn: The function used by the RangeSlider to get the notch index on which - to display the right handle. + to display the right handle. :on_left_change: Callback executed when moving the left handle. :on_right_change: Callback executed when moving the right handle. +Slider class +----------------- + +This widget implements a mouse-interactable slider. The player can move the handle to +set the value of the slider. The parent widget owns the slider value, and can control +it independently (e.g., with ``CycleHotkeyLabels``). If the value changes, the ``Slider`` +appearance will adjust automatically. + +:num_stops: Used to specify the number of "notches" in the slider, the places + where the handle can stop. (This should match the parents' number of options.) +:get_idx_fn: The function used by the Slider to get the notch index on which + to display the handle. +:on_change: Callback executed when moving the handle. + DimensionsTooltip class ----------------------- From 793f9cfc0d9b87446b8ed2d1fa72372f41589b31 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:02:17 -0600 Subject: [PATCH 0013/1082] Remove the Yellow Cubes Section --- docs/plugins/stonesense.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 3864b69df4..b9f6c93668 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -78,19 +78,6 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. -Yellow Cubes and Missing Sprites --------------------------------- -If you are seeing yellow cubes in Stonesense, that is simply things we don't have sprites for. - -.. figure:: ../images/stonesense-yellowcubes.png - :align: center - - An example of the yellow cubes. - -If you would like to help us in fixing this, there are 2 things you can do: -- Make an issue on Github with what item is missing and pictures of what it looks like in DF -- Create the art yourself. For help with this, please see the wiki page on :wiki:`adding content ` - Useful links ------------ - Report issues on `Github `_ From a62cdf1fc5f6013a69ea50e7c15843e2141e114d Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:53:22 -0600 Subject: [PATCH 0014/1082] Delete stonesense-yellowcubes.png --- docs/images/stonesense-yellowcubes.png | Bin 178548 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/stonesense-yellowcubes.png diff --git a/docs/images/stonesense-yellowcubes.png b/docs/images/stonesense-yellowcubes.png deleted file mode 100644 index 83ae6ce5077a512ae28286bfa0748c62d86374e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178548 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 z zdDB|>sxhn4<>NGl(w8p3tDaRi(xdxi%Dct%Tii2kqTBo^+=ahe0$_R21AIOP2frX3 z_r3_RzyD=5*3U`COG;zfDsNlo+Vx2X-3f2%qrYyxE-%{g`lavGoAzD2rVSje55K!5 z8(B=>qBN%Sy1!?h>fX()h-da>O2=~dq=-iFM*B$!fD zTLl=ocsjUJ}N7yM9(34MP5Y!nAPw5nXZ!l<=={%-C-~QK3`Q@QMJ$d$|$)7!( zrl$f;&D8)yoEvqj=>+KhW>Z__;kpwV z6S(~D6Zif$H*P0QCubiGUV@cj>A>8x+uHt5q`<#jIPN_qG&J;+(y4!>vV9FbK;5WU z+X=R(%i7Zg)&a|qhU1_M5Vo|`c|^yA9+>HOWMiRt3feekkLB0G<2f2Qw^{3g6~ zwGH>GM;ngvjFZ4R9&s@pB|hdDYYwr|E&dKm6^F<~8!uBg}{4)%_m!rF9w)jjcD&hj31W zbW0mMF{_aZTYs-Eb}wz@HH=)X!kWtNes8=!k5>end*CQH49G_!;*;=XXe&+}@*_xN z*~vq&8xi5zu!-k0nMpq9qAV1pAcc~s@SY96<2dVa!Xx2$=lwky=WUcvMslbxMl@pM z&1So*yNb*72%JyqmA#FuSJ6qD79ScXMg6OsGqV#hJ3XH7VWylNt_5itj<1U_O>N~l zV#pLux!EA}M?)*)!*ADH@(?Z*y>P)F8WfI@X`|eNn(GjD#;z0TT@N7h={F4{m0h1W zv5~<^aDSa-8 z=I11vZ|HeOy8E2U=Xq9o_(g@?QW-YpE(_X|s4N%yb8B+Oi=L*Zl}^9m4RxY?^6*i> z&%_5x|7V=aOOKSq@Vd$YpdpP{*4=SEJ>#m!qT1N=s`J;C|7)t#*Q1X*^*y79x_w>s zd0KI+4XqH;=oCdK9_r#x(xmQ5zo0RBe`Lz4tB?NRchi140KeQL1*h}bu_z$p!B1UE zHjFlU)N$lO%K^!ww@-D+`cMbgW1#O#`pxal$go>x;t<{F2xWjfw{q*x2G^rQCwHFC z*G4v`F$0Kcpd~cHQF*{4>Yl$kb)>D2%D#<`55&-jJC$mN`OXl|xAE#+m)Fe$B)-B4 z@3I`HAq+Z8W|Ud!0qcV4O5~&@UGmz};whT~5SQ}EGOSbg!in?3Ni*$Dr!gIo{>Fv} zI1U=@_Ut>62EKmQ!0fDpAP_!X7~c3+qgOalVW?T1Q0VZ+5c%TU~5FGd&Wg zr`T@U^G#2-*9TC&5_%+NAdZV39}GI`SxHB41q4)<5kn`B@Vhb4Yr*&)^YHXM^_j!y zCJ%t(GB@5Pit;G8jk68oGyO3+Cx!3piM7@Bc;$twVlwpE5F-qXoktv9MFXG%LAy>U zoCHRFoATfLBccp1_vhLSxptVkvCh~)6uK0sygK} zsv|EB9@$VXII{gTWfm|JAuW(jw~5EtKxf`zC0e1W_v&(HWw30r^v;xEi=WP`Ht_B(E&ECbpF1Z!h%#sgFm z#=&fu1cSW0!=A6gVU~Amc+jkYHz43t+`G*40ZMbAGIq9CRCmjdR|g{xj5pJ47>&U`Iwn;IK23Kusy2=2h&Y;b zRHH)9RaZJIIHuxH865EQz+W2z90GK9*CTjpZan#_c)lkesF%_foi$ZT7&{Z)$bF>= z(^D^)VjwCg~Rbl%e91KWb)X6t@i03H)DK5U{gO{g&xw1= z-`vJAHmDYX^{fI%mo%ekwsn55!OS3nU-) zm${U_(prv`4UOm-G@>WY(@x_~OX(u{Ih#U0)4*?ZEcqfJ4Ywwsd$bvFr*7+k#?qB? zByWA*Z(gmpvz6#yYN zPnH8V(NbXvS-xqgGAm8ddE`+F_FM04acEA)UBg_Qi=V+5kq2k)aAZ^lK!QkRYk|w4 zh=vZFV}=;Ewt4-fdgt75oHr*oy^=JdG-~y1_2gPa_F!d%5{$16(3=XG4@OclRb6>d zEbw5kFb<5QcrY5pbxLpJbmMp9jhwOs6=_iX2!(oUXhbrbj6{ z-}MxZqNgC~N`8|cV~4yLAz*wP*{duN(!qk?$b!)|PCVl&BYDy-ugc0gC=L9hj(O#b z^4ih3K2XugmuZ{&G`O?y=6ffdB9osaY5^3ZH{|EDrbg09Q4W8J)8$Y;I)&4wa|FAQ zZM!VWfafL6Za(x;23|ZN9fmRW%1AOSb)eyGW{STHZ-z=65LJ8hH+Av@_D5 z*E4a(jrK}8_h`urar;hdH9`Am037Qdk@0DMM#{Tawr>m zGEkqEQSwqe%(g9O;&#wvZi6z`c@Y-PNy8cmJF#;53MZsL;(1FV2-r!P3-kK3lcK{; zS;~K}_mQ;W_@#|+yU6w2R-VVtO?9r3=OtHHHL4+9O>G?(N;;H{-rnWp^j{-T2xqc7fD=g>goBRMuM~|r+PaZdZfN(Bt#E_aMdLT0N4NiTH zGna-nN^$;IU%U~k%j* zNM3<6-&di!1N?S5Zn*HidNID<+`Z*8+5l-h9nhsS!=x;^;N0i{Z)K$Ztf$a(rz+fV zJ0bcxnSJ}tGy3gqzOyqAYKVLy@vg7mj@7lB;^A&iU7eVFI5~q+1UK^G@~c-Yn0mU%hB~;O){XEgM`WC-Pv1LJvZ`3 zg$xtVm_97MVY-lZ;(D7;=wCW(=xxew1G+#OPfxk?0bcUfbQ;^ptUW#ZieYQe{4qa$lpL7@F6hT4doG5`Ddm_<2yh8 zmU!}UUeyqO_lO1q1=t*}OQ~(Y#IXp^hy^FYv=-A_beh---1np3`Ib*|b^M+(a{3YA zOm+^0Vms^y$%CW01u(5IFBph z#?2>{0*IsH{B}>AA-dyd5*h9iFbMdQ~1UV5DhaHFsY;aXsu@ptr(&-o!mk&W3>Av_O*$q9U_=Hv%c3 zj8op4D+A)jiy}MDj*9)|Bbf#oc=!6~=xpNH+**>-v4xd)v#TRA$U^$4(y5^+ZD&-c z?=@|d74v@r%b`n~dw1}I9zRnj$i@cLyz${tk4)Omn>c{qnO^za*xdmkZSjvf!Xuud z3B54y+&}^NSyL_P1d{L8AL_`aZ@oN1_=(r|qbQ%pG7w!53;`Boh;bruKvV@&lK8uF_o;2E6?MwJ|RMYf4q>D?}pLrG`iR$ZVC z74FDQpUDH_gfC^Myu?jR55>gnP>fC5K<~&ub2MRdoo%wz(R`s4ePq5GJpmng!O!Ne z8;8D_pVZv-7nRBnbm$X~M4k^h$Y1sVTV*@t^sH|x1vvE`l%yvubh$&sY(jVa6G+G3 z)SVmvdCVK+a46pQ#D(+Iaq0xGm1d(AZP7FXz1;%N*3;q?H7Qx?odq1%kK0Nv;d$k!Dr(BN;;RFHFk!U+^j zb}5FWT@8AD+1mqo#>Pfs?&MUQJTsfu6ny`pA=(EBAxsG&?`~{SohC+q8p}(pK^^M#4z*|Nqqa!C`P)-xt02aV8Oc-5c4e$vaH8t7>;s}8@Mtn3T z466*U2v}8cNx^pbpcYB6_#}9d&z_}&dAkPj5Psz8IevkLjt|m>i#rzJ4zL+zIX;WBYln7eNyg z9@kImVmj$SRy}RxQx}m{<}~mkJ=Q{ako)H9ezw&uIg;AaYo1uf-JAyWzVK7;GRy@o z)4Xj1DaZ%?0H0}3l($VMxLiNigPe}HGo<{~iF{Ol=^UfKTx%0A3LFowg*Ew(zB+&5 zL}%IB%hVM&{o>fa8L$A)R$q2_;c!@Z=WZ`v1%jjL`5*q!w`HwgpI?i`HKrIMxA&f# z!C>^{NaNR@yA|L4_Ve+fM^DF>USo0A4fC}-G*%2v#Je7ti+5d;G6?yXUfYQO;%AS( zYBw38p3lxcJ#863F@Wnaq#9~yEb4TPJr!i7mf->Z<82Ny%Y)HX7jv==J4} zmt#OYO6Ps!N8_!GR`N6i!V3`Qp|pieL!mJsG>ih_6%W*ja|{#oD@(>tog9kE8P!3& zSy@_->zD7QM<*>tpq$s~=?5iLt|-{eofb^LG1M=^?Zbesd-gzOL?_3&i^)A*c4!nm zlk#|t?1{5uF{%*=uzXvcyk)P&$ducdcc@}?XSo7xdBGdn zUDiGEcc->~vt3)%2OZ5du+(YeY(qMQ%x_((sk!4g;RziiIt%D}wv(ijj&paqEa}cN zn;DvWlsP#$9N+f-2jX3CyFX^9CnU43P2})l?&4Z!+STorg?Fa5Hn;pDE%36d3&)b} z;6sm|`XuWhoLJ|~2p2bU86&6GIXgla(^+Sx$6{=_Kc}$(5bel|<2cAOtlkAvV~ki{ zy|bD3ZTTDtX;rCUxQv41u^Z;d%Q4@kugMe-T@mf^%6H>1B81!6*vqE@QIr*%Zy0`t zw}~~&BD}D>wC0!M_zq6;@KikcGiQcpic}_WcIj*A68LWaiiZ}{z``B|13ShP7ex$- zx9k$xmS8|Z8N)*uE~Sw|VK`7~6c2zN8Xn5Vca#(*V@F&zavD?93(fg!luPj_BLE)q z5%!1wpMOA&bSK{bf%nDI(o#J3*kkef>zCtTSB7MODJS)cWUSy7@&X6-szw1dl#fv* z+~w@00m0cM50dx#lQB6slwsKWkg!dufv>Caf!h2oex)49qu&Wl8hStINWM*xMSPq{ zLkIYa|K!cIYS?Myl&5;(7$V+KiJ@?(0f%abo1n6cNo5QF!NG${_b%alQt2KR?GFjZ zBl&%I??LhTVSRr@4W4;@-y+@C4>0kYt`l?_R0#hvT%8E%YAN*NZ&2Lsfoffz0w{Wd_d2;^gO0|sV)uu`F&6IJ9t?6PN?h+@lJ*m zT*z6d%o9^kGy+u~Q5a^@rM&q@Iz8Y(7Ns-fBoF`d54tgs;L7@72#L!2H|fZQ&MO{V z5oe`q?^a$$73)gj`er_T;}x`{vR_OA(#BXsN7PJRZEcA2yXJR+sZ!o5F27rzK-MVJ zQ&%3mP!{+tD+Bp$ z$5K8`r~*2_D0^78=b`&g#D%jn=?rpeRpE?K7-29ILK|Y?z1Bx)U$7!+*F(LyXY0=V za`)24`%a9-te8)oPfw1-1E)M+&tb!_U0oF74yE^md!20el8Mie>>h5$&VhR0`)20A zfalq%_|6WyR`g4HL|S^X7__Q5Mis{7Ji6lfP7fp-l^(vsc;aipnO+)-i2&%~Sb$P` zyu88yoWYx&BLNU7IEhE-%)6)X1@w3dXS*j;;4&M7(1MYPzL+_Ug7@T0`(JqGH7U8v zV}ZDgNG>eyaZe{U37l!Ep%@*W%J1;! zyT13^b7Az{`E$9gk4q>2;ZOg==slQFXJN)YL5(mNEz$cy4)hxPgvY%=dSJX)!y}SZ zK>cQsbYyt?4fW;*+s)0tCR)FtQOn=d?;9D85lN#i`soE5k4Gd0Tw^^bU4a(rhwPb# zK;dg~(RxtM!wnVSJ8u}I=bt@b>5&_w4c64yDRqLzQ&+O#>7h5cvGw~j(*8G z8O%)nNzwKx(d;*4B0G#K{DK~KZDm<4KtAX^^<3NZ^bK{xcn&JgC;Yud)i}{ndf(Jf zjaj;YzOQc1D~&I@93A9snk;^spTsjl$ns=x_xJy%c>5js`^{i*;OUqg`%5b0>q#?< z?M0_MP3|F=0RxaTyr;dv+clNRhzOmjX;4NG}ECs zyzbQ6a1)383y#KjN5`e3)CXW%w%V+12ko<~zg6esfG#I*o3b;k$zQY(&j>j$NLAec zWPyxJulQYU5}e>Dy#Ywa1C2!|X{M%zgpjdzxycs@sR*ejv*(#SlDz{BnCh?z{2^{A};lX2L#Q{gaI3PGPR z2!^9bH64L+(unxW^#}fhmxc_ZDTCjnhjFAuKRN;k?Y8WzBl@N=h~VafEA?eNWg&-c+Qxq zA}L&!wI8RZ9}q6}y7ZexO6X^b&5akwVFv>wo%;TccfU1Wdg;aZzz4W3S4t|6|K%@z zO)U{e?P0n5Px?RUmn>B_gyv{PjfW8ejwBbF6@T2392~q=&g$P)c_)NpQx57^WK>>K zUYv-hBB9ItW?E1UUp(@QtB|)F1Mjk7bmThwly1sh&w+FY!(ao$2uubIdTS7yyZfFN09aFDDzS_NK)DBp=auJdtgQdPWE3CC@HPw$eoA%yg%}+CqS7yQ>QeQw zUYL)i4}L)yy=r$1QWiaQKxmXTEyt8DZO{e_N8xaHz6eh`6^D@lG7pY0N54LV36Ic+ z?)uETr3W9CgZu%7pUZr0z`0W=?kH317sb@WQ?Jm0gQ{~iO>I&h=ts5S>7yOs7|F+U?O^{o;e1%(-=4BL)M$5l z=5sN7@>ezTzmp>lir?E6P8mdO3~v%NPu&izD>k~+2pW>dDm&?+?xKe{hmjZK>X8C< zFGph8VAE{O;RATVU-6Xfbu-0M&-|u8@v|N8{NA1BI+La=2k0*y0|x+oIs4G?ko@ov zoJE6m$2#Y_9mMq1NPPQ;ACIS=yqFGyHIj^HW~$i+%#0Cfz2NRecbo`oD@?@>$pPJ2 zSdOne`%3iIBKidH3lRbyks8aJyZN?6&XsWWNlstsnH(92sgc2aMpRhkX+Hx@StapR z0T^ndijw3y1xmzoXv37sn&CJpDV-Wo2@K)#4?MknVwCg@W1K@R{gp5hNJ^Rd^G|UDi;79#J56~r@|q|!X*x@u4_F;SlB4vG_W?Z#csIuy?nD7J#GZT zUiRoYEx}#Lrzh&}DR7|dJPimJ{HsN3=9e)T*#o3!FaE&~J6_S1Sx`Vv5q#z|pNTuS zZ^zI4lb?-W`1xOmU;UL|5iRO%C}ECtaD4hhNx==?l#SwDTSMZ_5cldLrlnx#lK%aJ zhlK0hdfu<+DaCK-k8W0YaPw;db zn;RASlZ{T3vYHBq9zQCPLIoVs9UPQB7#USv z##OJ0q>Fk`Z`wWlC!Yc`545J7^kA@thFDv{pt-YHvg8xpa)*jh8N2WnWC#c2?CFbf z{_Lf=c>Yvex_>MldSFCh!lC-JfWF09FLgE=Nn{<$xF+q?O%L&D2V6=0_8$N*%v=7- zIpjwN-jKe*%TtiCO`Y0hWkiNoem!DSZ@=vv)8$72d&lQd++7OVV4IP3RHS*@q~^EU zIh`@uJAA@6vDH7%iex;qFl_r)hldV-6UJvwd&&N@lWTHpw0p9h>rQU3(lGedh2vMG zwBfny%lEvr@$On*eD&&(8YOwyE55DXf{lNAx&6a5rd1Zk3WZ`SfNf|ndVOs2%R0broVzp=6B?GmoYjThSecixXmsN^WG_|^G}y4JH`o#YJqJc8)uRQR$GyD* z$9srZIKc&A?2@wr_a@)Sr#2l0Bji1hGC@Tt1RrL2*Dd^Q#?}Wu^>IM(tF;iO}U3f}}cns>)_$dWxI4wnDVLJ1x zT+U)Jod~^@Pj88EcvMqcG;kK#_xJxjmHh`5Hk&jahrxlrCxi3f%g7*GJANzoL9A^q ziVqqcC9fD!(Ac;hW-MtR%QqiloMdS9M;txYJV>cJvH5^H@ixi)lC^iEg(qp4Rp0Eu zh##c~@U3uCXX?JCk<`x4?^hWoGtHa9$nYN%P4wh#H26!o>JR=8#7!zGX zGTz&&$7bBQb3?sXHq>~NN?UIHA!oi5HZ?UBqoZT7y1FV_*^FZyM^4y=%qu+T-qy|s zVte~XC-kRQB8;FfqdvXvFQCi*e`XQtWKI zKFNC}f_LPh9^Y(g%is|t`du(PGZx?diMMyAwpd`VD^l2zMLX%7Y;VqyjL2yeKm)G| zs7o7re)d27H?mfB`655YhX-P26y?Xrb7>>HV@TyGET+4F7j7=cN;3sErSx3nNV=D9 z@5N7jj!XZ#LqR-jZ@(9Ca3nh%{^3H66ez59gJG^N%j%QSQ-$_4q&jo%OolriC9sfo z)uX3ZPx&Z`r?iYS1*Y^cw}ekQ1%x>djk6o>bcR%V;V7KWLzwe8e^)p-62jYIhAvK~ zU6}WTF943HjCNn&@0VhpRe206e@)Meis$g4of~N>6UD*6GXI>)L4)qp&)?n9d!!t^ z$AXO%c5;;C^#;?(`T=-mCeEo`ruW?Fax{?T)#!#Eq4_|Wp3d@NI~Iu99TbP}QvSE= zu`vbGaLYmY`?G1V4i8s4@k2uzB}f@_&1GAPyyZiB7!$&3#Ki%(T9?9Tip$C^%uxu^N*(0T4}JYd8E{ydy1*AffB!!d&ev6+ zG7es+Bi3?JN|i$%_??{v(K$3cB*QzIX~Dgz#=Et(nKWCs$l5EO%7}`$!j`40brex2%>Ps%< zv)(b2(Hr0W9U669Vx;@N_{jTj$J^iXx_ET7(|6<6UVQe8TXE~oo^*dde(7JV#!Ihy zdY8I_(`^Kx{O6-aNwrk)Ajn^1I`ZHcGs38>_N_F3v7=X{iq` zjN}{|kbK5Qh|inqw6CkP&)N@wP9$%o-_VlnLZ6nCJ4_15IKnB5^yu%*^jLh?cRZO> zTU>p@SrFDsJ$=R6-a#HxB#vnIrTFYkwiwQQ-Pq)iJmFf-A3yfwLrFX^e_e)n4a1v1 zJAlwI9B+1wJT*zf*o0v`%ODV5h83kn5rD#(0?=PB^j99?m+10)0md4kVTg~^Cvn=D z(c@j+xSNe7l}-(%aI)AwK+m}q2GFy_;G?i8aSs2RczWarxs;mtfq~)db>WZ}SFed4 z7TYlR7}KLC+rf=fIx%&h(tBzLKqmA6H*#AiBe0W$(|FsRGR%Q7gQ=Vwc=9o1k=jh)O-%As+ApE?eY4mG=T+=x;3 z74`7f*6fhrJC2CFP)xb#2Q+y-#ZDT%z~NAzj?N0;Ee@@W#F5d30N$|ZkFKPSG`vVz ziIy~87+CRh_rULvVG3t_Y>LfD(jz-=ne9x=cW{w^d~963#Swicowu!e*AbGgHI$~8 zoASWPbQgcAcXwoCfOqNSl-7KpW7d@&dS>}mJbG(cW0;^3@OEmCAgRmH;D|bY;+5!3 zqtozkFV}|LK^z~Kelz2scsqL0EmCs&SdXvip)NM+**+S64-NIkU;0lk#rJ$-Ch>kN zaQp6V{P<7Yh+qCSFUZ@;+X<$@)gNuCXodHb4^ZzKc_ELY%QSWK)H+r{#RHY60DRzW zw{6SYo!!FOhKbq!d{|puoL`O`*Y4y^#FPVmctbvSQjXG*qY{aZIP0%F!+ah;+1>_D zwBtjmd7P2^l5_-2I0 z?`LB#1!bxbfF(FGdW)2wy(>Fzjs)EBi0{xfId&?~CL}%L1vqe)ob>@w z85i^;&oDIfw2hRF8hE>n$A*n5Ge$QS98Rq|DZ6Tj@W>PKgur8QBpb+p=W#_VJu)22 z`qpyt9G*;!%*hF|*1`IRWk>yR5YUqR5S6%>q00(>WR&n%1W~Jg$rDlRBsLu8#qH)IYaYi_aBSj)?Sh$aeE{ zOT1r9o+KnMz55FNr|zVoQ-U66M@Ah&4!hZzQXdX~s`Iid9EaV}0fv|d8KHCRK%@>& zJTe`B@{ipY?|q7!uX|!*T%(%7u6%woVDz@OexMPOdH)mt?XCE^UtCqVN7evzrGCLr z@rH8XN%1=EiTZ#J9 zQ90uo*~h`Y*jV3+8`tl|#@cq$+QtP?kEBmkCyqK4PV{Gj_mO<)k$CK3-bd0CbJKL_ zhI28U5eOUu$Kw2AT)BKTZ#o|x8;$!fU5qh}K-Skc`_7IH z<$4GXg!3S4TN+C3(inE)^@X*Z_Ts$Bs2lWN|e9zFL4=LHaVr?8=pAN`t8Qo;8f#I#UO8!Kb0rLlE}nm1IToQ z&s(5Wl*vEvLTlno#j_G|uq~A*X$DLuMwfII#zT4CbOyFE6Xu4|7+*DR@VhrpoiVgF zOwi#Gmt{nCscY`EQQB-Y!j}&&Dtp*XFEx2%3W2JV>y=#0wB}cGr1Mw&%3qns>4Bn) z2cu0eC3vSBy;e7P0JNm1zL{2uF^I-t{+NF062@IRm&G*siBJBc7g@aN12KQz(p~j4 zuGFChDlPR%1XLt(Lq86kk&N@w8}=?L4(GX7SamI#7d^={oQMGac>AXf1-~&2IQG&h z99ee2!_h|T_`8Xc1rW3l^;mf=sV>Bq@kP~@yOt_YZ4u8%g%M` z)+OU50Y+9hr_@jVQ6D>Yh3WgL6aDel$H(HyMuhb?*BQ_J-8PWB~4(_W% z4G%dt!-yPv#4fdhq)~qWSdY-GcEmZ72k$?TSEL}LVRhcISORl0n&N}f*YT+>zP3jc7-$rU{Hq&_+1T64L1RIXlyR>SN&C`90EHt@ z;jG?ZH_xFQyn0v-?VO3gsF9A|A)ib^(4fEBVtY9Z6pGQp>6r%;A5Mq{Kw|(mbO0Wm z*cb!kbx*yS0??mn_UNVnz7t<{q?eWJLBbC~(96NWZkc2Goklc!Vj}5WSy@vrb6Iq6 zsqS91r&oMp>SD^gaFQQFGe`i!+qzFXF;AFQHk|-85pUzqc5=!y?~+f@tI<~0Sva^E zfH*~F96V&i2wl{PQ5HG}4vZbs)tu?s+EyAp=z+4Ptk{f0+o3MM>$}g#fAsG^5RX6n z=5LJzKKJUo;@O*zX!PPuF9m0&ZpHgAeK}4|+-XAY4gcf+Iv;;S&rR7X>TJGRe*u2i zEJKCEqtYkx0Pq`rILuB2UI5@g7p%kBm~NYK`>?K59?B^ooOtJTho^LLk1XUJ24EUO-ExYyLy%JNFQ`pWCExUeL9Js1x?{6Ngi&E#VH)6cyc zm#^K5S=sQ5=T64->|8wm`i;1K`)ojhV_+Wk=sA*)JbotE zMOYs>GczLP7ZQb^7$1tcW@_wfSGVJ@{PKc~m~$o5dsI0++>V>W@y$&UIUN&|o*JW# z?>2UqMY#1fuL)FsGuG1*#c;xtQTn7o7t_gS=iGmVPMAzvCo;o%lj$_9t&bY9ZC3{Q_yrXPPNoZi5s8lTTKaBhO0lFI>S z1b(}hXTW0D)iRYWUYcc@dT?fCc%cdKi4fL~HQ5^%-%Oetv@NV)(h38hl-Ku+c8q+?$KLTaU-q{!9$@@5B>lo{y6g^G$3Q zUca&vuggyH>V~gByA%J%FK@(^>o)d`@{mz)08IG^La3z-4Hqw@#nNd4aCv8om5?=DPFFLr{m z5^6S}<}c4E5K5fVIc=j1dJwjvEi8RXmKweJ($@&q?w zmKV54n`HH=^_v9zxdh8s#anuKzC;@u;wEQeP>IW?_f_kJr2pOEWJs!8#&T7QhuMX~# zY!;gWXz$3+b)TN(W3kcH+AiBDdbj*mRXmfJU8k@B@G zEV$VSeD6Ci#(i^sD`NY7Bro0Cj=%fa#UoQ&R>FZe|KMc4PzLTS-rk9gC5Fu0ao^`g z(}Ge~0D;kH$OvP+D{DXI&Ts|9UcCJBE4h8m0_++&j3lSOX-td~+z_}{Wht1Evci+XBJvkoFJ@;J9&(Ei_ zkiMYmG&^x#Lr*nI{nZrBamZaldY~6RGKEEcX!2saaZOL0OZh;L4O2~sfli^pNqV!PHaw0n~`$I6ya}D)NAjU3`o>WD?@U)ctva8-w9n7)&Q+ zVTcJo3QT{Ze+^CdD6>6^hi{HY2F;r##pCzs*}ziU>V$~VfoU}1eGKK;sjvbGner+fIi@!J@pZXl{fH}&$Y{!y!%DThP zd&}6obw|+cy#O7eBUd=x(E!jN^1y%iOq|=SIweh-j8|C2%PV9jB)>QDGpB}PYF2hZ z_Iu&(O5DD|ITW`IYyxRc&P~Se{r(Ted!BkI^KnTdXGs8dZLO@X$BnDkV`+i+-bCD8 z--#v8{^c(fE1h8t{oLsj@on#XEc)L6;E7K%5}BD8ii@*jaduk5A7!rjFt3njv@$l- zU0^3{L>_Bwm`WbT{MvRNM#Rs|XtY6^_3hqx`U=dFhwkgm`R&p1VU0$Vhj(^t@eTl0 zic8dzpJH4?TsG21u>p*vy}TPT@qFUErl)7(dw$>(>fLR{t(&*vxBVNR(CB6`&Ye4( z7ZGx^?SdFF)IXMX8zn}th1+*oy9}vACQ+?i}B<~a5ACh7EklF}Wn!H2dPl=w#B&TIL5*!W1 zSzogb{1)!S+b;Kbnd)$n&IS(}`7o_XPxI3lXm9$F$7=vt9dB5uLZjC%1i|_o{L44vPJu+V7@xk{@#Oaebrp(*{7R)a%T#Rc=XJct| zBu09#$EmRwdB$p^|i26JZjo;Q)hQlD5J}7Pk9pRvVoKpc?9J5Cw#V@=*GKk*j{w= z0vXoAnPu{S>M?B(U3EORgV74xgc-#T4)w|Ya$ZXvt~?XMRPji><ZvOqrpSvTcqaJwQVLbSRdK7Xf!0p%N`L^~%(2fM=sT2T% zjHd=*CetcirrD6S7rt{|SUAsI8jp{C?Azk=pZh{Olqa8jGNz^`)1mPB=YQhAiM~T$ zr46`8*6tw~5J)a;OF7EBSDD^r;}Hox{oHHMf{w_;D6?DQ_=to^lz@>5VO3^1Bd`|B)a0gW|zv+?~G@b8{!+h3B7-v**s`$=~}g-It4C|KtDoH#J=H^%zzhD>O7i zD>OUIz5lyQe@k>+RYU$yRR^y9I0_q^e@i@3W1IL1{koF0FrrA>B@Zu{(~G|M=plQV z%HuGXiIasg@-io1Ba(unJ^AkFpnzT;4GDdyh6GL9yC02}^&b^a-*{hC8z#s8vWD(I zC%p5CC+Tc-hwt#{jb*yGeY{Mn%*cs)r4F=4BmjBZJ$dSWG~T<|DvnYLU=-%M>aYg7)SgZN-9um1@dm3L&=aky}HA^!4z{@3H<-}dz;77s4skYVX?;q-{cd4S{bZ5W&C zi;1}bbwGP!b!9!SU%4#@u@NU`C*pT~?|b5{Ph61QWY&P=7=3wK{k2!G#EUP!l1DYp zo;@9pKmKqG$%$OMH6LI2>I=C|_=$(_i)UVZJ-+_J(4cRz& zB_{9Zp6;e`rx0woFh=D_fHre-$BGcqAleNc5BrYyaV7R{eC9KslSBDvT)BER-yrwQ zH=a`u{;l!(&wN=UD6SG|dIC_Ze@snTSO~;5LLu7m7AQN{JfBT^MjnLaqwm>J^}WCb zw%L`{(Co%XnChW=QKvLK%GYhEhw;&mek{&iJQ-j5l85h)J@#n4_`-|v=wpwhQ19No z8(;p?S7Ku5bS_+W6f#M!1%5Z~sz*=Uug3jUtZ)6GXr0WsHcXBGbvb3O#bB3<_}|cE zdBblxZ07wtb@cQO^dp}PNj{QSKU5}ZX>>S}`i(KpJUwhA5O10hT<)+s)&P(_INL^@ zun6jWGFCVKsD|MmRGfDz6$}h;yYSyoeV!3crt;it&NTpinkLZyVvK>Z<+< zEC(DWI@0J`hB5tjT8{AtVsq>Jg!5#)@0~BjfAEL?X*~Y$HPvBXBar179bre!QF!)- z^!(0)vNf_hW6#IKGrtkz19wHwo<pk_*V071w)~!WedRr|wKpE; zr*FoSXP=Ad(dCRQfZul)_BASDBzYMB(@!nLKl%C9Sl@839|_VHJOzy|)2><5WMSO8 z4UfuExJ|-)JB!qb)H7^?+fwRPVpP=U9cRbpy)xi zLQnRqlieE=V?*)oC(p-yXQ!3+Fn;6d7vuR?uH}y0aXFpok%4&Z(Fb$=h(LTv9`l2BiIDOlsPm?cmD9VKl(|C*v=ESOo#L+QD0uZNCCV)m)Gtx3SsemVV$0M zPt1<_;>JOxBmecK4LO|koC;gp>4`--6K>t4kq zhj}?%Y!=B76imVKu)q45XTBaUz4U6%0Y3lSi*f7bow$1WhDJTxarw2Y%Hgf6-~zAy z1B>b@JjrlqGHxK#D82+m<7aE48xDqmSkmZQ{_E1ruP)7sY-r{;;ou~l4Kt~6MBVal zUcVDhKmFC1zq6P_%jchcDem5xkLR9!AzpgnmAHP5d$)RGYAp7VtO)<@`FEV6}WNGDE-@z)a!%#7>}p9m}@^R1%9t|^}Ny@7h;dYTgchxYv8{Q0T>oj^aFzozzxPq~n$lL+8@^E>0ngKSQ! zUKdoK_lU;n3~xif>eAQ8ZOL--Js#yzm*i#DCsP8hTk_4nI&l?7X#TGcYEw9jk8otG zKWHONe_bbEIE<}uW`5QEP`!L))6;u4`Ui((w3g$-`4{6IZ~c-+)Yp`7RmO|gF?T60 zSlXOYCw44pm>szm=O?}qlS8*a4D@ctaQ}Kf>wBkSZTF1I>l4k}LJ8JFcNK07;Q*PP#*RE&f3SRep9uiIrEZgp)lUb=iUZr)jl z9@*6)iF`~Wle1?}NsfcDw7g=6p?sn)`qY7EOZScj89YGeWcx#>C*o9dr&fDv>p1-E zm#@SV=ceL^KJZ|?a(nfjmp0b>NTP=+jD9JP7jp}!Xzhy!>Qrw*#nULuU}Ah~YO8?W zrAz?z$nC!x@!;s%(4I1?#rCz0zaQWEv90(E|DV4V=T3X! ztPS7$@BF{x3r{c0X#KcGhIg`Vfa~cyu65I6e9V7!_{sd_>QmOym2_U})$krBnY%y!3l)0C)r4Npn+9ds9Grx}d9f zc3|$YQ%_GBb@H@EfB-V19MZa+qk4r7kcP5YY~R@W)6%2wQ@S@lGttv?Cq{<y0{Rprg;+1ope z|LSip$1nee7v5Ly`Zd~hzU?@m+a7%sZ;y^nt^Zy)&yK7BJ*g2(fk(f#7d2|JKKeH@ z0xz<;mtA`!PS5Vp5zejJ?66}!bXbFi5x>lkC5iE&{G}cPCdSG?W<;LZIzP7@3@(9TmYg62JD`&Ail+ySQGKBVms* z?S*t)@CaO<-;aOxbxz*eb7LH-OdDVh%u@edea0@4?2)Ia?13t#j1CP@K0d!!Lj|`R z_dup%TU+K4LG1=!(^NLRT_tGaB#syCKqX-~n8YQVoa7`W4X_$AP6b>(wM!2}VJ$~e z=Riv6l$ZxZr+TIOxt)0r}vS2kAJ4uQ;JD>I3j39K`4WG8!p%H6Cas~55J!m z1brgTo!N``zT@S4PAmMGjy!_ND~NgO23Q-m-csqcPk! z*3d)!ydX_l6dHXY4sN9+L_2zuGz*t9B#o)$s&eL2erk^+;ghhebIP%0)Rh0=;EZ_u zq{4k0bQ)`9pg%V#FsP?B@SJ3RRWkJZS%3i=9sFzLP0}{amMvlKh#)WUJ1OaC&~_$O zXCYS@yk{zdIwEKH2va}vQc~->Qu7K5wK{Ap3N8@{Hof} zsyi(WKhxOgv+Cn;@x3W4>z#bJzEECGcTgkJ5t#a_IumByq@Ha5Ze$~z+38VQ@R+`m zjq5W$H7J8LqwsgAkhSPNd|9DK9l?kU*8ATx5@$}^ApZ7`j>Z!Y${;o3=)j?LR5$`! z`M$5w*ntiCZyEYeNx)3+BIr>+9LQ+F{L+5>>Ss6OwQCsth|hgtGhV;!sWmpa?D2jR zRlDSbywShpJ3Lin@>_gErhxU+?VNlm>xOs4l?<&*%~aPh9V30_)dQs5qys?7vY?HV z^;|kFS)TrN=V06Q{zLir!rP$14hSXKjbtx@Hiv?Ff~H^5yx2X z7LB*&SK{WvN*)_Ia=eLa`&*AJw$oF0%2!*9?Wg6*I9Gz<;AHzhICip~ z&xGa!pmztu=sj2IEVAeHm7wieVQOdlujp!@8plx;<7x`34R$2%L6!i4Hcs*_u3dNI z6|_&W-wWD{s&#nph$Bfl9A9vJT?U0|pKRxjqUycp`GaiW@WH)*_bgq{awKg3rJUr| zVV9;K{X=Ks5C8D#n3^=cZv{U2UtfuT@G}dsy6)jMbz+TVKg65un=#rysb?z7LIyy| z5U3G{d*#p#J+9~Ro;*N9AP$)jmi$my)TG1Hce6pk(d)qYAB zJ9$J$$)%v;nX{@q1#O&+K>R-a?pAp|X|jXk(!YU&TnC>X8I04DLoqu#7%N+QadU;~ z{I31FFf$ToCp}};hV`BOSXfuv{mtG-ava*{NFF*hp^*&At%7F9^mydb-n9jDaU^97 z+u(-acMi0L3-jAkUo@g>EM>rP9x#vxXgdY+F|`G5Gqh>&%FAyW7ePbI5dLa5DK* zC-spHx`e@m|Im<)Mh~ymWv3gNkx$9cI)d}+Its^IZ;;J5o7y70oR8(j$jZiqi#k(x z^p_(Yb-%NBD0!i8q+v}GIy>!7bxR#mdicot+@^FCJm^flJ|H_!Nk>gHnI_Zc>9dkS z+ffj1BUaNdjWbtTJH#3#b{(Rf6;oI~`vT49?w`rS#K7H~8?m;?R``1x56UQh7tRys zMm3V54iP{6edF;vJ~0+EQ{PN)RhR7-)Ifa+>9+(u(#OGvMZmq^k5lj)U)hTP=^wAf zOPA%qxA$XS_1Rpzw_cW`=<>7WxWy;rTy~D%r7J+&QB<2wy=fc;%plL3>SFQ^=Y@<~ z&PSaOZ~??`Y%S+>BXTsIo&}(sqUlg0l7k_Y({CM_8ykw@{@z&F*vVa3L;C)x?EASX z%lP!UvvK~y**GyfC4J&;t<<|K@4nyl-uv?$$&yAEJG(~>AHYe*dRNKDj(Wgq42)8E z?wcErxryPN>SFKpwg>>%>+{^7z+hQ;BqjuS&q^eKhqM;w2#7uhkG^5mU9|XzCH;-jg|LVpyvnb(H0GEJVk@c z23HPcTR0OhDU&wRxy;O?Kj}1%Qv;~_Q#4flq?6i!k)K8`VEo%=2{UBnG%iz!sxn#L8C)v z)KwIMtC3w!BbsU`Y`(dS=1Z1s8r(1~n^u~HW*SIksB2&uCGgpNm&^*68+z%8OU}5Q zB7-7+JH`gZ^`H*sC+*2b$>I8r6G$2hEEn(`uMZ2e8K!*M=^>go;rmhX36VM2r+;OJzxZQ%yIJ;X18j16Ig~NF5I`0rU zi1AV0#nRAm1Z1#r4!vBpqB4%nJR}g`H?3z%kJ9z(ldxm(%9XwN(zDy~?903H^s`&> zwP!bDetuUSrTv`3^6PouNCRQiN8enZ$}A+JoEeChHxKk~Pq49WCw z#Mvziyq0epCmnS8VDnt#8Dgc`iC*M9>qP>r7A4NRN(4?gf6CkcCrITgL(6WzV>w6F@nthjAR* zmo}C`!jR@DM7)E3aB>kNpMx5C{KEhCL_Boq=r-8naCzZE{Q67p*D&;r=M}#1ZNH!) z?QPNj=4$ZFt2^-@{x=Kp(rfA&OOcir?#OUhE@P9t!+#*wSC(S9cOu67XF4)3p#IrY zmt3j);`R;o6c1zeG&`@T>wXLliQmnH`R>lP8uMnX%-@cIgW(wNn~-!_gJQ}@qsZ;$ ztpDWL$=s2bQ<%%90H%_PwUArgWUW{);yBCO# zxF?TuP)|*=DV_E771O33nP*tP^#o(es1E$#sxmtx9#se2r0b3%BG$HelO&@WM*3Vn z$w4#GFTDa7u~2XFLI)vnKpO>PYQ@{ z_x_QC_1bkj4r$nx4xn?&$jox7QOvhKwFQutILgg1edjMXqcB=;fIQ$skI)TwqTV|;vFnQ)QWh)8Bc~@jQ(HSCf~~?pITCh;q0lsR7*xg( z;5U^jWkOhlm4b1}Tb81-Jk4bxTF7Q(LH(w|W$#kISu95glrcTW^~VUOC%*lIPsKal z`k)kOIdyAz?u|GT>s}Z4;QTcL;PD5oz zBxMYDws~9NwtBi7v9-1qJDXcErk-W4If#K6x3=+vM~pteP2+*TJSskTJpuO*ZKxNO zJ*m|NIV0Yf$N8nfbRzkF6_t%VFwA8TkOO4^B`;**O&wl~fgrBdyBvKIp$9 zgdcuLAH)OFaq(T^)L-f@yYR%T%%gIM^#!2h^91B`XfExt$U`O^sIfrTVPBEV{%p_x&e)lr4n0fqF%O(eDz)-fV@ew zI#Z0q%8^tU^@bCs!A3MU4yax@9xfi_Oc;1hp6rkN9vFM!-DR~?ZFg-L}OiLg0{v}~H<3<Y3rkV{|9AJh@!Y3!j;=zljqwkM=$76YpWI>*XOe~~x7KD)q1Typ<=8y`j_Uet? zmV>FeF)nM|5fMy#F*M@p6dcdW=3#vOD!aC*gOwI!W;hIk!7$v77Y0=0%L-HRrgsWR zJPkuY*9`}^^COrhzi@)XqY%OmV@e}k-bXMoF&yuF@{xGU!w+Q7aAJ&GMn~hmb7%B9 z5-+}TSq`T^CiQ)Ny?PDMmW3s5fZvo`G!^jMjYah4^pfez3hO`DgYWQ_{GKAqM+!4K z++9#6E%eh%pH%1S+?ifEkG^>P;om}Le8Az0?aNAOku6>jK${eaNJ&A8XU(zEK>OW{t;sPXg;$x$$xTN7h24amr@!3Qc& z4Gaej-xv+*0g|^0+u7Mn$I^Ewy=fNH`2#wmJq2U1_=J~0HkeRnMZp-t!+jc%jXsOT zZe%pzt{gD()lmUR0}cFHFB^I3k7B8-fNvGl`+69!)n4-#L~(^{PL$a;|pKi ziW|3fWB%@*Mk3M>mC1QD_|mRT=|w_Q&$hmFszWY)r;cgIBs0<|AoCU0+Pz3(^P__{ zc|k~p^Xcs5wjDr@B)8*GsB8qAl>8g|Ekl4jg*-8X4_(OejhA*kESuOXesX@0Sth2m za2QN=F<4-k0%wsvKn`R;4xfEi_y`6K8yYEaWz+0b>s7n$OB;Dp;)OY1@6PDt|9s{~ ze9t>B#D^a{6JL65K0O%lFZHV37EozM8T9apZ|~XyV0e4#i&w{*)klY#H7tC?1&dOo z<@MW{mL9`nkDinvo{jgs?eTc)V-Ll}v%az=r?xaQIW;>Iqa&UkSzKOKuXQ#4%fIq> z;?`YX3%|A5FNL!ZF!#-R`Z8<3xkau=i&*M6n{@jA?-et2>OuEEk zd*YU^rel;=Qx4qb`_T6=93ItBzQI$y>{9cTFBfo*M81s?Jylv9B%2MO6WLS`8vc-naNxw5 ziI|xilk@1!-AXIDZs1OtM>lQ(E9#dg^5<5k~4Ld`gY<^&7kSZv9{X;&OcY%d0Xz+gb17Y3Y!h z1V(dwdM*tnYb(hM_#nry-8i59RW*b?8{Ym2$yK`4r{2!S+HyLQfqjjV#DCUEvT^Yz z{^sPFA>@SXbB?XK1_)6v`W`}I7L<)9m;(dCeD_T>K&Bcq)C zQ1~IE3fFT_qmSKf)ju70YIo)V%;V~XS9mwxR28^P(q@~G%v7>zZs<&&=sRVbPKAoZ zVL1^-lsMFneP}c;-De~H!FLSDTOP(aw#$jQetSPY^YoTFf%~zsc@Ure+-AIf#g3AG z$KDRTc{}R@ zfc>iVzA6u$Y3~%nhBA81_$EKU-H|~uu8(b0mCa1RVa&`9#MBf!9eZPXm^XN;^RTlY zi?ZuD4aN&NlRfc^jaH0A7${_05ZSp%uTM5)^*wQJ`jbpe&5rXvkzw`R`s4KENZzBx zyGC#hBm_X-W-r4X=F)f;)^_66+pBSNc{8pp?#A^c5kW=tP%$cqf>Q}9R}~~4rvbC6 zEa?Cb?Jc~#zFpxE@7|mjk4T?+V4$8le8UKb`9NjLd?!?wxtWQ0+Y=AV;BCj9`9%$N z*V3V!KXWoanId4_gcsZcPbuvK_nnWEbJOwa)oXEgVP51=4s)vLgop9a%PCI;T2m$k zX;4zA79#N|1>1L%7vcd1x9YyT`ya~?e21LV+vAV?zMJvGzjGsEfjBUVf`EF*qM5$6ik-w6Z-N-?;VuxVC&jy>@jh z=B@@W3OWw=o#~IaJu(#U`u~&mA7GYc*L@$lD(9SYbq9b8sdQbi&QL-dT3}UcJP#{Gji2#8CFnM~qd!|!$cU5&)&N+wo z|E+cQy|;QA5dCRapT75=v%^|zuf5U^=UlDLEf?9UnzGoei6`K51L6q= zDRXIf7=%jcYj_6XTOxXv$pS}du>2fF0dM3d#`Rof5wzq@-fQ&JC)LM4tu~&~^`M?T zA3dL zedbK|L@$gNUQu0MF2yXvhq@VroUD*|4Fv=L`QiZ4nY*-&VMRTM2miI&`)r0oAwKcF zz56|RAyZaeQ)WAxhqf+DesfLbWiYAcz)f|=yHi4m(x1?fXa1?b2=PTf1g>Ey@;kI@~#^XNOqOuk+)L-2Eo?T(bf-0Gp-9NV+2w|8xzr_4@DsobkbhV}gOU)`{m z-`uk6$~QG8FJyUF-Y$j&^+(&_kOVL76yg#v=bzJphWW6wkvmdO%5skCb5c@?L*t$atIjt$e z>kx57N?UeB$}De4oR2po0&ns_NKXv5xc72zV%64owrXp5?!f-=on4EUp~OBC?-&C( zET$b<{Tm`79m5NX*vOcO8N@-kFtE8uJTIK=CNWri5*j%DPWm5VpLr)r5!!08ryn_M z9jz@^SIfCRMY32nZDDEEl|_R*lg_qQ4fucotxIX(o}8Ie-Yq?Ev9G@ThW+lp|C)rI z@Q}x~y@}=zn0#I)qZJAQrt>hB7$Ai`=wO72hv2}OJgx}$?BD;|3-;g{Ubo7-rgx=0 zu+WaC+0_>N)1l{WeH)>>Z=X8#5ByvrQc3X+wxoamPfZ81Ob_Ilh%Ielv)8_L=P%mo zPK)SrXrF%Q%hp^YOW~fxX{lT&+c&T7+yD1>wry&j-v<&-3r^3H(lXoj`IJK|uc?vu zBHFGjSyNfJzlxRwr(kt0@6Nh%VI!nZw2S$Rb!~2;NF*3kVY0~J3uu_aUHo<{o&l6m z(Z7kel2AZ-V08HFhBdgjN7$^gR7$I)))rU)q3V0Y%NC~cvd>Gf{~Ph>xXMI-M;{`o zpjfm+7y>--qzN?&)8+2i9vPwC$0DWyd~l@q{^ zj*zW&DV(VtTbE&*=%5}K9E6xNh#}&J^C*clee$W zs)`ajHBf7{wPha4+L2};#I-1ey8%ySO{A7iw$)mH6Vn}%K{7HXSGHW?qy<(bKmVaq zFX4rh$$e%u1C_0A?f6NLI3j49rkV=(I@mkXTE{C}(V|82P2Jz>*k5HhqxX8!Fck+-RZ2F_Fe5Zyc_A-3;WS}NGu+`Gas?UY2ZLn~FHX(Yng~C|ZeXKw3QzyqPqaZnBhR_#+zE_@1Lw&6+ zEUy8H?jbONkd}k|2!WXA;6VtNP$XDt`V{_k4b@gv6>IY!lvTg6y>C-<+iL&NYN}vd zuPT=aJ8kY%%j4LzhN=}Ctba{{Z&d@BV>CW zxKGGze??B)ycHiV+xWDs!gmgAWnFZUfMO$~Po(=mgTX`a5pME+60gJnfwv?`^_6}t zmtbWMqN=LG`=mmj%Hn0cK1!%A%6NVH4R3lKSw8+M<=yaVfglllLVGa?P&f!SEPDFp z(2l7uA5>d)R#h`C%Wd??RZ=pewh5VN@}4%NEH@7Qb3=+G_O)cN19=8h67JrxU5YoAbIRkf77in2SMc@8GlcQcudj87g1E4es-i$xEF zxK<_eH(2Ij-$zoIu1zf8_t3_fxBjhMY14e zG4Bd!h*4gS8?NX2$EqBbl`?TtSJ^7yBYur$M^O--paTjgwn<5mxc8xYJj6io7@(B*sNJae239iiGdwqV0(@J-$Ts-t=GnY=eFD z11#+g9PXdQ6k8Yc@x3R+|C3X3jakJxf&9HEIyaWzv(4oz60%$N+SLa8-~P`*85gm% zc5`hdC-_UsVt$FRRT&!)%gStaY}AT&i>{fS$)GhUT0yNnfkO%j{TH8lfNsTds*egm+qCuL_R_XUS)YEmLmPl@i%Oi zjAfZImB^gg_PIfl_1E+-icA;eMK3@Ow5~cCQ7G$*}aPOB8h=p%R#8UTFak)h3E$?w8IEgf*G9-jM&eo-9kd?z2u#ZQbEDwHYu)^Gl=hub?lHhtr=txk_S zvDswU*%)Ci6iBVy>C{EnrflIY~r z0Sqb>$v{u5eeA`joe10VFfLvlvMqUD=~PSJ81~$g4_O6Ps}doD6w>2$lJ#{QMXv!Y z0^+?bEUnm@yq9GOwMBV2D{E`^TfhHxea2?N?JZ#O;(|YKA=K1XS!+k5uZr=Vy&HV` zqa*2o2db>AON^@yf98=&`}p%!@=)#_D1vXFH^yZZ_^GM<4!Hl5QgNdsU8jH-Loz5( zCc@+1w6XlI?QP9^AMEd~>snLu2lmGGCi{nlb*k#V7C|1`a&!`&JMHYJ*LLz6-ys#+4Vm9 z{~(;o$|JOeGVwVOUF>&)ws^lT(DWS^sB5gY`X=_3JY)x=?f%Y)Xn_YMR$&=$Hjceb zXrcB{g0z!_BKP30B+S#83YfSb!W`UEsFnmAWXj_a%8ju}U4BtcDHKV}d!}|g-BTxt zI%pGGagk^v<%u@nH~o2?jN?$WNeM(7w2jcu?(vtwM#|S8`3w6flxix&v#6gsfyHH% z{v4k)HkAtNWZsu|-3RScUHLkkGWzY`^#eSjF2)T?H~P)t$!M40aC+!|Qf5c{GmGvN zD_@l=F87vH7yTbf!95J+sn91A@$lN(mOPPdCjVIsz^Xvit)fiCd)d9ZcH5|IwxViT zg0-@mbk)>UTVr*(Uz`fbW<#_O*WAjMuiWC=)}X<)0Z}{`R(rKKRC+mXLSzV(5TMhr z*nA*>sLzowkhvH4$xjFjO9U$oCXS_+2n*`r3Pdy3p@)JvBz^c3fZ|z}cwSysDuL8s zyF#cS{lSup$m&BOpn>+v~;qJ(26 zsJBiJ^Gf7kU!JFw2G`!!ob7B*`nAt{O$r7gp-Xy`(3bavcaeHn%;$qfdHfE)+9?CMwWeZAN*wbcAw>;@7VKc#y~vNm1T)Cf(;r7 zk0QsV#Cff{`j&X{y6|T%L^y0n;pCY}3Hrx{z;U6YyR=2S-ja`JZ^eHHOnlG!QkQZ& zY?R@Te6DP=y8V6zdsd6*bBOlb}M@QPk#IGy=%AZ_&~35c&{qgIzf8!M-hHo~!Jhtdshg5iI zCcbgQZw{NxEKl$P#c5`@Bf@{r{g}*~Z7ZdIn4TQK3MQ^;wmBj^L3e4V; zw4w_dMA(VBg-jT$-+6Bf$DBZ`QY_ajpv+rBe?+xMO{ zwKP~~Hyb2NWu@=i%-EVAm2tmB-MJ%7l!#>$8sGpzgf;_iWpT$gR<~?Zif>N?kBxTT zmkekLmeq}U=UIMNGqEoiYAVIoQh=e{GG6wOhwf1d>s|@dD})k@RyZ6I0UO7%01M-$ z|5%4`3jSQ4m&dGY?>=Yfk;1sc!v{VIHnqbi@IrH^o9?;SmMO$Y5!wO{k{g0<*{hGl zZ{QJDITmY-XWF=Tp9vJnh45%gb&{wnS@Qv3zD#QKe(Bx`{1p01c!oU5FWF6rI{jG& zXFQL3Xp=f9kFY0SzRut;%F!O*gANGksFQr$rw=!G);xR#eeu9Z(29DYaiUFlm4Rn? z%;Y^P*P_j%p|3*PfCps-sy~Ci2|mgaGOpxgaCI~D+9(bVZcOhZNvZIN6rxjjEU1Et zsv7${|NY;xAN%Z&*y7@%mHf<4{m@Gscfi{8<&}s%bIEYAC9Af%of5l!XTmog@+#g_ zo%Pn&QfocUHFAAQeDa0a`<1O7_gF}a8wYlEf{yf`0w6jDM z{O<`ti-0IY*k~|f*`-d)UnnX%RhEMadWVUaUcW9#PuD%;cmgZ(Ldd|& z>`7?jrusU!;CPK>nv`EyUU6ZCq9E+WN|~6fUy-obvbvfYc^^`8mGUxFFE7H!%i)DD zOq^F#%1e;K+}=2pH7g+|YjAJZhpNLJIOLNvh8QRC1DCLL*jdaHNOt0?TDx|8*WMjs z8Dy!1(NQLgq27&DC3Kf82Jrrv37L z4&Nh9F4ou^H#=>9At{20ME|0EV8XFdDvA}DtJqa;>Z(1Kh+iWv5Jbr z8B3Y0&I>f^ViKM~1ia1z0-FICibR(SlMpBjU@SidRBc_AE0Bi9I_Cw#JoF8d&g`2Y zqLB;l((CPn_5Exu5ZxAXmBj@S5Wz zC6DrE%w+t>eM$;y|4s^i3=fKY@jNB!<_`C`pb>QqQ%bc0kN)GE?gb1ahr#O% z#ZDcRPckm?Irrc}j34h)eW%@ko9D^Eh`ipOl2cLNsXyS2I-}l@Ygw+`^C$|Ok@9u% zJ?crc2Ib#=7cCqDWy`{XA+ zY0p0MtUdYo<5p8$Z3_zvR`M5r{L?Ri)ZD_7P0!BT+Qycx3*W7ZMq4dj|M!7h7NZPUm{Wndn|ck z-WBESC7_}pTw*FxbVv{?SARAzJ&%xwfcH}*ghq&;Z%B#KnM}C%cefjF2BQC`-gctsJ2#b(dFkoTY-(9u!#kXu{k?3ABRltmfi*U1_VoI16tKFQmM@%4%h)b0RVm=@Al*C6d1BP6Lzo zbU;hGn)A8<9mT=Ya^@uv%vgf(gTHrQzbW6^-fSIR@=P0QoVF{n+SgZiT@es&%?pVT zGARKw{YHZ^X)X>tln1ZAVgCgRiNgo0Ys-wo4TUMC+`HxzB^bEG5?}TRxbkozL%53= zG)g2cj?QP#jMGXrxB!+_2;WCJ)myZ0ojonq(%N9P_0{rtW!T9pg4YiAfH&hc(MC|K zmV9zv)3ZWrHk2Pm8^S3ssGx&;8_EaXSjm)kG02$IUnqVkS5Lf+P>kdY+9f*EF9opbJ9Q!t@-XC;LX?U8`p@f& zi*^E!fGzqf^2V`OJ*|?7%@x)pUgaDu+8JNkwDILF$?#pjwlL_kxsv09gZ8sO^KU%u`QUOH;G0tgP7V?5q{N@|Az9_T-Yw8`<36w!!`$5dfgf-oA9jHr6+-rM^ar zLQF1eyXgA2ZEAkmF5X%2lYrU82>e}giMkr&eI)PP*|LA|r8&7wfe;^V^m2@(fP@cJN>KtbE%xx}C) z(JhfEL%LsR>+blJdk-jsnK{CB`_`jp5E(1u&cw7oCozY@1U+;)b7H_AI(xzv7FX0Z zE6V5~;U}f^#??EqcShdU#K^jBtie1eMd7Zz39^I>c>3Ox?0&fB(b<~v03}fi+8NPcE?`5Qg6TXTm3dVi5InPYs*U>(yFZHt!u3Q z5}kz)ieyh-UcD4a92Xx0lz!RW-*Af^`Yf-_=yx2B$VKvjdGdSjd0nn zV1|aIzQGQwm!O4~8_TlT*CZTN4fV52JmsUl6i$H0Qd@59s4RWveWU+fkn0 zsFe5YV|Pxpl1D_^;h@L;r07*zjrd*&!H^ebQD@MaC-7Q?O1OtWjxZDXLaFea@=gK2 z7dTQjg<^tsl+TAoNZfZkm5*QKNt4$Reni1UzLWx{D3j}N{?sloL2LSsoB?wPa#Fw) zUR=;KgnKBe>pt>B3nS1 z7hzQv-U-zI{QCRCV3jJt)pDy z5*bCaBAZ{Xu=NcDYUF|LK4~t&wg){;ic{7*JKLFuioOBEEPwXt1BgrXc57OF&+<-q z1c8|1;7$ke5{Y|wEIv?l-<7Nc1jpgvb#PG@<3u32SD5P?(Fh*oe^Njc&;Uo_K&DOq zrVqYjG{xXWSOlN35{fwqKT1a)4g#JFn98e4tg5zL$}}eDOEfTnGlX>r4RA>9!pF4B zYAlXe=QQ5h`YM%YWn+emE|&9x3m14Ko(WhTa{o=;K8AP#{ga2{_;?Qg5zi>Y#j~hK z&?!?UaI#tUPg8Cf4~`ca^-S&g1$MJvo<|tG>Y=1sNesx zb}5%@Tc0GJGnPo9AOk-Co&7ogW$p3|`Bey4f-~AtOZtcMi8=+f?^YU3fuTAxU2mkZj<*#7f6MRfXuz!S$!*{8Y zd62G-PJ7_odF$`&a3`X(t176^_Kzw8b8fMdpO07~&lyQWC;(Wo;uiDyqKf$_ndfYj&%J z`H@;F6_)zGd;PZEmB%tEPiA6%L)VH;&aT*$Jh7F99a~wFj)l0hUgSqW(B_T12llNCJN759Zd#3m)~NxG5P>J5 z(SJN`EM2zs#S6Bzdeio{XVqLD-jrarp;5D`Io@wA&qac4W2@XQ+~~36J?r-1$we{S zzTKTEwQpRgw3%snotvB1SlMAkQmE@2OI9p5Ie|n!ABu_nc$Ca)f^xu&av;o=UQShB z9xt!*5DPq@!BZm-q_MWsYW3;+jWXfLu_Aqg?bhDaBtxh{xJmKN1)T^%(FS~z=qc(8 zwvF-eDcY>=+p5|ut0=bm#tLg_5)M)%&ZEi$ zPoT&g9?{ml8GS_nxj@3RlGj?-P+{GDt-f@;s-nu)77jdIy0%34F7U|14h-M|=OkW> zEAY&4)#C&Q`I)!jIke@IXOtm4$aDIK8sRNgEmNlqU~)c?OO#X7hC>m=fHS z%hA#6j*C2zA#jQESsnUBfrK|m-ykq!tdK8xE@|A+CUlPHqPmO|hx0ydCVWEh3Loz$ z=a}4YuTy#XzO}RAdqJ2`r{4nCFaUG?3`}a%39pp+Jtc2DYu9nnZ)AddZ~9KTJZ+|Rn{)Z-qNKI1-KEvp7u=pdZS)i;u_C=s3$zT*1i$3AX<;m3c{s%_JnYAWsF z$DXpr=2k%T?vI|)7?a9Jjp4;~mMb}ogZGR_N zExZ@uM4d34;}neHOropsIrj;raG%W1D2@-zAFe-jg6~nr31-Ya-zzdUnQcmVaqRZB z(OvtRl*;t{z76&;-voR|)B4gy+gcg2?e$Sv&a-O!{gd)2i}7hQ=}jq8l{xC5+*gr| zOcdFdzb#lcIO0U!Y%wPypx7$q<;G!#=EZ+H)%70AtoU*o#Icsb=w3@mq(W1ze?Ar3Sdj-G@t-vqW zH~Jp;@_lpUIm>A?eiJw8p88E@5q-eD=+_Yb-YJ`{vDin`-HoD8)e)%g0ju8%n`ySpE^hUPMB>(pRwVF5^07!4~s>R(xuJ8A%0X?)RM z9$w+$SAlRZcn+><`~WofoR_=|_ykU10v&R^mU|v;`X@*$QHR&_Z2ekkjm;I;+geq-mDDy`V_mg1%N44Zb3Ai`VfUSeY&qNwEutjg?)$@M<8i zx9NFffCX$YZ-V>i^LQ4pQr;Ove<`Ip{h9vKAWA7ccR?j3!gX_a+g^TS(_VRJ+b&(( zG2X|pu}Sz&%!(JFfxxx9IVFKR>WXB4FTW22{zT_<(fv!SW(&)4N$>tg69xkt21~8n zDodC%c=O(L>hg>ZojpwnsvZub0XRHB4&pYjbPk4IW`lGc(|9hW9=fp>R z0#yd7p2TZ0 ziXuh*LC5<&_e8w~ZSohq%x5YaLd~BieDo{TSqQVk<@n_L4S4gkkN=}ign7VO_?(n` zo`W}o$**`0y?9P3U=Fy$fMqE+SHSxI6tEwqMQStZ$-@%xgIBbaXp>LW<2{%wfAo2l zl#OT(bEjoQu#5L4=ZdVUp-j#JD+XCPSn6{fwY8kLRcmL@oV5?W@S;8U>~mHpM`3LE zhOMmRB8r4U;#twP*Ze_}4^mV&JLFkf*XEQI5DnhALR+h`M zud%l;UA5KKbvtwNn0qj68f$8(w<}l2?4SJdANg)y2*MF5C=lGNiaLbTj$EQeAKWMq z!V}P%Hmc}W=lX5k(Ep{TG3=tQ4{%m^AQdqNnb#AN%izIOZc z_dhS1pCWB!*@C4R-nTyF}7!(GC1z<8(@L+|YR5_@U`-C(2OexV04uQD{ z+uiT^{n{i|*OggoSB)(8Vr#73wjX`|E$eRGu;aZZFD1)#PnwnYF*e2W=cYJOyHxZka ziOJ)%9{_sC^OVxwg2FZltHJj7zK^63Cd#C;3D2X?*_UwieILk<&AHYMd5QHDNQGZJJYZKUkGp%R}eu@xWp^)G{buz}1usr%atbaVp z`|(;V;SVnvp~&gO(Fcslqje_Q1)NNL2Mtn)E2U_YBVqFV?Qp6*b-8E-H+?UJ2^b<@ z$b%Rkd4{H3EZ}*6n<+s@;3sWvulk;yd-L)8-ty5mgw~=D{9Z;o?-Su4JcXi{^m5pv zJTL=$s*~?twm_~GwwazgeRYooJhIQMt-VG{B36^O$*^k3_J`aa6AtAq_P74d|I0q} z!=Lthq|28s*f0One`GfM#7nOH44+;SP{4*oN<1Smh`SpFGlc5?Y857{5^}guqzQ#66bEGGSjSYyZ@# zZaY5MX~QEEo|k%=$oDthiGbc7uDOQ_s9Ve>{RLr}P-fzXczR4&hjw=BL!H4VL~~+N zPJevzjLu1&M;k2vZEC2o)5rUzfP$EdGO^3`oeA}IDN2_6UcPqI>dOwS{BX zJoLVb=V<^!lN^n*defD{5e%Y$ET5cK>KkC&+1j_IrG5MID@FG8x9Y64qQQFGwym-} z-09V|@H}Ks_;+=?Cx`iar2B=o#-=OmTW>em+GgP7C}*%sl&F6QcQRqjaph9L`*gK-Ri8}qBKMNW_>!4-y z3n}qTzGRf8T);{47W`Z4FJvC$3Ou4M#=hE-cY=2=TVJx7c8jdKy4-rYn(c6J-Nx_S zb|p@j>dK`zY-M)J0CPZ$zvcMgsq@y>*(}Nms z5*~-g%(A%<0K+Sxay$}NcCnkCig?Kmih>XrFNJDosLQ;R3agiw!iK(<=6ZYlv6J@r zqbKAt_1YhP@l|;&!Bk-(@E(B*At`c5uJf*lWJ%Buw8JDk?mrjxCjv%zER;b~BtbN+ z9pZ=4S#E)5opnD&!afqdbJFh@zW7xuUmdlLjScH-skEn$H`oK+HP%^uXvdmMYC?rl`Q=OYil}GpJx@`C*4<(Oi7VQS*>lvl4fA^eH8M_cyl2GvqU_!wYgzi>#J5@ z-ewi0oRbxUf&@L1CB|Uh*?m}6;a|7*t~ca~e8HNUU)4Yuk(bDZ$xMj|_QL*w9PkNr ziNSlcZy1cUSJ-c)G&mDI3(L_5^qs?}dp`LO|9T%|(Fe?)SI=qJ>&oy-$qzKN68FA0 z)^==D{j;;lJQo`z?^PmEp9_m@tk8DhGp;0%_#V#?RvDjV_%OyIfBrcM?|||MBievA zflt1E>Lk5Sk*HGg;w}3QZjR7bpSig6GNQfL6K#53`b!=X-{1FKpMu8gPPizWx0~=G zaX%?_mjn6>c++Hiw2?gbgiEx6GK>10S+W#)l8IT(? zOzLV%tf{`t8uYolvn(YtWWz)6T60^cWOuFIzVfy$%}rWGWtE*c`>^hJ`Gh+2AY6P$ zfy~d(`zNy6J(X{L@z)Mf6m6}|9m7|Mx zc~$J?aJ?XgEzZFx-tJKe%fTpFWiG_ag60 z_X};^ni{Y_e(NK)w9zQ*ddJ>+<+`n|@QwvSol+kAo3_5YVAW*}R#jT>umK}|76t+| z$t%Xr{ss+*1{H6W(wMistyQNBa|M2gsQw|`DC+}Sb$JLyJV_l05O6N6GeS}fX7w5E z>5t@as2@nFPJ!nzQjZEn-@t2G{Un}8=!t$is??)S`X=KWr6h!sKqJLxd{6ZUKV&cp z9;jx`bfaS_=pMl{Qbf@sSkWmf&iK57EPq@p^SO%!dv~4t3y{PhLAr_*Nhja zpMu<{c=Tr~8%CtpnzciUKFsh@zwteIkg#)XVx?YQ(e3w@`Z_DEp|;e_A2+XW7-) z*IRdYw<`=13I)$(bad3ecXoDKF<~vjSHO-NgvSB!%z4lN%)ruD2!Y_mQ*p0ECMwHj zV+VRz*eDC;S-948p-{*_(BEOby=~Us-e|{V@UmDImo>Oeo`*bzb{P&Idt|`gxzcX$+^Dj<^1@bE zImOnS`@fZF2j1XBT=onV(grVy`G?Y?^0@Syeh216gQDWYIBKMd)g@S143~9%j@rTQ zk{cX5TdG^eN;;b>CF%%T$9<>AQBvl9Ca|DWj-IqtNPCr!I`cG-eo&q)8~IZi>U6qi z-}3cleUr<_U&L-`~%$JlJN02R0^LE-jomCEG$d9x9)orIl(=?2MzRJ?sqQbXdi8) zD{kvQie)?sd__epeP_$tFEs0%NJj%4wu)T`q&3w zVxFLy)l2d|Sb2m(-~~^;U2WFXSnr+)$EVyL8S|atZ1lnl;rt{B(yEJnTtf z!`{;JnoqnpHP(4~<{>7h=j_ve?pgcvr=GS?e*6iWon5h!kvX4ppk1uX>M8*#U@NL2 z3^dpMy%@A^nL%JkB7{A5smmEse;}2fCs&Wpww5}pud60_Nc`vyu4<+u@zzu5iP$By<+m9RnjS01jo0br2R6a64lP7FYqx zi|5npYmQ}%2!C+kSP=4*Ruo%zf0MOz)LLUpward!*y8M_tuEqX zhe1$?5A;m*La`$d3hC`uq3XwKbIVojx^O^i4T0&`wi@-tzOAk!ONy+yq0Fz=#xm<{ud_PI zzOmUQ8=25plxH%j@3J9XzB4_sv9)8vW7EQa$BrNClX16e!#6J4#n-=S7hn5^f3~%E z*vYdG3Qvs@DIb&wQvt|CR(+Lx?E9X3iKBzr!Ol`wlmc1`;ZFw<*5Vm+C(KnKi1}o0 zVOh$9XVulQG6xGEB|wM_0GTfVF;>xV{0faOudMn`dd{O_BCET*(FO-weKpnM0xx!f zz=%c-7Rf=zgdf6(;1*MP4~n=j92pVDsRL0e5E`Phstd+LK()5kim-L|;OXPAhr^Dd zq$~>MP)v6D#;BMQfxs#<1|~VZ37cFgF@dH8a|Xl2000&|IR+;J2snU0nAQLE2~)CD zp8LQb7{DO@KUH`>;HGY3=gyeAkHDl%xc@P@JXWi+u|g*5P0V_4n?87z*PAX#v;(|x zhEHZi`r(7-@%eRm_H`}g*3#Bu8{2i(T(=>b#(=&z-A|E>&s5s0@3h$Z7U6`1M|ilX zKyOcQg+K{mQI3VxBK~Qt@;63uqzGqVZLn`ba0#QpM=8-hC4?Ty2cTP#nIKIe18AM$ z%ySPXI?X}}M0KiFii1Sn=$n{(L*ej?`dC;3?6d{lsWbQjL5`4#IuQ=k;rRmVBW*?= zc;+ZB)Wb8%xPd}{Dxb&Uy$rY{fs~)4iGB|p9hdBnd*H!mf+6UiuaiU_eUxy53Oq|Z3^xZZ*)v@z_FUb5 zA6)2%7$Zpu1&*14k?Sw+MV)vc;6r#QPACpqL2K$wa0V{G5O?_wyiq6ZguqWYQMbdF zm2=AIcLx2D?y#rrz;*?Dm1I_5 zXM>$S)#)n$*SA?gT4Zgljn*xrzoWHTGQHAPCHsg%@wV6QjusgN+jimVh(CMaWWRN{ zH`}&kMEC|Yg({L9g;87lU zC#@|_PF%bbE|1m91>$8;>~=@7;3l!h0>uJRP$GCR?2_jbmj^{e8>-BKpPgT{F?NMd z&ANbp?6G6^@FN4(-=Dp>Nc@Qc*_RiBC&J}NZ4`JPklcx1aB-747nBWyk4b8l)^gFI zz=9_wFg)zGvbye_h9ddVANY`+edsZ}IJRjQ@2=VHnN6Ep+qKtjN#SiE=nzh7SM5;O zy@`8tI>`qD!qL5zxL~3LBQ)inGp%l5wejf!L-%}$(2oAdK|Q(heoueYsk-7`J~>n0 z(Y6Iu1K~Ij6bT*(*Wm#I0mUge?$ZY(>UX8+!i|BN@2@mygN7lX zQ$ILN;(ITz`ymv9ehxqPREA3ZURDnS2->;8(B*h~d4wJL@kgxVU0U&bGYegRxek2=6Ggp0=o++!5)1PN-gq6ozz2HB*%yHelK{9|6)!uj^K}c zv=K__eu?tVt4c0lqoRUTfCs!#fQMg#C(+g+gchQ?zW6=I)7uaJjq#aq58CNQ?p|Sk z@rhFOJs{A{X%Mt=VD6K=O|=m`5PaqBINZ_ily!Ks=UygjKc%2qzE2`AaK`V#i#jTPvt)<%ae``(hm z#ELFnnNak~SAOG=kP{mZ@kW923Q<4hzN^l=Wp!4tuD#~h{DbjNWD&?7Muc;=?P-M?O@sK=|WA2`zP-Z!2Ot|8O z@%@j#{vEr1Yt+uRSJ*Sh>+M*3wSDQb+09v5G2$_|a&@8H;0>SnCcdi9W}YF@1osn| zlUGC;3czsrPzqtN{40SAQ(80c`%Kial%XyL;pb^&KGbIKVFohSB38<{jLK-Gx0(yZnzcEr`K*w`nV&6_0@T7a& zgm#6`Q=)D9JJBaXMhKasa1^ElbK+xCgo=*tU7^1V`xyAsyGOt?=ob7Qutj(j-imvL zbjY`fFioMUdsB{A^&I};!VqJ*D+dr#&(S2BI)CQ-hI;cbsck*yp1%TF{T9#DLIm}P z(<*SMUp$ZgXq)!<4LtN=9?$&Pr>*cpkK&X1(CgF{^osl&+Z@}(qJVrKkWf&x39VD2 z{nVe-8PAjOPtk|(o}dl1PkfP2u|NR+BqjbOg+ki1UG=xnw%W$ZnA_Mtv`raz&DG_0 zqOaC!t2KtCC^+tYV>dtEc|q0I-o8Ee*janz^ne@rLn9M*>&}!teD;`C$?N#m+t+M- zdcpI9%Oht7?88r;m#5QccP8iTtFK(LvB_C$s;jaOJbKm!y4$1&*wj31H%FO62^#Ui z8C_@2K4?Gs+0R+YU-+?4zl0Fy94TJo$?2qMDiYoV32jBAT!e-2ESP|2WftBDVK9U; ziigvKW93#TBF^q2&#F8gR)bL&r~gtO!A_l*-n*ePW!BZ%=7%XV(a*$wcUOz`_O{r} zEUPGjU@#R76ih@4%h4HA&)oXb-O4g{16Q~(K=BXH}`CKuGrSL#pp1Xm^0c5SYQ+wjCK=4@Y@F&Za9C? zPJb|}3lcS&%JS3i>xz2yhvkF?tnbc*gj-a`-D$pLZ8)yfktND{JNip025goxCQu>2 z2VB5OUEIjQsNXyTSN{zjYTNfZZ0*a!*|(L=YMWiIv+Ex_>1f+M2Gk zSKev1mG!(~qM_p7D{D)vs)k+Noc1i?#nCRR*WcB$L;DV;5?}JT1kFVM>=qx62bbWb zoa4e1H{9?i`-$=dP81kz7AmHd!B}TPj&0A z|L?G6$#D@~a|qG4m(eZ#m+A?4B1iB=lqKc*&NDu4Tu!W;gaa#De8Ac;`iddf75TrA8h!+e zlsGY+203hYb$!dG=HhkHKvkxCTN>-+;ncWnZmg@Z2l#u8oo0w!N2G89jv;cc|4Lt zJOnsOfZucTi>_GkW&n!SUxWSK?p>ff2x;+()8Wwxo0y)piOCro=^rYs@sJvcxes6Q2(8 ze!w5^$ll#4v8|0_+uSG;u~uwicG5=1CS7T<#JRn-MP5{e&CD(NqJUja)twx-+6uD^ zqpP;ECB=9k3#hq#%ndmOzIU$l!t{DDQ)T z96kuMfvUfhU^?>ULlXfL-xITvl7fwa%RtKpYDPQeCn8@gCrxuQsSL}r_tm!;avQ5V z{)We;-3}X_>#*BXZFb>WmtDQxF2!GJ4@rr9H${R~P5%IMN4FTct;SvO9o5Te0UT_| zN-|;Lc!Ed3%m58qCYW+~3SMvlC+<1z1TWS%csm_MSCT6dB|Hi0VIImo0sZm2u+4Pw z+(VS~jniKJrTPo&OnIpzhN|~;z(Fa$pTVyD9-h%3DavMj<3)oy~9&vCoA%>=Oq}X*SKi6Q z%z_Q}blBq$oN%SlF7Ji;mD7XW*4-*NwwA2un}77HhX_*uLRg~^c%L|v>USh;Q6}9| z6hR#GiE@MuN`topa-V!00}oJ6XwPX!OkdK;wO*_~fuCkJ{R2j+49!&kp*XWNSHy?G6Q;x=1YB$g_MQ#l3* z%L8E$QV6Ixq!tPNf(M4++f5ALuqi!W1hcA?%W8r+EI&wHddg_PXKnx9or>LW^tHzq@ zDs60f-4<73?#V;g89a|X&|?psm*S|emfS4!P*-hrrDO&3)!ROKzO=ewm#&Qn?>#$n zyjMotybX`dm>-5J`HfdXpAB}m+H;Scaisv6nI8eiHp%~YZ;aUG>mz>p`Kkt#;x>xApWiTYGzhb#*o6UQ`6EwBz227?+DKqaE-`j6lb2$?DzM z*pqj%Z!5B}XXjU?ycTUj7cLz*-W(mbiqZp{SYks@n=3HdNdqPlpM@7GeUbzL1Sm)` z5E0aZg}S`Hd>*fxDpU@JjRAysBri|FB`&Jwd1CyiEAn|cZ=1mq_wul1b@SElN22aT zM?U?Y>P#t~N1JhcM}(pd)bB-x^}TCro5eP@P-PQyHMX!?YllS*R#Mh%rR9xQR#xeo zTT#@zQX<=%90{@?$IWCgGZ53{Xf!i-&%Habpfe(-do}|J$1D{l9_B8*TD)rqj zXqC|`|2!*CKK%hj2z>YfAw}M-41bhU9?Heg7FzOKL>#>hv_$s<6qpe#|0C>0m@_rpg;kG7H$cgY`Rj?gmQ z*Z1^1QuJx`jmkT|@jTs&KFI1~{BsG`3h9^nni3i1GK9-9EQ@7OS6XXJtzTW8jW*EN zVuSsy*3s4=CBjM28b`W_{P243+Nuhd!)i>zUPh~AD<_f6FRw{C@jidrI+S;|XG?3F z@;2ioeT26N^%K&Xnq9PoTa((9;*ztmBO@Of&+9kVrA%CJ z=qh^SYrlOsbbHjpRm9~8E#Wq?Uj!l$o?;g}8jW=7c)t`6akFKgE5R~bR|ABv;c6)fl9%wJJ zTeCa%i@)#}?K`iI*`+IY1>c6}PZmu|SZ^>dlkK4>@}Vsf`MH?H4u;CCj?8l@OkO|M z9?Yjd+}(VcIB=2}HhVD$kHuR9^KBt4s5=BiSak?KJdSwl9`BvtFv57A7@hUtJMV)z==%bkU29G~Wn}w6qd_vo> zsKa6{dWyx5Cv1`MLl8=kVqI+awiEwgdC}=AzT|-bDXI`GP*k_|f~$+r)t6 z9(dq+1nwm21`lw9e!O!(?uRExzo(S&;2!1OLoMi|9Bq`(>(pQ12_5qFfQQe26wred zeG4pwbq2pgoq=Zxh0-=K71qz#PB29{533wFqbQ?f8=*DfkduL2a@X%(~ zwrxR1;ImBXpBeBvOMdnzf9NHIzfbN+tBp_1==Py&JNAdL>G0Obn1`;IN9pbE5TOoi zX=T;pcDPeM;a*eiN2c)REtC?T1m2GCoRpR$M24ckHG&Z7SPQ|bjwp7oXic6C$WcFx zOL^Zlspohs)a9EerCeAU#wWYxAAR_QG2!{ou zBK#w-1SbmwHgv7YUDNN9nihYxg_l%^*MSluxq<*UeTR{f%kME& zXCM@jAiJj?F8$|q09!C~cnP6&vV8i4d1Fu(Ja?p^CwVEaAJhjRlmY>srw~H+obLe> zKQlO#p8-a_`3s9UTzj6U6oOObWw!Iy#?6gg+g{(doy|CSa7!0njH9NusEc~QBR?Ry z7cix^0=9g}hZL>StI+TM&x1B4`kv8CSL7?i1&=t|2z>L&=_!0z>dGn~t`LsFmrg{% znfi#jNInU#HpvrhmEohm_u(U4Xb*Z)Php}w zrQ!RXUEZG|V=Tjm{LvO5@IAv{S03hoHxE@I9{K|~;G;f`|I|J_fx!lRo}r*WQrr6C z^^w3SS^x&dz1q+{>LWk*{P#WeAe^a-@#VESv6Rcp&=-MEKpwwy{D05A)TdFN@Kl~R zoaWHN%Vo6jdNY~ezbUUT*>mb}_%fN~bvSNe7;t~L{$Ic#; zvTBh`X8*~ab+k12L^E@Bn=oIztlg9vb7SEY?OQUs5q zucylqoLg9SCE~|m=wbt6b7O-eho^zS=R%2~pg5j{$!&k%iRx$WB$z+uRtS}?tgX4m z^8qDTk`k&Hp_pIc>@GYMHqW5cIy;)}{Mi9Jcc#zE<*~f{?TcP79T%M)iPu9CM%&sB zp};_bVX+ACVxqHIkwomCN!m&)RvXQ7>)1FsKPQ30TlEBMFm6~b=}O7rR=Y5W-xmlG zONQBCs=Oj$z&WE9)RU{rnK5WncokYgAuNwRPvr}rMH@j=1`Tux!Ia8G>=#-vXcBJZ z15OuGS>8lb>Wl#xDR7cCEJoXs64~02aUqLkcPn1zw3{iBXglg6`M`I$Xe)$7t}XpZ zH018_Npd>z=jEd9T%D?4HmJTMSebhY%aAf@;HJD@r~ZOA`9363mb@Dr*0-}EzS{sq zZ!2Ae^>BGT&`y8x{3t%$CnS>khG&im<>Fh`cEFgPLuWh*+6R}^|Bg#RKa-zKslN*o z^9jJl&h0JXf*0$!^Ox}?Jxf{dBkv0dfA8-1_kjwz5>EG=FVtjGwkZLlD-wNrf2yt2 zAE_TB=~Hz^yFn}O1Nc;bDZkH)sI8zwz8?PklX**h0vB|Gx1C0`Prcq8V>IAqtR`CJ zanY69%=$B>C=+>Z>crbaokC{HA(K2 z${0tk-WZ*f$8g6+C+B=Vf+f1m4Yk%LWx%^aDyyobbYcZnPkXa3*IioMu&Ft=H5}TR z!ESr*(K9}|PJQ!|XUsb{stgL?_V|qFd-%+tb+$FyyEpIJyF+)q9WE5etYkg&Eaepy zc6_i`eP3!biz_y$wwR)L{oPwu^z}dZl|#Zsyr>E<yrSxSuOf|s!irIdeN&zJR__nn|%@M6ZL=~#x# zTNq&wI<>8($=xb`;|bjzo3ysprYzjn=3WE&d}Wvv8VZQI2zT-R6P6?szx$v6*Z)iK z#6faQ#M8kr#ok_Y?CQpxe!=u&8@1(s$`f-e<@TA+ecT$ z6hu7;bKun7EF_ve%b}^h(I0(7?Lp5_+$j_nbjXLK(g_A6Ur{BP0nPh3t3Qut(2JCw z({EnJTZ~j#C;KM25QvmPITZF=B+3TgBpx777#8AE-AjtV`8EfK?x;>^=Yl<>cM_7+ zk-}-v9=;&?O0t)tue6vjfnTCgekmsI{58=>WS79Bxn0!xt zO9-FywC2}$ZF~PvFyNgZ%81^#b;-yFPWIWur~9q0rb_JwZseJoU-Eq>qZ4y3Gb+nU zADe*a=Vk z4F;Z|m6dhX zMaR;$A&fZ`5O0Au^vTX}Wer!--j6@DsSQM|=bwg3f-=;SAaUA40$toA5*^99iKf z8U%cqcjf{m+6sDP@cEU8BU0c(KDX|bz$4L#fm-CAVJM4)jzjG@@thC-OAqsHQ>Rln z!zbVj0h$D|9~qP>E@~*GciiKmJ#Q%Jm%*Lzi29*zD4RSi6bo1qUg=)o8~G9+>1MQ* z75DlxSR5bqdB7AIQr&sn;I%|MJo1gG)XM>jO?4<+NZhoT+o!Mp{~0b?d)X zf|-R$@HukQYt|<+mPJe$2F2>%GR_37t+E!$)JAz3H8S=qB?J9XMH%x|)$&&KS+6`y zpd%+YBnJZ?UepEO$L{yV)peVgp7&#~ibT(<@)E1@!yhYru~bh-oKwi;J(KuC*eXSu za?ur;U-bH){Py8{*KXNyX~6cjm>_5QE1tl`_pZC5`2H84b>bo9*U%AeUQ{oiiK6xs56DT3}mY}E}ID10MB?jBI8^apR`}W|ulimht zcy!Fmb_yBha6lHKLk-}4=z)_`grQw;4&Su}b;hF)pUEk+zxm(&bGv+H#J!oYegsQp z7IHcy%oZ>10xtIilm{h-1>{R^GsY-Pg*%YH+hjZSAN4r$6XW421b%7oyT_tG>SHpU z=Y^B)B-&&*y&oH$JeDloJNe56I|oecT*u2{lKSWbehv@84IN^VIbe>wSh`0i#+`Ql zoH?naINzU`JWiG)79ei>Lf8h^BzZdd-*owKGrUu_txpcK!4!{g>_9Iq`rZ! zz=S2cv$J6vJ1cq`!if}c#zB&)&5E*WuZQyFg|O)IB#z=qan$Kdk)U<_8d+T z7aFQ{=et6I2z~(*um|lZSI9Te9)k#e1D>@_-t`i1B+b!Fc#;a+ByXZ^9Pgf|eM;a4 z%z2LlPa?{YP#Dm`Lsl6N02}!!M_XxUdrC)f|L&9R;FS3L{j>{1Ch*v?Z7F{Y0P5p@ zdVX)Bew0DL9Nt{v*iPjN@l3GZi+c`F`i1fDybwyA_v#S-ahS7v)YI2dW&K?hlA*;` zS6^rCT^-ia((E#GV{6N9j!f8%kx6^*@pFFe6|1X|uP71bJWwR0$*~Drn45R}TiRN! zL-laf6OI67m}f3bw9giYv8j3c53gLdk%`&tiP?$Xc4HSjOL^~%Pg`$emDOi&QY6(k zG+29QyWnh+x6XTuu35R{>;oryY;9@57U!~E^56LP|K^a5g*&q10T_V|Ab17r13`P8 z7?4XNjfPip;nFqTEAkap2yVQH)|N(J$;I+v&fubNPy%=`3?)tt<~t!T6boKWcW0Z= zwJ>+WyhuvCsgU-2dpeOO>YxMPXTm$yIhJKx`63wiMy7a!iz$S26e;3|n6lbM6YK-8C2biK&Buu(I$Z zLWse9Tnq?07iRS&vFRv>7URG!158A7cawKc*{dGoL%tt_@uv}emX6+z!+*(&#UO)LA|BQzEey<`y z(IGgz|Fd=q6*pnXu=Ij{(KkUClJ|%1(>K7+Z&&P@r3}1I3tfSG)W;9ti#noy5J7-N zD)M>12xp475XOQXuh?Nx9XumJBibd=rdOoIJ$_Or_XD<2VB{$*1HRBKe?QMNo^+4D zs0TwT>lghce&fE=A*+Mm@L2Fdz!bE|!*`^N{%F^^KzV{MQHFa5? z6DJ3KuN-p}%*9^2eb*Jq6AzxWb0>O*Q-r0EvB<&M**P1#I~ivps?Db6CTneP@kK|A z3yZcQ+2>n^C76t7VO0Et#4 z@j`i4R*{Er6-S;hH-eH$=hGw1lNM#e3~P<`)rkc$SgMQC0Wmxn!d0QT_PtzLS&1XT z+GMn=?`=_8>O;A4gaU&PH;y?QloRlhkF#a)mT=*g1RLHCunzRK+4*yQcJg?Kb#yeS z%__Nlgs~)K1se>2kia5wW`|Ph4Vc6?9;*Fp$T-~OcP7>!0)U6<9^WHR2r!s52?-a- zeg(6|V95`d$d?T+&y`Qnc3h;WR{{@f(61!W)VA`b=fFe>hsVqDn|c^95}Mrezx-hG zl)-0*tWDMB;Tq2wDRM_D#53>+dL?>#@;0fLb}63;ZDAC&38mq% z>z-%weEP*bU4cvd2{LbsBj7L-SGtU$3g~{>s`~J<_ zAU}eyux|qH5KLYl21L5=U`E^E7X780P7|(p;`fw~x*c966dDEx_`uNA<#lIeDHj)s z-_S6%3ocP-${%HTuGR{0P5FbSfv3~NpGDq)kKft7?2jAp8+la9%Rz$}570zyDo^lH z2z}?T498S=9>%~g@QF6^uyI@WQ=TkaAwGN$-g4;uI*Jc<1#P14lrIwS20zlb$s3M{ z2G)OFZKX9x&UAM+*>Ne7!TuI&ZmzWw8S2H7tr+3d8|4CS2UY(Ow#NfO=6Uo-LYzc+ zo_T2Gr7wP}t#P?aBxP%JOGfmvWZsJM@G9IjTUcDNnfYa#nqBfq^Mw^D0o8{SP*+`P zwUttS!gozFvP@01Hr2XfYiVth=1g<* zi!;J`-6ty>s@tuyykOV7lE1$>6TZm}zc+LRqcF%(PADlD5n+b)6c$Xl{0N0)ZKaf+ zBOnr!=0X%%-6Yz~GchY<-d|@tkC4y3iTZ(y^6sV!zQWMf{Zj0Q3EW8xI6_|xDi?6y zrA@3jo`V|+I$(K6=&OJ>f+gXRQem4B^2xj3DTVMRgck+@^rC(0Ti=q`y$h~+8k68b zj)?*0l)|V;FvWAWJqNCQ2}UujC3>e<}2nQm(%?dq zeI(pZ=_u~-O`=`mn=D)=7{R%)&K&<|a8O5KXv>YjY)pN(1P=i-{24r!$318hvdinD z9sQ*;w9hBBgQhIZ;aQ?L`8kK`)1P?A9(%Aq40Op%HfW<5e6CkY29My*_>5h>b=OYx zciGt!eS&}2-n%(w7jN9L6yLR;*J^p{&@!WpEm-niwNIK3CEpGE)* z(j#zFsEWyL$}>-a7l7wMqNAA$;giJjVU9r|l(Z<#iL0cCP6Pk~c|P%8*rJbSCp5*R zJooUB(6mg?6U(1EW5UhscvnP>U(x&ch_#p~g#8x3Ah;1sarVaz{|+P$oQkKg_+%ge7EE`C3h zP~a1F(ykwQ?t6!H?J|iTA-`B45V+Hh5Z3o-BMGAfXFOM*pxE^+ZDRGl)D`p%q2_Jt znU_-C2uR99r$o>AMbUz?+|S{Y<@4WJf2&RK^N^f}<+g-ZEJUH6gbP=+6m@tTf+zSV zyB81iPi?E8!Nbb}1$pkFP1K*!iK6`Zy`VL;i+%~3g~E$aQB$TX8%85CWDE7)+Y)P zg$@0=PdMDiCv^34hmvXKcJf$k$F6u|%b9rP1DCkp~ z*MHo@!yp30slbc{j#cURJt2~p-VZX|^qglH=v-;BLJlAQ&A4WYK$K^48tWf#1Vr!# zeEvZRjl`UXud0%QiPxrzxz!jUFT90%xjqPP?xTo6DkTuZ@}HSo@aHY6$5&=C|Dt=m z_ZQ8`?+95%S=_$n#(F7|I4p8mN{3~}gsfQg1w1G*j#A+{!X7VaZDW z0ADR8jPa6~gojYgtcch@w6?Z-`@Ro9Xg~1r$L+-ro>$AfJ7vQ!tZBTEn#yKx1EItf zv2`q6luVjb&EUp@k+td!lL^ya!QH&VI}74grVP&*1@-c9ex-Q{sN2H#O$)bz!34`Yf!IdngbNA7mFdedkJ%#|~Ta zo@g^0bb=diBhj7|{Uv-o1e5wBELlE-4o4~B@cUtiG3cOkVgER0CiA z@PT%DzMu}2$okfbznKs|P+=T#30HA z9+|Qhe(@cp9Uep|teBK$<0IBP0vMQm?4bNKZm6daci;u45X_;FsY5sQ55A?(qK|{# zQ6|CZJR-S)3Gztk@In0FyTw;d@3HZnf; zzzJ7QUPf?IFK2EEeXj$!k>B9MGn5ow4Q(O2aR7?`<^TS74>=l`eIE_=5k|tgrw}i& zA~Th<`pD$@?CJY78dmihHxt2qD5X~MMyE8uN$^~TvjA%s^!{pSQGc~Koz=h9K zCk7p15?Y`66Xt7Tfj~(dNSSpqduCX@_1atS`ZD4=Da3#FFTQMZ^Q^%S1|}a?U<_~= zG89-vY5rIi!bHH~J&+}yr2!BMCt($N6;`Z;NJv$_tgnJ~4Q|U*fpsXa!onB#`mzPyJACcfg;D81xL8lly5e)zi z?>jvsf6zF1-S?3o>=}RRZ-5J&V{Zy6Xo5V6=aKLX9nf%^mh1pT*xaZHJK9)m@h2}9LNY0wCo4KIY{zche1fRM*aLAw&_ zf|+8Vu=En9iOzt4K+IeSN`|eK~%kt-{o z{Y8rJ49Cco)lraK80bo$-j27Wqz}KZuL-WWt-q`cO4aY_d$g9d1zft^%g_~Nshf79 ze)6gXzwggA!1#+4@OX_GzNFl9uQ#4-wwa?I3@V<&%p!12tOquUzq<0%tun; z{iAJ?0v6Sg^-bK1`-KU*f@ky{chd8??>C(Wz$9bD=?s0MKmF1;i8|spW5>sj*Aewl zKE_R^TvToFe^8HRxS9K{JOOR|!E^8vkG)RMllw1HdY0;OzK-YohJW}ObPgrL_acXf zGQz{heZbY+RO1II9v^JA-mWIe3XUeJkzrls!renas+-B{h51EWSX}aV8?o&6&cwX$ zPWK(-@s zStzx#YU``3wx7+51a9ha$xh&cO3)o697RVPC>%06pYqQQ=XjS1aFCb!7v`62Uf1Nz zg53e1go`flD!TUiA39~HXXf1MXXkl}yOGdhC;)Fr39X0F#VdLH(p6hsUAHqQkGX>2 zDEcP3N+^Qcqho$pBb!-{5BAyI0u+85kXn0ttG`iwE38bIm*kd?TYy-=8k~BT4rMG%F)^!hIqsZSJgj*?UtGbVvIjypKXW6a2sfzAn`D zm->_PJa;*OOx%CgU7&?-#dF8)e@8E zyfY~Asq??ubpFctKWJOHNCTKqBGg4F2EiEbU(dsod20o;n&gghme<4lh4xR-)Q6m!MQv*Fzp`viQN6E93C7urFVPEJnSwQDzhfz!s$ zzKtzy+S2Cx`LpQqw}1bT-Q{k6BdPt8E62D)9DFqKw3a z-}Be5lFP31O(3*=+wiX572uh>5HeIK-sec~h5EF)4GTp5se zQDf`h{@U-@^{Ycphr$G32d#rv36A)k(JAA{gh%29>W;Y>UU8A&WZzME6VL=a5>3+c zLLMgtE{v_94Rtey|KiX6ls)~-6HbTDwmOekU})bR8L}H!-?qK&4VMvae5)*m>GZTb zj%*rWUGm5eWkfhjnBz^54;jXLKqhAw{dKt4M<=bjt;rP$p>_DOx}w5n7gv1mN3E0> zbJ)nOTVvCH?iTWTTgJOd8J_I#vd-2fZ?mbX!RL~}1^FCrvfZ;a$>gQwbfn@Nl!<_ISxXo!O&^r}m7n{umvVClssM|f1mU@dW@Nqs zLXu)N0MF3El@17&IsgzZLdm$Ik;lS4gg4#_b18T#XgV|^?}<8E_{r%9WysN4?~SkgO;R1}XM=5wHtRt7X~fN6P70BwtYf3n7ItA zTFmIeO6`PjQRg@_!`%4+&1XtUQ80(#il^W&CBDPx%#*lsQT}8-`H=I0grHadd0d)7 zbM#MzJio)A!RGMkK3)X0Be@VPc+M4VQZ{?0e6rcRjMuGnf}`3jhMid&;>UJCNW@gQC_yM(=UWu)SU!I`kjXfSo8NtE@<>j_Ky{n z=$=5pVs-%oeDtB?o2RR1@+25Cx{`~8U<|yXZ}imfsfWEC@}+v5F1qKFzYb5(o0O+_ zD0ZF)+}!ih`QQF5@JZHoO5D%Wp^!GTrFt^j#dFX_;D)E*uoMOVr8>&X<8{`I!;gOK z!}b$D^`rL8vrpN%^JnbL*^_?pUY|E!zhDy+Q$8Mp@ARp97~2>Pd;%LsC?#H^{u~dv z49H*={hV*~2TY7J5*SmK{Lne^P~gHnXoq(cGR1iSg`JH9`lY-Kxm4lDjr{yy`f2-- z&;5{{KY!Nx1_rIAz1y1Gx@==(-6qCHB*S-I#vwD1l}=I zVm-RLq%a6;5k7(|NV8HFL~A&B(LIGM?1aGQ9VEb<_SJFT7RSHOEwNZ>*J*fspvU@p zJFHgiA;(khLD4+lBLTloZ@ld@-D(yg#F`mc4QiRLNH#)18u;Ey&`3TjR+4* zjd>+GIP`S3I8H1MDEh`9{hLGfT(H-J$#z^bcC_D>Yt||ahd?F~vO>Vq8B^1k)2w|RYCoqy-plTIlMR$y@;P;?>*r3VfyStjAd+_-hel^HwmQ=Pr4 z3nfB&`_dIZa)reKct@?Wrhq->V|ZCttjA`)Ms)RrGP-ed!v592`V-$ZPb%EgvAIKs zg$sx!%PFW>qcAkj@g$OffO)xa^A4%HS15SdcO)r8>d=sIa5X ze5hn$b=*T2B+3BacT2zpd?Xim2yM}eb}1V|F_d{q+z0*?{vrM4zu#^9yG8b;f4*!> z6B0M7t1wiE=a>4BM4x%cQ-6i^6ej3RyXiS?ro{`SdqZ2~iM&A@tYlh(j+6nW_lNMp zK;fPXU;U9c;dsA9Uhf-yCLZK>dd~+!zr2iNV!}fxOh5OR{=9wi&wamy?Kue>HVCJ4 z_n5zojmnI>!nXhBfB8S!AO8NAJs<4=Cmu4q<}2((SK8!g8WOzo9}8`T(ukDm4E_%u z*xirt7s(H2E$GXXdV9O=XMW))-K*;E?)Em)JZ&Y0b9NL{k|U()@nL)In}2AFGk0As zA?%rCM{XbwF;v$jvoK^aP+8&&Dac2Z1EGx!!)qWB?Lpq%o30Iy+bi!3xtD_Qe({NO zek45d4uih7M)0ZJj$9z-sHuan*5b;V@B3r^g=qysap1)>6bj{h9$PSBh?lDz3!!Q; ztko{>BViR6G7~Omu0_=p&g}6K$1BW zev2(dl+d~m3E>Y>*vGLVR|t2AWy6HF+?`WCyadWKAAyyRwxl!Iuya?Q2>E>_fB@y- zM8a027>o?ilHB|T2_}UgJP(V2Hy-k?g?P@3ujniYx469Qgu|P0FGKhM3yRMd4G3pe zkFj6?Z4R+;O$mX43v)FPE-VG%tczX!9Ba?6{*x!W?c|9r8y}xjoiS+BazcbOpDtXa zU`Y}CWw^%uP#Rbc8VKCvzP=|xLmgbX-x41FJhOO1NQj=arRU@e0hd{h8bny_ZY^a4 zFcQy0FwnMJ#@=okkU*AiKk655qBZpcd)$|0$iT|lrT!dlSyK)dwfTdAPI#T|Wy3Z6_OP>bKstlqC-P_|I zue17A(M}}VT9s9|jso@4ULS4b=n_So-lWuC(2qU{TuI(GH1M+Ek}m_EF8mAeaa_O& zT*ylwM?dCq%j3iSd+|JaFX)vnZ-6I$Uwz9T&-9nW&lLf%#(d=W%ky~t#b@p8`O|j# z>UMkO*KRyu9va9Lel&Eb;6XK_*Y*Q~0h-s+p$19#;^Ng!mAIe?1L4j~?%A4_qO zqvYfKE0zM!Ew0+UjM~+8HX_I7L1YdI1u{0ZAOl`9r?1mm8|x$|2~l%uIS~mA_LxgX zNpSi(GR*gch;d{5V@NW#VpSP*j%^eeMsc5%Xuqp_?5KCSt9!hEggG1%o=h?lOMdW^ zAASip$TuTOn<9A8aJ==97rZmM&B`VYavU3<_M_Ypv{>)E(p2QfqhO;T6YA`2XNfNw z76pUiKwF{_neXWAY?ZdFaZiMabmmT8dE=rp#hDWWPBaL^Z{h46z@$9{xFdxF;R$J> zz)%EuK6p|nKn{TVBBz6xhCZyF`+*_ zGw%wD&cSO#(b2gafzLaXKl~*dq@gx3B)mMZuFvA+?(ZuM0(4=}B0oN!Abo z2;>lkSwa8qzf}hoR|uh4zDVM?=g9^KmX2HL40Qdu1?!YkXH(t>AHq7kAoosm2ik<- zLji;ppKCiSA09;vVBjSc&Rx(h&uP~OJeH$dzJf8si;MChlmd3>5c#6cL>ul=4o_%( zYstffX_D42FB^lAL|tK_hT`Nt21>M-hdIH^J@SEjVS3~fX8jKy+SJWMDU-N|BI!9> zVS^7<*!*qIR@Q@t|!%{jMY`u_CLJz z-`dao{7>2APd?%yAkL*j!O$L&hq*Zk#q64$owb>nncQ!_cSsIA@$@71p^rQ-p}S$X zZ{D#jS@4kpFFxV5pa)|i(VJ)REL<+duRO^+Y26nTu`jnMwSE0Fp5dB-E+!e_z_`BGL@Zyh}+t);!srY7&&{Opw1 zk7du?2GNb^uLv6*tq7CkRG@{h#i zaGG#cS9sYN9I+RIfk8?Fiq2x-6UK6(V+aCwLcq(toQb;>jxY>i6CV8%YHcm%p}c+Z*8#Q73g%oRwUU|G9EI`<$%LTFUO(J73hGo}(qf zjDXA932#(wgQKS)Y&?%ml)1iiSQ-3zzI7h(cEeC1@!gepK|4GP1r!1~{-NySIc=rH zX2BHtBQ)m?V5q`ayr$Du=YkQW-z=Ej=jz*dC2zx;*Yv^T%~HCL`dPqT%MU0dE_ zpAhiiQL*)5!v#Bu_kav^wfNzU%t0VSQJToby}bkXYJiDw8N1u51LGYJBdv;M{Npj; zskkSk@5m)4+le^gaTJEokgKG&=7`$hi6FO;>A+3s3yzqZnGNqT8(&+(VR=ik>9e2y z{+F=uQ6dQCR$2cv$^~G=k`bb4Q1}+c1W4~#H3H#0E{c|uCgjKz>Z2}%bA+N8+)*Cw zO=tK-c}2WM5eS^ftULmG`{Sm(g|zdHz!VWbu;_X3CJ=7c7oKRz{zqjOUBG@_2YVdg^)k({q29 zgoGHCpv>3_4Q*3iFQ>1A~%!Bf1Y;3fKh6WD@!S@JY z#uD=i@DuGVEG+nU@*$`Rg^`zcX0*tFY~^%s8E3OI^FHR7IA(<>V;g#)9Figl+(Zxk zx$?-AEqL(T@mB_vqOXip`JUb`;nQcGT^%0E`tV0yv}4By)D8#7a`MH38>A?QrY1I1 zb6{aC7~&J$zzG~sj^JM-!T$U*R_l*d+6D_Kz2U=w%m*t|TKvU27{Vs2|q7XIc= z$T8p~qJW%X?xaETiVcd)eRQ@rONLcAt&ufEjMz>=@ z%2)pFA>pVr*-oW2O5;qt=&Kk-UV*e4&n~`u%?ZkRRs2rr#?4_}qxQlF9``UAX?k|f z-K>+x`n^19VR6}oJ-f*hgMD3A2nevVougW4n|TZf1;NP2>IWv<6M`%yCLf7Op%iYz zy8&({+fg7S7?F;kJ~|_qqr_nu9h-Co#nJYK_!$Sr356{f&rC##da6k&N$|K^S?sof24qcG*@l;KW-5B$x94pYFK z%K09e45sgvQk@_9nI?PYhcwU|O6#eYF{7RWk z4Pm70+Nb}E7CZ51jeYqa@5vHW-eU_k^In5Bx7XSy{%WmVd-=dFepz0~QhuQUp%ej? z(H7W)HV8WIVJRc95sP>zcpjVEtJ&Zs?01xo_a^teM0;PHQGomRqKykQdX`>nk_7O&7Y<1*z-Ww6Mv zUAyL=D3Y$OE*I3~Pko&7a^V@gbK#Qx|Nj0zv&$E*I_(M?FljlRUhlJc>Wfu8Z#l@0bU_jou(3k9@PBtYXyErC=aMjf8 zqRq}P`4U{h^C%BwE%my=D|ttq7|97HWEO@y@(`uL&iAIqI(ab_Zp^cyYjJta+u?)| zXoyF{hD+oyP61mpxQ{Hwqd~#YZplx4_LDC)sxkyA!j(CJG_exigcvPDlnZS}xQPmw zJ3weNvCSL;uG6uBUe6D(zH&brmRmZo2-w^&#TU0o(BS=fWQUqOc)F+1z`#Omz ztpOMe6(tdDVST1LCFpc7=n}BeW^So2w2Br~Ix{v9I@FWFN8+9fC_tkwwV6UQOTq8L(Ghi8SKDIpI~4$14s>gSU@g%|ZPt`O{7+gnmNor38Z zc@Hnd{Dh2N61i>eHXJ#~(dyRV5qt!i&$@sk7%?;T0wGXtzCBf)PvU8+HM-(NoxY+ggNFpH-~sHEL4cDF#lk(j92nSL zc;!#H;A!CLzyNd@c+)OzaTq0gVXCU-s;jd|%*hb)0-w>l6Y{tg92XMAqz+&~dH7x$ zX>2^Gp6+IQ@`+RS?6ars^qFqCxltc*Fc?^d$iI+We-I6!rp)BR+u@4$ApM*cckiJqIzFtHpr;;WEHaZLO7I#u9o=Ef+Gl4x zdrFXBgsYgSXL6jYMly_Jp4iWVcaat}QKu@9lGt>QgOrpAZ9*&P7^}cI05#q(1GnnA6aBY6$)~H(u-R$X5#Uxf)IRgoJ!du*b`|?%y0N7Kq zzP=^U4jmYFL;?V-x8ixV4fglAM?vBlS{6e1ppoL5o+nNja~U12&AvnS&3E3_u#%-9 zrUogL#6#y#y7kXWGLChL@Y9~R!5jy>@kt;YljnQxDd63pm>?8wvuuxNC=(8%M3znLHYG90apB#1bb%3@;c(Z1ISB-4d~fm)hW>bt0AWzY-V`jju>OGU z-hCu6Q-nh9Q)h8_kE9e%C=8dSqf-duVBHdrrv22WACV#>VM9uT|Ij}F zd%tDB_>2F!HA=?(?yvu{z5MMPCOb~_%!jrx93Fn;0sG7UkDs+CpL)!8wm0qff8(Fo zdlz2wXTR{*|2zB8$A3ujfmKSO1mJDjBTPrQ4q0^i^l1-O6{dUfNuezg9t@#6%7RbA z18;=ijDNf~LV)B0e{Wy;$0sIiba=?FT==G~E=+`+lmUudCA2jf{0r*A%b{nx+9lJWiR%J3lkeKx z;)=C2a_V}$%S;SvCiRnCAeH>$&wlPDmimU(9&3>WrTt{Fd zFnLX6>;VCxT?q=gd-~2Nljpo#3Zcu&D}ePC1)1>00!uET$MYCi`i(ZmUB=U4b0l@4 z1n4Z>FP8VNuE@Pq+gSeImS8}5Lj(|xaMzA36L~Odi)XAt14eN133uVcC-tUDc9aK8 zlpz$)P^@@H{H9%B)+~I8XHt$%=NX*>9LdA+KivZtgbIof7zm9KegodM)h+AjXw<-J zx5cH{n?f0w24XSbp%K4PGB69YLI`lt2EQY`6P`}8f+-i)tCH#>pLakmF_6NO@aD35 zX+D*wNQQ~GEhq$22n@a_VHWLsO<6vKS*-8plZ*V`ka9(Rj;Q}mut%M7pE{#;m2>#; z076iD8yPJUp0&-zcH%J&BK5`2hVrOb;Gr_=Cu$-!_Y_&ji6X0M%?79Pl}WkD_EUeF zgjIz#)JS>PO6l$#iXV2Zv|Ng5@6aZ0u`vrpUSw6}RkppLK{qZsR_$43$DTEvD6zWM zN`C|A#Ek>HdwI{suga*{DD02uub>B4&<$Krn5myhsc*9!x$p5j@Bv3IvQXA(?R8{y zRDYqo-8<1=wFJ!vUwq!4e&$Jg;K6hD;)kBM{=q&UZ}2Y(;ZAv?3uywKDY+>dzcXUDuU}Ss`_|soZ|5F<%39jH zysZQqS5lt90!-iw%;1Md!FTe<7$-!&n+sR*oqI`%5bh#T2RNk9L`Ub%%u89$lqhW@ zhnid4tfRYM^{k3kGcxXZr9~Kh$XWW=H;B`%d>3s{JjfBD;h{0)F8NU`+$6e^!evYW zaQZLBORH!O>B1L}h4Zt}isXk$AWU+l@JbNcJR^LCW(I*fY9E5IJeZUOH!r6mbOk}tp6oYa zzYFs*)W<3^lpJ-!*whOigrxWcQO=}6si1HOwLu{AdU#BqFc~2`F6zf?;fyd83NTO? z`B6enY{9_=^D?+;GZYa=+}AqO03)XYpy7`lYqQ6nIB6ev`i#B#=1nn?ECU3C{t!Gd zaVS9~iz_^l1Ks1yoe&HN4)S@d*zFMI(7h0#A-Hlvmw=exN1y~lP>0{+)2}E)c{~h4 zR0t-o2ZAZ3@PS*+`h(WgNx~2y@oY=hHDNlw1DEFm=B&Paiu=Im7IZf{?tx zl)Q^Po(I-@rouk`*II37(`;r`0(BQb0Zl^iCiu_1P;8HUtVkBC;4hK_lGjnRrva*~ zS_+`CPDW9=`hLFLR%Z`wPP}qhBL0vkx4pM&TN})$RN3qev!#)JD?cc<`ZHTr|KOH& zoouq&`cP)y_|7ed^Pnuw%yuU5O-t2S2mC6`@LEWl|D? zseKd%LKEeHw}D_}e+9x69>?2Ab0GXiC{M`x<~f3t-{c<~8*@2;2hh{glUqtlIfOLz zUU~1byqPQZ<{Pit)psx2!L~e>>LPhUo%UD%tG{kP`}rDb;f)OmaG(GS?Evya(37cbdsufJh) z3nAO^C^&jvT`d`!&E4o9ujs(jz$-z>`*A9|wp`4K6clnZf*t-X8g zmY;yj`$;%11R?wKD{p&cSkVuiJMBBYW4A8&{eHx~5gsm(^_#qW4@IHA@H&D{&WtJvJj(6&QlboPLI4+l z%8HO8Toms?K^b8AvZo`29KQpmNLXlu&!S zlHuj~^Gb;JlVYo=DYB;SVte3)8tXn;p#f85H{aZ{3t!o=r+={8dd^Bn%9%PSUN`lE zZLP~&xOQMSUOBLu#sjOWEwP=sTI)PkWL;AMy@T|*tIR&SP>?NveqZ-Y<;HI=U{ryNs)a* z-h9!D%};FG^{-b+VPOc!`0*oBB3uZb;GF0H?m_SH%Dj(7=h$c(;{&A={T!Z%^Phfq zw%6N(=esqIE8Igrb?T(m%g`ge@X^o6l0W5nse`dPG&JNwk?#ohw89FZ3(pg}p>6m! zt-wN{B9OUHUcQr$JP2ZV9XW$y0tb`|!j!TIT~>aPkFt!NRL}VMn2n6ww%`1Xf30$6 zpZ&~_*zna0cIDEW*3{f)8%32ixg4+l9_a10=bwAtZoT)mU3m3RZFg(k&OY>{J^8^; zSWR7%!%(=wDk%|Qhc8ipC^Pb*01M#&7w#oRM1DeWDByr|+?5OTK#}ka+@V*Z8~MN+ z+H$d{2D$s%x4&+)lXpa$D&ab8(^GMJFkvc`2k>BM0}BE@MO#?0+E7>R%YV7gWIBB0 za?GPIthSQ3%>KI0ZYv%*Zk1g<8Y?xnUtVd&GZR*J>#A+cO#6G^RnlR_E1?{+j`2o5SB{;U=p8TMyHs3yWX2=fr`!Q7rl z7oTXj00878JciJuJoh*r1@8oAp2WQ*6e+_q?xi}Zi&YNfcgrSIK;Y*gGvULt zs1K_@@PP@zDJdrEjZ-+;Kv`-hj(6Glb8^AY_E~>_n-qDIESfqoBo<|?NI_5__>+dG$9ll>1<(jQSY>Y%(ok4pHa-!a&tWV%S? zE%;9T`bnL%?{#M>zU6;YF6Hw!GdL4Wex*{%v%P+36W8T6Y(|)?_guMkpDwr7-ZE=w zFZESez7nfWd{lu(kpkKgAFqr4mGZ*t#V5t;H+d<0Wl|*S(_JZ!lATK1m@Bn~i6UEF zkYd>{wyGN5vm=ku`9=z0Lzk4!(#)Zh!;a04Z)h-XTXDH~ty~^YDPEo|%GP3Q=`XU5 zlZV#ODG$6({85syND17MvW4EF1%2ZRKhZMi6LgQhCwCYPj7LJfm{m z8l;snXg6*6)(v?cZ`kW^yk@Vw_Noot8geDUDy169iN;2?)xep7p*URD6kLo+aNz~C`IsNsIwH8{CEpI+m`XeN-p+& z93nreYpt}o)rwl$#Qsuprm>}aCYl!2>(XbL=v+l41vw=nAK91if*+tGx&ht<2Bs>C zzWxWla>!nX8g(j#-jhdyaAu!}PYCPTt8ZTR@3=|F`g@$PBwo{tyL94MZ-kka!L&UV^nPK9GrSPIRi%(2O7A1Ek2 zj;{BG1Y$PYCe6+-QFA$zhS*oAS?@K#%r47N6D0eU|r9cS__VOa{FWl$0l|gVBOV z0-Pzdl~U9RPV{lYgSGZS)B7dbuBtDxpZ$BC)^jF!=~w^u9lQ4Gs_pF_+Q)ys-Jbe^ zX8XPWWzn{$4(&u=m34h^-L@;Y?VDc^Jj-R)D9dhob<1jHEuQVHv$?fxf3E~PlB>&0 ztf{)(`kJcjaJSS(#*6IO2iL6Yfh~LEcPnMFZCmT9RhzzEWh;{s8v88mj9B*P{!W!O z4{Y1~^rF4`J0&)Hq15)aWoY=Gl2U)aN`wL<(I@;)xD&>LK2BSVJqfCcvKpt!z3H)s zdv*0#cW1NnB8jt-);G58cfb0owKdnC`GRm17B~VDK7M!l^Y+fPgl;ZQU6jvS0sa|G+A18|u5#QK$T76Y@wFHpKTT z*HTkqUG){#CmHtRKl|6MtN)a@#oiFS4Ll!|813L?@C?t0=Xeq*2jEBveDNMoion&^ z*XPP3C6p`AZr;4<9tjH+$OB#}97Z`VzSGvb7vHd}?|jELR_9bhk>ts;hmugK_4RCD z2)-qO6Gj1ZxAO}ts#k(u#^>BZY;C~MW|0$ND-s?Iytcl5;Bo>l#UmY3CPaA}Kk%GY zKJ=)s{9>$@ORf{f=Y?3?>fe%E*RA5tO)HjyliB3`N>qr&M2t)fYGfZFHH>81E&0Wt z`P@sqMi)ila}xp(uLA)#JUZ@k0PGV%i9i&dGht2Vp{W3v$synSp+>Ooshsc-4U%Vm zana+^6hcXHA135n7+f#Tz+&w)mN_9N1S+tQj}E6^pEHp#X9+Mdyaf@F5EjS*JA@_= z&j~xRxsm!H9KxBrL&JC6Lt(Ch_V8-Z`Vfb~Nq#z-I(!XwCg5QJloRy>FY_zZ12Iuf zBoxfT;)-`F^B;IkFaY;RwB`6n%X1%GP%tQ8>RZq!c%ekt=?gwQr*9~Ge6ZUddH9(1 z_O#l>nzdEz?)3Px9czf_N&N1cQ^!W}1t zwHsa$^->mIpam32guUq7m{-XiX`y`7M@q1|x2x~e%@v9v!IMwaNB&Sq362Qwz(d@3 zII`#554hy><*)+K1J4v*XI zv1z+KGGcoh^M2HMPjA09G)72>L>pWP&V}_+8(aI1WJ8TL)^b{Rsm-ikwW+nswzVfA zSF&df)g7v*UHlwd2DnHFTtYn5J1cAa`sFw5#(NiRRn~aDya;xOFE1_Fr8i%(nW=GG zUYxT_*RK0GVSfcanOZS*r+DL7#qBP$a5F}RVV9EE~>CBi|JhcQQ9=MCk>80EJs4Z&W2 z`kYk`p0phq42N5rF5{3P$Tz}h#ceXkMc=)#J0`@fw6@OohwLvcTG`Z?mCa6C(Z-s* zC3&K1b5{z8h!N|=sRP*tY2ZJyI5+a~TSn|pmWQ0c&93%!P&k@Eb11{dg#e_V9nN?25 z270{R)FDjrbC@Dabf>20d^s{_ae*940|fv+OwOaUzz2BQKzQNORXy8ue8AN`5k2#H z2nj1ZA7H{u5|G{j;6oX_DexoAhxbMN(P7Hr6Cny}#}@JBRkx zmsV}?5zfbw0aH_CTa&W*Mk?)Ko1-`m{R|-lQn~mOL4^|Go*w1WlsuG)m33P?kntkJ za%;H4TB^zYskm{`m91VQ;?jr>;y2)AabL z6w1rCFgxvIW{0DYMBi!|t@O#Nln6(T&~GU%ZtU8S46ZMH`K$H^fAD#G>y@wB8{hnr zU3~o;u9Q*&w}cP4B{)gIN*If979JEdM$sgC@SF=HlRU@+!ghExD5Ju}y~B zL;Kd(zhpz#FWdTB7@7Ab`iaX8D7DGn4fc+(95~5fycgsi@(OPQnTsr~mJwQe;*?dL zf7nW703?bKLdR1f>U1c0ca~RdcXY(|C+{*?NT{p!0&=$iP zkJgW?SA8%X(I8~Chqe%OOibg2V5vg@tY?H8-@7DWIr0N#^0k-W@Ov;AiUG}Z_S7*K zws;sEe zrp+%bx|KkEC=facd{}PGZh2N*(W%g>r&Cs!eh1@mT4o%1&i5(70p8FH?Ts6V$HOrz zv>O5dh7=;g2N-A%yuk-=#l0|HbR+^CtC&?7gyVkex4vVGi`hPuAfczUdPg-oLn6H6 zBw#{Y`7l=O6M>KsjzT!65LhH?h=fAm;vTf1&JcXzBBw+N@bYk3u5>6KS1S5TFol7T zA@3$>)9IlQs7AXKJmsDT7yeM*d{SW-m+VZXMYCs`_AY$C^*AT1Ax< z#B`n2nFfM*;)eQ?@JM}WxpmdaY8M#qPA=Q31`ci1Rm#Fszq5jiu*tgaQ_lNVgKSSQ zHdL0`>GoQy*C#^m^2CZQX<#$=^y!~(wZU`cwj^U`>dvYy&FtFb+wHbGQ|5*jJPF-U z1}R*}uK5(&^0y^=Xfh5{WX}&g&F|RoDep*%;u9Zv!1}rgwG@dLn_XZqAxaUZ}JWzbYvSMPv5 z{o)VW7ysnz_WS?t^Y*cie9X?BJ8MNH2R5|y1=}l`w@*Fw-&jNWfXy#W+dGTDY$fs- z9`E>h>#UE19)aW4Z+^wT`1`+U6L&|grmM&XKU!{;ZF{!9ylvn4?G;b#`ZI z%Vt+Mt-|)CoJ*~)qQtIDt=i10j9&FI{XmEjWzShpy#5vC!uUba&^LH1r`zkSU42`k z`!{A)er?anE351mfBqNj=l{ZA)Ohc3Z-NjTN&^Lu=5hGW5??}JcqD}02=fufOOXl` zBIOBJ;f1h2CM_m_hd=*2zh>Y5##cNlQVl;*Mz5a@=-x_i`LVFcGz1H^p z3s$kQVh6XbS+V5g+WNM}$hvzP_5FQT`QW2gwzg~sH?G*$vOYVzt>U4_ZLgx*c4wx{ zhTgM^#ToGx-f(y%D6*1Y_zRzX31)K5B0yNy>ejH3Vg7^6cqlM|aP=2UflT}kfxOjp~+n2-+9#fOo#5*ciNor-%u|2*8T+3qgv7d+37@R~>rbefYkb3=x==_V6^k z4qea&MNyH#1FU#BC>ZKxA|9a$ZBV?FC#BN?@UB1=fk6E*4EQA4^68Ul8Z4}kpgm%w zMG{mf;f*!UE~=4dSm(+GI#4ePf$$C~JOv2jKlK3~zygfWEQL=(skv98gcVGGP%H#k zgjRqPj{>|R)Dnsz>I`?Ay6{Y*$@o813Z7il=WR#rUJuFXqx$?V-#w`$b@P|I7yVV+ zY*tYt!7dMEeOboBqVV34cU>jeHjCUF+1?aR)d*)P;dKdZhrP&VB)qnF0)A@U)_~j4 zZV$^~t<~4CGLjw4 zW!B$KEMA_o1jD9yfH0N!o8W`4vod^U7bK*`@AVpc7*h>R4c@z3G9C$Maq=v$YCSd3 zZ5=Ia#^pUet2QC8e_?6Kb`G|!wzt&A7OvRj!VM{`)d|7VWP3Zs z>IYe~hwLxGV=A?A)qiv7wvEr+lt3P}ja7MO6_O*ht9DqtV{6MhwluqE<2QC}V^Idc z?v70_t=OUziOT`;I`m(cbqw9<2SQlT*2j!~M}z>Lz=)zh*%@Bqb6tF2)A*TH->iu~ z2m8A+j@FHY)GqhD}Y3+2wcMkOw$y`+M7}HQ{f$ zA;jus#nFjlec1pvd+n{uPN?qAcDHcYOouQ>$a3x!ghVhi*$xsAf&(Buo~FcMU?>m{ zgv7hyo#;%qr-<16+DYk+3-5aP>)EFsb>%{0@*E{Z z=!o*v$&v7Q9&CmLpB1^DZ(g|Knw~il1{L>FD4g$wpvDs*Ka=fApaU~-0)q>2l||Wt zC;6Fd2X+z!jg=Ddvi)R*jZe7c6DlSg^(uU0eS8gEl$u46lgA z04O6o9Oz9yKs#W$eskEvVV!NQE^APDzy2Fvv+Fk|)x)glh%gD=jWCG`+YtDbvs#SWg09W}BvgSH?3^9^?9@oKyI`5jvw4+S;3EE-A>#c+&4utrEk1DE$u zBb=~c+v_TAOZS#!;bH9}tWrX`AdpfBrB>D@ilj#Qh|v>b;TiXtjHkYow%`jXse|=e zJJ=ma^5ns+u&)DePiXWr?k!K&V z4)H%_h|6ESb=Tg0_m*45t^K9;>`%(NKPLJglv?@3M{Htv*)H7v_qMb)X%zr}K!Cs1 z#nyYO(gvEIv(};)?2&WNT2cM94Q>8k_Q$_<$=>_Us@1fu+DBger2W7PzhH9}U$N`A zzHPU@xM?HTx80*ibS2Rz8=^HAiXGl+uMOT`+=ye)D>Uvhmh;5>(eXw|^a~yfeZ$CjcLlX)W{3c^OR6h@WD_0#82DDzW?La+!XuO@KEqH82@-0?6OCZFz!=gKL}wj z6bTCxm`nccU;F3w#;f1Vazw<$Um@=E%%w8Mmt0n~HaEEJTk$zz-aII}uM*Sn0CO#B|A2T9h!b&U{ z9E*UtlL!HEnBs;DQeL|6b0;%%E}VH~B!hx{2x9PQYiqK;-Y(x1NQcoF0w1_|gChCC zlea@6#QBbpAvrG6@`Ujk8ylol*r-*Wg}atKKk-uV#v7vDL{CBtOlqQJaBWdaC{AE+ zYi;rQ85oIoGSCL^7hvTKN5Ig*tn|VIB2e-T6(MTG_I?Dp~L1uU)~8yrKoHTKdebK~U07?hztE0HIJo zr6yAtt{9Z3RRV3WxyHKcD{~<&>L3J$5&>rBRS-}JK?E3|&}EaI{v|tBQn7E{XT>Mg zW*VZfBKmCzuqP_euiYkjfuCW zRBmx)!_J)Ov$KN(R<+w;dsDTxJ6U5zjSE&*BL#HO;<90Vd)+qH#ruaFwp%f7XO2H^ zb@kOYw|&`;cRy~=JoHIK6q ztG|8m9sAC!-}30pvB6_LwwOO*0-cZ*eZkxg7qml>4Br~E3vaw^7heCCUB2+Tl=B^p zy9m$v@_zBijuazf(Ro1f5G;{7C<=ZfGmwYmsjjZ@us7qJMK{RUO3915(bk{bM1<44qdaAt5>BEChTx~ z%jt#TO=1`Q%33VbmQ`VUQbfDTw=3nbEAOSaqEa4BmzA}3_z45$7?dYeKl@U|qs7Gg znS%fU)Wy=ra48)=+%nx~z@ZQj?1dc-KCH)fW`-WB%Ll6PsZ?IPGw!3r@Z1~^<>6D+dK)|s zh9CwH44eiK@l5I^4hyb9GvEv*jDlgs7kz^(?A{kJ=sCDJZPgBLIAJl`>FsT`vuFG5 zp@#>2f}U6^r+{K4;6!7=@I{TiFQ(;3wJq| zWXEO}wq>PDaqFT#@cLCG*PcX=3nqjRljA^(P*Ig`xl+bMCb|zVmMI^C3+2%&ub{EI z%!L>*lw$bM&(1I5fo#bV#u~;-rXG5BzgQMt*^ZS-kqA`VT`RHq+vT=4BgL^Fb0z7b zU#rwsZB2!>x7PXO`PoyQcIrf@b#=A*`#ZQAWdO10rMIh9UI_cDO8s@MC;|2mKr?6v zpTjqVl9!j+T*oaENwYn=^@P^`ETy{y}^6kw<-b?b7nB4c)$E^D-p& zHt9>Vs>VZW>OHWU7I_l2qNA>|^5~ngQhkc{gsq_=f9&0X#x9FQQ=XyZp*_l?rMAqP z)i-teUaE{9g3S7cx{?P%D3iV+Tn0~!j@_|0-+s&9c;hu+>BXKJWQfZ-8G_Ieny{pK zZEe+-78m?ln4htUyTf+l>Lt7S-UVA*mHf$!NX9iZX1vGdK#p8t>;%6%0{Rd6#dxL+ zp>v-KsH(DZT|^-&B*?2;TCMKXX)CF!aYg5fM8e&ws;yYcPL`(Qb|B-bM8Da=ekdGv zh1a(5*&eyA@1wT66sM0D3$J43L7^h^{MdSpS&2o#Bbh7s2s7?2%u67}PWIB)b}Mb~ zw6caqD{E=9aukacPtm{mr+@Dd&!SUS4Y7U@!SZH4fdsN3xg-W-K7rqacQ{`Cz3W51 zB8f?GmWsick3DkE1?zQLH5?~G7>V)-OXfxp^bm}W!F8jPE?v1U;m-R+#LQwIp7X{u z&e(@p`g^;5Zsgj~Z4dWQhWEoqCl%Y|RII`PevUlh)t1Kwdz}eL`z0Jm0q4HzVcsa< zObU^d>iphl6effi%T?i(CJDJdS=W%a{xx}xmsF>^wD#PF)yX62KG|$Fb#XxCAN^PDmWaME*^Z)S{Kvv3 zdBQUMTzT~kTHOmDv8CI?Hv8>w+1~oPH9ho*b$sNbwsZBeZHx|E@wtbsSVEo1L6zj< z_T4)+^Np`t{kikj`1q5yEn|KA`W4$Bx?-!62jJ7u*63amn-C8lc+6iUwt4X_D_UK$ z;^tP{KYmus&by*Qc9uy7Ro2(r-o-a;TWubmI&T$&@6Hss%$pz|e$q<*+F$y~ zmx%c@2oPde<%~a{@DcMfphNRx9mqA5WgHXC)xK;MucEEF(Fsgv*Gc%IgqW1~@Q%vk zrXj#td4!Td__0J64Tk{V6H|~lJQ7a@p~?xJSWo1|5~O43B-}jeMRCFKz`|>FQHIPb z5dNY)-~w)N05(Ev={vaU!*L}{=5rCcf$&Apv%-q9;L1D?UIB2SpjZKhcR|O;d1L8_ zg>5ez0-m+y@!osN(H@GE5FY{)7$^&&>&2Xm25_ZayazTA@schS6R?GXk8~|jS+izYf%IEhd9fWo)@1{I?KU$Tt45#!79(B=v3KeT1rl>y z^u33Z^he+F9R~B`=LIu@owC4#$CBiB&^YE;+>=q>xS#h@Vm>7EFbbbjKTx4y_ijk4sFYj%Cx1r=cu>Oz5V5en<5tMK;f2t5cyaHKw- zqvQ~L%bUA4v$o?)RkshNw2F4^+^KdueWFAC)NIEFTGW@-K8C1+v4W*buQ4vhrsi#U zY|2(7{8?B5z1Gz4gl&1Yx~Z`yxZoM+jv+HQzv4XG*V*Dm6CtqjE_o+q%QiByU^SI> z@@6}&t*zDhjdrMGw@6la`Giexj@#1uiu3u345Z~H88u=-#`7j&VESvjSo|&PTkVvT z9NO~ij*X6t+qEmVZAss^7E4UkU?o^(_qH{iEV72~3ahTKRNe!-d11?LU)-|YRg;ph z@V-O9bGbL7&QVeK0tqv=iPjBOFhWtTZ`ebEl1Jeb_6dSPAqKh*@V2Oo40S<;{2qoE=<|@*sx8FkJ!cQ z=4R|*e%7`LLovnB*kl!*-QM2%@UU$R-?H7sMJrM#r8m<0yo;3TUXu)kTFJgjDew9Q zJ8Y8IDG#GUZB#5wTj}z=l}f>stf)M2s?AdMDe|OL%B5Hy5IF3L|4RPWfBm1m#ByAY zQ)j;iRyYicP@(`#?3=Iz0D_WxOboG1n7IgE(b&}yZ##s+VRYsYU}j?aXnD@z!fNI{ z9*lmNlNBLYR@8^(ji6*-2y+sgT*-U|llv$Stg&5n7|5_$57;Pn#$@^DcXtD)%GiOCtCsAT>F%OBhkZcMm?6N(EOAl#WiXKx8G zv5E}Ez~nwmgD_`aXK``Gy&)793Xn6(P^efgTv(B;5F^Gxdu(LH8=`*ZOz=V|M=X|b z9k_r)>Kn?l$^#1~jx&gOF8#@z3Hx0-T3dvVghOw$J^aw14GwnLn{N)uOJc$@gc8<0 z^Ct-Ybc8v02d~)fI=TFaFajAayQbZ4S z;3JsGgBOCbOY=MgqELc|rE00-z~(ucWRm!`E?Ia8kizSDYmHm12%c>X{MBtX&=$$d zD78*03WOC(g|HQan*oAgsh6-h)?97PHPF7u7T0%eMaqLXyRQP0uu5|vTwu~7#dN%_ z);c8^Q7Yu!RKEikLX8zN_x!46P@+R z+wd5h9-flXcV*~~y?0|&LPCmPN@PW2YEFE|>l>CN*wb79AtJ`yj67Y&Q?>Z$fm8jy zy6dg?Zd=o%s}fpkcJsY8>u&9}b7zkE*n(GCHH6aGDPFbt>T5Q;F>ULs2aEuNe!lSi;>oAM${%J$^ZkJy!~S8RM}*Ou;<+T5_l)|L#+<^!ueziF+f zWeB#{%h)NkJJ)vX^*<6GclLbgHSBBdq zK<72yWZ;R9FdE@qPSwDmWc(u|5!z|46l0k&pOVWo^+~C4Kt3T0*@RhHU1K#*K5J!X z9`LZ_x@fmMJ>^QYOagvS3Zv@8DXZ)swB1|RZTIFi+n<`Wm7BLbq}BZ7Q&!U4VjI)b zwsP@;6v?o!%t8qf z$dh4?q`S{bVWUUP4BA3nAEI!knrQ1XvFIp#ceLv9|-O8_$P#gRq*59q1fkGBz<~7cO0M zuYyA(PoEsLsi`@eoWVU86Tm#G^Y}oIZ(vNP{$f?(Rd@_ro#8t~1s7#PVDGz^S<`TM_ZLpR6$dnko)X_Hd%`*=Gg zoI)I)LcBi%B@m$}R~FDm`IxUjNl>29mcLU)8IyAI&=!JE$(58oX@lp4uv~FSL7*t& zIXRf4NZ~H5(LE{4Qv1+zr)4}e_#ok!pQ*VeyCmVdwh>1~kcfdJB$zJQ^k^+(-r;iw4;R!?6XbXk#t+%f!UxoE|v{vZA=X?3x>ozjAU@NSKVmu0GVBwSVw?6*dBi1Xggc@Ic@3L)_E!gQ7Dy_XwM%8Yq zz43Wjx79^8&!` zye-(J(3f|%?d}S1lw<*^nQogerjvPehZHNyUvnYR}3>cw|L!PCTt=qfe_NqFofBZ?SJ8{zIM3|@8nVYqp>+jj# z#F!O{AM2lb*2~Sk_NwTyZq+@#*7C#?R<^!sd&5IkD#cTF@~oAp{LHt%Wh+BBtp5B1 zR@L2OhvK1~OK;ik@J)xc{Op5P-rH~ccW%o()Hv=xZWXQVRbk+T)@l=+q?K`Ar=tELJKZR8!~( zm|&U&6HMd6NQ0HhtQ|41Yk3VW@FCQO*MOq%mn$i6z$?pMpUhvd@{GEHDfbL`9TzMH z+~8;j#RANcFO-eL4Ne(8u0(}{d zNN@ooN{6^L_@FR?wn_QKxj0xx&?wz^I8{FuQcDXbAvaiCTfLn;(Pd{&_u9#m-9A8Q zKfDIPpn8+%;FIGn=%X)rmj3sS%cbmImf(;7e`D_%BUzT+`JJ#CnHlN5udFQF<=a-< z>F#NcA%||3nMlE7jiK)_-lK!5~&QGXC12ofX#3rJrU(g#5xNbPFHrMS~G7`8W4 z-PPr7W>tFcZA4^b82x|e=Ig2%PHTCqUq?o~_pUkj_;c>P)I~k~&A6hAN12OYwJ?t0 z_CugC4^Llu_3*ulwYJE*=xx;)WQj^^ZxZ8emT+xtthX*%vfX0Jonpu}8Z%5BM&nwn znt*}3gNur#xSb9 zo>jFT*hld6ku})U?y)PPBh8sz7K! zA6V&ZvWIJgj2bM`PAM)PMDJ{MO9lqUmM64Hdoa9=;DDdLKGk6LaS5!$}T|JW`+$h3p=8} zotPE&I3q^Cl|&OTO2MU|@g!@-#CJ=!I8n6CrF}cdl`UOOSY14AwNhAcWo28XX%EU0 zZxJjLf<-~|B3+RwU_Knyx_~uS1#{ytKb#wAlX*$&6ZjSMEJ^|?#>_c*Nl;$@X;2a- zvg%2R?P-@%s5z+<&DM!t>5KC!)dNj)@e(*2Hlq#q;X`<8;GkyLfBO21{Hq&qsTMpi?-rPZqN4rnHyv(nb46*o6SU|kFz)oXuk)pD~l zwl52&BA&*XBLu<|*#=9rb$C3*TG}k$)MCkoMxVdR_NJ93w90$CP@iXmsgq6tj(;hA zm$ibMO-x8aNn;XD#t7sei2vxlig44nk;huVI>4oZ#=N?=>4!;)zYVGvmCXm54K%H&Z9Ix~im;X(h-WR5VnBruHCw4o0~fxm%u$nnlgO{Q4kWIQs%<76u6ojFH$3))1~N zVnOr~?UnffVG)?uZr=0yn6KkUha7pC8-xn;NA5asfiW>2gb|r2|JT3%+kE26{wC&6|X4k`rW5e|Q!O&h}s{9yTDb%c{3!yG+be3kxT zUBCCkXYK6iK8+_VW@RS>gzeJ>m&^xW-M6)^-NRqZ1<28M&Cj9Wc)GCN35@@62wr^a z4KYaHe&gvIhlRKb42nXXXvGd+)dMqyp&`g<*9DrM{j=YD%LUz<@MTxlCPE@ulrS6S zui$e^nl7#ga0a(2iVQ(rEPC$eq%C5tg6mkQz;91@1MZQy!ttumA@F(cI#Tw4; z+1H=TvbYnoW-%QYIqs#iJ&l4%+-5fNwz`MHQ}sAgS7VK5RLyM4(rzl+nK#R}IvumA zTV@Sid%l3Mez(<<5?E2`yPUG#R}!|ly=K$5_ige@%2wuOJ!)PM@R3k|xl>C9PSA(F zjZIlSQfM`X!q>mB;^bpJEn#^2yd_VZw%Tmg(z6p58)IerrWoar)jt2SXKPhA)~tH- zD;IWl?6l=?U9-ad+umN{_kZ-=cWEFi(}h98yx12D9?jgIZo$bB2gS#a#sA z13Wf$ggLRl7SE7Uj8=>}2(#mT0L*<{Km3LOf`Jid@bxOHpI8do6zeTu@-R~f1V#xH zj55AFa1MZV4??1T>hiTANJjyQ>@5n`(nqL>xj|sCc7=sd z^qYEN*0>1W%2ON6S*Nt+=Pn(ymoJ~zct`HwYW$SGz9(!q&Pftv`;!Hi+@pk0$eDO5{B^0!GQEL76S!^ zrYNQt;D+^9br(nEEuaQp&m@|G!3Ap`ZBiEPq5G~yxWfJMxfOdfvt&z}Yv9B4)I~Zx z_nqDjBn)%HwM@~rSMxT1XWO<{_N-Af(%;l)&FvZM91#ri67JhEJJ?Rz!mQ|c3p@#y znfvnvKX#v8&j+SWL7=kg92l{9vRanFtZ3eLMS~U5e0E>&l%=RveOsHoc+uCFWCx{` zRtt9XvwfSmq495Nj1>egJ0Ax@fYnL=u`ZY|;wC*!8DD%zwjCBI^FoXZHzWPT!azYF z6RuZ+phwnWS4)GeS?DUPVT100GbO-3WJXdD`tT6Y72M-Flw1@XcXP>VM0h~ooFAV# zGGfV7XKh!wxVyUQI6$4?FqvDv0==!>b{|QZlVy<>?P8r2x3;W0K4vx}2A|utl*S6b zpdZXYqPYhmsn$y#Y;s?)?_Rpi^x`%to-1%?Jq7^d3VR+d&JdREY{j)Rg`KpYX}#V zi>wC_%sVPTz*Hepw7z9ARi5Fuo~{n}$wowvwZXnr&aA}n5hM%<1m!uHgl9Bp@&N$W z1e4A?oDC17gE{gn8k74GbwOZQEc6A&iys|gAvc`+VUc8DU})=+yUmYQRr?ee9TN>R zW-MH^2jjWN@{kY!Qd2+H ziziK}UWNkRL?B_+F&>yCL~wMt%iehXtiAo#1v~T1kPD>XS_(|wT~4|X zB2P~81M;{LB4Kjq2GhOhLKyf%hedZ?&|qcoe8|Fr#fEpov{CPqI{41m>5I>=ega1r z8_#=&jK;(K0UuHvkM?!iiJ=}F>F;#w4>!n)1jgd_zAbDQe(A!3z>@GRtiw7H0x%^k zj}ZF+7QwZYmbOi%P@A;tHZdyPWMp;mE{u~o;eGt>kQH%>!Jc-XV{i`z2$(Py{*QYz z+TsQm2krA4V?;Re8*>DHaJgGkI7B6(KS7B=u>*X(>-SaBP+y1B+sfvygvqi^FRXgv z$NmAk5A2I3ff;kBH+O2yR-43FQdSTXT@gMfG}n!?GLxH4*51%)Bj@X^zFByX(|G4) z`AC>BM*0XI11n+`>;izx3u}bJ|8p7-f}5>MiutM)=7oQ|HP$FTfiS=VT-97G?iOqv zA6C1p^V+;;A|72ZApMJ*$ zCZ0!JsJgad71p5aY+HGA-Kq;S?s`l14)_|D*#G?B{+CslpcARC3+GOGCn+#c5StRM zBkAqx)Hec-r64SXL2OL|fqz@UtVYm40G#tdoglq4F##HD2Er!;i*rzzluy36;mi_X z(KlRRtm%LWk^#0Mrig&xeG2X;GPN4S)FV@p05{u`0C?)e{S>)CSVw|D!)hRy3S(uw zQ5M*@jR!Y`3js|sRvHNep&w+Q;ZD#biO3Naxae>hjy;;P$CGnz(ST$WPGIER0W6*! zAqtug%phu)-+>G6xM++gjtv=Sm@qc7!GI@7OqwzC)6&%BFh!UmIM9yR{TNt(TLCU0 zHG-2fRu~hjdFczt3B#J7TerXZt82EodAK>`Q>AdQr!j}*27=?jBAr0EJ#lTJO>^vS z5C~Fs(+IhsabunQ>N=8dD<_Y2+xOo(D>#PqYz{WU#K)pU`C;6+RAIcp5dqJFSC}_* zUkI$89QuELblJ5fiA~Yw?s7O)I(dry5{$wKwwRS z6IiumA4YgX@plCqXez@F>Zhly+VsTjw>4F^$3KxUohL&xZKuziw4M06-Ce$EOXFf* zdv(@PU*kd-cU4nuTGp5twP5mib*F?y!2z1gi0@EkW^yMzeYOl&X0LS7W$s30(~B7q(Ahw=t(t!;3~ zRj^hlz&shj?mESpGKO*U(>i!WIzt+}~&#S;JIkALS~OgOeCM~fv+5*3h-!i#i-U><;jZanxa zf&uBUP!K9G)yQOa2B%5!c!r(@@c}5r4&f7jp*Yz{jLyG`rA-0p%lT}X@#$7<2yvKK#8|_ed7bb^=L;GBei@^jqN&6LC8IN01 zf-h|}$-<#+9)wxY7wUjv&}Ywj((fRzTVc9K_8m-$I1Y73%mp7V?n@uH`i030T!A;j z3s)VkFXA=KIsNJCYPO!9W?NrJ!3aKPw>S>RRDxI9;lP8uV-x)j4L|0U>fSSd4jtsOrA=6LZ3R)`fp8+RfY8>#x z`&rG`K{alR8+*1Y4R}xUK(Xi% zMu8Tni|@2a9sZJv?|+vO&ZJ_tzEre{`+GL`s9@WxWvk6ZzGL|v z!Mdz@(LDm&tk|c&xX$>SWCU8KFZ9XXf_k54fF1X6A@Mg*1WpJV?h{YpOqaasz^Z7d z7yas~twsGlTe0j&rE!qX3*3NZWI;fC5x(JJfd->e z-#=j4;Sm8s%{?nTMF@HPhE;*`p)g4r@!2QZ!@g2e`uC zTh--7D~ylX&K!$+4y{t}vuH_Uk9GB0Rr7c-KWp**py&pE5w5tNb$7h_mH5y9`JcUu zOQyTC&0Qh@kir0{FfyTtX`rc3&(1qSqq)Q2xELtE-+wsn8aRzGnAP=lw*uN)n_ZYh znlViD$UyJmg1f9ApoK}wxd1GpgYlxxQ!f+7CuSRM`E2SxG-vwA1yLc8Skdi8&yZ3K zBj(-k9EcpAMOa{o@Y~Yry1xs8*+!%d6Vm~?@qq(ZV94OZ z+=v0|o5!KlZ{~)zCTQQl6lTl3(LY>ToGs(#HZWAdtw$V)c|}+)f*bqvVPk<=!_-eSu!qo!Vb`A=AougV9?+*;|9i9 zi~Nm3l&Px|zPH=#FWB;S-h-`C*3YkA)Wc##Tq7)0o0SG0rS{gkrYlWcpCf%8 zc1#w;$)R5FC-<4#ATVU5>2DZNv}HIcmXUBL9z|>oMn=pMmjrH$RS9m~UOWdZ7&B?- zK}jh%4yb#s!4g}6Rzea&(U}o=ic*16g4L2qW-Og=wy|qD`{ZZycK1sXR4R7*yJ}Mv z?JSgSb8W{~m-lRIP2(;G_gu8)3)6)g(1m~)HgSsJ)(iNHAaRD~eI*e?^QW=vL;Vnr z=~>{pT`1X{;K;&3u!2}qyb!nLM+devCTnIoVH-;clfrG=dDeQAt);JOXQYS^U#MEP zy=n_HQt&q5-9f0~qGW9jbwhZ>YYSjDTMV5^#t)bfK+Jexu!}G+%t)T4g093OS4IZ2NO~N}$PHzG!lrfx#dIB}DddMM?BNEl4;&q_D7HN#R7><(I8a_h9VFo_;@|sx1Cp zoSU%<2U7`0oR8GiThp^ISj!79S@z^JzV-wqv~>TDfF-=mHdy7sZQH(c%a#{bETR6^ zAxLDI90_-pIy?XZ`gKW`J0^8$Q0+$mxb2rV=#k^o?+8TD^;eA0ys1*yp3Fg?k< zg3;4|Vi5SFVU_?9VF01hF5`jFh<(7o(3A3kU?kBS;`z{w52I zHn{JBIn@v2#*Ouht2gbn%a;VV275d?s~#Q()YV4>70;0+rNOydkllY2v_8hfgaBY8GAf!Lxa7>#(a!nLrfkk zlDUa2A_NFW0w8RFKf>jw?|)VSEXy5u9BBcE;k_glFl-2sq+?xAz&skYcN_j!-R+sBfx|)IpGM9TrQ3DG|#~l7$`uZc#J?mamlUsBH@9y z2=nE4BtHZ}d%R0#4ik#IE0uUcD4IW?ESQ?U`h@z?k6fx76CFeefJSo*YXxA|Fk$b< z;dm%K2NMBNqVe!sBt&?gL6ZT+c(7c^9*bb$b?I*im+|mPpLmY;z0azX^8jG{eij4c zIc%GYar$nZ`o}(Ez=*mTGzc2BbzB3XK8SWkMzhA?DaJ5{@zYII?R!;{WhY{!GTU2&^3s^Uw!d5Kt7*cjhwnQXb-{@$d8rW z1=HLuOZoT_>+bKiw(d@=>FBa*O-9U2%w4dk%VzC&e*6#Zo$vif+Ea}zjYid&lujo_ zL{?BWo|ZDhYW)B=#zf=57$(IU64sn_9R^yvTbuORXq{p@+(RyT%!By_Cc2o9tj5qN z!PFp}=R%-xL0n-Rw8cCG?Gok<9Dy-#U1HaXL+Wku4;`PeCipY;nreV+P&~!LL#ihS0BW{ zxuVSesb2o50h z0&JiMEIzCeAEPcFVxA*j!FYX+poNxJOExxJTr|#hK?yP3OlOzHHGjn|Y3ZVS)_t({ zBHiBM%seiojCCC~V&DnR_COhxg_572_e`kj`kKY_IUh)@KI>Ui!JWnUnpByco)7Tjn_&WQuuRw0Z2mRYF1Af6^G1E+DKsoA$xF8 z+kj}mFmK#F4Xg#@M`&cOaMxXHK|v#CjK)7yE6& z80j|xfa7{WDi{V^gfNb=@hP`%@c%QO$RdG3V=?r}+F`>*&NsoeLqVwcgb1Y<7T27h z84E5iVm!UlAOqtMS!0ZY!dMs^;|a{4V^D|Or8M?<$~d0@SmWO4QM;_0x%=R;6Czd@ zeW%VwX(Ib#_Uu!J2F*MCwy?D9sk|_47!8HAUfKi>Y&*|buid`yguq^8xL3G`fF@Ya z4zRe~u#nJry1Uyg95^OTOM-=YfT-vP<6$=e`u`XI_j{g-P5Ebk`Um#hg=bsnQ()dQ{G|T zgBusuS*@+79@|{}j(z&qd0TlLw_2Ew=s7A+uZKZkv}J2#!IeEm#5N*WA{5+xob655 z5dY=u8;sl{G+QVo^qycWr(v z?^Y03R2pSj9O-Ja9$6xc6-y0%gy3D5#U3rx{k8&(So?v3$Xu{a1m^ATRLyrnjI{Z? zziD~R&C-V-xC^L$aL9Up@I%|1p0+hv1lbFhtoG=L*T-gxRayUwKmVD>McQ9})ehAD z_QQv^bN#9nSC+kAw);oGZ*J$T_3huVwD5QJFVocI z+n}tg&HA(Ftm)i&KYN7t_9iE+`|WouRob`R>t9*@h37oA_u%@MW*lmzwl+3*EphaO zwZ8JY9Xz~k2ls9{4y9#X#g3k|%DtPOdYm6TX60<7a8=3>nW*!V79StA%E&1z_6%BH zw-)~6ERcj;bLcFOj*WRCw(0qpDp(i@w2d+3Fnuf=ei5L4>s^ zOepo!Hg2#;)1z?KIs%G-J!4_4WTdesgys^wdf!H1U~RU+TeNm)F@=5uLuj~&|@U(KlRXm zeq(I3NzKGwKK|@$PwQqYeh%T{J~+Tx79A4!2szs4$`@Gh80PiYm21}@8hbO*)(4+l zwHvn|sxMeg(pbf;j=%Gkb&L%A*)hIhpZHaLn$)4BRXe&Z&IWVRmiF1>CzF+>Ds4>o zbMyK&J9+Yy<~VN+jg5Bi?p-^f?+@=@we#f0RKEG;A z6tN`)48jM&K-w*5z_8wiJ`0t+;J(_BAi*t$1wrAHf4z<3A!?6;u%SN3NqwhZr@OP> zCpODm+}iiP@H^{2`r5&XkoW*oCSFBBn5gTn5Mlu{{-`iFSnR+kD#3arwLZ9(p%<)W zKknh^F-!ChS-hv;b|$B6?H8Y0YsBp>6I#>o2`Rbdq^!|a zSq#G6-A9jXcT6}hA>u301*2x+CNxQ$tKro*tnSFLuM62{^}701H!vt0TMFIWjOE#+ z69q^Dj}+W;Op0tFXX%y}t3Pqd4#YDG3v-q@5bvI!vz)A+n$B*kALzFXO7yCf8P$Ez z-fh*sAxpNl+s;>CSZVaGc%We1(`(k&>2Vg^ym|E@A^h^)N(7s@a15_NG21L&D+Nq_ zO8h5(_J{AX&lN!BBqH{V0+Df31x+{Ri}ymnsNgBJJj-`J2@vuLL z>jaJwGPr!!q(vi$m|%|^a2*m9fTT^2o#=Oj6P=%$SqM^F6GmI00{8JFBOIugNn^7` zVj?g;hz%_St&YGpZ9sHr)SUA{|8X&3Rk+(teWGrxEs%|jB@ieu-g@spu?Z3cuts3g z6f|@C76fI=+u;f;xN!rKO3PL(9Q#|ZwhF$MBfv){uG))s^(Fp5`;3J(C@?6}r6a#2 z0s^EbxC)GUj=AA?=7#>!57L1V0@Mvm=>zZZokH7TJax{jiRB;w*hh)D65D^{W{Kto zc>eWIKeR{V)1H?64}bgvJAGnUT7Nh#4u*;F=J_C$a8-W$HGHJko;fvaXHE~>=;KMd zKl)hx-*e5qQ<~(F<5l+KwM{q-Wb~4$qOq@a-Sc$4*(g|A-yTOj#jQYk&Lc zm-fX6@7vPcoISXI&mN79+TA<1Z9xQd=k_f(*#~+~Bs33?)&FpC7a5r4Q zxEKlGhAoO9P~WicrPnRf*=ecGWe=!Q2SSdtRG17xhyFD*kVuhm{A64(V=PSEWQ+lP zp?;J(o@bu0W>MtUq=|ldb4-fMsx8lzJ@W~-5*7lML3AM~5Eva2?i@QzP#l3lY=Fgn z?4FC;D#S>L7YJ@HF=E!=!e)vYzef@S8(k7BVNqb%Ofd)&E-VqUu7=y|J&l(&IUyU2 z3^57l!};Otb;dHip7(lyJ@58NsGl6{wno8~`DMP~OUy6XNx%;Ul*8qK6-puW10Db) zS0WAzbu#zxz~~wHMva(forFC5RIwM<-rT&H^sMhm#jY}BOl5leoRRO0KD7M9`GZFkY!bS>P&DY=Jq_k zlIR=o46W@)V|K8-w`xR5eXVUSx_*spMP}487Nsv7tfw@js4O1j885X#E7-P zBE@w{LkQ9*OdZG6W-?$j9&plk*RJ&)0%MY3E)d$<`i4)S6X4;b!%P7%R)ohcq-{Z* zff)dikV4Aj!_a2umpy>oEXj%njq7XVNWZ(t`un=<_T4dOh@34#V3{$FX)LTEq0l$3 zv!}(xq@CbW!WG9@Tu_Lh7?UrK6oT_N?b1H(Bl^uV9G6S_AY*|UL&&(;fDPlpQUQT+ zBf6HV=fZe`rsSDEg3;rNS%(=yb##2n<1qBSue;L)$Im|aTyMmDK_!8IkpJP>q%#nH zXKt`Ch6Z|^u!4q5o56<)Q85nSPoWEF<7}Y$!6#qp_nccmzxln_{ZvX^UkD27#%1ty zxq0WI-MTmC0udNQ+utC>IKKw``qkpDH4!OrS;{onf_5TYm(j0eHVeB#=` z^@&g*5rp{x_dV_S1r@mZ)quBoFNfgS+tt5JHCS}iicRCv~7a@oOco2 zSfmNlclw6)hk}8Splz5bv;~bYX8H{M@Gf}D`#i(n^!<}-4{c_7+2FOoEfMAwAr0QGu5USvDfAWo0=$3+f)~pUI^bTT1Tu<% z3t8O*hV;82>x;y%=GWh{#xrNEMsvd)7dJL+|HfC+!l!*sn%;azNZf3T@BiF#Gc%UV zHdxR1f8cy-_v$aK{`6U^JAcVzB&%P3>6A ze5;lS4uW(4fHl8x*&2q2rML(dntL{KtZ=06)`p*cSoi!ZmfYR4vXnP>S@jKC=Hl~~ zoS$~Qv@@4{+Z6h{_r=F{u(58bXD(XecYpZWyR74Y&>(oUPuIMqZNfBQTrgvZ1}zJV z1V%{uTQCXn9|nIv6M$w9<0E|$tl>A%4}@iK)WKx)8|yk4D8V$|gZY8rkrodl_SNS? zXzGcyWSFB8t-Roxy@g+~Zv)3Sy8dF`?63m5ucX;bv+Hgind%A!z^6x*2l@`P;Bgcj za2VVK2WSgCf^kRgEf1LLnY?gbeBZ55e9p!qPR&QEJ}V{^7Er2V{;URW#x@iUjh0X! zlWHrepTP&1BZWTGCq~4)@H_YEcfiXi`62}4Vanhe&-zgHzr2Jkg08tiG^D4k ze~bn9B3q2$=0bo(f%9mODhl-?5FD<;V_(ru1L_F$g5`nL6Xq~vWpT0cAA!XCSShT0 z=Uv9nT9l~hQEQiS*df8emM`sxDZKCj?lB%tlTvT04XmoSV_um48>S951mx?H!1KXSw5w;RX;DD_Q>!`y!QO@cef;s zIy*bWpxWF7M3aPp!1O^VTrm;xI|EaNXqvcGSlmUh8wL^1RTg^Sn)41xpz zgbd?(G&$>E%M5oazn3Lw>tt0NX>YROfo?HHEYn~S04HF~e5G@{(wd|#iHONy!79ld z8MgG%;}#n@V*9P#w%^!h3HBahr&JLb_{CVM2ONzAK6pgGBSG?X!4<-9z|q|=YM(d> zOb_qv?!Yc5Ly`T-vJDMtQx+30)(y>}h&KiCo4(N={Q;)TH#qM7kix5|rE8vUF>^HJmtUiOwENN-(8GFWX~dwl_Ix$?bIsnWUAa7;$2v zkj`CN4breWj~=ns*Wa|p7ca|d8L<5QI}%c>mO1~d^PtU%Nf%VaZk>0tjXk_1w{0m| zO&nqc9VF@-tY&ybv~$8@9o>Eo{yqYa9fZYie7(x*qQxaFD!qf2zjw>>Uth6<$;X!3 z&r67PT54d(V*m2L`A@3=m|zm;0yEJWN#Sf`i(4Wf+OZ=8K{|_v5U6!+P~Xu?Vc?vJ z0g*6~epb3B2gV8WW>TkS7knZ?o`L>u5nafznv)R08ag>LWFe}rd;2m74y!Q@y_ zhh~rK455a$jKB*FThEGl1+6>g+B8h@)bU}j4=aOrutFFQYi1B~XoN6e7&8ZOQ5X-w z_U-KuCt{o+7-PX@gxEDebm@O==`k0s-&f{S0?y^!p*+Ccu z0QexNfwdEh#!SB;BCIa1fBo-%Cb%rA{R3YQg!KloAESmEa&r zfVoElMxasnJDP{65Qk!pc-9$%#>M8>1k8szrEmyV z+~7Fh_U7PPU0dH4t%jW@oF8eN2o3(`cN86F8TTZ0GGE*y7C`|P;A2fj%2(fjrH`Mo z^@pRj`S76~h{oV$zz!O$(f#JjueckhxV&U-&pj`TqQ{Dbyj4fGxG*Q&E_+&XLPCcv=kxkKFY5+; zajWc*C)J$2sQw+d@{EL^=(X)IqO1-U8^R-@>(p~En|1YC<p)@-)$Co30@VU?6d1eW1np`VoCb7xnuY>#7q1Ak~#;p?#2s^$!dJSBz^_ zst;nts-bI=-8zv%-h!Q2Z0x4?YqCv8G35R$6$XMhWc#JCtMzG|37QBsDHu12bnVv`_Hb-6GP^vbbr)yqN&K8sMSvkb%W`AaY(#^pOJX{#I_EcpA>*Mg#sgeJe~2#y-@4bQcB!9$ITz`?8}5<{Cpltyan?PqEQALz zgP~9-ZY$D_fgw8$hh2nO%o%n*AZdX<02AO5UBou{bcQLIz&wEmNfKMC6Tu48WsI;Y zk4LE<;7Fz!`-gEMpkT1R-UKsV&#|dpRm2@%U$*Mbrd4F6RE5Lw{H`U0&t!IiHv+g! z`Fe|GnCE=Vrr+H2KCODhG8TL||a_r$g2|+nca^fR9PfyAV>pojL0}IL_Z(&@%2R#tz+pUnn5D7?(xB+6?+a{~>(GNm(JdjY_ggiel;qiwn~H*KA*Nv#-ypa4wDj(F>t{ zS#iQ!E*b=nxPn_(^sWBvOBfXw=B>K7OM0iT)o?{XbHP}W&COQR-Q(wmsFPNO_o#hv zFxAlLTb+owt)ll3Yz0=eb1sOKC^l(iT(v!YvTnpX)wc5h33BkJnBb^g@nCRkbtxnR zXbh#zO~fUhr5Czvb>OawnZ8d&rWrq+q0PplBu zrT{=nSa(rz%K_vFu7gcyFu2=5o?lmZ9|Q_QZr|5}(3VS7F(C>Jj0`LSk!+!YwLpLv zrWpi_d6EHXx=RwGSUx+NK>7&+QaFy7ctDiWOc|7wMMfEYqYW$$`U3(p9#Ww>Qnx5( zfQ1Ev=8O_dYx?->iW9sL87lxLjuqn8hQ@=yLfd7$xQJL!fR%Oi#ywkK-*Pxdu)T0r zX0zsNO@ar3fdKe-|Necilk#8ui+}2yVMcHTIf#`YB%*aOJp1Lf+deM(`<+*w_co%! zyWk24_NxV#Cc-u_&VYajQ(RUQ&Q`&?Vk{&JTz>w%zeD>7OJMM;C4c6k6{HF6eej-@ zA%w#MGi7~*U3|&PU47yHiZpqZx_5rdVm!BjOJXy^!I z@PdAWQ_F%IF%PU1+M%$4D9izx!>tI82jM`LPo3ZuVrDR{`sbxlo;h#ZfBT-hLBNfC z;lLMu_5c2lt!7t(@Ltd~(ViEU)`jyaYv^dTdi6b3*ppdy)Am**G+Wv&^U9l6uFYCe znr!9DN0!=H^3+&&FNuDjU$?f@C;G=YD2&B3|FB-wFA7#B3K<1BfL~Ga1qviMiW)Wgk=C<;_Nh5+NDjsadDiiSAn(BQk6OtzA zM^}4`;}2^<$SA^Mj0!Ze<=dD@V2PF*bul)!C}OUnf;JAV?P=_#Qq>xo+1xZ(&KZ=J z;Soz-eBPF?UbWp@H?1X`aeUm`&0FVh{2qaBctUFRUT!bRt? zJK=xVTW?!+dBO5GzqW+<5yBcCzPuus)i+tE?kD%QZ2#8RR?X!sIds%A&%R_OS#A~4 zQmw4x^1`h1IruZU?e5Y){L$<0dhZSk0E#qVm~OB*q|r-TS|>jmiF?REnFtsx>7*sJ zKUEC#Mbmb(UjU;aG+ne-7zr^07RtG1t^$JNcjh^?Q0iu~KzNAKHGDD8U=fk-y6o#m zK#b^p4vXRpcYwk>t}zP9h?9_djDMecV5ATi^}F_tdra>K2^+?Pka72h#*7eSlGKU| zS?Wb7QC|RHS^_pKOr&1=;ueH1v_lVy>Ur9L7=w$9-^o)am9b)zQ*+K#=o1A5BP-5f zt8s&T-Zzbl%q(1Fz!R+k7(qa)-uuZFo)5x_Pj(^3wRh#~+rE7ZLizNGqju)hG5)+%1!7L$e~7=qNyO*HS|ZfS4F5-n{O z6U?i*JxdLZc+i@73g=L#_qMGfLIR(qma0dZJK=C7xR_t&C*TM1tB~;qbLV$p4c|0r|&k{)kCd(tHe(i~2M(u!$ zSThxkhyHQYXFrg$K2KQ}cc%R`of;0C;& z)UO$)&eqm&N~O>#P0XO7$#JbbKkKec0^hvDYV_2=5qEvDR)kF+GX&nnU$HO z`b_us`#P42`eSRWZZ(3Z8iG%}Z(x}ee&{pa)M}}i3UyvZ>IQ(T-fLxX4x45`ueSQ7Lp;7>o0&=n)I_4(5@t~;?5dve&6PV_p zw0wjCE-BJLVFVy_w#ri5fe3xuh{Oj2V02#iRG@D8i(*_oFBI>^oYiz{oMkpz?B zIW%{EC(DQ!28{+zquQo+?F zX4lu<<(3E9Kk))G+Q^PW;9(6Sydohn~BUAl1EQ-~2F^o{xAa^jFy&nNxh;&)(4fw&nfzejM4+$#b88BaGS z$$`FcmJ104{LQ+X9>Es@hVTY&xLU;b+pKJznqSveH^OFhxMo=NhuL1;-n5!aFIk-J zRF)SlDaH^9lGKKXNP?s!oFO*}_W|xLg7V-&q-9~1&|k(&Opf_-_=-08{)CyBwaDUV zy7atlj6JrJ=7zb@t6qFwB}BHTXDlPDr0&!+zAJ0J7(%AI+lnIo>guv3w$^P^LZ5oE zGM#a%d#s)DEVNT+Jjp;Jz=^k<5JNl^B)B%dVd7%kxb<+e0ef7h&?5}gl@o-5`UH)` zM46YUFefNNj??;wTNio)*OOu<4eD28W20RkU$F6|O}8vqTk^C3i7v+P*@Eg1N8!$I z%hJq;x8|R1-Py(@+DsA4l66JkKgt#TBr_3VMw}9i1-G~t2}9-$TnxhH z$(XVu$F231?^sf>iU}`3ckqU6J5Sb;f|;A2bHR~4cfo35?t?+dOFmj&@F$as1%AIO zNQ$_(ggY(IK49 z!RpjTos^bTYnz7#$`Vj<;To18;{oTHZ!E_YuDZ5%U-6!nU=!Ok>+JEQ0@ju!L_5SK zBcWYxXtv7mi6^cteAP?{rcp%jta1e5$cK-9BjbnP_`9!9l2sCv{Ca}4Y-w_Dblj1T zLPI=<8A+g;XXr5ZSq&eA1S^S&Po&*FK;1AITpRq2xyyn{7=FJ@z_Ia3&&9uR_Jjif zhJ%#?vv$jbwl(;dU%2250O0a23l;z0pMI$KPT850BhJ7XQ^XJvbO@X%2E=wIFpjC& zd2dI<_=d@Fk*O91w_*G+buzfZxWn2F_dlyI1dW+sm@6yMIfSaOr(0uA`7y%ezYp|w zyAUQ;K{nLJ#LR!4~zlk&&H7V$0qD|fAE&zRkF!xA&15aqX0Qr4|Gdo zq|dCCdF4eRl;FTvS(y&Jqk=1tH8&4Nu|UGckid*Zk@O!y2P0uVD2#)8f&i7wJ!F_@ zcM!2Z6(04%eemGv!gt_Lol(7f2X@@!f*CL-#>_S>%nt?lU?rivAZ(cjk|%Kg-5(uu ztLq>pY4g$>mdfv0dScXy`JB6iYF>KHcRnRBy7%*c?;2qG;tQUCe=z-6z{q(L1%!!T znFI%z7^Xj4l%QZRl4v}2{e#x~gCBXu4a^$cC`~@L!mY2ZyuGdWIlD#-K{&3#*x*0< z6DemR`0HP}f1RvH@D|~~;=KsxiCgmCzTRu77oL$-Ba0{Y;I`S)ytt^8wDZqe>ewl- zH#RG)Gg-MeA&_AZO*d->`o7F(Qg;>T@P(AWcv!#EIFB*{<^o(LG+v2Im~ zhM@<3i{>jlN5DQX)79k_+tpaP&lV^M3j_&PLbUY>|NQe14Muu8+>J$b(Xqm~dcYg{ z104f@Vl-G(WW#ky*dRz&*R}$l=~?;@9+EJDG6DR_8ZC)omul*){_-1AA_g6opbHlD zm2O_Of&>H03s_S^-)O)2jki6Hh1+LmV#2)$O;VEM8przT`e8b4XfqlId@bAGZ`IT1 z{DeUp7ygNs5`)9m_R`CiZf^Ef=D4QVsIle3MOhm+Kz8f;&YNz1tbKLGl@DmL9);=h zD<0F^{rZZm*Iug~IpJAZczjqySgmh#SR#bk_%VOymL?<|2Mi8E8x=A5jv$ns)9ySuwSfxZEv z>H^siO4Ne}#{|Qe(9R(&5F;<b`*y%US43`3*>XtKO zU;{G^aAC1s<2RNW>6I{F-`ZQxL3p%H(~JiOOC}9WiRXYL_vkNhAd3wmM{w{i@bIk% zR2RRau@h4Po{W!95vyTdh9o2ivYfW8q&eqZ0?CZq*TK})TSfRAssv|&nd(m9RuJ*CT?La6hfWR`cVZ7#*c=cDh~6kzFGaN%6rznglS!8 zho!oEtlHG7{%FkVR|S01yd(5tQlhKyF3lT@8Z+WG72y;cJOX1s1reIlT$8C)6^}@_ zv{_QLim-`^QNug()03EBzTcK4ns2VixL6&}T9A0Q(XDTADMs=@YrB*wDJ-(4N=pk? z+1;|3-h)y~SaA~4tb<93Ckr9Ow{@vc6}@{Pr6Rw*WqVUntTdkb-hQjaN+GjLeemQI z^^J8Uu3YGQ;`e{>>bq$3NHUlJDRgKESO<}L2!bH3KtLv&iN@`!vZEpkMye#tnVn=;6S%)#ni!gcou667XZJ(wDU3tS%VTXcS67&3kv2A zoMC*pho)v&_$NzCFu+w+wPCYMm zYQ%6dC(k(1u{A{Uh1YG5ZL`-&M-Pb_;68to+aDK`ic<%I|2x&Ra?XX>V%M8dZPSci)kw9QRC{%JPEk$Vvu=-Lm9aSA;t>_0sEppipsn(NdKI z%SfnI&s?-hSHCktwk%Jme{9!LSy{Bg&1+UniJ`Q0T4G53o*B1C z4hx^f`(f%%|6*u*j{0$TLA&5*^IPAy#PBIwotU=Gr4`@Gr18`vt8 zw08J5JGIA7T22B3!QJ|uxBTAj%8G4`J+i{OtP2&aBd0mhXyc`4t@ZgALmKe;Vj>)9I(f=6C0T+G?pov7vzF=YwcOWVTBbvm znd&5&0yh~}SqWFH7<=~8i?*xZ7C-n~+nHamr236ZG(P>v%3}}2@TKeuA6Vm2|IAsp zen>vao<3vAqsQ$2Pv5i6TQ{s#Fe+?s$P(GMS}8iX8u#^X!wWBaOo+vgC@3-EZDr)N zZK?hB&)&D<%90eH_6FBD#6_^Z&2oX>hLXU}uv4CJ$K_Fy= z(O5Cjb}L9z5SD;BW+TjsoqGzI?dtpV%)G}S*rAtxu^13d9;S>C35bsisoRAQ zRs~~Aqf1wETW?Sj0X*jegj)93fiF1=AZwY zpLo_A8r-F`C%rz}Mu2y9wmJh~t`LCuM}f;%*YCJR#+=Qs^@T9tj@C`gC2)DbE6H{(ic^nDo-&x4&DQv1x7tj*yy8n;}@tm%#>q*`O^o|F;I*2wRA-l^2xN+pz{Nkiq#@Z$KWmDX1*bLy;FPWL9p-1qO+pkRtH zjugJaME7B+oFp~4Lw<_#PSyF2hMzwc-atlp1O>Tyqj+dRik#aJ%z5(EHkk{t$0 z5JU%eq5^$FA5WtTxBwo~p3}5St%N}B5eZN<^{zh4t*qMWCm;G5<7of&{rzH=y}m=h z&g`rNz?#Mb|H2ZL)rAGv+~zCGcgIF;@9~%mdIH?^r@5)#ad~GaXPFDnT2_K7CJO*p z)PZ2vTdcITVR3b};oJpFsXt}4zk#44=CZeCbuBHv zCMB+ZY|YQx`t93NOu`u>#KqRXzGhq3uUe`qC8@1Vf~V7i^lTfyK0fXnMAAV#Rz}ivIqN z6a*<`SR;a8?a;8V<}a_V1Y8o#D{Ns8y+A(*0*D*Lz~$iDrhW^P7|c`h<0F$Eq=GrY zNL{1*-{oMgjn#!?-D!wv2~< z(MJR${YS7d78oQ}3G?E@+zAuQ5MmG4)byN9>RMP>_OxZvecb|SA+sxN?E&!vXD^3v zUJ}90%`d4BvV7EE#>)JD+r@q0L|=hFjN4ryD!{}U(BTF1h;WCCaf2t|)YDhQw7{ir zyKt3K2LhZJ9O>EM!ou>Z3qtPEm&n3_$#B(6m~|(r)*mnXRkyxuvCVZqCjus%5))0x z)K7?^q$FI(pRY=jPs&2^y!z=$OGw*G_6@n)pmuQ3nogau`r%`~8rcaKhJofjGiB1o zEGfc_NdQ!3DX@J+L3Oa^fUPW8D0U#6fl2uqky1tY7Vr{R9h(#G9mvWPaiTf1(mBpS zN%~CG$eI_kui09&l!QZaMOyUyv?Un37*M(_wxIFA;Hv6tMc09}$?AqIV?9%rl~ra{ zwU|&GH(5iIk1Hli?ZAQpz}zy=;5zsP{X(-8rwxsh`%wu(CQ=ME&#qxqtxd|@@qsQI z>F=}wSslHd&DI5dHfKd^H7abk zGQB1%#Znq_&5>hP#iN1SO8rU+2iSQR?Y=C89r)hwiXfZtE??sadyfIQX~rFk6c-) zWgj)*$`OuI0Ad;OIaw8X;l%Ew6b|NEbHsX%gcx{A3>z0Hf-k9gr`@t}6u76wt4P<~ z7p?(&Yi#nhE&DUmuGHivCafr4!?C@bA5y}?05|n4UK98L$s~K~S586@*VVd&=E~TZ zZD`zhVCH8RYtY;w83go7KK93z8a7~oObo9-czAo3OM3@L67!AHtga~dd+KdpC zH6So|5EbUlbH0s-7%C-Xa_oN-yo(T;F(VLQeh>ib9{?`q{><#6t*YKr$B(#%j5YAd z7uTF&_w{xjo;=v*YgU+03S*%^r!`LEKxm^b)DSFcH~6EaBuf}UeDnkBjG+2o{@eFl zNb%c0`_q5m#}@l)eSQCR1wr%C=T~icd0o%M?bXYdTzLF_3k-w)xjXk37=LfXno=&p)*NN268|v&cMi z&gw2aXT>i+^*xSC$InTADYMKP7!m!DZ# z!V>0-G6lYZ|E>gfN?4w`XfQFm|Y6J>*4B_f4~^*Y&X zSO^VXp?ENkA{#>P%c6Pl6>B8JN?JI5CZ19F;v*|hjr;ozFTLtp=FI=>r?&OrzW+Ax zyT5M@eSNm0=V01dS)?RhY~8FlU%}@>| zADfMwv^480B*+R^KDFJux7>XVZ-@#?F5_-{`!_7pH(N`3-;e&1J(}ELYL0}x*x#3nNeQbQ=yRHx}efYkw^I#oG<162>?YTM6@ak0mS}#58 zb#8z9q2(tYTkVDCZDwN99tuAbvP?NAKNTx`OpDzHIMan~X~^E{YiqQQhFVMeeUzM( zbqH4)Wm!cfA*H46#aAsQd}S?Go-K%~Wcbxs;z#eiq;68 z2#c>i3aZi$43>j{U_7qLpc$md)(HFPfMg5`#D;iC9fjHPJnkIo&?BCy0Z|}4007LK zv&F;WH?*UWp@fjZa-j|g1Ivf&L(=c`fvrs%gj^6jTc1Dx2t5QcZL!5R zL1Pfk(;IbpW{c{=4aUlM>Wl;@1dXMGKqc-mJ~8Wo@qTIL$0U5Y5V$N3#NQnWI~JIc zeT8sdSX%KQI!poLCymx!L5D&ItA+8<*QYOFg(gUyZjQMy3&NUvzw|uga^Vng;6HeY zaexE#f%ik4l5)!V6D3kP z^?VgPS3Qo0dIp&CJB9UP&>1vB0hgnD6lk5kLMPBEpVR}6F;_IoCo~SDE)L~lu(Wi!W;s_>IXbrEQ;kyad2UwnNKI4H*Fntf-tmE%d0*VY+*uHjNl9Z*~|$) zRKJ?{^Iq7g8EcX?J*+E9f&aP*e7IQSR4aj;85|Cqqzd1wYCkT^2^>ub*PzYB?|kp_ zyJ!S3AP^5h!VP!dD^(UcKBWIzSqe!Xilw2!rdHGwt`pg}~05KJIq zhHPJg21Q&30wQS0*78^f4j>UZlS?81niKtgG%;-pi%3@?_+gzq7Z_N=if)FZ8HdF_ zP7ne&vq@EM&5NqeLL(`T31P~W}$8AABVCMh9%jYH3&7!`*aGuoX=d) z1z!xvUQ3m@V5Z+2kB(TQm_&Bum}exhW+lF|;8|MieYH0;b9kcNWm~&((^h}+g$VnB z?Mr)O^4gz!!QB@*DNXFTl+6M z*Kqc{+3b|%Z(Ozf_+#6UHL@`;OJ#D(<|ITG*S4hWNx>3~7PdG#Ashq1mN$Ev3sSUE z%h-S*L##>ym^cbCuS9pBwY~a=WzSx)xbSgbLdq4TQ)h!~c#9ZK%>F-Cu`|+;4y`Z% o000hUSV?A0O#mtY000O800000007cclK=n!07*qoM6N<$g2Y06Hvj+t From 91529fa5fcb6cd1da426e8e5a24169f25dd0edb7 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Fri, 31 Jan 2025 15:59:07 +0800 Subject: [PATCH 0015/1082] Edit military module --- library/include/modules/Military.h | 1 + library/modules/Military.cpp | 101 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 013f804415..dc69dac52b 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,6 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); +DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 0477c366b0..23fc15a871 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -4,6 +4,7 @@ #include "MiscUtils.h" #include "modules/Military.h" #include "modules/Translation.h" +#include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" #include "df/historical_figure.h" @@ -289,3 +290,103 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s } } } + +static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) +{ + int32_t start_year = -1; + for (size_t i = 0; i < hf->entity_links.size(); i++) + { + auto link = strict_virtual_cast(hf->entity_links[i]); + if (link == nullptr) continue; + if (link->squad_id != squad->id) continue; + + hf->entity_links.erase(hf->entity_links.begin() + i); + delete link; + + start_year = link->start_year; + break; + } + if (start_year == -1) return false; + + auto former_squad = new df::histfig_entity_link_former_squadst(); + former_squad->squad_id = squad->id; + former_squad->entity_id = squad->entity_id; + former_squad->start_year = start_year; + former_squad->end_year = *df::global::cur_year; + former_squad->link_strength = 100; + + hf->entity_links.push_back(former_pos); + return true; +} + +static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squad) +{ + std::vector nps; + if (! Units::getNoblePositions(&nps, hf)) return false; + + int32_t assignment_id = -1; + for (auto& np : nps) + { + if (np.entity.id != squad->entity_id) continue; + if (np.assignment.squad_id != squad.id) continue; + + np.assignment->histfig = -1; + np.assignment->histfig2 = -1; + + assignment_id = np.assignment->id; + break; + } + if (assignment_id == -1) return false; + + int32_t start_year = -1; + for (size_t i = 0; i < hf->entity_links.size(); i++) + { + auto link = strict_virtual_cast(hf->entity_links[i]); + if (link == nullptr) continue; + if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; + + hf->entity_links.erase(hf->entity_links.begin() + i); + delete link; + + start_year = link->start_year; + break; + } + if (start_year == -1) return false; + + auto former_pos = new df::histfig_entity_link_former_positionst(); + former_pos->assignment_id = assignment_id; + former_pos->entity_id = squad->entity_id; + former_pos->start_year = start_year; + former_pos->end_year = *df::global::cur_year; + former_pos->link_strength = 100; + + hf->entity_links.push_back(former_pos); + return true; +} + +bool Military::removeFromSquad(int32_t unit_id) +{ + df::unit* unit = df::unit::find(unit_id); + if (unit == nullptr) return false; + if (unit->military.squad_id == -1 || unit->military.squad_position == -1) return false; + + int32_t squad_id = unit->military.squad_id; + df::squad* squad = df::squad::find(squad_id); + if (squad == nullptr) return false; + + // remove from squad information + int32_t squad_pos = unit->military.squad_position; + df::squad_position* pos = squad->positions.at(squad_pos); + pos->occupant = -1; + + // remove from unit information + unit->military.squad_id = -1; + unit->military.squad_position = -1; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) return false; + + return squad_pos == 0 // is unit a commander? + ? remove_captain_entity_link(hf, squad) + : remove_soldier_entity_link(hf, squad); +} From d88277fa3fe6f4ef7ca7991859ac0478126e9b94 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 31 Jan 2025 07:57:35 -0800 Subject: [PATCH 0016/1082] Update changelog.txt --- docs/changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d28decc8..1bb7b42bcc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -37,7 +37,6 @@ Template for new versions: ## API ## Lua -- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings ## Removed @@ -58,6 +57,7 @@ Template for new versions: ## Fixes - `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command +- `createitem`: output items will now end up at look cursor if active ## Misc Improvements @@ -66,6 +66,7 @@ Template for new versions: ## API ## Lua +- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings ## Removed From e6e1972fb9bd1b97c27c255351f314170d7a8689 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 31 Jan 2025 07:59:53 -0800 Subject: [PATCH 0017/1082] Support adv look cursor * Update dwarfmode.lua - Make cursor fns support adv mode look * Update Gui.cpp - Make cursor fns support adv mode look * Update changevein.cpp - Use getCursorPos * Update createitem.cpp - Use getCursorPos * Update cursecheck.cpp - Doesn't use cursor * Update stripcaged.cpp - Doesn't use cursor * Update zone.cpp - Use getCursorPos --- library/lua/gui/dwarfmode.lua | 25 +++++++++-- library/modules/Gui.cpp | 80 +++++++++++++++++++++++++++-------- plugins/changevein.cpp | 13 +++--- plugins/createitem.cpp | 10 +++-- plugins/cursecheck.cpp | 7 ++- plugins/devel/stripcaged.cpp | 1 - plugins/zone.cpp | 15 ++++--- 7 files changed, 109 insertions(+), 42 deletions(-) diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 793acbd520..80d30282cf 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -7,6 +7,7 @@ local utils = require('utils') local dscreen = dfhack.screen +local a_look = df.global.game.main_interface.adventure.look local g_cursor = df.global.cursor local g_sel_rect = df.global.selection_rect local world_map = df.global.world.map @@ -38,17 +39,35 @@ end ---@return df.coord|nil function getCursorPos() - if g_cursor.x >= 0 then + if dfhack.world.isAdventureMode() then + if a_look.open then + return copyall(a_look.cursor) + end + elseif g_cursor.x >= 0 then return copyall(g_cursor) end end function setCursorPos(cursor) - df.global.cursor = copyall(cursor) + if dfhack.world.isAdventureMode() then + a_look.cursor = copyall(cursor) + else + df.global.cursor = copyall(cursor) + end end function clearCursorPos() - df.global.cursor = xyz2pos(nil) + if dfhack.world.isAdventureMode() then + if not a_look.open then + return + end + local u = dfhack.world.getAdventurer() + if u and u.pos:isValid() then + a_look.cursor = copyall(u.pos) + end + else + df.global.cursor = xyz2pos(nil) + end end function getSelection() diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3e9dd0dda9..ed6ddcba7c 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2744,9 +2744,18 @@ df::coord Gui::getViewportPos() df::coord Gui::getCursorPos() { using df::global::cursor; + if (World::isAdventureMode()) + { + if (!game) + return df::coord(); + auto &look = game->main_interface.adventure.look; + if (!look.open) + return df::coord(); + return look.cursor; + } + if (!cursor) return df::coord(); - return df::coord(cursor->x, cursor->y, cursor->z); } @@ -2911,7 +2920,7 @@ bool Gui::inRenameBuilding() return false; } -bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getViewCoords(int32_t &x, int32_t &y, int32_t &z) { x = *df::global::window_x; y = *df::global::window_y; @@ -2919,7 +2928,7 @@ bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z) return true; } -bool Gui::setViewCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setViewCoords(const int32_t x, const int32_t y, const int32_t z) { (*df::global::window_x) = x; (*df::global::window_y) = y; @@ -2927,32 +2936,67 @@ bool Gui::setViewCoords (const int32_t x, const int32_t y, const int32_t z) return true; } -bool Gui::getCursorCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getCursorCoords(int32_t &x, int32_t &y, int32_t &z) { - x = df::global::cursor->x; - y = df::global::cursor->y; - z = df::global::cursor->z; + using df::global::cursor; + bool is_adv = World::isAdventureMode(); + if (is_adv || !cursor) + { + df::coord p; + if (is_adv && game) + { + auto &look = game->main_interface.adventure.look; + if (look.open) + p = look.cursor; + } + x = p.x; y = p.y; z = p.z; + return p.isValid(); + } + + x = cursor->x; y = cursor->y; z = cursor->z; return has_cursor(); } -bool Gui::getCursorCoords (df::coord &pos) +bool Gui::getCursorCoords(df::coord &pos) { - pos.x = df::global::cursor->x; - pos.y = df::global::cursor->y; - pos.z = df::global::cursor->z; - return has_cursor(); + using df::global::cursor; + df::coord p; + if (World::isAdventureMode()) + { + if (game) + { + auto &look = game->main_interface.adventure.look; + if (look.open) + p = look.cursor; + } + } + else if (cursor) + p = df::coord(cursor->x, cursor->y, cursor->z); + + pos = p; + return p.isValid(); } //FIXME: confine writing of coords to map bounds? -bool Gui::setCursorCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setCursorCoords(const int32_t x, const int32_t y, const int32_t z) { - df::global::cursor->x = x; - df::global::cursor->y = y; - df::global::cursor->z = z; + using df::global::cursor; + if (World::isAdventureMode()) + { + if (!game) + return false; + auto &look = game->main_interface.adventure.look; + look.cursor = df::coord(x, y, z); + return true; + } + if (!cursor) + return false; + + cursor->x = x; cursor->y = y; cursor->z = z; return true; } -bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getDesignationCoords(int32_t &x, int32_t &y, int32_t &z) { x = selection_rect->start_x; y = selection_rect->start_y; @@ -2960,7 +3004,7 @@ bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) return (x >= 0) ? false : true; } -bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setDesignationCoords(const int32_t x, const int32_t y, const int32_t z) { selection_rect->start_x = x; selection_rect->start_y = y; diff --git a/plugins/changevein.cpp b/plugins/changevein.cpp index a73d9584e9..f143a25bc1 100644 --- a/plugins/changevein.cpp +++ b/plugins/changevein.cpp @@ -1,13 +1,14 @@ // Allow changing the material of a mineral inclusion #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" +#include "TileTypes.h" -#include "DataDefs.h" +#include "modules/Gui.h" #include "modules/Maps.h" #include "modules/Materials.h" -#include "TileTypes.h" #include "df/block_square_event.h" #include "df/block_square_event_mineralst.h" @@ -21,7 +22,6 @@ using namespace df::enums; DFHACK_PLUGIN("changevein"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); constexpr uint8_t NORTH = 0; constexpr uint8_t EAST = 1; @@ -212,7 +212,8 @@ command_result df_changevein (color_ostream &out, vector & parameters) out.printerr("Map is not available!\n"); return CR_FAILURE; } - if (!cursor || cursor->x == -30000) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.printerr("No cursor detected - please place the cursor over a mineral vein.\n"); return CR_FAILURE; @@ -232,14 +233,14 @@ command_result df_changevein (color_ostream &out, vector & parameters) return CR_FAILURE; } - df::map_block *block = Maps::getTileBlock(cursor->x, cursor->y, cursor->z); + auto block = Maps::getTileBlock(pos); if (!block) { out.printerr("Invalid tile selected.\n"); return CR_FAILURE; } df::block_square_event_mineralst *mineral = NULL; - int tx = cursor->x % 16, ty = cursor->y % 16; + int tx = pos.x % 16, ty = pos.y % 16; for (auto evt : block->block_events) { if (evt->getType() != block_square_event_type::mineral) diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index 1a720c3f4c..1f4b582047 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -30,7 +30,6 @@ using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("createitem"); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(gametype); REQUIRE_GLOBAL(cur_year_tick); @@ -84,7 +83,10 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t out_items[i]->moveToGround(building->centerx, building->centery, building->z); } else if (move_to_cursor) - out_items[i]->moveToGround(cursor->x, cursor->y, cursor->z); + { + auto pos = Gui::getCursorPos(); + out_items[i]->moveToGround(pos.x, pos.y, pos.z); + } // else createItem() already put it on the floor at the unit's feet, so we're good // Special logic for creating proper gloves in pairs @@ -395,11 +397,13 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { auto unit = Gui::getSelectedUnit(out, true); if (!unit) { + auto pos = Gui::getCursorPos(); if (*gametype == game_type::ADVENTURE_ARENA || World::isAdventureMode()) { // Use the adventurer unit unit = World::getAdventurer(); + move_to_cursor = pos.isValid(); } - else if (cursor->x >= 0) + else if (pos.isValid()) { // Use the first possible citizen if possible, otherwise the first unit for (auto u : Units::citizensRange(world->units.active)) { unit = u; diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 9c41d73912..958e571271 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -1,8 +1,8 @@ // cursecheck plugin // -// check single tile or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set -// if a cursor is active only the selected tile will be observed -// without cursor the whole map will be checked +// check unit or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set +// if a unit is selected only the selected unit will be observed +// otherwise the whole map will be checked // by default cursed creatures will be only counted // // the tool was intended to help finding vampires but it will also list necromancers, werebeasts and zombies @@ -38,7 +38,6 @@ using namespace df::enums; DFHACK_PLUGIN("cursecheck"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); enum curses { None = 0, diff --git a/plugins/devel/stripcaged.cpp b/plugins/devel/stripcaged.cpp index 14189bdc30..25f764841f 100644 --- a/plugins/devel/stripcaged.cpp +++ b/plugins/devel/stripcaged.cpp @@ -35,7 +35,6 @@ using std::string; using namespace DFHack; using namespace df::enums; using df::global::world; -using df::global::cursor; using df::global::plotinfo; using namespace DFHack::Gui; diff --git a/plugins/zone.cpp b/plugins/zone.cpp index 612e042282..1e69b7e193 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -55,7 +55,6 @@ using std::vector; DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(ui_building_item_cursor); @@ -844,14 +843,15 @@ static void chainInfo(color_ostream & out, df::building* building, bool list_ref static df::building* getAssignableBuildingAtCursor(color_ostream& out) { // set building at cursor position to be new target building - if (cursor->x == -30000) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.printerr("No cursor; place cursor over activity zone, pen," " pasture, pit, pond, chain, or cage.\n"); return NULL; } - auto building_at_tile = Buildings::findAtTile(Gui::getCursorPos()); + auto building_at_tile = Buildings::findAtTile(pos); // cagezone wants a pen/pit as starting point if (isCage(building_at_tile)) @@ -861,7 +861,7 @@ static df::building* getAssignableBuildingAtCursor(color_ostream& out) } else { - auto zone_at_tile = Buildings::findPenPitAt(Gui::getCursorPos()); + auto zone_at_tile = Buildings::findPenPitAt(pos); if(!zone_at_tile) { out << "No pen/pasture, pit, or cage under cursor!" << endl; @@ -1069,7 +1069,8 @@ static command_result df_zone(color_ostream &out, vector & parameters) } else if(p0 == "zinfo") { - if (cursor->x == -30000) { + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.color(COLOR_RED); out << "No cursor; place cursor over activity zone, chain, or cage." << endl; out.reset_color(); @@ -1081,10 +1082,10 @@ static command_result df_zone(color_ostream &out, vector & parameters) // (doesn't use the findXyzAtCursor() methods because zones might // overlap and contain a cage or chain) vector zones; - Buildings::findCivzonesAt(&zones, Gui::getCursorPos()); + Buildings::findCivzonesAt(&zones, pos); for (auto zone = zones.begin(); zone != zones.end(); ++zone) zoneInfo(out, *zone, verbose); - df::building* building = Buildings::findAtTile(Gui::getCursorPos()); + df::building* building = Buildings::findAtTile(pos); chainInfo(out, building, verbose); cageInfo(out, building, verbose); return CR_OK; From f72118ed26ef9487b16faba11c64f2d366b1bc9a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Feb 2025 04:13:35 +0000 Subject: [PATCH 0018/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index b1aa3b365d..6a952903e7 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b1aa3b365d611432195b80d1d064c942f54a78c7 +Subproject commit 6a952903e72199e469b44e0c74764c1c89f4b2e3 From bca91ec460503fdcf02ca4b0a6c60fff8932e521 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 20:47:11 -0800 Subject: [PATCH 0019/1082] increase the timeout and retry delay for steam --- .github/workflows/watch-df-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index d86f993430..6e72333034 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -31,7 +31,8 @@ jobs: - name: Compare branch metadata uses: nick-fields/retry@v3 with: - timeout_minutes: 2 + timeout_minutes: 5 + retry_wait_seconds: 60 command: | blob=$(wget 'https://api.steamcmd.net/v1/info/975370?pretty=1' -O- | \ awk '/^ *"branches"/,0' | \ From deea1b0ffdfd84f4e5517b4216a6aff3f6bf4560 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:43:19 -0800 Subject: [PATCH 0020/1082] add sand example to changelayer docs --- docs/plugins/changelayer.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/plugins/changelayer.rst b/docs/plugins/changelayer.rst index f986a1e3d1..44fa1a3279 100644 --- a/docs/plugins/changelayer.rst +++ b/docs/plugins/changelayer.rst @@ -22,8 +22,8 @@ Usage When run without options, ``changelayer`` will: -- only affect the geology layer at the current cursor position -- only affect the biome that covers the current cursor position +- only affect the geology layer at the current keyboard cursor position +- only affect the biome that covers the current keyboard cursor position - not allow changing stone to soil and vice versa You can use the `probe` command on various tiles around your map to find valid @@ -34,8 +34,9 @@ Examples ``changelayer GRANITE`` Convert the layer at the cursor position into granite. -``changelayer SILTY_CLAY force`` - Convert the layer at the cursor position into clay, even if it's stone. +``changelayer SAND_RED force`` + Convert the layer at the cursor position into red sand, even if it's + currently stone. ``changelayer MARBLE all_biomes all_layers`` Convert all layers of all biomes which are not soil into marble. From 95b56d96f8bcdac0aca1338b8cb11e2248e4de77 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:44:16 -0800 Subject: [PATCH 0021/1082] add more boolean options to parser --- library/lua/argparse.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index d9ee57d139..a76e9d3c6d 100644 --- a/library/lua/argparse.lua +++ b/library/lua/argparse.lua @@ -237,8 +237,8 @@ function coords(arg, arg_name, skip_validation) return pos end -local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true, - ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false} +local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true,["enable"]=true,["enabled"]=true, + ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false,["disable"]=false,["disabled"]=false} ---@nodiscard ---@param arg string ---@param arg_name? string From 149f5905d29a05a04d7677fb64cdf3b811123ac5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:44:42 -0800 Subject: [PATCH 0022/1082] don't add suffix to focus string for non-difficulty tabs --- library/modules/Gui.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3e9dd0dda9..951482d23f 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -817,11 +817,12 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.settings.current_mode); - if (game->main_interface.settings.doing_custom_settings) - newFocusString += "/CustomSettings"; - else - newFocusString += "/Default"; - + if (game->main_interface.settings.current_mode == df::settings_tab_type::DIFFICULTY) { + if (game->main_interface.settings.doing_custom_settings) + newFocusString += "/CustomSettings"; + else + newFocusString += "/Default"; + } focusStrings.push_back(newFocusString); } if (game->main_interface.adventure.aim_projectile.open) { From 974e5ddc1f3d696278b18fa73d689544504ba8c3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:01:35 -0800 Subject: [PATCH 0023/1082] combine into one file --- plugins/spectate/CMakeLists.txt | 3 +- plugins/spectate/pause.cpp | 187 ------------------------ plugins/spectate/pause.h | 76 ---------- plugins/spectate/spectate.cpp | 242 +++++++++++++++++++++++++++++++- 4 files changed, 241 insertions(+), 267 deletions(-) delete mode 100644 plugins/spectate/pause.cpp delete mode 100644 plugins/spectate/pause.h diff --git a/plugins/spectate/CMakeLists.txt b/plugins/spectate/CMakeLists.txt index d2de072d8b..3f205171dc 100644 --- a/plugins/spectate/CMakeLists.txt +++ b/plugins/spectate/CMakeLists.txt @@ -2,7 +2,6 @@ project(spectate) SET(SOURCES - spectate.cpp - pause.cpp) + spectate.cpp) dfhack_plugin(${PROJECT_NAME} ${SOURCES}) diff --git a/plugins/spectate/pause.cpp b/plugins/spectate/pause.cpp deleted file mode 100644 index f94b1e73a9..0000000000 --- a/plugins/spectate/pause.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "pause.h" -#include -#include -#include -#include -#include -#include - -#include - -using namespace DFHack; -using namespace Pausing; -using namespace df::enums; - -// marked by REQUIRE_GLOBAL in spectate.cpp -using df::global::plotinfo; -using df::global::d_init; - -std::unordered_set PlayerLock::locks; -std::unordered_set AnnouncementLock::locks; - -namespace pausing { - AnnouncementLock announcementLock("monitor"); - PlayerLock playerLock("monitor"); - - const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); - bool state_saved = false; // indicates whether a restore state is ok - bool saved_states[announcement_flag_arr_size]; // state to restore - bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) - bool allow_player_pause = true; // toggles player pause ability - - using namespace df::enums; - struct player_pause_hook : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { - if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { - input->erase(interface_key::D_PAUSE); - } - INTERPOSE_NEXT(feed)(input); - } - }; - - IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); -} -using namespace pausing; - -template -inline bool any_lock(Locks locks) { - return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); -} - -template -inline bool only_lock(Locks locks, LockT* this_lock) { - return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { - if (lock == this_lock) { - return lock->isLocked(); - } - return !lock->isLocked(); - }); -} - -template -inline bool only_or_none_locked(Locks locks, LockT* this_lock) { - for (auto &L: locks) { - if (L == this_lock) { - continue; - } - if (L->isLocked()) { - return false; - } - } - return true; -} - -template -inline bool reportLockedLocks(color_ostream &out, Locks locks) { - out.color(DFHack::COLOR_YELLOW); - for (auto &L: locks) { - if (L->isLocked()) { - out.print("Lock: '%s'\n", L->name.c_str()); - } - } - out.reset_color(); - return true; -} - -bool AnnouncementLock::captureState() { - if (only_or_none_locked(locks, this)) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - return true; - } - return false; -} - -void AnnouncementLock::lock() { - Lock::lock(); - captureState(); -} - -bool AnnouncementLock::isAnyLocked() const { - return any_lock(locks); -} - -bool AnnouncementLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void AnnouncementLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} - -bool PlayerLock::isAnyLocked() const { - return any_lock(locks); -} - -bool PlayerLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void PlayerLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} - -bool World::DisableAnnouncementPausing() { - if (!announcementLock.isAnyLocked()) { - for (auto& flag : d_init->announcements.flags) { - flag.bits.PAUSE = false; - //out.print("pause: %d\n", flag.bits.PAUSE); - } - return true; - } - return false; -} - -bool World::SaveAnnouncementSettings() { - if (!announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - state_saved = true; - return true; - } - return false; -} - -bool World::RestoreAnnouncementSettings() { - if (!announcementLock.isAnyLocked() && state_saved) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; - } - return true; - } - return false; -} - -bool World::EnablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = true; - } - return allow_player_pause; -} - -bool World::DisablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = false; - } - return !allow_player_pause; -} - -bool World::IsPlayerPausingEnabled() { - return allow_player_pause; -} - -void World::Update() { - static bool did_once = false; - if (!did_once) { - did_once = true; - INTERPOSE_HOOK(player_pause_hook, feed).apply(); - } - if (announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; - } - } -} diff --git a/plugins/spectate/pause.h b/plugins/spectate/pause.h deleted file mode 100644 index ab736ed531..0000000000 --- a/plugins/spectate/pause.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once -#include -#include -#include - -namespace DFHack { - //////////// - // Locking mechanisms for control over pausing - namespace Pausing - { - class Lock - { - bool locked = false; - public: - const std::string name; - explicit Lock(const char* name) : name(name){} - virtual ~Lock()= default; - virtual bool isAnyLocked() const = 0; - virtual bool isOnlyLocked() const = 0; - bool isLocked() const { return locked; } - virtual void lock() { locked = true; } //simply locks the lock - void unlock() { locked = false; } - virtual void reportLocks(color_ostream &out) = 0; - }; - - // non-blocking lock resource used in conjunction with the announcement functions in World - class AnnouncementLock : public Lock - { - static std::unordered_set locks; - public: - explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } - ~AnnouncementLock() override { locks.erase(this); } - bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) - void lock() override; // locks and attempts to capture state - bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the Player pause functions in World - class PlayerLock : public Lock - { - static std::unordered_set locks; - public: - explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } - ~PlayerLock() override { locks.erase(this); } - bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the pause set state function in World -// todo: integrate with World::SetPauseState -// class PauseStateLock : public Lock -// { -// static std::unordered_set locks; -// public: -// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } -// ~PauseStateLock() override { locks.erase(this); } -// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked -// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked -// void reportLocks(color_ostream &out) override; -// }; - } - namespace World { - bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open - bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open - bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) - - bool EnablePlayerPausing(); // enable player pausing if all locks are open - bool DisablePlayerPausing(); // disable player pausing if all locks are open - bool IsPlayerPausingEnabled(); // returns whether the player can pause or not - - void Update(); - } -} diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 36bd2d94b2..29ec27718f 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -1,5 +1,3 @@ -#include "pause.h" - #include "Debug.h" #include "Export.h" #include "PluginManager.h" @@ -40,6 +38,246 @@ using namespace DFHack; using namespace Pausing; using namespace df::enums; +//////////// +// Locking mechanisms for control over pausing +namespace Pausing +{ + class Lock + { + bool locked = false; + public: + const std::string name; + explicit Lock(const char* name) : name(name){} + virtual ~Lock()= default; + virtual bool isAnyLocked() const = 0; + virtual bool isOnlyLocked() const = 0; + bool isLocked() const { return locked; } + virtual void lock() { locked = true; } //simply locks the lock + void unlock() { locked = false; } + virtual void reportLocks(color_ostream &out) = 0; + }; + + // non-blocking lock resource used in conjunction with the announcement functions in World + class AnnouncementLock : public Lock + { + static std::unordered_set locks; + public: + explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } + ~AnnouncementLock() override { locks.erase(this); } + bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) + void lock() override; // locks and attempts to capture state + bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked + bool isOnlyLocked() const override; // returns true if locked and no other instance is locked + void reportLocks(color_ostream &out) override; + }; + + // non-blocking lock resource used in conjunction with the Player pause functions in World + class PlayerLock : public Lock + { + static std::unordered_set locks; + public: + explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } + ~PlayerLock() override { locks.erase(this); } + bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked + bool isOnlyLocked() const override; // returns true if locked and no other instance is locked + void reportLocks(color_ostream &out) override; + }; + + // non-blocking lock resource used in conjunction with the pause set state function in World +// todo: integrate with World::SetPauseState +// class PauseStateLock : public Lock +// { +// static std::unordered_set locks; +// public: +// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } +// ~PauseStateLock() override { locks.erase(this); } +// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked +// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked +// void reportLocks(color_ostream &out) override; +// }; +} +namespace World { + bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open + bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open + bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) + + bool EnablePlayerPausing(); // enable player pausing if all locks are open + bool DisablePlayerPausing(); // disable player pausing if all locks are open + bool IsPlayerPausingEnabled(); // returns whether the player can pause or not + + void Update(); +} + +std::unordered_set PlayerLock::locks; +std::unordered_set AnnouncementLock::locks; + +namespace pausing { + AnnouncementLock announcementLock("monitor"); + PlayerLock playerLock("monitor"); + + const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); + bool state_saved = false; // indicates whether a restore state is ok + bool saved_states[announcement_flag_arr_size]; // state to restore + bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) + bool allow_player_pause = true; // toggles player pause ability + + using namespace df::enums; + struct player_pause_hook : df::viewscreen_dwarfmodest { + typedef df::viewscreen_dwarfmodest interpose_base; + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { + if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { + input->erase(interface_key::D_PAUSE); + } + INTERPOSE_NEXT(feed)(input); + } + }; + + IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); +} +using namespace pausing; + +template +inline bool any_lock(Locks locks) { + return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +} + +template +inline bool only_lock(Locks locks, LockT* this_lock) { + return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { + if (lock == this_lock) { + return lock->isLocked(); + } + return !lock->isLocked(); + }); +} + +template +inline bool only_or_none_locked(Locks locks, LockT* this_lock) { + for (auto &L: locks) { + if (L == this_lock) { + continue; + } + if (L->isLocked()) { + return false; + } + } + return true; +} + +template +inline bool reportLockedLocks(color_ostream &out, Locks locks) { + out.color(DFHack::COLOR_YELLOW); + for (auto &L: locks) { + if (L->isLocked()) { + out.print("Lock: '%s'\n", L->name.c_str()); + } + } + out.reset_color(); + return true; +} + +bool AnnouncementLock::captureState() { + if (only_or_none_locked(locks, this)) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; + } + return true; + } + return false; +} + +void AnnouncementLock::lock() { + Lock::lock(); + captureState(); +} + +bool AnnouncementLock::isAnyLocked() const { + return any_lock(locks); +} + +bool AnnouncementLock::isOnlyLocked() const { + return only_lock(locks, this); +} + +void AnnouncementLock::reportLocks(color_ostream &out) { + reportLockedLocks(out, locks); +} + +bool PlayerLock::isAnyLocked() const { + return any_lock(locks); +} + +bool PlayerLock::isOnlyLocked() const { + return only_lock(locks, this); +} + +void PlayerLock::reportLocks(color_ostream &out) { + reportLockedLocks(out, locks); +} + +bool World::DisableAnnouncementPausing() { + if (!announcementLock.isAnyLocked()) { + for (auto& flag : d_init->announcements.flags) { + flag.bits.PAUSE = false; + //out.print("pause: %d\n", flag.bits.PAUSE); + } + return true; + } + return false; +} + +bool World::SaveAnnouncementSettings() { + if (!announcementLock.isAnyLocked()) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; + } + state_saved = true; + return true; + } + return false; +} + +bool World::RestoreAnnouncementSettings() { + if (!announcementLock.isAnyLocked() && state_saved) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; + } + return true; + } + return false; +} + +bool World::EnablePlayerPausing() { + if (!playerLock.isAnyLocked()) { + allow_player_pause = true; + } + return allow_player_pause; +} + +bool World::DisablePlayerPausing() { + if (!playerLock.isAnyLocked()) { + allow_player_pause = false; + } + return !allow_player_pause; +} + +bool World::IsPlayerPausingEnabled() { + return allow_player_pause; +} + +void World::Update() { + static bool did_once = false; + if (!did_once) { + did_once = true; + INTERPOSE_HOOK(player_pause_hook, feed).apply(); + } + if (announcementLock.isAnyLocked()) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; + } + } +} + struct Configuration { bool unpause = false; bool disengage = false; From 1fc6737c1ecb701d90ecdec7aecfabe2f727e7a7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:02:32 -0800 Subject: [PATCH 0024/1082] move spectate to main plugins dir --- plugins/CMakeLists.txt | 2 +- plugins/{spectate => }/spectate.cpp | 0 plugins/spectate/CMakeLists.txt | 7 ------- 3 files changed, 1 insertion(+), 8 deletions(-) rename plugins/{spectate => }/spectate.cpp (100%) delete mode 100644 plugins/spectate/CMakeLists.txt diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ff71159e5e..8f230ff583 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -111,7 +111,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) #dfhack_plugin(steam-engine steam-engine.cpp) - add_subdirectory(spectate) + dfhack_plugin(spectate spectate.cpp LINK_LIBRARIES lua) #dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) add_subdirectory(stockpiles) dfhack_plugin(stocks stocks.cpp LINK_LIBRARIES lua) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate.cpp similarity index 100% rename from plugins/spectate/spectate.cpp rename to plugins/spectate.cpp diff --git a/plugins/spectate/CMakeLists.txt b/plugins/spectate/CMakeLists.txt deleted file mode 100644 index 3f205171dc..0000000000 --- a/plugins/spectate/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ - -project(spectate) - -SET(SOURCES - spectate.cpp) - -dfhack_plugin(${PROJECT_NAME} ${SOURCES}) From 2c27dc6b48753a1cf599d829d4c629138de8b65c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:03:12 -0800 Subject: [PATCH 0025/1082] update spectate docs to reflect update plan --- docs/plugins/spectate.rst | 152 +++++++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 36 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 1e400a06e4..cd7a8b96c5 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -2,8 +2,31 @@ spectate ======== .. dfhack-tool:: - :summary: Automatically follow productive dwarves. - :tags: fort interface + :summary: Automated spectator mode. + :tags: fort inspection interface + +This tool is for those who like to watch their dwarves go about their business. + +When enabled, `spectate` will lock the camera to following the dwarves +scurrying around your fort. Every once in a while, it will automatically switch +to following a different dwarf. It can also switch to following animals, +hostiles, or visiting units. You can switch to the next target (or a previous +target) immediately with the left/right arrow keys. + +`spectate` will disengage and turn itself off when you move the map, just like +the vanilla follow mechanic. It will also disengage immediately if you open the +squads menu for military action. + +It can also annotate your dwarves on the map with their name, job, and other +information, either as floating tooltips or in a panel that comes up when you +hover the mouse over a target. + +Run `gui/spectate` to configure the plugin's settings. + +Settings are saved globally, so your preferences for `spectate` and its +overlays will apply to all forts, not just the currently loaded one. Follow +mode is automatically disabled when you load a fort so you can get your +bearings before re-enabling. Usage ----- @@ -11,52 +34,109 @@ Usage :: enable spectate - spectate + spectate [status] spectate set - spectate enable|disable - -When enabled, the plugin will lock the camera to following the dwarves -scurrying around your fort. Every once in a while, it will automatically switch -to following a different dwarf, preferring dwarves on z-levels with the highest -job activity. - -If you have the ``auto-disengage`` feature disabled, you can switch to a new -dwarf immediately by hitting one of the map movement keys (``wasd`` by -default). To stop following dwarves, bring up `gui/launcher` and run -``disable spectate``. - -Changes to settings will be saved with your fort, but if `spectate` is enabled -when you save the fort, it will disenable itself when you load so you can get -your bearings before re-enabling follow mode with ``enable spectate`` again. + spectate overlay enable|disable Examples -------- ``enable spectate`` - Starting following dwarves and observing life in your fort. + Start following dwarves and observing life in your fort. ``spectate`` The plugin reports its configured status. -``spectate enable auto-unpause`` - Enable the spectate plugin to automatically dismiss pause events caused - by the game. Siege events are one example of such a game event. +``spectate set auto-unpause true`` + Configure `spectate` to automatically dismiss popups and pause events, like + siege announcements. -``spectate set tick-threshold 1000`` - Set the tick interval between camera changes back to its default value. +``spectate set follow-seconds 30`` + Configure `spectate` to switch targets every 30 seconds when in follow mode. -Features --------- -:auto-unpause: Toggle auto-dismissal of game pause events. (default: disabled) -:auto-disengage: Toggle auto-disengagement of plugin through player - intervention while unpaused. (default: disabled) -:animals: Toggle whether to sometimes follow animals. (default: disabled) -:hostiles: Toggle whether to sometimes follow hostiles (eg. undead, - titans, invaders, etc.) (default: disabled) -:visiting: Toggle whether to sometimes follow visiting units (eg. - diplomats) +``spectate overlay follow enable`` + Show informative tooltips that follow each unit on the map. Settings -------- -:tick-threshold: Set the plugin's tick interval for changing the followed - dwarf. (default: 1000) + +``auto-disengage`` (default: enabled) + Toggle automatically disabling the plugin when the player moves the map or + opens the squad panel. If this is disabled, you will need to manually + disable the plugin to turn off follow mode. + +``auto-unpause`` (default: disabled) + Toggle auto-dismissal of announcements that pause the game, like sieges, + forgotten beasts, etc. + +``cinematic-action`` (default: enabled) + Toggle whether to switch targets more rapidly when there is conflict. + +``follow-seconds`` (default: 10) + Set the time interval for changing the followed unit. + +``include-animals`` (default: disabled) + Toggle whether to sometimes follow fort animals. + +``include-hostiles`` (default: disabled) + Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, + etc.) + +``include-visiting`` (default: disabled) + Toggle whether to sometimes follow visiting units, like diplomats. + +``include-wildlife`` (default: disabled) + Toggle whether to sometimes follow wildlife. + +``prefer-conflict`` (default: enabled) + Toggle whether to prefer following units in active conflict. + +``prefer-new-arrivals`` (default: enabled) + Toggle whether to prefer following (non-siege) units that have newly + arrived on the map. + +``tooltip-follow-job`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + job of the dwarf in the tooltip. + +``tooltip-follow-name`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + name of the dwarf in the tooltip. + +``tooltip-follow-stress`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + happiness level (stress) of the dwarf in the tooltip. + +``tooltip-hover-job`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + job of the dwarf in the hover panel. + +``tooltip-hover-name`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + name of the dwarf in the hover panel. + +``tooltip-hover-stress`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + happiness level (stress) of the dwarf in the hover panel. + +Overlays +-------- + +``spectate`` provides two overlays via the `overlay` framework to add +information and functionality to the main map. These overlays can be controlled +via the ``spectate overlay`` command or the ``Overlays`` tab in +`gui/control-panel`. + +The information displayed by these overlays can be configured via the +``spectate set`` command or the `gui/spectate` interface. + +``spectate.follow`` + Show informative tooltips that follow each unit on the map. You can enable + this overlay by running ``spectate overlay follow enable`` or, + equivalently, ``overlay enable spectate.follow``. + +``spectate.hover`` + Show a popup panel with selected information when your mouse cursor hovers + over a unit. You can enable this overlay by running + ``spectate overlay hover enable`` or, equivalently, + ``overlay enable spectate.hover``. From a065980a1fc65ba0096f00a43ce771d04704b40c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 15:32:09 -0800 Subject: [PATCH 0026/1082] initial implementation of most of the new behavior --- docs/plugins/spectate.rst | 10 +- plugins/lua/spectate.lua | 149 ++++++ plugins/spectate.cpp | 933 +++++++++++++------------------------- 3 files changed, 461 insertions(+), 631 deletions(-) create mode 100644 plugins/lua/spectate.lua diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index cd7a8b96c5..f49849c850 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -35,6 +35,7 @@ Usage enable spectate spectate [status] + spectate toggle spectate set spectate overlay enable|disable @@ -44,6 +45,10 @@ Examples ``enable spectate`` Start following dwarves and observing life in your fort. +``spectate toggle`` + Toggle the plugin on or off. Intended for use with a keybinding. The + default is Ctrl-Shift-S. + ``spectate`` The plugin reports its configured status. @@ -73,7 +78,8 @@ Settings Toggle whether to switch targets more rapidly when there is conflict. ``follow-seconds`` (default: 10) - Set the time interval for changing the followed unit. + Set the time interval for changing the followed unit. The interval does not + include time that the game is paused. ``include-animals`` (default: disabled) Toggle whether to sometimes follow fort animals. @@ -82,7 +88,7 @@ Settings Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, etc.) -``include-visiting`` (default: disabled) +``include-visitors`` (default: disabled) Toggle whether to sometimes follow visiting units, like diplomats. ``include-wildlife`` (default: disabled) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua new file mode 100644 index 0000000000..ba7e9ba7ba --- /dev/null +++ b/plugins/lua/spectate.lua @@ -0,0 +1,149 @@ +local _ENV = mkmodule('plugins.spectate') + +local argparse = require('argparse') +local json = require('json') +local overlay = require('plugins.overlay') +local utils = require('utils') + +-- settings starting with 'tooltip-' are not passed to the C++ plugin +local lua_only_settings_prefix = 'tooltip-' + +local function get_default_state() + return { + ['auto-disengage']=true, + ['auto-unpause']=false, + ['cinematic-action']=true, + ['follow-seconds']=10, + ['include-animals']=false, + ['include-hostiles']=false, + ['include-visitors']=false, + ['include-wildlife']=false, + ['prefer-conflict']=true, + ['prefer-new-arrivals']=true, + ['tooltip-follow-job']=true, + ['tooltip-follow-name']=true, + ['tooltip-follow-stress']=true, + ['tooltip-hover-job']=true, + ['tooltip-hover-name']=true, + ['tooltip-hover-stress']=true, + } +end + +local function load_state() + local state = get_default_state() + local config = json.open('dfhack-config/spectate.json') + for key in pairs(config.data) do + if state[key] == nil then + config.data[key] = nil + end + end + utils.assign(state, config.data) + config.data = state + return config +end + +local config = load_state() + +function refresh_cpp_config() + for name,value in pairs(config.data) do + if not name:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(name, value) + end + end +end + +----------------------------- +-- commandline interface + +local function print_status() + print('spectate is:', isEnabled() and 'enabled' or 'disabled') + print() + print('settings:') + for key, value in pairs(config.data) do + print(' ' .. key .. ': ' .. tostring(value)) + end +end + +local function do_toggle() + if isEnabled() then + dfhack.run_command('disable', 'spectate') + else + dfhack.run_command('enable', 'spectate') + end +end + +local function set_setting(key, value) + if config.data[key] == nil then + qerror('unknown setting: ' .. key) + end + if key == 'follow-seconds' then + value = argparse.positiveInt(value, 'follow-seconds') + else + value = argparse.boolean(value, key) + end + config.data[key] = value + config:write() + if not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(key, value) + end +end + +local function set_overlay(name, value) + if not name:startswith('spectate.') then + name = 'spectate.' .. name + end + if name ~= 'spectate.follow' and name ~= 'spectate.hover' then + qerror('unknown overlay: ' .. name) + end + value = argparse.boolean(value, name) + dfhack.run_command('overlay', value and 'enable' or 'disable', name) +end + +function parse_commandline(args) + local command = table.remove(args, 1) + if not command or command == 'status' then + print_status() + elseif command == 'toggle' then + do_toggle() + elseif command == 'set' then + set_setting(args[1], args[2]) + elseif command == 'overlay' then + set_overlay(args[1], args[2]) + else + return false + end + + return true +end + +----------------------------- +-- overlays + +FollowOverlay = defclass(FollowOverlay, overlay.OverlayWidget) +FollowOverlay.ATTRS{ + desc='Adds info tooltips that follow units on the map.', + default_pos={x=1,y=1}, + fullscreen=true, + viewscreens='dwarfmode/Default', +} + +HoverOverlay = defclass(HoverOverlay, overlay.OverlayWidget) +HoverOverlay.ATTRS{ + desc='Shows info popup when hovering the mouse over units on the map.', + default_pos={x=1,y=1}, + fullscreen=true, + viewscreens='dwarfmode/Default', +} + +OVERLAY_WIDGETS = { + follow=FollowOverlay, + hover=HoverOverlay, +} + +return _ENV diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 29ec27718f..ae33031b14 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -1,716 +1,391 @@ #include "Debug.h" -#include "Export.h" +#include "LuaTools.h" +#include "PluginLua.h" #include "PluginManager.h" -#include "modules/EventManager.h" -#include "modules/World.h" -#include "modules/Maps.h" #include "modules/Gui.h" -#include "modules/Job.h" #include "modules/Units.h" +#include "modules/World.h" -#include "df/job.h" -#include "df/unit.h" -#include "df/historical_figure.h" -#include "df/global_objects.h" +#include "df/announcements.h" +#include "df/d_init.h" #include "df/plotinfost.h" +#include "df/unit.h" #include "df/world.h" -#include "df/viewscreen.h" -#include "df/creature_raw.h" -#include #include -#include - -// Debugging -namespace DFHack { - DBG_DECLARE(log, plugin, DebugCategory::LINFO); -} - -DFHACK_PLUGIN("spectate"); -DFHACK_PLUGIN_IS_ENABLED(enabled); - -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(d_init); // used in pause.cpp using namespace DFHack; -using namespace Pausing; -using namespace df::enums; -//////////// -// Locking mechanisms for control over pausing -namespace Pausing -{ - class Lock - { - bool locked = false; - public: - const std::string name; - explicit Lock(const char* name) : name(name){} - virtual ~Lock()= default; - virtual bool isAnyLocked() const = 0; - virtual bool isOnlyLocked() const = 0; - bool isLocked() const { return locked; } - virtual void lock() { locked = true; } //simply locks the lock - void unlock() { locked = false; } - virtual void reportLocks(color_ostream &out) = 0; - }; - - // non-blocking lock resource used in conjunction with the announcement functions in World - class AnnouncementLock : public Lock - { - static std::unordered_set locks; - public: - explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } - ~AnnouncementLock() override { locks.erase(this); } - bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) - void lock() override; // locks and attempts to capture state - bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the Player pause functions in World - class PlayerLock : public Lock - { - static std::unordered_set locks; - public: - explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } - ~PlayerLock() override { locks.erase(this); } - bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; +using std::string; +using std::vector; - // non-blocking lock resource used in conjunction with the pause set state function in World -// todo: integrate with World::SetPauseState -// class PauseStateLock : public Lock -// { -// static std::unordered_set locks; -// public: -// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } -// ~PauseStateLock() override { locks.erase(this); } -// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked -// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked -// void reportLocks(color_ostream &out) override; -// }; -} -namespace World { - bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open - bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open - bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) +DFHACK_PLUGIN("spectate"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); - bool EnablePlayerPausing(); // enable player pausing if all locks are open - bool DisablePlayerPausing(); // disable player pausing if all locks are open - bool IsPlayerPausingEnabled(); // returns whether the player can pause or not +REQUIRE_GLOBAL(d_init); +REQUIRE_GLOBAL(plotinfo); +REQUIRE_GLOBAL(world); - void Update(); +namespace DFHack { + DBG_DECLARE(spectate, control, DebugCategory::LINFO); + DBG_DECLARE(spectate, cycle, DebugCategory::LINFO); } -std::unordered_set PlayerLock::locks; -std::unordered_set AnnouncementLock::locks; - -namespace pausing { - AnnouncementLock announcementLock("monitor"); - PlayerLock playerLock("monitor"); - - const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); - bool state_saved = false; // indicates whether a restore state is ok - bool saved_states[announcement_flag_arr_size]; // state to restore - bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) - bool allow_player_pause = true; // toggles player pause ability - - using namespace df::enums; - struct player_pause_hook : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { - if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { - input->erase(interface_key::D_PAUSE); - } - INTERPOSE_NEXT(feed)(input); - } - }; +static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle +static bool was_in_settings = false; // whether we were in the vanilla settings screen last update - IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); -} -using namespace pausing; +static const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); +static std::unique_ptr saved_announcement_settings; -template -inline bool any_lock(Locks locks) { - return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +static void save_announcement_settings(color_ostream &out) { + if (!saved_announcement_settings) + saved_announcement_settings = std::make_unique(new uint32_t[announcement_flag_arr_size]); + DEBUG(control,out).print("saving announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + (*saved_announcement_settings)[i] = d_init->announcements.flags[i].whole; } -template -inline bool only_lock(Locks locks, LockT* this_lock) { - return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { - if (lock == this_lock) { - return lock->isLocked(); - } - return !lock->isLocked(); - }); +static void restore_announcement_settings(color_ostream &out) { + if (!saved_announcement_settings) + return; + DEBUG(control,out).print("restoring saved announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + d_init->announcements.flags[i].whole = (*saved_announcement_settings)[i]; } -template -inline bool only_or_none_locked(Locks locks, LockT* this_lock) { - for (auto &L: locks) { - if (L == this_lock) { - continue; - } - if (L->isLocked()) { - return false; - } +static void scrub_announcements(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); + return; } - return true; -} -template -inline bool reportLockedLocks(color_ostream &out, Locks locks) { - out.color(DFHack::COLOR_YELLOW); - for (auto &L: locks) { - if (L->isLocked()) { - out.print("Lock: '%s'\n", L->name.c_str()); - } + DEBUG(control,out).print("removing PAUSE from announcement settings\n"); + for (auto& flag : d_init->announcements.flags) { + flag.bits.DO_MEGA = false; + flag.bits.PAUSE = false; + flag.bits.RECENTER = false; } - out.reset_color(); - return true; } -bool AnnouncementLock::captureState() { - if (only_or_none_locked(locks, this)) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - return true; +struct Configuration { + bool auto_disengage; + bool auto_unpause; + bool cinematic_action; + bool include_animals; + bool include_hostiles; + bool include_visitors; + bool include_wildlife; + bool prefer_conflict; + bool prefer_new_arrivals; + int32_t follow_ms; + + void reset() { + auto_disengage = true; + auto_unpause = false; + cinematic_action = true; + include_animals = false; + include_hostiles = false; + include_visitors = false; + include_wildlife = false; + prefer_conflict = true; + prefer_new_arrivals = true; + follow_ms = 10000; } - return false; -} +} config; -void AnnouncementLock::lock() { - Lock::lock(); - captureState(); -} +static command_result do_command(color_ostream &out, vector ¶meters); +static void follow_a_dwarf(color_ostream &out); -bool AnnouncementLock::isAnyLocked() const { - return any_lock(locks); -} +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(control,out).print("initializing %s\n", plugin_name); -bool AnnouncementLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void AnnouncementLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} + commands.push_back(PluginCommand( + plugin_name, + "Automated spectator mode.", + do_command)); -bool PlayerLock::isAnyLocked() const { - return any_lock(locks); + return CR_OK; } -bool PlayerLock::isOnlyLocked() const { - return only_lock(locks, this); +static void cleanup(color_ostream &out) { + if (saved_announcement_settings) { + restore_announcement_settings(out); + delete[] *saved_announcement_settings; + saved_announcement_settings.reset(); + } } -void PlayerLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } -bool World::DisableAnnouncementPausing() { - if (!announcementLock.isAnyLocked()) { - for (auto& flag : d_init->announcements.flags) { - flag.bits.PAUSE = false; - //out.print("pause: %d\n", flag.bits.PAUSE); + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(control,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + if (enable) { + INFO(control,out).print("Spectate mode enabled!\n"); + config.reset(); + if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "refresh_cpp_config")) { + WARN(control,out).print("Failed to refresh config\n"); + } + follow_a_dwarf(out); + } else { + INFO(control,out).print("Spectate mode disabled!\n"); + plotinfo->follow_unit = -1; + cleanup(out); } - return true; + } else { + DEBUG(control,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); } - return false; + return CR_OK; } -bool World::SaveAnnouncementSettings() { - if (!announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - state_saved = true; - return true; - } - return false; +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(control,out).print("shutting down %s\n", plugin_name); + cleanup(out); + return CR_OK; } -bool World::RestoreAnnouncementSettings() { - if (!announcementLock.isAnyLocked() && state_saved) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + switch (event) { + case SC_WORLD_LOADED: + next_cycle_unpaused_ms = 0; + break; + case SC_WORLD_UNLOADED: + if (is_enabled) { + DEBUG(control,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + cleanup(out); } - return true; + break; + default: + break; } - return false; + return CR_OK; } -bool World::EnablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = true; +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + if (!was_in_settings) { + DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); + restore_announcement_settings(out); + was_in_settings = true; + } + } else if (was_in_settings) { + was_in_settings = false; + if (config.auto_unpause) { + DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); + save_announcement_settings(out); + scrub_announcements(out); + } } - return allow_player_pause; -} -bool World::DisablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = false; + if (config.auto_disengage && plotinfo->follow_unit < 0) { + DEBUG(cycle,out).print("auto-disengage triggered\n"); + is_enabled = false; + cleanup(out); + return CR_OK; } - return !allow_player_pause; -} -bool World::IsPlayerPausingEnabled() { - return allow_player_pause; + if ((!config.auto_disengage && plotinfo->follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) + follow_a_dwarf(out); + return CR_OK; } -void World::Update() { - static bool did_once = false; - if (!did_once) { - did_once = true; - INTERPOSE_HOOK(player_pause_hook, feed).apply(); - } - if (announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; - } +static command_result do_command(color_ostream &out, vector ¶meters) { + bool show_help = false; + if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "parse_commandline", std::make_tuple(parameters), + 1, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } -} - -struct Configuration { - bool unpause = false; - bool disengage = false; - bool animals = false; - bool hostiles = true; - bool visitors = false; - int32_t tick_threshold = 1000; -} config; - -Pausing::AnnouncementLock* pause_lock = nullptr; -bool lock_collision = false; -bool announcements_disabled = false; - -#define base 0.99 -static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; -enum ConfigData { - UNPAUSE, - DISENGAGE, - TICK_THRESHOLD, - ANIMALS, - HOSTILES, - VISITORS -}; + return show_help ? CR_WRONG_USAGE : CR_OK; +} -static PersistentDataItem pconfig; - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); -command_result spectate (color_ostream &out, std::vector & parameters); -#define COORDARGS(id) id.x, id.y, id.z - -namespace SP { - bool following_dwarf = false; - df::unit* our_dorf = nullptr; - int32_t timestamp = -1; - std::default_random_engine RNG; - - void DebugUnitVector(std::vector units) { - if (debug_plugin.isEnabled(DFHack::DebugCategory::LDEBUG)) { - for (auto unit: units) { - DEBUG(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", - unit->id, - Units::isAnimal(unit), - Units::isDanger(unit), - Units::isVisiting(unit)); - } - } - } +///////////////////////////////////////////////////// +// cycle logic +// - void PrintStatus(color_ostream &out) { - out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); - out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off."); - out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off."); - out.print(" %-20s\t%s\n", "animals: ", config.animals ? "on." : "off."); - out.print(" %-20s\t%s\n", "hostiles: ", config.hostiles ? "on." : "off."); - out.print(" %-20s\t%s\n", "visiting: ", config.visitors ? "on." : "off."); - out.print(" SETTINGS:\n"); - out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); - if (following_dwarf) - out.print(" %-21s\t%s[id: %d]\n","FOLLOWING:", our_dorf ? our_dorf->name.first_name.c_str() : "nullptr", plotinfo->follow_unit); - } +static bool is_in_combat(df::unit *unit) { + return false; +} - void SetUnpauseState(bool state) { - // we don't need to do any of this yet if the plugin isn't enabled - if (enabled) { - // todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings] - // lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete - // The onupdate function above ensure the procedure properly completes, thus we only care about - // state reversal here ergo `enabled != state` - if (lock_collision && config.unpause != state) { - WARN(plugin).print("Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own.\n"); - // if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings, - // therefore nothing to revert and the lock won't even be engaged (nothing to unlock) - lock_collision = false; - config.unpause = state; - if (config.unpause) { - // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock - pause_lock->lock(); - } - return; - } - // update the announcement settings if we can - if (state) { - if (World::SaveAnnouncementSettings()) { - World::DisableAnnouncementPausing(); - announcements_disabled = true; - pause_lock->lock(); - } else { - WARN(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); - lock_collision = true; - } - } else { - pause_lock->unlock(); - if (announcements_disabled) { - if (!World::RestoreAnnouncementSettings()) { - // this in theory shouldn't happen, if others use the lock like we do in spectate - WARN(plugin).print("Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own.\n"); - lock_collision = true; - } else { - announcements_disabled = false; - } - } - } - if (lock_collision) { - ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); - WARN(plugin).print( - " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" - " The action you were attempting will complete when the following lock or locks lift.\n"); - pause_lock->reportLocks(Core::getInstance().getConsole()); - } - } - config.unpause = state; - } +static bool is_fleeing(df::unit *unit) { + return false; +} - void SaveSettings() { - if (pconfig.isValid()) { - pconfig.ival(UNPAUSE) = config.unpause; - pconfig.ival(DISENGAGE) = config.disengage; - pconfig.ival(TICK_THRESHOLD) = config.tick_threshold; - pconfig.ival(ANIMALS) = config.animals; - pconfig.ival(HOSTILES) = config.hostiles; - pconfig.ival(VISITORS) = config.visitors; - } - } +static void get_dwarf_buckets(color_ostream &out, + vector &active_combat_units, + vector &passive_combat_units, + vector &job_units, + vector &other_units) +{ + static const std::unordered_set boring_jobs = { + df::job_type::Eat, + df::job_type::Drink, + df::job_type::Sleep, + }; - void LoadSettings() { - pconfig = World::GetPersistentSiteData(CONFIG_KEY); + for (auto unit : world->units.active) { + if (Units::isDead(unit) || !Units::isActive(unit) || unit->flags1.bits.caged || unit->flags1.bits.chained || Units::isHidden(unit)) + continue; + if (!config.include_animals && Units::isAnimal(unit)) + continue; + if (!config.include_hostiles && Units::isDanger(unit)) + continue; + if (!config.include_visitors && Units::isVisitor(unit)) + continue; + if (!config.include_wildlife && Units::isWildlife(unit)) + continue; - if (!pconfig.isValid()) { - pconfig = World::AddPersistentSiteData(CONFIG_KEY); - SaveSettings(); + if (is_in_combat(unit)) { + if (is_fleeing(unit)) + passive_combat_units.push_back(unit); + else + active_combat_units.push_back(unit); + } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { + job_units.push_back(unit); } else { - config.unpause = pconfig.ival(UNPAUSE); - config.disengage = pconfig.ival(DISENGAGE); - config.tick_threshold = pconfig.ival(TICK_THRESHOLD); - config.animals = pconfig.ival(ANIMALS); - config.hostiles = pconfig.ival(HOSTILES); - config.visitors = pconfig.ival(VISITORS); - pause_lock->unlock(); - SetUnpauseState(config.unpause); + other_units.push_back(unit); } } +} - bool FollowADwarf() { - if (enabled && !World::ReadPauseState()) { - df::coord viewMin = Gui::getViewportPos(); - df::coord viewMax{viewMin}; - const auto &dims = Gui::getDwarfmodeViewDims().map().second; - viewMax.x += dims.x - 1; - viewMax.y += dims.y - 1; - viewMax.z = viewMin.z; - std::vector units; - static auto add_if = [&](std::function check) { - for (auto unit : world->units.active) { - if (check(unit)) { - units.push_back(unit); - } - } - }; - static auto valid = [](df::unit* unit) { - if (Units::isAnimal(unit)) { - return config.animals; - } - if (Units::isVisiting(unit)) { - return config.visitors; - } - if (Units::isDanger(unit)) { - return config.hostiles; - } - return true; - }; - static auto calc_extra_weight = [](size_t idx, double r1, double r2) { - switch(idx) { - case 0: - return r2; - case 1: - return (r2-r1)/1.3; - case 2: - return (r2-r1)/2; - default: - return 0.0; - } - }; - /// Collecting our choice pool - /////////////////////////////// - std::array ranges{}; - std::array range_exists{}; - static auto build_range = [&](size_t idx){ - size_t first = idx * 2; - size_t second = idx * 2 + 1; - size_t previous = first - 1; - // first we get the end of the range - ranges[second] = units.size() - 1; - // then we calculate whether the range exists, and set the first index appropriately - if (idx == 0) { - range_exists[idx] = ranges[second] >= 0; - ranges[first] = 0; - } else { - range_exists[idx] = ranges[second] > ranges[previous]; - ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); - } - }; - - /// RANGE 0 (in view + working) - // grab valid working units - add_if([&](df::unit* unit) { - return valid(unit) && - Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)) && - Units::isCitizen(unit, true) && - unit->job.current_job; - }); - build_range(0); - - /// RANGE 1 (in view) - add_if([&](df::unit* unit) { - return valid(unit) && Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)); - }); - build_range(1); - - /// RANGE 2 (working citizens) - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; - }); - build_range(2); - - /// RANGE 3 (citizens) - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true); - }); - build_range(3); - - /// RANGE 4 (any valid) - add_if(valid); - build_range(4); - - // selecting from our choice pool - if (!units.empty()) { - std::array bw{23,17,13,7,1}; // probability weights for each range - std::vector i; - std::vector w; - bool at_least_one = false; - // in one word, elegance - for(size_t idx = 0; idx < range_exists.size(); ++idx) { - if (range_exists[idx]) { - at_least_one = true; - const auto &r1 = ranges[idx*2]; - const auto &r2 = ranges[idx*2+1]; - double extra = calc_extra_weight(idx, r1, r2); - i.push_back(r1); - w.push_back(bw[idx] + extra); - if (r1 != r2) { - i.push_back(r2); - w.push_back(bw[idx] + extra); - } - } - } - if (!at_least_one) { - return false; - } - DebugUnitVector(units); - std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); - // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) - size_t idx = follow_any(RNG); - our_dorf = units[idx]; - plotinfo->follow_unit = our_dorf->id; - timestamp = world->frame_counter; - return true; - } else { - WARN(plugin).print("units vector is empty!\n"); - } - } - return false; - } - - void onUpdate(color_ostream &out) { - // keeps announcement pause settings locked - World::Update(); // from pause.h - - // Plugin Management - if (lock_collision) { - if (config.unpause) { - // player asked for auto-unpause enabled - World::SaveAnnouncementSettings(); - if (World::DisableAnnouncementPausing()) { - // now that we've got what we want, we can lock it down - lock_collision = false; - } - } else { - if (World::RestoreAnnouncementSettings()) { - lock_collision = false; - } - } - } - int failsafe = 0; - while (config.unpause && !world->status.popups.empty() && ++failsafe <= 10) { - // dismiss announcement popup(s) - Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT); - if (World::ReadPauseState()) { - // WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is - World::SetPauseState(false); - } - } - if (failsafe >= 10) { - out.printerr("spectate encountered a problem dismissing a popup!\n"); - } +static std::default_random_engine rng; - // plugin logic - static int32_t last_tick = -1; - int32_t tick = world->frame_counter; - if (!World::ReadPauseState() && tick - last_tick >= 1) { - last_tick = tick; - // validate follow state - if (!following_dwarf || !our_dorf || plotinfo->follow_unit < 0 || tick - timestamp >= config.tick_threshold) { - // we're not following anyone - following_dwarf = false; - if (!config.disengage) { - // try to - following_dwarf = FollowADwarf(); - } else if (!World::ReadPauseState()) { - plugin_enable(out, false); - } - } - } +static uint32_t get_next_cycle_unpaused_ms(bool has_active_combat) { + int32_t delay_ms = config.follow_ms; + if (has_active_combat) { + std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); + int32_t delay_ms = distribution(rng); + delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } -}; - -DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { - commands.push_back(PluginCommand("spectate", - "Automated spectator mode.", - spectate, - false)); - pause_lock = new AnnouncementLock("spectate"); - return CR_OK; + return Core::getInstance().getUnpausedMs() + delay_ms; } -DFhackCExport command_result plugin_shutdown (color_ostream &out) { - delete pause_lock; - return CR_OK; +static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { + if (bucket.empty()) + return; + intervals.push_back(units.size() + bucket.size()); + weights.push_back(weight); + units.insert(units.end(), bucket.begin(), bucket.end()); } -DFhackCExport command_result plugin_load_site_data (color_ostream &out) { - SP::LoadSettings(); - if (enabled) { - SP::following_dwarf = SP::FollowADwarf(); - SP::PrintStatus(out); +#define DUMP_BUCKET(name) \ + DEBUG(cycle,out).print("bucket: " #name ", size: %zd\n", name.size()); \ + if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ + for (auto u : name) { \ + DEBUG(cycle,out).print(" unit %d: %s\n", u->id, DF2CONSOLE(Units::getReadableName(u)).c_str()); \ + } \ } - return DFHack::CR_OK; -} -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; +#define DUMP_FLOAT_VECTOR(name) \ + DEBUG(cycle,out).print(#name ":\n"); \ + for (float f : name) { \ + DEBUG(cycle,out).print(" %d\n", (int)f); \ } - if (enable && !enabled) { - out.print("Spectate mode enabled!\n"); - enabled = true; // enable_auto_unpause won't do anything without this set now - SP::SetUnpauseState(config.unpause); - } else if (!enable && enabled) { - // warp 8, engage! - out.print("Spectate mode disabled!\n"); - // we need to retain whether auto-unpause is enabled, but we also need to disable its effect - bool temp = config.unpause; - SP::SetUnpauseState(false); - config.unpause = temp; +static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; +static const float JOB_WEIGHT = 3.0f; +static const float OTHER_WEIGHT = 1.0f; + +static void follow_a_dwarf(color_ostream &out) { + DEBUG(cycle,out).print("choosing a unit to follow\n"); + + vector active_combat_units; + vector passive_combat_units; + vector job_units; + vector other_units; + get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); + + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(!active_combat_units.empty()); + + // coalesce the buckets and add weights + vector units; + vector intervals; + vector weights; + intervals.push_back(0); + add_bucket(active_combat_units, units, intervals, weights, config.prefer_conflict ? ACTIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(passive_combat_units, units, intervals, weights, config.prefer_conflict ? PASSIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); + add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); + + if (units.empty()) { + DEBUG(cycle,out).print("no units to follow\n"); + return; } - enabled = enable; - return DFHack::CR_OK; -} -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (enabled) { - switch (event) { - case SC_WORLD_UNLOADED: - SP::our_dorf = nullptr; - SP::following_dwarf = false; - enabled = false; - default: - break; - } + std::piecewise_constant_distribution distribution(intervals.begin(), intervals.end(), weights.begin()); + int unit_idx = distribution(rng); + df::unit *unit = units[unit_idx]; + + if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { + DUMP_BUCKET(active_combat_units); + DUMP_BUCKET(passive_combat_units); + DUMP_BUCKET(job_units); + DUMP_BUCKET(other_units); + DUMP_FLOAT_VECTOR(intervals); + DUMP_FLOAT_VECTOR(weights); + DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); } - return CR_OK; -} -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - SP::onUpdate(out); - return DFHack::CR_OK; + DEBUG(cycle,out).print("now following unit %d: %s\n", unit->id, Units::getReadableName(unit).c_str()); + plotinfo->follow_unit = unit->id; } -command_result spectate (color_ostream &out, std::vector & parameters) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - if (!parameters.empty()) { - if (parameters.size() >= 2 && parameters.size() <= 3) { - bool state =false; - bool set = false; - if (parameters[0] == "enable") { - state = true; - } else if (parameters[0] == "disable") { - state = false; - } else if (parameters[0] == "set") { - set = true; - } else { - return DFHack::CR_WRONG_USAGE; - } - if(parameters[1] == "auto-unpause"){ - SP::SetUnpauseState(state); - } else if (parameters[1] == "auto-disengage") { - config.disengage = state; - } else if (parameters[1] == "animals") { - config.animals = state; - } else if (parameters[1] == "hostiles") { - config.hostiles = state; - } else if (parameters[1] == "visiting") { - config.visitors = state; - } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { - try { - config.tick_threshold = std::abs(std::stol(parameters[2])); - } catch (const std::exception &e) { - out.printerr("%s\n", e.what()); - } - } else { - return DFHack::CR_WRONG_USAGE; - } +///////////////////////////////////////////////////// +// Lua API +// + +static void spectate_setSetting(color_ostream &out, string name, int val) { + DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); + + if (name == "auto-disengage") { + config.auto_disengage = val; + } else if (name == "auto-unpause") { + if (val && !config.auto_unpause) { + save_announcement_settings(out); + scrub_announcements(out); + } else if (!val && config.auto_unpause) { + restore_announcement_settings(out); + } + config.auto_unpause = val; + } else if (name == "cinematic-action") { + config.cinematic_action = val; + } else if (name == "include-animals") { + config.include_animals = val; + } else if (name == "include-hostiles") { + config.include_hostiles = val; + } else if (name == "include-visitors") { + config.include_visitors = val; + } else if (name == "include-wildlife") { + config.include_wildlife = val; + } else if (name == "prefer-conflict") { + config.prefer_conflict = val; + } else if (name == "prefer-new-arrivals") { + config.prefer_new_arrivals = val; + } else if (name == "follow-seconds") { + if (val <= 0) { + WARN(control,out).print("follow-seconds must be a positive integer\n"); + return; } + config.follow_ms = val * 1000; } else { - SP::PrintStatus(out); + WARN(control,out).print("Unknown setting: %s\n", name.c_str()); } - SP::SaveSettings(); - return DFHack::CR_OK; } + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(spectate_setSetting), + DFHACK_LUA_END +}; From 516296d7c3351c778cfd042e12c20dbd52b25d2d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:14:04 -0800 Subject: [PATCH 0027/1082] add global keybinding to toggle spectate --- data/init/dfhack.keybindings.init | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index f4068a5cd1..1fa7b9facc 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -49,6 +49,9 @@ keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeo # quicksave keybinding add Ctrl-Alt-S@dwarfmode quicksave +# toggle spectate +keybinding add Ctrl-Shift-S@dwarfmode/Default "spectate toggle" + # designate the whole vein for digging keybinding add Ctrl-V@dwarfmode digv keybinding add Ctrl-Shift-V@dwarfmode "digv x" From 70a02dd9c6f17080dd2d90845de797d3b18c1b3e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:27:18 -0800 Subject: [PATCH 0028/1082] update changelog --- docs/changelog.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3668383d2b..80b8c76045 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,10 +55,14 @@ Template for new versions: ## New Tools ## New Features +- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit +- `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict ## Fixes +- `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled ## Misc Improvements +- `spectate`: player-set configuration is now stored globally instead of per-fort ## Documentation From b934af10a892d05faf15e537a6a74113dbef5bfc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:33:58 -0800 Subject: [PATCH 0029/1082] add changelog for global keybinding --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 80b8c76045..d521d96745 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Features - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict +- `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S ## Fixes - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled From c762108dade1d7b5a19bd29117b28aaf18bdd38a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:36:01 -0800 Subject: [PATCH 0030/1082] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6a952903e7..b6e8e30fb5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6a952903e72199e469b44e0c74764c1c89f4b2e3 +Subproject commit b6e8e30fb5f98d1f1925f988675471a44a464375 From 41435de0ad02bcca9660797f7683481cdee5a37f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:18:40 +0000 Subject: [PATCH 0031/1082] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.30.0 → 0.31.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.30.0...0.31.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01094ed98d..ae54f7cf0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From aa61289ab2d06fab8996ae1f868fadcb0fd89b21 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 6 Feb 2025 04:13:51 -0800 Subject: [PATCH 0032/1082] Update Gui.cpp - Detect adv look focus string --- library/modules/Gui.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index ed6ddcba7c..6dfe7f920e 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -848,6 +848,9 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.adventure.jump.open) { focusStrings.push_back(baseFocus + "/Jump"); } + if (game->main_interface.adventure.look.open) { + focusStrings.push_back(baseFocus + "/Look"); + } if (game->main_interface.adventure.movement_options.open) { focusStrings.push_back(baseFocus + "/MovementOptions"); } From d6b16e4d7fe5617c7b6d69ce2fe6a7ee46d66633 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 6 Feb 2025 19:38:47 -0500 Subject: [PATCH 0033/1082] Disallow changing a vein into itself Prevents infinite recursion from occurring while searching for neighboring vein tiles, which would eventually result in a crash. Fixes #5231 --- docs/changelog.txt | 1 + plugins/changevein.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index d521d96745..b421b7042e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled +- `changevein`: fix a crash that could occur when attempting to change a vein into itself ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort diff --git a/plugins/changevein.cpp b/plugins/changevein.cpp index a73d9584e9..a8151f348b 100644 --- a/plugins/changevein.cpp +++ b/plugins/changevein.cpp @@ -254,6 +254,12 @@ command_result df_changevein (color_ostream &out, vector & parameters) return CR_FAILURE; } + if (mineral->inorganic_mat == mi.index) + { + out.printerr("Selected tile is already of the target material.\n"); + return CR_FAILURE; + } + VeinEdgeBitmask mask = VeinEdgeBitmask(mineral); mineral->inorganic_mat = mi.index; ChangeSameBlockVeins(block, mineral, mask, mi.index); From 6b59c65d80fd67c68185905484155554b26db9ab Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 6 Feb 2025 23:01:06 -0800 Subject: [PATCH 0034/1082] fix incorrect doc comment --- library/include/LuaTools.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 53c17e1b21..51ae098f49 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -513,9 +513,9 @@ namespace DFHack {namespace Lua { } } /** - * High-level wrappers for CallLuaModuleFunction that automatically suspends the - * core and pushes either an argument vector (i.e. single type variable number) or - * an argument tuple (i.e. fixed number of arguments of various types) + * High-level wrappers for CallLuaModuleFunction that pushes either an argument + * vector (i.e. single type variable number) or an argument tuple (i.e. fixed + * number of arguments of various types) */ template bool CallLuaModuleFunction( From d0e03a8dec53e3b8465636f9a65866ad4dc7ef5a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 7 Feb 2025 07:16:48 +0000 Subject: [PATCH 0035/1082] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index ee9a53f73b..b3202b06e0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ee9a53f73b4d2bf918dca0fc9dcb450f53d6e7c3 +Subproject commit b3202b06e04c03433565e8248f2638dddde4ee22 diff --git a/scripts b/scripts index b6e8e30fb5..ebb22f3adb 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b6e8e30fb5f98d1f1925f988675471a44a464375 +Subproject commit ebb22f3adb6070c931a82b5f6a1749e7bf3ced03 From 9dc6e22d6e714d959e076101b0c2a82e59ca8df2 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 7 Feb 2025 02:44:30 -0600 Subject: [PATCH 0036/1082] Docs: New Guide for adding art/sprites to Stonesense (#5207) guide for adding art to Stonesense --- docs/changelog.txt | 1 + docs/guides/index.rst | 1 + docs/guides/stonesense-art-guide.rst | 71 +++++++++++++++++++++ docs/images/stonesense-indexed-sprites.png | Bin 0 -> 10256 bytes docs/images/stonesense-sprite-sample.png | Bin 0 -> 694 bytes docs/images/stonesense-sprite-template.png | Bin 0 -> 515 bytes docs/images/stonesense-yellowcubes.png | Bin 0 -> 178548 bytes docs/plugins/stonesense.rst | 17 +++++ 8 files changed, 90 insertions(+) create mode 100644 docs/guides/stonesense-art-guide.rst create mode 100644 docs/images/stonesense-indexed-sprites.png create mode 100644 docs/images/stonesense-sprite-sample.png create mode 100644 docs/images/stonesense-sprite-template.png create mode 100644 docs/images/stonesense-yellowcubes.png diff --git a/docs/changelog.txt b/docs/changelog.txt index b421b7042e..142447527e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: - `spectate`: player-set configuration is now stored globally instead of per-fort ## Documentation +- `stonesense-art-guide`: new guide for making sprite art for Stonesense ## API diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 6660dfae51..f47f9565ce 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -10,3 +10,4 @@ These pages are detailed guides covering DFHack tools. /docs/guides/modding-guide /docs/guides/quickfort-library-guide /docs/guides/quickfort-user-guide + /docs/guides/stonesense-art-guide diff --git a/docs/guides/stonesense-art-guide.rst b/docs/guides/stonesense-art-guide.rst new file mode 100644 index 0000000000..c9e966dba3 --- /dev/null +++ b/docs/guides/stonesense-art-guide.rst @@ -0,0 +1,71 @@ +.. _stonesense-art-guide: + +Stonesense art creation guide +============================= + +Understanding isometric perspective +----------------------------------- + +Stonesense uses an isometric perspective, a form of pseudo-3D projection where objects are displayed at an +angle, typically with a 2:1 pixel ratio for diagonal lines. This perspective allows for a detailed and visually +appealing representation of a 3D world using 2D sprites. Unlike traditional top-down views, isometric projection +simulates depth while maintaining a consistent scale without vanishing points. + +Understanding sprites +--------------------- + +Understanding how Stonesense deals with sprites is central to anyone who wishes to modify the content. The +scheme is not very complicated, and this guide will give a short introduction to how they work. With the +exception of floors, which we will discuss later, all sprites are 32x32 pixels and come in groups known +as sprite sheets. All sprites are loaded and rendered in 32-bit full-color PNGs. The image files should have +a transparent background but pure magenta (RGB: 255,0,255) is also treated as transparent. + + +.. image:: ../images/stonesense-sprite-sample.png + :align: left + +Here's an example of a typical Stonesense sprite. + +When working with Stonesense sprites, it is important to understand how they fit into the isometric grid. +Each sprite is designed to align with the isometric perspective and must fit within a specific bounding area. +To illustrate this, here is a template for the area that should be used by Stonesense sprites: + +.. image:: ../images/stonesense-sprite-template.png + :align: left + +The solid area is the floor space taken up by a sprite, while the dotted box indicates the volume above this +area corresponding to one z-level. + +The way sprites are loaded is fairly generalized: the name of the sprite sheet, and the index of a sprite within that sheet. + +Sprite sheets +------------- +There can be an arbitrary number of sprite sheets for Stonesense, though there are 3 sheets that are +always present as they contain default sprites (see further down). Configuring the XML to use new sheets is +outside the scope of this guide but there may be a guide for such added in the future. + +Sprite index +------------ +The sprite index, or sheet index, is the zero-indexed offset of a sprite on its sprite sheet. +The index starts with the upper left sprite which has index zero. It then increments to the right. Stonesense +is hardcoded to 20 sprite-wide sheets, this means that anything past 20 "sprite slots" is ignored, though less +than 20 slots is fine. The first sprite on the second row always has index 20 (even if there are fewer sprites per row in the sheet), the next row is 40, and so on. This +boundary is hardcoded and changing the size of the sheet will not affect it. + +This image shows how sprites are indexed. Grid added for readability. + +.. figure:: ../images/stonesense-indexed-sprites.png + :align: left + + +Important sprite sheets +----------------------- +`objects.png `_ is the default sheet +for buildings and vegetation. Also used for all hard-coded content, like default plants, the cursor, default +walls and liquid. + +`creatures.png `_ is the default +sprite sheet for creatures. If no file is specified in a creature node, this is the sheet it will use. + +`floors.png `_ holds all the Stonesense +floors. Unlike the other sprite sheet, this sheet is hard-coded with sprite dimensions of 32x20 pixels. diff --git a/docs/images/stonesense-indexed-sprites.png b/docs/images/stonesense-indexed-sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..dc749a21965f0472cf189ecfe1083ec0892ccb87 GIT binary patch literal 10256 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 literal 0 HcmV?d00001 diff --git a/docs/images/stonesense-sprite-sample.png b/docs/images/stonesense-sprite-sample.png new file mode 100644 index 0000000000000000000000000000000000000000..35dcbefd55151f27e30aa656df873d0681d2b253 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCil&0(?ST8O&t`t2nauOECHJd(Bf=5zA0u#~7r?biB#Br15j!LI|B<)rGb&L0pkLQsURC!7eGv!0%U^# z6VO~Hu*x7y3m^-s%h138BrCW67yHpxt!7}bs(QLOhFJKwPHNoNtia=(xpL9}|A+NG zxt+`JMV7m@-bo9*{%-B8Rc4{Tx6R!B^`DE~>}z)po!xaidDXAGcWiZbl^WmnDO1oW z>wfW$JMu1%)pOQGUNd<=PGET6IDgT3o=LqwOW92X1Sij6V4umzl&4yLspw!|@-p^` zvP+Ug8iI{xEYVT6+r(-R<;S`Bbk9s~zm;iTv&#E*otgJNn{@c{DGttx2meaXw0z!e zy+EL8eV1HOvZqKZW0#|R%%S;znID9HZaMr*QEK7y4Ggm$xE$5{KkI5&mP63=A76I` znk@YB=e+GY#jlTLww}G!P$>H&=D7U<332ZueLe{ZzibrevtQ%YOh|ELK2|DN?eQ}v xeN*WtCJ!6qeT)M33`>myuy_`b4FaB$j0`V;o`TV6mNEdP zCa^QG096_o85=MzfS3xhk#zyYq$xl)2rvQ7Wdf@Vva|rQpt=kV3_!B_m-4?!{`z+k z$eiTq;uvD#-#al{s6l~;`RTvu+cY)5vF?z+mBRhF=RAkUnzQWA*Behu^j)>feBisd zac3{v26o;D^>;Qz2D)BfGsAFdSlHWNXJ7ai@lEA#I&x>p98Gf$8P7j#hd62$ghp^? zFN<7r>z=5wH1CUgTe~DWM4fSd50` literal 0 HcmV?d00001 diff --git a/docs/images/stonesense-yellowcubes.png b/docs/images/stonesense-yellowcubes.png new file mode 100644 index 0000000000000000000000000000000000000000..83ae6ce5077a512ae28286bfa0748c62d86374e8 GIT binary patch literal 178548 zcmV)FK)=6Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D|D{PpK~#8Nr2Pl5 zrfGKHhn<^q&bf0>&vZ}N*`1xuiv>Ys5CR~H6e)H!glT+v0eRIy?yTSk8Iq$jmy?43?E92aL`}@B4eZqOp zbDoo)@K*hw{^s8}zVXa8d-mDuHZ(Y3RaI3sH9cduZ{M?{qa$l=ZLya>^@7#Z)Yy4= z>#cX~-h)Z|`~TqoYy12A#oZ^vr(U{cU;5(b>~pVv+S=M$y`GOgx@*%jvtAE%U%Phs ziS_*9AAQH(dG~$$=l|ljZDC>2(|q+SU$BM6Wqb5!%G%poRmQQcuWvZM_4W0(va;%* zt*y;AIr&K68@z7no1LAv&dv_2R1R!oW7Ce0kBgejT3VW|sj1QK-Fx8qhldBx;Iy-| zYg=2}XYxdyb;|S5^A8O4oypVN+ihK{@B44OWm>nD(+WO|U(=4Gu`CFQrti7el>gsBXy8)Ba^R~BN@$apA-XJ`oJ)cbt^|raa zYdd>;wz0Y6WmfB1M|+Fcvn5bEitfJ&vJ{@k*|!^7f^Sz}{^HLDFX>UY{SG}JG8H55-xb+lMtZ;w5A@W>X$Plu&4 zFE(shYxRMDAMI|j?$!qNMV0L~cGz}vx3zY*TYcSW-_hRnjcuzOL>{2Fw#Mn++8nfp zz6tY7tF|Ftf_IyQXH#Rn(}O(C`n|7y+YxQcWwwd-)iqVNrGBedzci{X@JT{TQ-jsj z(qF<;V{&h=VymL{zIZ*M%4)31I;^9)L*=#Da9^J_i+>WJ!Q#q_^99h`+3Eb1eMCFw z^_*Xw?V^F|1HYx!Rj)IlNw{_KTr{Fj7WEun?r3Whzifzib_@>Tcf0T?^Et4u{#+LC z>=t+cS?0TU@7f3V?%Uk*vgafJ+UBO?-l_7)v#qP6wbk z<~X$L9_>ij+}>8-Y}rAHmne(zKp%lS&wYF-A9d4C=m|bMySsjGL3E-lXhvF|*NT_v zH_9W=`o@5L>&?HQvHD_B(<%I=FZ^TsiI@MWeQ$ZiK3ECfy!Y_2&1wu#=BLNo?TeSX z?D?@y>uYQD=Vh2%-?2waJNC{-z3n#l+Qitf?jGBmWZ;KyzH4(+F(xl|x7$QlyS0l4 z8^nKy$kW|D(MYt>y*iEe%Hfgj?c1(s$nRT2y>_vu-SgK>OpLv@va+UzF_=ZHjt*yb zgebzcOH9<>-gai7Z;LR~)3f%iKmMM5@WDq?JhLuvZ#;9=UX`$Z;rZw6=FJ;6IM{DJ zJ>51xAC2DL-m&hkE|pN_0+%wI#Gp}6hY$F2J*#3ogvQr@=l5JlFD@>52houiE{uD5 z2%QEAM1%w7)YeveT?lN14+4H|ZC&4MZESQ{0&~+gH@75U8eI^f0LfohFgu?JP81RO z5oXj!nY$t^gZsPygI1)4jtpwbKxiF`@Z3KT;Ws5P*48$})U`G`&|$;6hWa|Jzq`#^ znj1Ax8#R~_CUMn@Iblf3P}N>8f(QW)+Bt~iObL0=)_fP zZSO4Fe(3qBo3g8`Yiwk6%r0KKqCQ&_Eq5GGE}%ogfi|L~p)o+eq++M61lq`8L~$St zfb1LUr40$t5u7*G@ALzGMSCzJ=o{L%qjs#JybkuQvwGj!Y7VTW`q0MPYplDa&RV7L z$9h_9u%p>Jn-TU^c6d;+ntk!2KCQ0aYSlLgq9grLJLT5b*LwX&ho|_^UeL?FAu(yW z$0u}V{P8>b{xmObMeu+(ICz?)pmUw>p;&+lMu6J1y|X7H;=t3w(_GN9S!1Q@NDA;s z{p&nh>->TeaOK0NXj`jtQ6OO8_jCcY0UYUD6bSeEq^$@@#v(MPOy?ca3qN=Y$UHm; z$V0nOT-zGoiHDb|eEIz0%#2OSAedWPvW3+ZH(&thp30NYRBR}sd7wSgIqIkH#$7Pf*A8*P>J@BHB4_&KLl1oxhTauIHLGhuexKJjP zSyz2*)fMsF-kPk#O>3{)w6U&xc4^>Ed-=lq_L)l`*ozbI*_FZXOR!JcPG!gHjt{J( zR##2M#yaZk#gPtsaje5Gh&Q`h>ecR`@7m73Ep6`F`cB2>)^==WZP%6#B;RVAtX@WE zU9Ain@fmI06R)V_)DFov(PB;G2xt_qINvB;ZNZc9AL)@9kO0`)JFvwq$=a5dme(c= ztDZqNF)`{w7Qx=t^>HNvEKA7VxqHuk>$iW`-h2Ne@06R*U6;lG8T-i}{}KE2%P+bz zK{z0Y-;gD~vbtuE9#7lI@PHaq?^ZjOeAbhSr3@&jhm()(pZt@5VUMS#qakWM9e7@m zkbiV!NCW&pY4-m|DH0SIWsoli+>;au-V4li9?FU&pvu!B)Ub#*l zrGC3G+ABrUDW;OwBt?Ri-^?Iadamj!gs?21tsRPUoLY1rM&&ocn(|;wMmmF{N*xag z5hw_gT?tkMlMi?aE*J31gF*sOKC~+bAipUS4~GH6y%&JzRw}-^)d#*&( zF2)LFB>iYZ)(>!>@2N=mPMh#JTnh$|aj$SahZEo4@%xLVC-(MBtZSy5$Ptd_C(T z+&6#s4{ckGqoWC5VbLN~X2nDdxbvYc6yEUApgnjv=^+XR5X$0dp)G*H&7dbQgLQRv z-S085@O~N_n`~qxLR&B!ilVPqj3Mvq_MQ7DA+B<0i?mYCUIt9Z=Z(vQc1enZI#P(W zs-qAHo9YA?GU}^g2~!vuL471k^H3eVrp~1U>CE=F7RP&2$|YUyvB6%q7`G)flF6Yb zWjbzBQnZo5&L`KllmP7k2K&0KMtEV}p)k-&F%Ts zk;k~`gL79mffP`Wc3kPOU2;ZJ*U{q@yao9b=6U*5C?^?`W(`&*5+Qjw*v zhy6WaC=gsCGwrz@x6F-uhaGw4iyPyGc9I6q1wl*52f^AZI@40ZJG2d)u*?y@@C5l0NaRJZ!o!5VKpcg%sdDDT zM})9we@j!de^NGzi27R8ui%3)$NHxp6o!Y7RA2I5jKgYMErqsHk-#Gfgv2l|)`c^Q zgnZykyUr^=6asDJKH)KxAkP^CC=T!dSK3AUJakuCv&z=I9UrPs8fvV!wNApiT6|Yy z7kXP=A&`E0b=y|9g0BzMU-UP?^C20cO-c`LwKdTnq%?SIR@1Ihf_AMF$bM03-ea9u`ayoZ42W{G6Ej&Y-+H<_7-cF zQpxcPjtMB8R>{6bx(qf%@*EmB&NQjR0a@0x0%@H^;w*h_;GRWr)41^5$kQadx_(uci57J}p zgE29F`X?`v;nsozf{6hDlky<*)zsEo|Imbuj9-=aan0H~d#zDccy+BTNztO7OM?Zv za3Q!6*j)TZND&T2kkhuF9(iL!!{VDkdB7L#+VvaqRIba2859m7xM?3e#K3fX1muPH zju`yWyfaXvfrV#T>jN$Bk>@}j1fTE@or&5#WhHPYej~IO!XG z@*5iSoN`gv**8E8-k=A#^GW038~T}xK1Ybb2ha+7Ggk0S@rw8)AN8depl`to!A_l2 z!aans^OwqTp3-+haNy2`0LDmgUQ#`L(gt`BT&aiqgnTHAdo@M7z|H$Z_en$DC>Zdf zTm*JS2F1+cf<2m>vB&eX_IPg2CTC{s;mouP_bjIjNjLI4PDMRF9(3`Y_VFAKCUHDz zdjZ)-#wEhvJ&d9}KFOaKblH^hKyjiFNlOTfe3b8b^jR&Qu!@Yqy>+WO+OcNBrnU0k zR>U58OXHH~Q#>O(yAAM(@gu3fR1NxQz z#ki_kUA%iTB2_e|i*)ByQ>qT0gPGc^F&Sl^~o?URogK{a;n=~+e`5yV{d_qbn7?^iW_=7tZ z8eyjc~Ek3U{Ha2#K9}<{oUz5f5#v5<=^MQc@yL|bw=c6p>me+6n)^FP1`@8?C_+n1h zW|LjNe&vLhfMt2X_wGOR5M@V4yUjZPoO&cJElqa$(gnZI+!}n85aG=ld6NUyQ?+f) zm36B=KD15^;^D4lx2&2pfUXU;TZ@zf@b2RkTic1yNErq?nru{_3wg?bW%$ALs>h~* zjkZBMl;;Ky?(av)m5F!yI{`(&ggXK?C)=L{t`yw6+4qo@d!?oJB@D<5!jr(OMd0C~ zz-#p7(#pE?N<&?P9d1kE*G1TdbXfbV;w5ANywk1mflxdH1>ewb{asxyoYKk%fA?I9 zJcM@e6o4*`3F@J3cp6N!Gr{f?_E3XsWT&kxThq>_jdiw)rqxm$ zyLL~sCv26_qVX}_)n=o3%90~^E?hv(-}vACd#~{YPXnR+JKy+y7qBoXjGvcJYzpBk zmoNCr7ZeR0Nk|9ALhv(KeF94tf*rxZ90;MG6khiw2=3gyZ{Pjin{K6IHLzj~>nJCy zu^bVYSYQk~zQcrAn+Se7>--!E0}`dh?;Kz#ubd-c!V|?p+tMN-4{>$ENZbdP1m;+% zA0?H8n?cwvr9*oNpX8)4z=YIiU$|t?T^+S6GAuzCr$VdA(qyVr4=senh70GvS zfq^rY&I%u&5OZ3b0t&(VMfJA$Kx%ZlDT@mNEOE*KFVgUdVqors`%LH~jMD31AOX-J z+Dm>Z)b%avAGv6s`NEIcvoCzsE?0ML$p zK$(yRMT_F$UZ&v_d|Fysr97_N^UuE^d7@QQhu1_qy&0TpAMfv@*9ET(X zu=L>1<;{ItR2tIdwYafocjq>2etp*mIQ_kaSG}~T!6~64zF{6J`vxHh-U#w0S)C~I zTu|Yjs;Z`a@C9}9Y-@{EauruBtl(nK13qJTL3?B|Y(eEP6aq%7@Q0^g|wuAi_T=3lEFweuk0| z?4*TfN&C^GhxVQK-?8`Z-?7T^zTLPsVOK7W+J&(pyMB4xUU}&```o8)+4TIPeR%(Y zg!r-)7M^{hhinNUA$Q!eF52SqLbx?44fVrs@ECZK-p8fVa-Zjv1+Br)7eNTJ?cj>{ z!Adq1Bq2CFE)*Z-d)d^fv_58cm*kZ$%S+w1{$>-e)yLeL0Pg`_+Y#L>l1UrlwPpR@ zQlHXq^y5HBqZA19a}Caa&Rgerjd_pB#Z8;jCuK6}O@4sTEWclWup}k2LJQ)dC^DmPNYPLk?gB|tMHG~0K39>Ti4#fk*&!yS&$(^NUmElh`E$% zhzBDuxs0_Bqa;CNkj9k)fr)VlJTo&VCXFSdKoC)yOqM(=V91Nr*w@$V9hmjZ%`f;8 zUOFP7t(arT$x?_(S>*dcfq_Yk9pUyQNLLgbV16Ll7lWxhQCqIJ{GEKvCyG7kE)3e0iGI5<+%0dU(FZ<^h3m_}WCaa_nTcvXVP*tG)P@R}I9*zZM*_X*qYPv5-RW}fLK$OFphTR049=o&Gfl$a;Gli+OJDZug%@A4RwVgMD=5=JPe=b{{h1J^c|>9UlrEZ2SUp1REYiM~%kO%qTA zDEGLS;0yr<&=UF+g-TzNpRq|l6Sl(W@QHNs03eNlvzFO$xP5&)EV%598^ zLsuS*4F^4PK2bkWAA%lQLSM=S@BIq*B0NWZcp%gbke<4HjHxZ|HAs;lut`s73f#SX zed2}S0WGes*xji~d-vWQd4L;saeUaGyLQQLT)AK`+_-9=eepT_{LAoQjV-OMO7U+< zupnC@ z1~P2+#S_RY!1+c0ad8i&vMR;1ps~lbuW|`-aaBFI^ZM}{Z@%MuG~RvhL$Bn8=byE& z{m_@}GoOCRl>vkJ&YgR{a_T?%JO71y0>IDx>`&N@8`tc!uYB4W`;&lqmwWf_+due+ z|DA`l=pa_oAQXQ5$9~vD6L;@D^hs2_o$;|zS8jM2taw5IeNt$PL54RH6W<@-M*?jK zNl^}vtEkRHt`cG&3EtGpFTY?99zOQ4N{3uJyt5QyT^-H#sT(8K-H9vw^pFu@q8(}R z&W_VPAy|DVASOG(9avr46wY-v(A#-pSpf(gguhetxX=H1_oGU zo#2UZAf(cUr=#y>JU2=c_m`jhf(=dFuq#)ux$p;|5oG`={NI0{RaYC{ch5ZYjEsf8 zlV?CVJ;Da1^X8jx`u%oUapU9Tu7J+d3lEhg-_z66-p44Go}M08RDb9H`5)V($p@Z} zuod3P(9nSQBYcTBLbc4PxuC6**IK`AP1Vu1i~aJ1Wc5?!dyiv){nA&iT3b`CJzU&6 ziBA(^*eR^ApM814M!IEE3hpm#DBY(0{)h8!U0xsV@L2Mu2JNGzO$jFnNR5G?d}-7t zM1kA0>uxh;)-g6_b&XBV19%qnXT;Y}X@V!RrvVP#5Po$s*+-%7MDuuSnK$>NFe)Jua;Y=hGswwwtkH!YV7wGQn zbPoj~Mq0v3;J|aNcD#o0W{#{?iiNP&ci(;2K78=dh4Q+T$5GXxl*dK8dHs@I);MK> z07_|TWzBiIQ)3)F!DCsn;vfI|H|$%lzvDEzFf?MTxmcE0+F7e;-qV@d*^l~05}XI8gm#2mQ7_`E@fsd7AuQ@b$=?^>V$=XA5tI&m$AXcUFZQbLI(xWrY`-^sXpc8T zsXv&UvL(qiU`#k%YH#+?R=W%o_;hk*!xqI)z~j{oTM_Mh+Z*ghZVlV#RQGSaHEX~7 z{+vIrdFjPluf6}lhyJ6lx7P*dbI)G4OP3}zh}rXDKBquP0XGPbWOQ^m<`_goR>vT~ zV|iuFIq*9#1p4>ic*{Pzea}~IA)rZzph6Jw{pyvAUN)gE!c+_t6j+wEzfWk3c<+-# zTL8j?uuodOXkHkBi%n`U2W2ui00a?}xwMhIDU7KbVGG_^Dy-0Am0Bv#He%}i9lAPw zA4s=^MhJB*VrgdmQKyHYRIYn0;4Y!Vceg5wXMDmq;ES+l0Am#q;y|GA3>uS`eC&}y zVTA=6-VPSHhPKy9-3$Q_CGbTTIC-c<`Dr_Ja6FVAVcpR;Ze9Ij);l<6oxLMg-xy1B zp>cWwIk56OVJ$2=`VS?N9!aJHGA$S7G4BCTW`?%tBUbQnp+E?kEiZ>xNjj7W3Wv0N zyF1Fqa`a9;SKrD3r~jB2VwE55t2(S$vj%f>^?|&OBO7U}woWMuya|@?5~FU@fFcBd z$H!zO17=$T6MBT0WR*>g=bZoGe4 z$y4x}`W&7PZzd+>X(Rl=#OmRm{Inyy$OvEQk(3qtPQqG8h*LIrXJ{0G?x6d*v>Cv9 zM=-&w060(=Jb^OdKI0o}p0E(%p@roY7slki?SQ_zN4s4a>Y^-H zILb%ceRZ1B^LurD$fo9>x0=IE>u8v>F2bfw(bpIs2ztH~zGAKdZ&qJzN4$}|CbD{X zh)|Snc{r3g4R{CT0Df5eq=(m7r3DYeYlK+g!@bIpZOQPVEO?eW)qF3%S-+W&Vd~}m zspoBS`HGapwvDv!$~)Kds)}_sRM~JxgS7}p{I&+s0#fX#4;D7JZAHe*zW8umUVByb zv6Nk#jIvfQ8zYJZE`8$r8Zpgl@4bK9=P8&J|M{Q$DPyO5Z*PweW;*lT_dYlgq(Af1 zKW102Uh*Ym-}?6J_QsoUOG(`IbTBUhn05Ji&Cbr*Kl<-~)!zT$BUz3!5<;8a5qJ?W z7uVAiiI?S`0~@uTu1K(W5sCn5Qjs9Q7!-tJ=##NefhzDY zhXeuTz;0Vghav$wREArx@@CZ6E<6|n2rTNNdZ7KWFs~vTk6Yg$1WE^BErh1s9b|d3?unfEFkK=0{pJ!1CFXF3MsxAfKe` z>gw|Q%$uM%xMpT%T-i`Jd`FqX!^6JdfwU7BF4@S)xV1@Q8 zml3kRTV?xu5dwt=5k2b?#xC5|xAYZ;4bQRk;X9NEb2qH4Nzfv z>V#Gp6)2Sr89*P-?Afos@vqtJ%IEDXmmk{a#+K|to4m?KDUo)Kjb6#7iu!+x`Qr%1 zqnu`?NLU8EE8f{wf3-B$s{U?o8w!!%W6H}_^-F*G-#Gra|H{8*-O}z1T0%SQp?Em? z$ey`=)h$8<@k6;mgrvUk`OkV~2z~@CisJwBfBjE997HGxB9{SXoK#)}Bxwj?k%ll4 zAqpnxfBeV3_QcTEJt+QCMA5p9LRwN8+D;T+*47T z8SK;nO#mP48tClRV8sqcVUUh66f{NykQR45Js!${;jvg?$_?cSkLAccBLopv9D{IY zCsqkj1|bgsLHyId@JrU#(c`$VAB4FDaK#fqp%R9IhJ=<NO9)v#Orw0!nSbu-N zD;)ZSc@WYL4GsBs%0%f927CMMx4nE66CMoZph)m&P(~;h+CZB0c))?a0N9&?@*)kZ z>?Qx=&aE zVPD?RmGIsaE3iUgv7%y`@y9kbyXZ>b>ez%I@d8eSycnaj5#@*yVJv~C z8Jxbv2j2i^zu!P_hpcsTVFD*uO6XNz6xeba7^d{f@R+YX+Hfcm1!y^l`foD#wi5~g9NFxEd4>L?QiFBi`d2Bhtg zAVvYv;dll!QYyHZoP)F=WrRn9mw`}#Ie$hr*TLPaQmGEldNeA=4Dykh9;BQYE+WW>z6ux1hF2=ZJdK}Rw;m`u-*mQeJo z3mMJX`w@HxmwdRH<DDF$r3tXJLI%F5XmnGx?1CwRH{FKR9XwL*v#r zKnQEty8DK$r+-9!8+$#VM^1`EH)w#Mryo!TT!b&VP&U+09_UVe)I&cb>@#FVxC$l0 zZ_-jWyhI-IbDzHC8OjC45PT>_p>*IuI?By*P%c%65}w;qHe0J!ceHDrk}ch`u6mkl ztXGVU``_KlV1UA|k9`gU?}H1v;9Vfl2?ykr8@)0Q|L1q+Kd``Bb&(c>f~%npPb0Xf zp!!K3x8#aCSqZkfU6DZ&o(-N1`Ri*%JB{GItxele8!KBIcC@`|$2+pTMN8KC!yE7e zW%7wqIN>#YhlXXK`^!AR7$*(<1P?(|_z%T|;Srtqr!Ih1UwBx) zS48)q8GV{Y4#Ju+6yKS{!^-E=J!byAg78*Rkl+GP7IPlVb0I_UkXY)@>Nn(y&l!m( zT#OsQy)en8!M+|F9_+KBfnHrb*5B7D!?x8lf}{kt z6b)CE(jU-vU3;R@?tzRgUHeDsmtz@y@(wXZFkHNUln%v^yasQ;XTfWAC%lFtppVGo zH1R+G;k*W)ag})mBO!S$!Ryrj%)4Zr>*6zv9{8WoDtygY+O1UC;`WhEZywl;ypJU* zj@5lBSX~&iJ@QV#iG?>rRZcG%sltPB9iC9Or#ybD{`G(LXO9VIaW>YU=Fmp0euT}R z`Qb0S)$zfHciggI;Ionos{+sA2k}U-bhuw0Ae56H2?FKf#fdXGA#6Cys+&L*RI(={m=fEegAiV!}cU>``Vj) zxg-ObUFetk!&89y-=AJPu?)*F*4^T9X7ZE)i|?b^bsvnt9eqzL@5|2&*!7_f3Evv` zG#)RS@5d z1!b;o*z7OycmRh0%0q>fCYIaC>8c+z{|`(u-XaBA1vor7k$G0 znJW|S@!Xl5@(>o`H11LVdAN3Q%tnU>Tv?)Qz>N?ZV}gYMd=hf|Pyef5v2VZmp67Y- z+A}uP+voKYy2|CqcqVutC|25;`$(YOBN;<0N_QTX=KqXMKmIrD-~HiVwXfXx7xrjr z{fYg~1o~h{lPmKw5UOGy2>i*$Lnh(zZ0WbJ-Wc>ra$ss@%Q)@?e&)EIvF>J9F67I7 zBHih0ofRD;MqB)ypoZ5$q6> z!QCfqpNt9MsSwiQtSvSoa&RND>L)1@gb>OBAw(RT)lZc5gD8@iFR1r&3BNo|kuZNj zozwx&31vmXpkpaAni)W;@f~YuSzZ%M<1UVLyMU(6BN9j~kz>hhx$O*MpTHCYvH*bj z4G(Y0yII?a0|)UKxKH_5X($TjKq7X{ntlmm=3Im`F>KN?AlNVnBfDFVr=oPE@k?d0 zDhro0>sMoa5ICKbpSCklQ9>HR?eE>Y=d>mC#d8!%Dj6>3I`CXjF6=YG%b*PEg|;XL=0ZOB-~(4M zq#=Y>RwUrVMLHDHH~xozXnPy1oa?Z)$@gu0c}87!;F|-7W1Rxv{UL+Q|ENKZ+@Z)~FhdSlO zol_)OaBi(B9jn1O#sZ}ShY|~?KJcN86f`I+tUh3FugX>rSY9pB?WvSs4gVV0AN@Unf3jsqPYPgtY2KvsjXhGNJzx@K%@` zpzkRMC4mA#;Aa0&9w9OC;5ocYm<*mKTr{ozL)kcGCH)RyXkcW}yKHVnSrE-wlh=TfzAZ*`3$_UUJLY_K=@y9^u44{c$7%BGff ztWjQmZ$m8qW-^@>XoS~TM1YZl$He$zMIv)BK+WLbz-tI}<_oY8CMIHsGS)hq3K6(4 zOc_>XC32L>H-Gn!d|7Z-ud9mV#V>3QJ+0OHf zjWVyoAfS_TvK=AE>M`yCgyE?BJm4`cY2xk-tk5C|0?Ky5vv);n~?ZoT-5??^|X5q6>;6htbL zynys1m;<5j@Hp@kGTa0`xj65P{LE|MWl&!9wFH=WhjOTga&yBX%7(n$L&4x_Faghf z%EnWnKg)&yX-T)Xwrb!0wg1K@-+SGr?tWm+wrgEtXeO?)_K4LYd{G%pb~CsbpwK#j zK}C$3!OB3*Hsf(j7w2ekv2>Do5hmhax!h-0?U0KAUw>7m|V-re~ zm0b~Pg3io)(4I11F;B+41U&9-)kS&)`hn^^K0qLe7WA7glAvhP`vu@Fc5VC8V;K?j zjgNKl8O9Alo;eii@J($ZKvRQo$AA<+A~0E2OPLr*^e=f?8I?vKb@-l;!-{W093ASn z-tG?frdheQsPV}XV3gsdaTd7rx*R~s@QkzQs0#(dl3~UI{ecn#?+c?t$~R>FyJs2&saYzRE> zk#M10P*%M20Y*gVJk29vU?iZ#5cnvVypIrqVJXvvx`5{}(e>+>ea{EG&Z(#Dk+9hh z#hA(m-~`@RZcRAzQ9iNQnD}lGgGv~B99F_mG-+;baAu}%fZg8+cYuL|!s5LT)C&v` z^th$S#CutwB@l*yzU49*6nGl|2mVp+ri4kB2_OiVl*hvXSA+x#g_Svf?$7@(Z0zE7 z?+ZLeRzqF5aKY(78T>}br}sb~LJ!K|IrK~~1SNq2Ng5}R=X>A#p66krJwspo#+%{d zIZA^t6iawfF61K(7nAvTEV(j^x>AW`y6FeE?H~L{f5T=b@93V{6_y;zivTVUw7CVw z0AygI2pCvcE2IUmR@L$)^m+rReRDJ{cvVY3XO^FpmhSu zY(&ZsVX|^qV{dOa+L}HQEJOW0qIWE?C@X@5kL@EljC~{=+L&~Ke>uz$fe*hvZ667i zzlXN;jQV(n0>ZdKC?nJX!euk^Bp*&s`>`l)c<@~OmF>#9%b~4{GCI3D+w9JRDVv^M zaDE5KgW`bCfvXqCY-BL@bofL(;kUw|;z%OSvV+GLg$Le0(4#bj;i&hY{r2zc`a|E7 z(%jN&T|K?N>9(u2#XfiAh6`7)%khywecj&9V%ND0ox4+0_U#Wpv>A=B*7_>@xoZvf ze1GuW1K)kFF|H88=RFcGya&9G43~lQ(>xNG0au`c=eX`4USuP z--u0IzF`fG&FZHW_dX~SkASce3IgGd&}U+sPXs)IpL+R?;^0EClLz`IfOq0PrGZ16 zQt@z~?>W(q$3k5w8Oq7QNIfWDJRb7n@ery44`69-%07DIk8Sd!ckS`*cWv&@8&*}} zD28fZDaA%WtRm>a$`qC$V$~Chg3c%pEPJdnCN^^tl)ACr5c&*4ln+9kuog5%fuKBC zfkIxw8kD`VjinrdnKXDn@Cq?)6bzOat7s_qNdlbb)B%2Y0fZ<}1eBFt6vs;lK^G?M zXF-^RQjAGjLJ6@;GWi_l2-&7eg+W$5US59ODX{2&W;?@rqcv#&h@&FANpW%e+t?C_nm_dKst9 ztVR+ZK4H!~k}eDl+fYxp4eI+vHo_L3Hu!>P0_72o;}Zo+8{uWj?P`b$@9A($jr6|kR-Fet zoQ|XB%l7~n5^o>}DM0uNreBs4W3Vxy>2DmAf@b+I1DHC>JKF)20`XwNXgS&bB*;29 zHMmfkgs#9Bh5JAGYp+>P|Bw$zggBNya}NvxEPf71L`j?n`j_kV*I#$q;)O7Ajv}F~ z6b@Vf_rQ()C%7X9~o(FZaQj3XkLRRd0M^Qj~F7P8ClkAlD+;h*JgtTbS zw}1Or?VtP?f7@2(XMB*c`3b=bJU`ZT;oI3L`&?jb)n?JuMy^h2zk6&43Y$dC&pgP`^BM+N0s13CZ}1p z3$VJ%C&qnAaCjGGzyl#{Xt!^5xcm&=5LXtP4M18uRK?ME=g8Q&&i9~+;YAdm1lw{!_g`TOG06cITXmV zS1(v!v9TH;?$6uUW5fL9nv4Ip>CW>mwdjf_o%-PFp!_ z5~WE!988HoChh<52Y=`mJb!V7v;#37aUg<2qy{foqnZ1pg{s235yF@0!_aFEQCle<=qDl>|ehA zeVdYZ*>JpX-N%y47Vj+i{I&j*MFA&@gk9&n$?!k;5C68$DIjEC`OHi1Io!QBDF!6| zUH?B?k&xdl5MAlPoL3~z-+I=CET+DR;{-cj65;hR_ zcDMROo{AG3c^C`N1Gkvn(J%z%g=JRf%tqaZ3C3sQD36vEH3Zc8J%`GYh zFL_yJjK$&>VIlmOL!m6_hda*2VBzUyy;)sX^4Xl>-9((mGKjp8p#cUiJ zy&!yJa+67J=)>f6PGaMIqyi`_D%!)vsl0f3xpE4@o>0z1`OKfd7vKHvcl{Y21>Oq^ z1z=*FE9XTBTV>b_AU}#G=U@nZeedhPYHxq*8@9VGCf;3_q*;)LJ&3TAgfP}MK$t2g zF!2_c^MaCi5m-f7`v_&$;y-E7F^Nt*T4=lFhlS2}ggXP5V^TKc9dhIZ0v=$+4d)&K zl$-M|?kUKdAYP2c$onIxn`eZ{V#j;zQ9%*XeD_Ysum0yg6PG-Lj(8%e6!E+=%*AiU}nkYS5RyR zQXWHgc~JZ*-eo0ZEW|~S%kxn=9Oc5?38x#UA|a&5edGqIQP?OGaL5&D&}*l#>VfaP z`<8w9;GTXT+O^IqyDGUmy}T|u9=KP^ycm=1gZfUtLl<}regQCWc-1Y2FLNt+((u}b zjDY)_wYDM;1bkk2=BAAgjd`9bXzSnPsfiA>2gLv*rXoSsv1yaNGV~$+?}n&&0L92N zwtmod_CLXc%WJFl{SV(&pUzsnMBs~k^5*LHeGg1EM8Ha9vmwX8b7Vq-zh^>>$AM(g z`K*MBl{e0zKLEhYD4d)ye-aQzN=AT?oW=`KXDSgon8xF+=&PsY9%*0{m=uA?diW}F z2Ac+X_eWbxqwhc;9_W%M(&G2<8W>nG18#im36V>$x?m27#-L=kEv*C}@S{`8ZGavw zkc<2PIDrctPhRkWRvwyC1{eyZ#o&ZCv;)hL&g7zv)Il4;4_puu^^L98($;02-2*l{ zamB_jUbnFeSJn1Ys}W@#p+8>pTJK&#;>*3f0O}zPeaQvhWyo|14Bqs30PsdhWIs?h ztG-Y)+^25p$bp`H34S}9YqqtzXiKw?ZTjx}Hu?VdeIk8n>VX|al98peW8AB5N=piy6t6i3^ymK z(rc9KW$QU*b3qOi5EsJ~&w~4u2Yz@b)W2ULv=bM7#|kf2y-*$}P;(yzz@{_sV#OHz zQm&IS!FPM_v^^_Qg4rIdS$KzZNGZV>xXsrch|lLgM@_ zF|I27B##7P^(XBk;m8#l0r0%+fh17Q({{FVKNME%BSC0?`O}wT4~MLI>U$Dk?O^f4 zjBMls+&d7HZm5k2dw@0VSioaLeLi3kP%cb{^PJUE)J^^jZP5;fM=r?(59-9Cb3q_s z)FF?EWxsA2mGE?rM(w1`p^0a0;`(Ro%GK+lX-t}(AAHUBsd2PZv&5qFc|3xX;D77pkVMcScyeAjj$T!K!Xqn z)$S?bHGTab{>L_VA6`53l}*e~AQV{=%NqEff8~Oo=X3vL65IUvhzvRX(6d9n;}>Dh zn+tLOfBT&owMSOcMEA)&2O+Su+JF3oQMdNJ9wytB&%GWA^73T+pS?AFGTB~!AIZ}~ zTUd^HXd}-q4V=Eu^pWTDqlS3QRBgx73R6b7v3`kaMF%DPOBFscogNc5;e;DGpMs$@S2K?NY zzG%;1yy$gvrWAWea(n=s)y3Wt+KEL@+Yr3vkPyGip)J~iV&pl>3qJ4Bb4D_UL;A5Q z#q;lc_>q18?p=AlJ9eX|)_(L-y^XhrC!OyjfrpS+@@Q@P~!3)sgx%`*B<+HZ<-K4>L zl_T6odTo5TM;bZ=G@+)j!eWvTCFcyGI=Gr8;5mbcXUt>Z_MX%!oL~X~4+RB*RtHD} z<5D*VC{hmfF%aoEI0kWl{68%2Ub$oYwU{-i5jT^z3nC9K87H~CQvEWJheqd-sq zcqG(|!Xacv9?BqpDjF0SWpEE%!D)SE*XVRCBG4n-h=68YKGr%eK7$NJ0Bz})*c2$Gwkv_6!HMEP5VQ0bVT>X| z;8GT698nMVse!yqd~eDiK;W~o3n7nEAsuzFESUT#FTzSf4uOWefd2`dAhf`h@X^t+Jbqn$JxsQ@xu-#-fHvlQ zBy}QB7&k1*&#}dQ(x%bHyw~>rk;;>`-_T?)T)k$`PFxU=b=zfD2)2cCpl;-Vw@vMb z)?Boma2R!-JXamiiBKN$fb`Id@y|VsK=M+*%Qk(256>x!_VOH>Fd2{6(B9JOOME|C z+O_xRTkX4dzhLh?{F>dF{j}Yl`Lw+?`J#RA{_}Qw*=%-i+2;4;QMbtJn7C>a1A{i& z+v6S;p*G3}M-(3Tf;(;W_NZ=f0I%|M?twe~jiRTmdf@jmFYRDFIgOPDh)`i{Ltq2+ zNN=xQm0>AkrQY|EAdnIGcnUBF#}qtGkzi4i2joEHX^Mn$5GVi)kcxzk;68$h<4kz< zF696`r=BbFd{}*vZz237MS?PFZ*8zo-<+_EqrHBxBH)wMg(C5RA$(94SSxfYOu^d( z8SpS=#E3b`l!>}nv4SFE@IY7?f^q-`O)3Et2ug;!P*@C1KFPyT7L*Nbd%AEr!vcb? zw3*m1>E-_*)N?{4l^n`0pf z)@hzM#b!dR)ZpEHS?Cxig|Q<*s30I&s6rZI=pF{vcdU7Mg1Hg|5^32-!VxJx*P`DD zXa>43F*py$j%X1p>{H@495f>goEdJhX@0K%*ljUQwb2O3Zby3@mky8zoLi(sD2LyK!zjbQ z`zFZ{hD@x}p(qbJ1w{-4lAj4f(hCjyi~rMq-}-#QOx}qfq!`MAPQ;oc3_G8ot^}PfpSnhlu18uFFhTU29xNl;$jk>XDEGWkB5>t4Zt(<;JgW_Np_>)#( z-7BryVvYfUisE2%BJXHGpkhfql(NZ*Zv-%2h}S74z|LQ+*K(aG6IM#)Wc%0Oopyl@ z1b+V26%d;n@hb3;2u0vAkS14C0of)1Wtm60lmR8fs;ykTLwGMITG?lCb%1wh#JebX z5fX}mx<-4NPwFHrg0cb1b>g|;9Z@GoaD4Z{imgidH8r={v$wt|Lu0^sk5CYEcZ7O4 z=6!Kx)r}#-A;W#W&buD&(D*?(`HC>{E(X{6BU0dR#vqgRY${|Q$<+M33tz@D!Wbb2 z?}H_Lw6MCUHgDM%UwOed5TejOxcks}%`S^fFzOh$3rnk>C-#ZNn?M&OFXmU|J&Qhs zf5ry}>}xN+ByYD-%4^&4BuqtE2W7?_210mOc=~2Z{Z4)+tb-B6{aF4V?=YbZgmel} zLPL010BuDuQzo>5Zsk|)QZBT~kPzkY34cOUXu|T~Z@u@(CgrM_H-(rZRr`v!LJCAvPUvF*zbyhl~AOIVPZEs zp%~)ru?K=lVnQ{fEr+(SK0Nj**A4F@_JG8|;ImbOn0+9_gS{F^gr2~`pL(aLsoq$h zOjmmD5sHHlcn3vMC*A{0f+FDJJ9)r~w3Nf#34#WkbBQhkl{Bt+sKY-v@?FfQ`e+Lb z>Iy++`sP7hsc>i`em~`;<)VMqFD1geS2os`eREWUTyydR)Qh5^p8)y;NQK~*zk~pv zC>`c6P&5E#kRH6iJ6B(!0LaVWLDA&i5uRf-;L%V91BEicfij#AinfrJGD$~SgvwAZ z_ile^@4orEZ_=c>m-{$QoqZs2!~&D!2t_P>ybnTIE|_JFBOo~nf_)=QJ~FR?a7O5o z2jPx4z%Fr~waNNs=`UUh3JThySg03|fcpFhm7-2URFp@ZC>p#Cb`Yaz2q}60svmF9 z#^yj24!EIcz=xG$108rO>_!j#D2vH<1U}veWu(_Zdr&@nryTf(*Ot;A+Rh|D_#gO~CYG2^at_i@=vU^_;v1$a>*IK7Nyz=Ro4iJ=#KEAnWFne6*YJ9yoa2U-|ME zjtOV|pdJYm=UFKLB4v-{(xrxxKSptISX_M^$f^B?khX+aWN?m^Kx`I2Q(r; z0k_D+4D5C%&-n>{UvjAd!9DUbk?!YmsYA@V7LFT83Eji*PrfB4~tCvy=9 zd4xZ-0S_(&IqC3LSc1#)TtZabM`@sB_)Zv#{zUlWaWFBCCjp?CzVn^$xbnb*;o^I_ zJphV``4EnhN4ed)b<2L^H-5u@bYOS|b_D_Fm z+>eGQWR@O9`Orq@S=j%904FRq+THAUa@LTa&n4k~9tf+UbW&&~uwv_Jhc^DyOJlz4 znJnNkFYgJ3foDOTC>n4odp>1&{r-|GouB*6ga&@KhyPHX_ZE)qn;&w_%g6ccC%*E$ zT^aB9KIYvY#N81XtT)`dM(56qCY{yGD4^JzT@E2v}#5noE( z_2Xrf%;nj+yj)-jev8On-;Q+Edk2fPD-7AXP_WJ42%0Nk^iR|Z|+v+}7^1BwF? zTMF+WfzH7q!g`{SC=}}9&_z~NL8PtiJ@+sm3hA&eeLXyu8%yh?AQ0{d#!b1KurSsT ziU2POjZE3#hTujyv4@0JS>S{<=a+DaeIm@UV13EL)!-(i#k>mnnJZyX0X~1EwB`1+ z_Y8Ung87ocu`71z8n67FbebUe2_K;d$ODbJfB*h{$9-aA!k^=nfG0R}p@0Zup#U-j zMW~DZgjPJm!yw$nedaX|jL;r*M``h$c9j(g zznRC$m0}$0!WD(5Pa=z9!_!)eGrH!kroN( z82b_Phpq@>@CXBudC%y3#x7}nd?`K32tYv*_My#gyeSV>d4{P{kx(Xi%J@nvOsGl@EqkaIqz?-=g(sLn`kdfr0 zZ)h83rXs;3&Esi+^JP|ZmOlg_(2ukui4MN$(O7wDBWIMml>V?$l)L{^%xvLd_*tp89la!Y0Pg9i-8z}Apahg7>8#(V=n z!#(CI0fkV9aTpl5=6rVosco*Q zyy#Gr4z4&f!J~pMwYtEE_RyAW&&KksP2PLoZomJAJ$`W8mKW!3b7M{T?8y^pbtS@; z;U_L|qW$0wjsR`Jqd*z(ocn~ZP%`A@JI~4E16)j6?$0bI`adp|4Pi7C66ctG_~C8) z<3Ik6-M#(5Hdc3SXHx_09uWNr*A|juL^+ zSk$}f3+6%Q*LVEFdS`_c7vZasu4eZV2p_RQ5#dTc1Ttk2N`g1J$K|02ABqwNQaA{C z(vl8k1}|`|1?3>1xllwX9bdWSp`yAIfu9HOk)F^F3I|2VL_YO%xv(kf$8%v12yKZ_ zRI~@YsGqiQIek?o7re_66+xR=Sx8wZqXfDKx+QLSP0ZWiy)l9R{?vwjFvAKr>Qj1y zHyDfbYg}_n>vr$af?xB?vNB~vvEv&B-B91)UJAkn-o+z8KrvPj>><=xWfsDZ@qr-X zEs8O2;-Xy`OYj*J-HZp)#`ufplTa zK;mH_EIo!_C_m~W4CFXE?h%#&2XI0t(ihav1)wxM+*P!VGVnU6hdex^Cr~1UyTIuL zH`1yc>Lw33QNBk(Fv=N|`c9jWE0jll5lxBkIb|mu2ot8_0%wdr@FFea)|Cc$C?B|T zB|X5yjsK!fS7xfu=SqsQQ*lC5=tDUuyi|I~)GUkkaM4!U!d3NG|H>~Ozx&=t-XM^A znn&Vs{Q{U%$(`*8e<0>aw$FGZcsU5{mtTI~<`w2`3NJu!PRGCi~GZJ?BCO z;GG%>X_o!*E{^nYSiM1r3g(8m0Q*89s_1YcoY@(hp{>5|4&TkrT6YHKc_7b|_K`4X zD3kY)FxeOJ+Ebc9c*0%f&=yQU2#Ye$?;{}%o>%V91;&Q^ymJ^lFcSE2kYl!u_mPZ@ zUA8~_=YGjr+Tv(}1j=TTosbh=1Ys?_1ZNo018;+)NeFel@WKo3k>FuwUas4>Z~Ghv zN+x&4XTKzW@cGyN+F$cME5Q8JmTj$z-cO(@l7 ztHX8HEUyJYJ1OCZ7lPH#5hGabz%RUV!L4kdd_)Sb&*c@P2=Ou?aG&-*k}{O{k@#eL z-|1xgbE928R{`7?E_g50S%&iaNX`S3=y)RZCFgrBDnEEJ@&CE21MU^kzVZ<%gyWds zI1ebG^kO~<%KJ#(pOzw-7N1+Q6dj8@LhztS8tbsC1LuQ7g!jkG{nU*M_9I_-USp=+ zg&83%gdV)kq&$KJAmr6Ae#2NqaOXY}!ae0f8wn}F%Xm7x&Nlav)Hl>vv)cUpjVmYn zNDyk2^TC~mey}2Vb3_Ug{lMbNx;>tmvu}Rub-O-($$svKzb0WAlk%NXdFP&Sh`G>BP;!Y<*p=HZj7xL&6P*sR$9V{1-t?N20k2Q=u{GXa)x! z3GN!{@lYT(9zt{$)^BQXXQB@4f?nr&qHfRomhqB<2M#5773v- z@^X3E((KR++*y!-VxUdn4t`jicq!@O5r!iT!itaxWy2t$hzOMhT~XQ#vy*;x_3(0^ zXcwCS32~8!-&`ndEENE4M~p?vfgbyBW-OiJr3 z*z}nm6Sz|k&+_{ZtVmEIgh2>>fY6^tk#LcovdGU$uYM_!OR@m_x?8QcyUlvL;;bKB ztxkCmAxLWrPvFoe^dKx=fr@jX%&IDcE})T1K0Rcn_QCi7Rus#T31!fRb}3D~9`MKN zg7H~KOvjKPNY5zS!sYnNin#O9dp7gvw$0BxuxmG8vBsuYs)r)ME2JOz4bW#Oj|@ja z?>qyG`njB0m5-&ve6lkguY_l`^YP=y_V@nY-?QKS-QTtEfBy}eyt`Pu^vZ9Z~ zoSgF{h2cVo&M1N37cPfKShCT!!~H7T-#Ipv(CUG#=>y?SKxYZ?gK8xr(v7_IWd z6t4|-`1x0lWR0^4k@!FROtO};B4OXf5&;MwG4YK;NF@SD@A#5qJQwn?C?Ijc(|K{C$IHii;uw3BOj(Hl zWktd=WtK-13L~_4UWou?#iK%jQWjyb*i%xkI`%{lymk7AzJza!OMiGFD3aP*_LeYD z$dX(YSk$wzwQCQi7wq=KSy`{UcKzb8yn;>_aLf~+2)H;(og?F8kxH!0!tzHLB4pqr zUST^uH|K9AM5qq;_1Tav=12~U4T+qQg<`=ACkg-_yhmiPuiN*RFxl>}deGQ-JTvbL zV|dT|#nBO^t#<315FGVUZz>W5BFc#I%Ws5eIYa|))C=yBuLw02!W3#?fxE!eeJp&h zM|_R)M9xt+N&}pIqFQB9CW6iuE9sYQ@%DW6 zD_=Z@5CDxUhqf+XzUYc2wIRH-t~Y@ubie&q;Sh<;=~Wp8lI~>)NnQ40roR zB$yk4=f^_p55_^Ep#-Qmgl(g(Z)~}s#zOH1={bysw*djluqQ@h;~*|M7S~W;ml_|j zUEe1nMZ=^b3LwtJDg?ML1}%hal>n!HewW`Z0z;!Pc~=QbTv1L;x;MxgY*J_8op?PO z5Lgf>0tN`m2}_TTW;5FAnmS4JLJ3j^3jjErj&H+nsEAWS9^;Oj_Uem(c#eB z+0Z#|&22q4GBWBby6`$s5=@Mv447OeWOeZ(eRR550fmkdLXp4=H*enbcZ&S-FaL7v z9g&c_|G}K?Nl-DWcK0G)4d9L8jnq^#KNW(B{zGt8>mHu~e1hO2yyYPl{h?f#Bf>BN zm*&=L>+Dil8XKMU$M(5_BU_ZPKp+!lB8&vEySrP09`6pNfER$0#Z$n0K~Uqd_z@{g z3<%gYj-aLc5Vkofk9V>pK}~u>TZElbkfS`ZzuAGGtF|~)4+TMC6klo!<$bCz%24L5ni|>_`Rn*zQ}6xb{drti_39(ese>>1{*}*Mw<{9^E)1Ec zAQS?vuxJVEr~(&k8QNkjp*)ZWq@{iYB?61dcHWX$Cwg#-@3U7g`n(L|oV{O+bJ8;j z&)7tmedAl-x4RD>+pT9V+eq(#RUg$^trQ`Gk}`M1qj>w|p`}a|$HJPhyb0-G!J`0J z<;4mn_>j5?{~*};&3%L|$^bfm2W>!jvI+}Jo^-Sk#e zT=W%+0lDPHx4uIg%Ay?jjC26MNmCAu)r^b`y@rdHQ1(bT!wMo%0GS~G&p?U`0Acb& zc!Ut%2cZ_eyMR%C4gw=a&zlU}G>BoycDc#pQX>65?fzS1#mi*yK};2^`f$slo?0l1vVAltwJS0>JLlL^6pI{BbS?u|M(P=k|7(mE#Ts%S@ zg}|H(eLz0af~T+iBD`2UtD&QW0{cNwKqxNi$~tKU?#_9q2=URUPh`dRlZ5KOFDsySz{-EyhJdL!;9cLbAh@GLg#Un0}hO71TMlDLzD5sea0X4fGe_q zHqai%D8N0^au0byew2xarc@tgQU>2M59vT7^MQw#U(~^cBA|Z8H}^?H8QgJ?n1_1M^n}Wu)*h@Oyq@y=NS^eDME6dF6X64xG9l|+({rp70Hp{cfpabj;GOPt zK3II}^FM0OfBLJ+d*G|99zA+=^5#RB4JH78a7M{bPEOjd|N5`nTW`JP9_ZGZtieTA zgFrtNdGtd9!o$N=`h)Sx&nprVt|SCZw!?T#=(=H1P2C8$2>ap5_?b=q94n0pQwf-Q zv%b+*pNIwiz0Wk+M0O07P=_^;IENXpq6V^Q! zyTlRJz|X!s;hqG*Wcx#TM!)~zyn6}PhdW#Wln-shyZFhMM%^m{{PS)^;6!rZqIw2KlZ|?{eS*o(SGOs6_ty!p}y#gc&#qsm{Y!b65uW3y`xcU<(bsi zk_Ul=!afa8mf%KcnXxC-UGl=UlindC{MOy+2`1R7br6=dCJ5~V2t2BVCB1^ zsQP&Zo}|SS0eFr#!Z^oUA}#qS1K9#@;7EJQ;}t*w@tiX0AMyk4fvFti3HwN>`#h8< z+mUw(S>_oGkw6?)UfsMp=t7x}YQw6O(8VK( zh90X^_T^b1v?{(z46A{>5S=R5K%!!o5!3_$QlDbY4g1m-zyFW1Svgrp60L%A^bLwP77R^&8dhG|!2#*s1B61?^F>7S& z#696r_EJ2YRnR^1#&ZM|iiF>UP5gBMO2=P3&kz}*Q(2SEvW8@lQ`9Th8m*%x+W%~q z*`U}`kBr^zthj<6Ok5-E5zf$?JsMc-0Gk7u_rNNz76Lmu!zRr1O7LuW{{!$?%8vR_ zB)Pm8d^iO1VsD!(lpQII|M%}avUz!oD80$$V_R641*;}RC=rD%qvwb@OhU6k8$wu5 zQYwLew3WW{FqeLFA7$jvL~G_Xk|zdwMOzs$jHeryNA3A*6B3{Z+*3tzB;#RgWy&gh zn~_Jdw@va2+OPnD@EmQNlLvZ#dDk}gkL0h^*t+=kSPBrMuU|$k;bQu;&>v#`8<@ARpPV2+$tVQ$BU(+z|VKphvcowt&00+4)e)zbLcj!o+y# zh?JNhjujIXNV%`MdOu7sMF*&W)cp4f_xxlWh$@Ewt4wkzs0x=5; zChNy5fgs?>TMY4>=oC7E=e~|!0~nW@&;*Q!(n{|E{7FlmaBsss<}oykb@;fSv4V^_ z60AuWnsQJ8-f4PHoB8A@i(rD7)Ynf}B;W}!Uqe}>h4zl0+Qnn~2BiTRP((YM8@4b% zWh+awZUh`wF@Mvjz6<3;IEy~}wO{)+d*_{ZZDwZH7G^i(iAXtb3-^QYG-x-JJ@Qz2 zD219L{E-)M@?3uiAcPdpTw&+}vOHHbDpf7;^7uP_9<=89LFHJ2C$23C(Yc*to8CCG zYrSMrU;8PH#Zpr~_d zh5a#7N>T(UInq)mb05na@(5Knix1eVLRo~%INytK8}Dkt3qvttJbeAVb$fq$*Cvt4I&N5!)Mrc|-S!=D$*0-34;yj_$}{j@co)5efJakL8Mp%lQ2 zI>}2q+Jan3B?CQJ?6kcb8!fq?Wq!;f0r+GY=#xAW-kP|&wsCT3V?s{0ac$*2I>y3}=Lra;w1&wa&ri$FFxk^W zDZL{t)+#{+jXI7ut@C(Y!s*ajC1l4X&=J;Hw?D`cDXhGjkVk-^J`d&hkubNx#6HU4 zxzSGh^u=B;yL?0nOPQY;?)142pd8vN0|GQ(waeVcZ@n|^rwN}2mOu9}kE3#*c0>vx ztiS(zkK~ErCAUd1v#S~&z?)>Vp9FY`wgC6nx8^_drm zMjr-0)l?BvueJHr`_|F8W1qg%B^ncDi1+U2f(Z_zWR42ng0?77ytOi*r1P*%mSX?c zZ>`!}vq7h87sve7xJDQwO?&Z%j?1;_K`3S5`NIo7U3vD&G5Oew3HG_S(U(V zLSIL}{vIf^QQvZ;f% zA^Zt9;cX<~3E`PUdr!|OOhDQ2JUtIy0!9aL4;;XSJt!z2lqxtSp45%fK;E#BAtB4G z86O*c4aUGqr6G712rDesvPZ&zrcjpZGM94x{H#`e?-RohN(k10hzL?y)3v^BN4V{( zgZx-Q)PwM2avAR@#iFNL(qYqrdu;Bb+*nqOM{=a{-9k}f zgeuA>1Ui#WVhGid0-WzW3l~&wr9PvcqVtuF@2SbT5MsEr2u$)FNEl+7kuL>1Wioei z;!>B|;~^=fB|Y`ik#HZMcrR*}<3Sp5$72HryedM0e8(G!&+ww~(vofoJl~YCnv-?C zw#ibyeVd(;QLv>lchv{mN1_$&jF3iHsS%1o0EK5kE8{sAWpdA-9rMglcUo}4EBQR` z6wfK&OiNd1G>2GGv_~W#m@IV_HW%;&>2OE!N*vMOuP(QAf&{aK``kHO9JzgI`9ncjzL2ts56pNyy zZ%`f_I!RjS3(Uz=6SG=ZW9;$pH%LlIA05PGJG@J|E(bJT7&}~8_57wy&eI40{iD7x z(&Dg2gfnBC34g{c<2vO@UOY$0Qa|HcMfi8O*!3F=9)av*U-wWPq{kECJ4OY3>f>F& zZ}y-;V9Ewh@Nv1TdyF-%^hQD^M0AKc(p%s{(5I3qD@tg>GXUi?J-=YHi;M1Ecu#|) z+Q0>byb8HbS;*(CgY@|vyh6q`Sk36j@N20=D6pf0^O6X%nmMmX z#>D_P)bS0n>TP+|cIS1?t=is_yp5h#m&SM+-Z?H9!?KReE=3`NP$Vq7jiXQ?9vAK@ zJI4{yL6}0}2tT|WpRh&9$|C?jLQDLn z7_>jz1I@@!Suic{i6HI5%9h=Eyk_&uJGQjCYm3t=Z(YX17D@yXA@onfN@Y)=+AYjb zF!7x5U9ozjNYD2SQ{`OA$#dmpfQ532$#$QpKSpp7ijxx5y)2sxd=OaY6-jzQdlI7C z5-Qu9;#&#BqvjUdscDwA)@+|058;adVks?_KFXkM?V=C}MPU&$$C2|Wtk&Y)AcUOA z!y6jeZI7V4DKF%u3q5vOmb9AY7Bur0C@WTAd1yw$&)>dS)R`-*04DJ{P570|eJ&^o zZJ}7^x1>lmn{4T**|w{i&Row8w%Bt6O)9&=6&7#G1E2EVlT;)tG7RVodXxUaOMqUX zT%fO0)br2}^%Cw4b!_x(R=asS-nQpq)fRnD|D6Z=kjsq>|BQN2NO(bnJX_kOoO|l5 zoAbNcJFQaN;%%v_YqHObwb<2O&N*xF5G3&1S`sjF@`*J86w5N#WTDU%0+G>{1LJ!4xT3t-1AG%-Dp?62!8Sdq@`S* zhtYi)lj$1ow9S_g>pmV6_p^RNQ`wI5iUd!F5aavz?%Nw5erWd}KlZYb8+=k1c%m#Q zi?*?ni@LxYe9F&BAN1rMF*2+^|G8IAMk*DSS8Z{02?YY|Nnj!zPlb-bE4ivZ?y0G= z;SM!K7E`li@j!cc2MO@|^vsW!*z46Vk%k;64n*eY7pYmNLof!d@N1Frr-2 zmj@NZV z&jV$;(oy++10B{XPYb?flPOntxu;N9S7jGQ+0+&W+vVmx>#66Qve+-v+sa7~7)Pgz z3Q#c63@;4J9VNfI6``D~LtQq~!_glyma2Mey?Ve}+IoB}AS>9vmFHU}w6Q5stYR2f%YW&O0@PVTiTx+EiQ`r_>m=})B&IB% zJg-E^DNh7G1@MonT0*_5uF)1X7N<52Ws&oakYhjAeS7=R`{v}@J@oc0tRC8f#Y1n; zV|`!Vt@5_)91+rp`pX^_Q0_}SzauFLmRo!1N`KImG-cW{mxaRc&4^MSWzW#dk@82e zkQboK>ZHUPq$Ig&WJJcu*SIbEqud@skc3dDm!mCQb6dx@wjX_SBpi5Ec80N%o}20$ z=B4NxmQGg*@9dlQ#u^WCb?a)Xm0@#8q##050ON)6%EcI>5B$(Z87Dpl5grAc@}5&i zWrU^eWnfsU3yCI zT!5lN$s8QMzt%Y&`fR50$8+$C6F_<4`LL`QT)~~Czc9dCcb05m!w2iNc4wo`Pd(n=*texQ)xY;~J_l%XwjDa);k9zk zn0uI_!T6;?Yb*B4EY3=huAHuDJP+x0!dn>*>m6>E7ZY=Vf968HeQ8V{dwrEJPGGeh z^kfc-iFoFOP?kS(bHpwWw0W5i7dJhGh~hqM9JIscF*~k_egm+yITsAxjmhVaPqrfq zILHwT8(>oTNt5kZ-N*(HH)+gK7UlAMbDIMWPZvsXpE7&9x=!Ajn1DcMBAzi$Svm2I zAZG4@yiBw+;f^o|2uX4Aj4@7rJOn%?pnSk0iikFR=iPVh-s34xM?JKM@~M}ysFMiB z1=tn{>L>TDl@z3ObWYV(AOgzsh5`f8e=115^GBhN2QHA&I?wl0x zb62~qq7I(LLeNbZ6ANuxoyvu!hoTr1lMJ`1!9s-NEc|E(VxcNS?Xrqo`b9D4x*E^q zXjOZQ9ZO)>$ik!HGzRM*%L{-=Smqvv65(ke!atP`N`d(fJQTDf9UjjQMBJELaSs8c z1%!)GK7L4?2#nI;33XD|4CjxDBtG{bC4vQjwnzA5A)w5XX3?fN{TJpW54ghoSVN>m z>EQjPBBKuS5rT6|M7TwnF&JqJb%K9bDpVP zOI!JOH|%hCOK2X6KgHk0W;g^Sdr#7nMX2RGM_G}qORy0V^SLuUN7?ZGJUra04U1c3 z!y2Dmg&*-`)jvB}(a;k*(Vo-3Nksx>;4x@c$Gb^1j^MwpmTJ2-UT+=E>{U3lU-`Wc zZBojN{tU0;G~AMq3IrO(m?iWYv>|;wqYHx{06dw>A%+kqcGp-t8$V?*IULd7k!MeB z$#ChDc+I^W=P5j-c;3?1U=5-@aCL}x-E)F~`fp!`mHOvcUIE@7p>jMBaAqQ&@E1TI zFxd{>%8CTRk8D6WF!nGu8Qbs@GJ#L5a_)gIR(#T)aR!eP+M*Ve0QHiVJQ+ox5BS8B zK=7s_K|mvzBa|KIT2W>KbyFUSjWT(Tu%{dr1R!6akIx$`eY477{)RSz6S9CZsDn=r zofY`=60+sBbytvc%gdoSlpZ`&L6j8<_c^f{WtX&}oKz%?>!c%TbAIFm^qhRO13btlbX82pl@s(OhTkviJyLEo|-DK5rP~SJL+MQH7!SI=z`rTquj?4M@LY& zwX8qBBkUqC{S;-E(ws-zV3_L$bgsyW6gSS^oRNM30}}#oPz;C7igMtD+L{RtRy? ze|RxWwzCootNBSDC-9Lx5R}Q%p2o*=#a5U1{Gdx5GI6%IM^tvYmIZInK0+v|z>g0_ z*ZnGQ&#sKHWo4P#R(nXx;j20ECX$nzlywP`^I@ge`Q{yJ(XSazS+{P zdC}z2rmS2k@5;`W%OGAjE=9khe|lm~u1=M&?W_K=45ffr@NL*R-cAuS4nXW15T^t~r4kFr1JkpRy~=zp%g z!7dIo8M|;FFYMU-#;&c(^19O3Dz{VCl@tj~$IjemhT3d)Qy$4iG?u|ND<#n&S5DFJnyt_D60GxVJLDx^ItGcda5}-t+1+evT1JfKChsL8zk% zIi2$K*a0TiQs}0~GBMomVWq{@b${z(qw*8R!V{t48QN-V&3Bx{d<}zyu+hremIg<) zwd(n{%7Kt0gMFUoJe=%@QJHuz?7GDg!X?JzKw&Xy&$~&I&fo$=FsNN2sY7TFod|P7 zN0beRRJN$yyaQ!!VMW%)roDG>N%e(AHTyvHk~Iia4wxW3t1vuFrO%Tv6AV-xgCK#y z&GS?wgpm;H9(vLr!Urz_fe_a2X^CgcG9G0JoCtB9@+E?e zJRa}Y757d5YI4 zUzJzMmG{^Hq=&{RoDiyVw~UX6`>1Q_=0j|E`hPeb1Zjc2jL;`K_MY*8OM}|+{p5MBcud9!W@H+un!6d zSwNTxYaSpZMn1~#?G|sfwyA7+6H-9blgi{tN0UT5;$TF|2k;oc9p&}bM<2QO0U%f4 zA?TiMPldvTCxXJ@yW^~S#|8(jw=>>rlzVR|pLtu_7fP5gVJs>j&zi~xf717Lb-A(u zYOY+l_!_U;#oOrAHK>6zAP=KcgN!$saY!9Y4tHb;LvSXd_k|$eQ6ehS9brua5lT@iwswKoL zYJ`Uybe~3WYHqHk(m%%qaN(#~bxQFub3UEu&-iIPK1jeFA>yj1X^BoTd4I3*z zm=F(-2MdC7*!W1g1nQy=lpTtsqKnX2?&>A%K!?)_SU=v`%14LO9=>}Wa@nbe_Hff% zqq4z^JbU}ZICpJ+dBc|Eeaz0X`BRpGtij!439sigoB;vwWWQm&=bb!(-tw&tktwloOU}D7q7%Y`1U&BERTV z)EWPCpDTU`uH*x!=p+6@IAawH57GQs9?SlozN=rc4HkDqM+vuQ23vgv5tHeJv6wr- z$YBKw;jbq3BcGJZ-iKv**7w&AZIOv^DUgR6*K6vNU0Kb$vamT+8g!i|s?%{Rz}w<9 z%X`s&|5H-W2i^39-+T!=DX-Bt`l8=>vO>@2T(D~E1#swsZ%79qkb1FbgF$tua{0~V*y2dkBOTi&mi>?pi-A*rHVRxtJmH4{E! z3_A`yOr!*)1CS5Y&GQ(uVfkZ;r$x_m1a4zJdp+Vv74lPuhdPxG`GK6^J11pR22lrJ zSVNzvv$!Y=MMbC_eCR8LHF)rwdQmv!N6FB(h2<4nUSIQ2*38196c=H&VsaiCgiHlb zr*E{0mAB-lO#shv5KvAi7uu62b!6MY3(tf0Arq0CoWaLoyxxb(Py4;EROiX^-)BV2 zVO>|{anLy^gnP^Dwxz;=URlK)dRI{)jbhvmRop5BHf7=eKmPb_A8=0sRyw>e(C44P zOCvqjEWx>7384xMw#h5lJCNHNyMVFu#$|crYLuyoAv;hWfJu2QeD(~`hy(;WVJ~MC zT`OyGehkZh%jwEM$Kb)ZD_q2W!#nu+8xjws{5TvkW9|T9CicSc98ZUO$xodd^3YH~ zw2?5|`8OoO)F=^@6QQv5ND^2?K+Kdf?>%0zckj%rV`DYX(zF;`{li`iV%_C@2m{}x zUQoAek>Yj4F7i`L{*X9}m<|gu!%Q&dsr3)z#H4yYPmA?DO^*l~KNN8utk_RVRO#0aS!nf=P7fL!G@tOk%5lgy+oEoK5Ru@RR-rIka(LxZAq>d6!6y z?=@Oq!eH4`=?Kgxc^BXmLeUpy6wk{O_~he0_$9CL{q%YC7hYTb_IAlY?-^{irWOfx zwdXHB*I>{0$2fghnrvS`Znx#5Zq5>Rp~oaP)^bj^mjS~LPXHl}kY!>Ve!}v{Lt&CV z#}l+>LYW9b0s@wI!V}s$4@5@7duZ`8vF;JF352E6%VEyp{A4?GC)Azqd5Lnh%)1y>(ottfcK>!PiPZLu1yMqGRolXP`ci(PWPZv$t0lYs2|0G z!XZqCCzCQ8x^iSq9-CkFfBRRycHA$<+uN$xfi!x1bB%iwk5_kWK^_K6@hU2;UP2FV zWWBOytqpa)gZ=j6s_kkpy>g+?4wa7M8s3>(ks_$EOWkcgKY~U1YwtdGMRcJ{3@Sn~ zi8iP~>6%y2>@iwgV=@0K$2%V>O|?z*G}~}@qdjCYuV}$$Z>JqfiF9=2q!H)Ygw{sz z=NtziAv7i~CY^&N%KnX5qF3=_Pp||SFj#RYB0tDcX}O3Uk{_jlP^bKE)tQWm3!8OP zA4&n>HYY?0R$&IqQQ%L0w-_{Vd>$PqD-Lt9D$TV7znF7o>M-G z!fCAg2&_}#aP~QSP*5T~Zf>l$YvY{w9PRtsnEI?a^5ywiBELP{sIuE@CXX70e{3`J zN51rzb}}ym$-$Yn2ENcTtWi)uZBO}mdUPnsQeIUbv=0luEV%W>ONte7k&n}oS&j?T z)E`>4t}2beEsf;`jZNkrNLyc;Z0{fLvX<5m2nX`44)zJLsy$MSM+bNcR2BRZHPW|? z?Wi*#agN`3C()j0XY_9x7)Ag4{S>rORy;?jiG!4#z99sO2T$MhbOg>X_Ep>EPPId6 z)~ow%xq8@^wh0TzMnu}k{)~~nJ{R5q^9*syMPaoo4PzIXfz^&cXIvqe5u^xDypj~M zC<*9?aAr|biDiY)aatKd>`Ut;qk8}ul(jdqm z&CSXCSoL;!WJGwQbWRkB%4=(mcm3nNke~7p@U+3_aMUhv&CnTT@r?Jw5ZaLeViKN7`h5tfi#MWGxUY-w zS4Gb_1_CdH+HqUe&r$!@p4DycTI04Xqpe-rm$kpkE^>7sv0yqXy$6&4LYxeDG@w-u>A6oR%XhDfc~Cwe zPJI!Mi#(L)N=oIii!j!K?eBzu zaY3uWituER`h_6XAA^y)z(FiBgcz<64uf;Jqj_>lK@pr9LBNGOcTV(=@1+pZ?jFN2Y!T)x_zv zEgx@B&=|bAz%1JnwBs-SkuS<5U(iwI@z6of^*_%E3DFiVJI0V|ww9Jw3BFi5TwAB{ zEhCNZc;Q?aO^p(aoMJ7i1U=w2dB5!MzMlY!8eZWL)^ys}D?XDAZn1WSj>$0|?8t&)ZO;kPczqD+J| zpQL4s=f1QA(lOq7xgH7ukJ0I&^Z>YGs3NQp?5k279BG0_f`<~cOo+ZgPLcnlnxZ_O z@y?LsGjKz(Aaj7E6-PYr9A$XYo>Cql+QM|kX+8P8>L3364?9*vU5)ApVqUVo~(9E@_J`x#kRz#^i^A9oVeOfSV-Opp|f6p*;Sx>Z*j{SWH}CXH2Sh* z&gW4T)^>Tw+OLgTmC~01;+;RV5#_MX8w$xN;OP16xF-QidFh;(Y)4CTP~_P;5{OC| z%Ll&7<U&$PN2E1&O>H;yE~-(zv{+fV>{oD?Rr;FHfj5fwr`FvkbP%Zj6oDr!HT%4q3Sm5&5s10;?I}OWWD7*mx4}f_FLOBRhLOFa!xGW~4`5kXujeR8L#Wz1lXp2x3 zp8ww1J@D@^NuxCTwunI7xsO<+7==Us z*mu&TPHK_HCl*R*Y_v=6nG_D29SL8UU{uQAun7x+)tPxFFJ{daz0^F+&LG{dIl4OXA=8d@Lpg%hz?H7xA2|LgISrp z^6(lf!K5UyY+MluIRq^h$HSR*TUy&yr|sF)qix$w!XC5(#Ri}(9G~)Y;TCNx@(R3GmB%OLlkWU;&m(*& zKid=O&bG%rQPmN6p2A6fJ!6-4c!ILzw&OkXT+&NkSC$Kqo;tzB%U4_SIrkhl+6mvg ziKRRKkN>~}D>l;dS3FVvFz`;=qrZ=}r{KA0FS$-3@~hu8m}7y4ykNC~HqjQoBj>$V zrQ*B|<*NAVwpeKOq=|9$_a|G$BlB7s`h6bIf`>HNKG<7<`1l z2)abr$wgafAuuEdR%I^2moiks-8?-{4PSq(|X#(H=r`StoNSIq%Bz!JZzc3;hvZ(%H7SY!iK; z5#>{t(^MW7#xeAxEYj9|acuZCHf5y};o|k23#{k%4O0-L$~P7EwKmv4`(V;~=nfI?ow;QXjlDeFFKvfa8I$XH zAuU1_qOtMl(m;#x_Bs^FF7Koe5f?V44Cti-4pq+n@`mj#ZCGVbjppSz68s2om;;93 zqEk2@s|+X-It5RGPC-#{vEKvB0_OA(naZOh2$QiS4nQHX&jn?|#gKxacs?)?X(uM2&BZ<7V~Rx2$j?ObUWFG+u>q7x?GPdmCEsC^ zWdXkDLv$lSD3a6X2q_oJ`CAtdZ3!U+oGDZI$%fW<=nM})uV@c#iT0eUqf}M`c}csw zv+w&p5LOi#d;7Z@G|bmk$aA)C%0ZDLVB-GS{^3584cRxOQA?u!_*T>%IOcQcj0YxE zy)CDTH`_W;;cD;@FZZUxnqo<_=5>bIQV4I?roo`zPy&^r0N1 zZ{h+@%M0AN&nLVRZHd0|GQHh@GOv+_wjao#jK1LvG~^8jbgsCt?2u*6{3kMim%K4~ z&GOg9l~tQrTy#ak_-E4G_smJSB3SWY5WaX7jB6p`UI*ho$1QjMO@{8@txq+UT1LFUe(G7o!o{;)GH((1^SPya?9W4|L=5B20yxgf8C5y`?P| z{_F~#+1OGC!u)dc#i6btRWX{4j{R_H#d@0??caRyx=++I=Q6b}cTET}XL6xe9oHb? zeZ;XVvIyA;EWyq0R^DsC?pGAsV8?0a0y`SqvvSYpH`EbQfPJ5T)*4$mL%&`f#YA<* z!%Zj-yby#w9td8BuhdXkOpJpg1dNbV2wuWa5Ia2`I)i603|XNhOr_KR%!_K1b?o- zay>v;q0r%!m$FU-aNxv$#dk36OJfD;QC$`Jez^?^sUvY9dv@_DFCN-(CxyHKt8mys- zaCw7${@EKg-q+{pmR2?HB=ne1AsoXbHouwN9vv8Pp-AKcVO% z$_1svSmVWA8Kwf@C#-x_Cwn<^E+Xm9r-#0jj(0pUKY>ype1xommiMQoJ+$R~s`Pv^ zPlAU+WCo>(r-4F5$uQyV=W3yBl$JC+r%dKYP)MmP!QB;%6fK7~_H}joJ@BS|p-foG z8F12eDb_3>$WR=HHuS3bnd=u{W20d#&#kh~<~n&2@%{$92`1i$yI4^m%_QN8S3+7E zx6MQ`8daW0b|j%EXDnwR1W1i<;A&w~Pl|@$Ovqp9>9l>yl#-xkyb>AMgTqa;r; zQBPS-b=AH<8%4$Efj3H(>VxIEyE|z2J!yQkr(KPc@NN(Rca=X5N;~uuj0rO#@F5Fq7KJPrr4)q(I-)rp&MZ;-154dnhye+tcWx zl=L>eHOh9TPVNZ;XX=T2l;cl4OSC`e0`5FH#V2SeoL{PdmXQ>H`vU@Jrzn zH>s0*1^x9IZHYEI=mX{EG~`FAPEXLeY4pZy=;Nt+Tk#xCC* zL=T#CLPP3Mnbbo`I0PsYjArWd=Xw@xk=#>zsH^Jl{`6OmAFs-yR0{p}o$JhN;H7Yn zW3a#UOph(g(qGxcnxY_Cc%s{*m2D4Gk$zpmn@=i&arY%$x*8g-P43X=FZM)(Brxk> znqUKY4}0=-_U%$Hak{woSY8k-2gXYKNSJJgeK?ARCB`fOSQT-4+v;tgqrqBbZK^q* zc5a>B@_}ckaPMntu!Yfn+miy}aLB&y4r^~?N9yUscp0z?p0tkykH*VvZ}DB`SRv&( z7diru!$V+VUhjM{1-serK3cV@1t#u}q&W6%ac0|<$CH3T6%&mi7zh_pHt?j$_A;DZ zyC0z(MkD><6W{D>(4dU@i_rKhMs39l*`ZaK8lJ@6dAOl9pMEf?P4{I zTlXh)IJ=Le{QP{K=Yh1);RKI!^v0?#Pqrrwfl#)IC-uj3Xy>N}@TW4MV+wxS7aj|^ z$3g}2g}}}-&+a2B?_DX^`6M`t$7$OgfAv?E30`M;4cdms0Z>Npn(Fla4m@c+?~3;_ zoz6_KpVAJzXrn6;>MA@Z%1W;&@cTHup9E(o@YN4B$G)$Ig$BS^Zr!wJMn_d!xH6bs%A3XBlg_~4*>B?)*cJO@wmJ#Dfb z4+yUg#Y6}Y@2={%{%1dN%#kMv8?usD<&B_7m=!jx@e|F85w)cHA7)q{79@Ledc0YWNv%gR>Ik&^S4>&wN5Mv4V?iLZK zx6=;VTdhHg1t!9KfpB;xKw1o(+c_)<_X zNcl#Y4BQlG`cPSgWvV*)gmJS>@Cc7WIk>NKQ2g8@of`p_t&_p+C-FLdSZd&e#dKDY zaKEm)!FcDr&owp5qB|g_%xg)*n4m0{cvcZxn<{JvZmNIX<$&_Fy;(81kn@KZZP zLC1r3k_Nh*;%d|k#HwNG|H?zC=tpmtm-1I zZzvR<@;CLK;6Z8^`k_p`EyYzXlOG7?C?jwO0TfI59{IqR^!&xMs1t}f5bCGxab6SM zpcNO-oX)hx`+|Rkhfvg+`J-Hwc|rsFCkz+*HTmtd%xxO!tJSvm<9Kb_>2QfB~mA(+6rLo2aI~#oY?@*^KY5gU%G-!ZJh|jN` z9%6^m!7CutMVN}yfqR5Ne`})+w(087cYSs@#fxePDNU_y`%>afDI1gyFS%iic;j?& zrk3wfQQ6c1i#9jteC6o#P&@;Ua$u zo=6)?Bk%`OhzAW%>rAD6u1yJ!3w1=h6Gv(ZVHY&aJ_wqFOVk~8axZ8ec?ul;PAE+; zQ#TM?w3EF2ar1nc0p&WyXGNZ9&-u1wSpwxD9~lCd;1y^WZ9A>=gidD920jKk|8g-6b9OPs#+8nA}+>mmU{w5q|ve5)<+}xI}p%!OFCq4S5|K z2R6O1YImk*bKPRmm4hV(I>(e;Y@iLRz9(>wsN1mTqqIBsC9W0ZzM$UQ$NoLQE?&E zsTU>TAvm>~y7=ulDn0j7c?BMEz+x&MaAJzWWw1W^%>~{lrIWB3^$RE$8JHWqS`Z99NRWf{45ao$s`v91g={ z;dIiWzHVDy6Hfa5$8XQd0}8FZ_*l%L`oR+`^t|Vh){}>>iu)f6@92-`FdUX6u07zB z-NnFRA4xth53F*X@jR63d7ymqg=I>f2yMCdu(zRm(VjAt!)*ZNmdgiE275zYlQp%~ zSZxElr8U@gnZMe##+o+Ob7bpVOG?u$dbe7854-vqRKn*#41LUL2N*U%5BGvx*k#Bk z7`zM!oe1G!%@bxi8@hTDVA;CoQSMVzu0NTlO7u0HJ`RE+dpFYRCkl-lM zfwly%iFQtVA`EBgO}+F@mYFoq(AJZHJhUhK002J^eK`$jr|9cA2c8L(Pa5jV-{cFg z$^0&67e~gp~+0A^blHHjC9*gun2b5>NYq6=x)6c` zH-LR3c*?h~Uh#t)6Hq+hN!#%>2#ui>p$CRFt6T}ip*T0?wSjl;Y_GZ5-}{=!>(vJ= ztzDL2#^b;>CxPfF8RSq3(0frA-7y zvr`t&$ckq%za>Srwj<@W<2&6ciwQGUdO=`TaqWsQA1toe-KBM#k+^Zj&y$8QEcpA1?bkVU#EFYAZ_ir_qyz(zo#pime*1M3K76_)GCz%RlU zK9ISWb>ouDf{{na0N#`G*xx4{Cgn`HOP6vvPIYxyT@BVEFP5d-xXEGNsvcclr~YVH z)D`t4F5Y?}nV}BamFePH<^fxks&-_a6Uau!e`oT0UGY&ojxRw6vc_`+KKDb}@EiP3 zpXc}DTl`UnuE?M5i8S#$7zp~teQ1!@csUJaW!V7Vv)14j6mph*ije*(pVPbG3vPP3 zqCM%UkA4W=%jeM_h%Z&7taf+AspZ>BQAZy@0aMGVAY4pSs zfm6}XMct%5`3+wbJnFxdhw-_;BQHQg40*Fvk#J|pt$riRzym8B;m=$Jzu9q)L5Z*> z)Wd2|EP0fLdkBRE4?r`34=+z?{e%mB@|#b*1?YemgwRI;pcu$|;>qA0C=I+z8oY({ zO59V?6@5ZqS9=I9J&M4MXiB}8#p*AVA4;RC0prbl4j4s28Q_I7%{+lO;bP=5{R-)Y zKLS1dR zr!}hZuGUjU(%D>Z{ql4;uP5h79xX}hO1QHNo_P++|NhjXG2y?oz3cl%uo5=ZusR`# zm(;C%Gn?D?Pv3vw2T5{V{fw?fDUvmHET;{#pQXLN(G>^#S(-&alm*_C+8sEF;}$pe z{e2U3RD&3*T^-Y;dh3;LtG(F+FLWl$c@c)GNVv+eOulbQVSM-9DOV=TYkRgZwP#yv zQc@dCg2W&Jg87N~vDlI97XwV8n8S9n-r24_735&ctTM?0N3O?_8ZB z2oM%wp)r`T%BU+RV!ceIV;S$BJdxdfj<+Tj%)mzoF(?Q*A#h?(h7VeO$NCKKAo?7j zZr?xC#(P<|Y-63>v^(OFdTXxl^!h~s@ffGL5(2HWR?*&%>d;j=lKXzF!6KZg4<$xB zy)LICh6s$0KnOb1{<7c=S_F*~pmFp;2r)jR?HMKu`tv*UlZJ8~5B*U#-=Tfrqq_B- zdyFCP8~uSh?WBI{^x7zszXE5W{ z!5P78k(c|l6ATMpQ`x}>(XV_`CMgofv(NR->x?!;8K=DF`LjKGM!kGb@8L9U-~&B? zD3>-znxIkC;jPepAo52U@ho^UXh(TLyGR$F3l|WyW$sEcuEJ*1V}DzvA4MXbn_p)` zC=+jovhYxR{|X90 z^-#|A{QSu)cTp02;{9ZKcrD=SN<_vuX$ZrClb`-8*~pXzo(T2wie8jq)vIFz$DbPN z6GC+5v0d(Gw|aDLqufH`XzXfVSl@SRp0E^eExR$?YBMVmoD$r0@kX&~i|`YZ-u;~o zwj@u+k9L-j=U%sz%;ciH4k6W8Uu}<8@htXij(sGmACCy-0kdx&>|0-R%(eW{qdA|0 z`QoJkA+Kk<6?=1L+4@_WY_zjQ<<{G*6wAMO|FK=_Y4@|fIB>3Ab+T%U_K2;lK?8v~ zA3{?1<_S~b_E*{SW9=e%y-jT%2nTPvEsgXsf#e%+2yaD4?Arb56}$6rNrG0^!IqTl zBhJbRA#I1@M#J+i*en=0U1~Zyq5M1sR#-9&h_YZ}RTbUgWy(`PK)|F?6!TQEJC@(v zPhj9Opd)OOI1x@jD4!mY!s}F7S;HXC{2sC?aP<&~`h?%4p`9KoDRAH(P!4BM4@;t3 zJDUAPOdM}c{?_&;>*#8=wIwO69cU(@p+CwW9O+R5IOhrGA@+9n zt?E#8G&W;J{nUdXguXF%6L_FxX(wR~$6ItrB^19?xP)ThnZBMt`FRQ?@C;!Wc=Ie* z2~nmi0|YEc=rL(S`Ni`fP0%O$Sc;~2xmDsw{s>z}7%GHq7$8n3;Xyr=&gv{rSkz6K zK!(03GvMrV%Ai~_!voP)cuLq11`?v)9(bj|J3ddJ)4sF37Uk0q`8;@ykl?8>cbs*X z@6Ip!12@KI(1E%_5kWud^wnZTnZTLn`t!c=_NY$od*5SOW6(yKQGa2?QHMW=_R;S? zho_5bI6|gppvjtB2ya%~=WaY>mx}OD%pLHC(oI(?9M}kL@K%QVdR+h`*a;osx$r$` z7d+17JVG7C1!BmS*oe6A-T_7{=^sqb*pvjmZ&XwrAci6XsGA9Xgg2AxVJF@V7sje;{ z(>FKP7;j5#Vj^F{h80}RSg(F%JQhY)SrQ(+FsngG`)f8eKnRg_szca|f>+boSnub2 zwc$Z19ZG9MIOi)lebb|G*@OYqfxB|MXb;LI+oS44=p#FjQWNE@PHCB--(MDSm)2}~ zX~X81Hf>(N7nY>_qzva5wrqI?0mmjL<&}bi$tf!a8Nc%ii^=ahT1r$rj~kKxH=l(tYlEgB;3jjnq*mgVsGK zvBI7dLzVibR>ldgy=T|OhE2j>guw_kcw5~Y;uE(XTv@dOKdE#o@Qd&2Pl|CGMVWz% zF8>p4_|tEHUVJa(dy-erbiEj~R>GSMc z-SB5oTeO|uj33gUV!-!kbLNTbw9EwVN4C?O{#^f2YCz=8GK2m_UWc>K%XRrxJO?5_ zm*~P0hKD@@>{aQwU4fHL5sT*f1d55NcILVH}1sUFG?+@K}x!LSI=l1+~_xB7cudwpiX zo*U}07e>46-MLkH2z!2HjyTj4H`p1OzaEB^YW$W7qzI=B@glE47E2vmug;&C- z=%Pji0RVruwek#gHTeDyUTZ3w!QW|8Cxkfcu}2;NyViZVul^pbZTgWafA#Y>T zcOR^H2SBh3-EIDoEI;S#2scuUaa3cI>l=1|dCh+E=BTu$*~67hn_Atq1-Y;rYReWl ziecD=-!A3DOSPEmU;=(}rP#EjXUmV5ZFhRvwjV9nTaQ-l&Bk`N4R8u zR-VW(Xu>npFI`yg+q>IR!gJQ!+huKSEgs8d=`VEIJJ2B9TGzEEHy%&8-i|6&ewr=b zC804S2JCP(4@o+W%Fi)c81&KaSj~Jt|J*OpGM2rv?$J)SO zSYJ194~(PSVKqDo6cBAhICBqWgwV#T$jN=m#5>?}Z$m;FCFHBPxQdlrl*@Az2MUJr zQ7{Pqsku21VNnJOt+Tz|C)J1g`rKoILQopd1CJy(V4_e)`};lI^;7h2B<7Jo^qJHqG`sr&IkFnO98nF7S?Phu-#e3N!RRBkvv^D5zl9n#=&#&#->JHjd!Wb)67a_0B z%D(-^hmUQjy~##8o9&u91utoObw^4H%Tz9?>R@RxVKNSUWQRO+EVKpBsI9q1%BRtW z+FET<0v|=j{ulO};MJhKdRo}m5*r_RPS_2FNSN*YgBp|V?0C*>5W0i$WI4<8sO&O8 zk&ymaA2168iGhw~5EI7vJYP^m7z%)aBitY%z2tKoF7whcb|#^7apTU2ue^5UPn&y7%0V*2e$TO&dH^BKp8k{3Z`dfpnyC{PimZ zHEjWWS!JUHlC@aPvFM>x2uW67jgAeMXhC_Fjv;J1JEvcWT+hf+eJ1Aj8Faj@a3!UecTdp`H+R{PSc zE!Ndh2yT_nv-Qnm`;}kYwA=R(q9tL@AHS70HsVHm^EvGaMG){Cp%CI??teaT~f z=c+Rl8RG!O#y$3w)I8hQ{aSl{gCFz$V0qoXbYa*#hLul*tPt3(YCMAwxH=@Hq!2i< z7cYQ8iwA@7P2e88*vl8ng}D|s3le_fV$z#4yd?K&yO64~K6xv^H}B2Z^??ri+(f^K zjhiQi6c4cCioGKldgFa4yAmoW81|Di%9?FbL*JfT@+0Z5_jM|N5QX*-KI0T(TEm9M zk?tm+;O7}Qaoh_y-IZWk6OpzwSXWn${B3be&K)Aa1TbJ;2WKC7Gz7J1pzrKzr=CnpnE*J%z&Qm33XIBJ2$SitB$o1HP*RR# zfG`o>C_;Y@P+zGn-~+x0u+w|0TLriP^R@(tQm!;u!doL+QWrpb+B#bexv;;#ZS5Vc zy4r1*mxqZ@2s7|`$iPs3RpP>xI^5Whu06!#RXpS*nIy@A=lrL$Q?LE^b{ZUVu24sr;$x?AX%Ay7F?Il#EbtlcCiq zVa?K56vg7oil5^}Xo)4fOnC1QvQ_b{h(cIXpKmf0pEGB{?sz;D0I!A#bp$qo97RI8 z$km*9_s|*wUC#+$u@Z~<6DIfZUeasglcVNItAA3A@`Pk$H%R6ksXegVIA zk0r@?ASey$o?l*ZoKPOTTOQ@Ht@b1y;92zxH?ACWoXMmV35PKL@4xaXY06_i!FOss zJb;~peH-pjv|oAfC351YmjCz1eNwp}x4eZ(|bNUCs42*4tFM+{|Fji%)3Ll=aBpHwB@!F;{0`n&OvX1maUWN#v zws834`wJ`f(r~Z+*p+c9oR}k-r9IM~Jb!MaL)Nm0dBAdEDUhQod$+#DDk=w9g-8ZK$b}!?mg{@6o5g)gzS;5nsoCM z%4KftauVuVF{r{~OCfL`NJE$^>cN_+cSc8OMLuxM=K=01=$Q+)LP;?27=S+79gbx^ zv2MP>+vD>bx(=#0)UJx{?Z)vDHFX-aV&WM5A&4oTm2rf%n7_hAV227O(`?fs!C8q&px_3~N8GTrM*L`T=4Joz7`3l+>eH3jEPnhzkx4i$N%oowl zP^@S35Vt?24Uo^_4X6JJ&`#l2l=*-7U-jC*@)I&Jn@`96p9KHgzd2>U`kQO+Sq5$5 z(8i=EV<3-Zp?}Y|Gn4`Cq;H@DP;Mvm&y`{c;EYG|@_UN6F^QEi~DtsAIKVp5bA`5K~!}+DZU^LYz zoaln^C<<#<%!cR(z?rlzz;(g!5%vk|%B?)|sE|`@Inq(EcW+Tg@k5Np+{7 z(JurWRy<|0CnUXuz=yn17EB%Oif36zq|+t(>!GKKeEd8ectDn!<;1;cFN1d(Rr_p&_X5V)i=;T~-{qbQ_w89;SmLHaq-N{_xb`) zKRwplskB~J;2d<$^3`tgMiU+Q>E%&m{O7;Ja;HBilRBdwfHp)33&qFl5K_^AGx?$% z7j{LSxL@>j>7sln=2aJIVhM2c8-f+Q1E)~Vz?r;&fABkLp8zdGnP>fg3u%>!W&4F! zTJ6=Bo89Yp_2niT8DdP8%lKqi+c>nI&RTo+T7-tyLQdx zC@UwsF_@4!$RSoy(f3&XZg^2Iyr7G}1$i_GYrFvfya<8$9umS=x{v1oExcS^?xCoB z_@KF2^18Lo8X7fH8iZ@@u{g%MW zbvJkL6I$awc+sA!|N75=&ClAxvS;W5gm&23&da4jftby2?w;&y&wV0Ot2;i~O=t+E z!IAGPYHYK#^o71=Ym)U=N2d$eEssZnkRdEKCJ%?-%#lFDv7RP-e!RnGmN_qm2GBWJ zCg!`)Sp~+)!b@BGK99k3LRp;c)z{o&{jCjlbGSo;R0>SWsNMxw0F&rs7*U;x``yJ& zn^lV+)Hd0HItqrEzQ6I*!xR~OwY6bc0|)H0K*$KvdisHOnTv7U3e+DRg2xf}&hDf> zYmp&LS&4RPtuo($M^g5JHg}d?nWRSp_@+YrrAI=YZec@o0cHBYJO>DHEVBQ9JQ6Hz z1}Qt+%Vo!N+R4kWb{t=X2>5#2lsA}`ehNmdDBV77P`*iuQ zThOA*$E{J8J;v&>GESkW)HeNbA4@-%vhou zUeL}Sdk?lEO;J8&lD}*XH`TNJo>wrCABD#EQ@#Kk&l8$OoeAZg?Ub9(QI=6=)CEpY zv*gK-*L%uqs$YLW+aqgeII{oye=uTS`$C&P|I@Y=1V3+eL{X6L?&HU24sB#V2v$F@ z+T{~rPrh;0!DKqijqzG?`R~w3n|1ozBYF3U6wvOzlvZpm#N$9|FwfK5)!}1`H00;L zKd&(ES>DU5MLMjdrc?T%-T+`gpTEO!Yr>Po0vK7jv>2%RcRqXbc%;4AhC7>muLmn- zmNxh7(aMg`kL274UJCm>zWPj`FA09Iur0(jxNC`FSJ}O#O}oFer9sagks3d4{$h8F zbqN_x=vBS$NqAXOIINOlZUsexfOiETAx`KD;2G~n;Wrx>L4)PJYw~tDx*i3>DZeYq z&wEk;e@{tUlP?Lr+}|cOA*)jSaC@FkiaDRT+Im|SBX9)%`X=!|h$vPRYDWiRjzabz zkYUgS1|y0jCc;Aiz<54b^+}3gc%>AwzWSz6G%y`$qReDQ@`Xi@;9*dFyiD+j@CRvw z(NP*1XJ)7EdW5)q&`8)(mKvQF*m*?)1fBs|CV8^G3>0PYGI*{y8o_qL1?7IB0!iF%+5UWm#R4p?s-&0jB%lGSpA#Yjha zszmujGyPE?!Xp)lD?AC$82EuRSrS~J=nC2uUW9N!h%=}I5BetRa)G1ge5W30mi-G5 zTJ?H$55*DTucD8|J198-oVW+AQiuVpN=x3LOrhYyD*G&-W&e9)&a@}Y!PBs(_}2SV zcfCEyYv34d&3ec~yZBAn)CnD_>+*#L`=!6oWmhjDL^%Ml-a6V&g?_^Q#RUY6+uDF$ld){W1!8mo=IxVRp>I~YVuwqOlaNoy=(zy|(yv*Hk zUKev6cqStfaC+uMku0yR z`Gsf0d`EX@r%$#sHhgj&4?=&vGTb{ln2(g~?P<3*$w3qX&wOdHF1(r5wM}n#Y>0&K zB7DmVER>5!nN&8HFJ{_fSw81!v0S*x>aE2z3jMiU)j%~RjO}?)V-9GR);IW3BMuJxi>c9_xx%|HWf)YUuev{VksGifoO$s^# z8gW0;`Q>SY@iUL8N*2%SgIiaYN&YgBk5xDxwo;oBGVYx)NIZSy^ExP>8XbYi8)=I6 zc-;wS>mnb|qpWOOUbKRGsG~^jWySscj5_j)x&o&Vw0KjlVDueE$74XS>4xL#_$VJ< zjxPz;bA*DpL+ynI@#(@E#f|Xrd;wl>@fSc_LMU-%KTt=uE9rKo%}Vp6c9d%J3tAvt z5WWmflnRDGDygXd^x}8m#&zzQcs}k2cq@wb=!@Trv}fBB=>l&rrf3fkxJO=!$o6D9 zx28^7Rb{O$)iyp_YgaEe*!9be_WX@Td+BDAz3^Pq86~n^X_7(MZY$dzwz*p`;fRo9 z718O%LYDR2CQrk)e-QC{V4$zgZmR4TZ#LRS$*}Tt9^E1;AT1R^xBRrNG3ELcbn*H_4${v?q(rx_IPR6W_iV|g#VA;8t_$OpLA$r ztB5uvjoTnXae6Szf-_9@>+eiOh)mxrY6}GTg-?z7s<69roA#~423wVX!t*s*Kbxxv z;TZI3K_+1FA>4>(CsW15Vpu^5gtq9H^2B%SG7p^6qrn}|4g?tRX~~i{C!)`Tdo&V^ zJVywThqAz#eIS%`9x_iEVrP7m@4~mp=T^7cdUjBJ93l(`T~0k3z^&_2JIa&Xv>hwB zxuGKzLjfUV!%9L~k|)melC{+y?{GMf;Xr$%%m|e+pc`aBP&|Wud3DjYw|R$72+=IF zzP8EQn|q78Vwp0$L%Ru|p}YWoGeGcwJcL-Zjq)O_5&d4Cj4y+?L;dafz*%|1imy2d zFP;bTfD?EoxOd_7AsiOZ8|qs8J`f?A2yG!WxexB}Yv2rSF*imT+{2?un!5rkXhS(s z-pBjXP>nQG%zzAKv?neKdDjeOYJE_I<;iANWmct3RDA z2DU0)_PY;%(H^fq>%YHp?GNlq-viMtX#FJk@Xo&dpa1rfef#?nJ^|06 zvJb=SV86}T(AJZHGQ$u%#U=WNd=r4_x36c?67683|z zKV?x00m3t1!!awZ4K+3{S1uLF{UuhMRcuYx0F(UKWqTcMwjnEMZL4CpSB~ZF5c|~g zUFJiSNr*JnGRaJ;Prsp8>=TLK<>#5-e-(9}!dbyW zIpp^;^aw@3%cb@z#96`hQ1p#(6=h}!2cSm2ktPHimdNQ~^LmTA$;(Ar0Ba`}PsH=c zPx%@sx{oK2?I4c}Cr>G$9^LP1Z?-PE)cWao`6f{zM7dkr2=tR2N)ve;e+iSZ3q!t# zWMOVWI1+M!v9aJ9WMwr-K(B?sRDry&8@IYB$UmYTv>u@xil{|QjsQ+Y!uO;tv`l5{ zLMB^Nlot?m1za%b8TCYdjD!##yA_VxiSXinVSv!K=m_(y4Z{?SEJe<(^7Px$0r8U@J~*#Ces%q!>%Wz7S=HGlsr9k z0r={ZDKpwQXHE!(XX|n*_i`kOOM?aCaW*(Qc9ZdTS=L){FsvmH4{&tCuO06Dci-{o zC%Il^Q3m&xMhHw5(eRlfdg;zeTwPkv-o6hD_Qi&fLdm%DutSQ5^7p^-N?cxA%Xg04 z)sr{!%BiGtk%KrfGMWy9(FIq>Gj+uX1XaKp^s7?Ka#r(lF05A&#@YzeT}+#?vx_wv zt~=pXyo{K*95Uz61x%MsXef5LFXJKnBYJ*iARrSR1kQU>IqLCkmQ%Y?|(V?gGB){}GW}g;C~p?t0A-WMkHd?843~a&8Y0<9XWTRr z)iG%|&145ZG@GA2aas-K;+g4~o8k}_y=M*=idX$|B%2s4k22bMpqCqe?|F*eR<8hyatxQ&vB zDJpPt;IXEu7=f_Z&S&a}(or9#97&(M$JDtmV9}is4vvI-1Zf(i;}a)BKG3i$tbY)z zp%L>>|L}Jno}V3#o6Fm=vcX}Np18HP8LusD$hmM}Mh!*10f>b8OS@Znuhsd9vH0S( z+xb$eKm375RZ;dL@5g5@-%exy6-s9dVO2DYZy;qdGJ-y?`Nc!w03s4Z|<%x%ho&qo(j1*GI z6vECAl(UVeyTgRvt|ySnrS!QT(8N<7NYhK#eD7y2z7eRfp zo*kU72Mz1^)E13_DJ4G32vH9f<2jUwQhG}0sF4CNN;REOQ(I3yd@fGTOenlJp15>> zJaPYhd3XxNhG&avGz-fs@he~al18Cx$%&V!tKrTIWPWXFbV|_ao7nU`2 z<~CHnHvz>T9N?V|u3I(!wlU7piU2+(Kg7fJ?WN=cJRTdK$*HfJ-l10JZG%UD)H$6b zdLiBpsFhF$!rXE2G!r|{aOlX#{PV)O^~y%t>A{J7alDpcyPVtvko>e>!0+zV79)fz zvpd~Z`bE5*o{b@Tk#y!;7YnC#o%-b?{pjA9z=+1Q{p6oH7oYg>gcpsOI_`(RiJ}AZ z>of7$SKl3XR!@koo%ptk|8tz1csAqudMWpf&f}ZG-T|FZ>+d)2d`G-6|6z3$C)BR5 z#5*p0J*G#OGp+zRY;7}*=lcKUzrGOv@Ml+IX_X5D)w)$5>ViCcQKa-U>*+GGeTXk^ zGj4P6#`5dRtN42~-s^P8(DLj$EH;#w-wrR^QFzRImkEBC!P$Zw{SOZ~p_2 zJr?($I+gg_Ba*4v!I+t29fB!vjpXDc(n06}eEs}RPC=1(WQ4`k7QVC4j~=np3`fE!A6z&h>^(Q2Ga>?*(gGi4)J`zIvqmyArS|`Oo_z3= zOf?b6*$}QMVNuA_5(uR(AH%TrxpM> zuH>i#q40`cuArx%&!~ilQ}N&=E!%vl9HSquGvSQN9XSt1D$vOp6`UJRXkF>%)yuuI zu^-pv^26pz>PzjFbBFopKycIy)o4ipX>#4$w{m86FH*;&NUj6 zt&|ek;&^5z#^a>o*EPbllT%&tpGK17BQne~{u}GL{=xUGy6ET0g?S8b^E+v2=rLW^ zA2;f(xyoge2fcf*Q>7Dc3;fa&$b4 z%UpZj(~O#A7uFAAPd1BsqRa5SWZ4Bcr~egJJcqX#*n|Ta#~BKJ5|*-b#wH$_TGoW+ zUlV6&;snZ_aW1pzFf{qWL+_s3G8j3qONw(STxZljaF{m{kBzB*Bb*hH6O{dZd8kg_K4#SW|mh27Ni{N2U) z1Mht}E=-NbuRMP{Z{X!NUM^o`$JLs=)|i+>Gx)-l`MiSV^2$p5k@r8AySKi6V^Jdy z_AW`BJs0&^Y-f?5=`w_JLus$7;juB3w-L@?7ehs- zrx#!t4v;ejj3hit$lipWHV}@$^Avfe^fpYG%baxX7LsyS53~RU^R?fmvwd*zc&aIgF|B59v|uk$0|z@U~vLUFtM?CHsR#8Eh@M|*oP zK$$fnp&sUnX>cP;MRxg$QyIEj8wO}SB|j6_F4D!%IE`hd4fI{@AA zfq3|^vd-N_8yP-4IH3{nXyT(B>ekka(wjC;Uvh~a9hIT^XxWC)FM=$A>QdZ#gaXe>lip3+7mN0oab0s+sx@E+7b)syy3MR z$#KAG9barmv&h4)$%2{^!~T+IQ*9ZiMO)t3kWWw;bh3a?I-ut-eAkB$aR7`&>SSsj zNL=2|^Ba`JsDMiyNA&qOKmURRJQVM_cp~4`4)ZQAs28b`1Y36*!EnXN_WquF6BxUm zxU0gMcjKVozJ|b4D*UOlbFsAH`?9zka#X}mF<+?soN9l|xhZ`&Julx9emM`e2fw7@ z@Uo0PqF7oxjD=NtUNU^by}ONabbAX3mBxfn5he{v0j+oowi;UY7BHatOr>bj)UK^| zd>U=aReV7yaRElX6q85T93ilw3JGZ~jZ5}Ynui8}vSeQIQD2fCpc5~_LleJ?uG*PI z7TMgednhQ=7{&r@42zZ?+pG`10qFdw>jO3LTn?r(eVyE1Hbb5orUH2{r9 zILVv6A<;z|rpTyJ>C%`8s?6ljy+!mr-X8rJO#8A4dl#>A&i?+gkx=L6$csgiD5}XEA2_zZ8SLM|WfW>xRvPi?O`- zme@UWNHsW(%2XBW8nUc40F za3s{V8!s72mdKtdEYg>3NMC8HgW-6$xjIv1{+h>@OP80#yG^)52xSGdVbuDtmjljp z687VxA9y(4@~96q0_0_(n?-GpE+t>tQ`%4-Cd^R>j>e9Jh4uF0IkYXWa>YGsEnE<( z&bm5%v>iIhOl{#Lz{Nv)MjM1f>&6yql zX>{WXy4S!bxDa>>GO?-z)uo+AO)bOuD+S;WW?DH6#CP-fPd`!^sZ?1`3$GsH$p=t+ z<`FnOw2Ak7%t^9<O3q#a@-P=8##9^5$v9Aal#k8?ikgdp z3LhC9mJ=C^>G8=_cpB}ddP?lZqUXlWqjK)9Z)5NT#ZSO-C9B3zWc^$5SwHv6aT>hZ zmoQ|&x|W?o@u+Kv%0YGT0^lwDdZ?>)#Jse#Zx|<_=qkVRn(x+&qMHu^1pugP1vfc#c@^w-7d>D`6p zJvlG_K^#4FIwD<}{FXs-F1#ayyK!dfZkBmw@=nKzP~FwFgSfl6pU?BJ?8fJw-iptB zc`IJI!l60WHq%mn@wj6LlC72@DTwEbWmfIgxwGcTEc21Ec{Kv)2G{xr#oGC zr7@n$s~`CXq>hQ-Oh0X|Yjm-^p3k;A*GyycVEAb_*ib%cM~p^1LLj`s3N1-4b*H1s zCKlONIRu;n;pE{nAN|p0c>x{qv{Aw+C)Zu%JhnAB;01npiK_AfjCODyp3ZJ$0fbo7P$-AbfjE(;k320#@4QJ^zf7aF*Svu{Q!!YwjTcPOGLa zntCzZ$ELOb4C2n#LOPM-Fg$uv!uPwU+mMDvG#d}oZA<~~i1+l`4`lGi;@wX?6uf^3G@Xf&rBK&eD>2-VI|n3cFBnG{@n;`k((%eB)J(LZm3U0N!i}$dQEj z^k)3X-jL|5X(ET&S;8wC@9`sGQy1^Er3tQ&yiHy&U?({0BhP0J*!bXKw9fk$e!??PBd^MXY}zue zc%bqWc*J9z=o1&KVyDQ1-dcZq;)j06d*egzeJJxVLSW>vy6&?yj5avWLA$D{Eei-?Ig)YNSVkw<@2zx9UXQ6OQgAd(O>sraabT*9JUQZ!Ee6=ZMgA(EPGT+QQ)b`y zKX&Ppj2-|+Ai3RkeK+o`sRA-wJbBW~K2O}Kx8ehxq zEG~I`=;TDqjSl5I+j&9N{nL|iZj#qb$`cC*C)-(^=V}w~{My>@jcYNG7b-64_tm96 z4O@KzeT%aqDgy!4Q$0b+BR>hF5z*jmFd(UVDegJ)&`41d8v{3f6oho8P+q4YH_jwW z$#|Pwd+}7-RJ@u-sD*D2lNohzg?lQh3{B46Dh&uT9bW_}WYsCLXE}VjXM^wo;`7=E zg+V{YVt-FgUxp66!$Xr&fC(wdxSSuo@X>t8w=d_wV@HD=MRS8Q4G;Wi9HeRE-7L$kMGBKet0h~oH=d4!4N7}yeH}cMenH*FZW}5jV4KgC1>j6%1 zB;N(J{heJOz@?*x9Ux88ob>S76K{R|e4IZ!lLjB-O~;*%8<)t^){rI32N--`qG$)h zC;>|PWV36{gDHQU2qTF;wMP;#0^v74ILpve>n2nWe3U^NzO(z4FQlm%2u4kO0yw|E z?`s?h4gjOhLN_CiJ8Lu?rXl*`@$(Z&psLyw!mg^`T=eE{Enr)pydBX@6@g;X_yE_T z49B}J%ybswADo$t^OG#1_T){$IIH_-#$r~EW>y|@ZSx?OWMp2DVlApst?0RVZ9O(t zb{i+sH4gQp!E1zwA)qG+pgc6p>XlXR!^%Tly1iKhm_7cM&;cJhBZbQ%og1}q=sSwW z=zwyJ(~a9s3CFUZyuArs2CRKjy9RA8{lya(jv2y_>1Y z3o)#o_0Zr%j-u>H#x%myNMd_MJZvsngT|yAJ;5O`&4aRfPM^bZC<)gkoSB;v4Trhr z@z|yN^U^sM+US)~FO(3)VpM{oVc{KGxTKDEGyH>J{&ald*%#y4*RCktU}q#!&YSv? zz60V<_UJ_yw2`Lxk#+7gLilJPe>oE5O+7mN1b@m0eCA=|75~{r9tEfbsy-Zkj2Bw9uMsZ{%(0qkeXB<`ZF*sj+p1-vi&9 z0i%$CbILC#((^_q0^GQ>7k~HX*5aT2`euCf`R#b^m5o?gP;Xgw;J6dv!2w$qZg<&X z6HUdNOfNp$S-X8&SJc63_{DFZ;jxXdeQ+Lr7Y?B0UpV+qx-6eRXc8XFoaKVev;#St zp*TaP%sk-X!I7LhGnHFz8SSG(I1&s#zm-Hfd6**s91VFmq2)|yyNq~fvs}5t2!>e#vg1VHCmUaw=E8x1AA3N)^$m>-l@6Sw?fKNd|NVz- z?*h>It9Li^Y7%b$eY2s%J_Q@QDifx zJ)KbC9ac8}C1eb!M5AvuDnK?Qj7utqeoMdzzHla!Q6fF12rPDklem7-$0wLDG(h&I zn?{fqB%)lxyRo^fUaaX!Mb{tn5s!l_yzQy2q7!hK^OP~_7#aQTY%Ry0925qXk;ll$ zY?e#TQhI@;aqTiZ*qv2izPlUC{9a5|hULx0&|q=e9jlD^}yi*fG6Ox`-m zf;mbCyz{X~C<@fe;doT<>F zr)bF$N~c$b-XNPUt(28GN!Q3Qb7M5bbK`QTQ%jfST&V|OU9@Ac^JM2x8K!O?fO-;7 zImutusm!R0}wQSxWMR0E~r(!vL#Pkpc&iL$3MRe+!j~k2AF{R|Za z(GmWf9usor(+9Yoge%>ZmYr8CQYsD=u5EE>)DJK5;d<*_Bas5rYssFGaPZFPr8QpS z#I+18{JC5j9Q4ym%erT!3CKW!e%Eu1Zyi3}1DvCkPWY&M7rOZP4xc66EH!1MZ;VVZ z<~)R9DAH(&Kjdp;BX`o>Dub|+C!twJ>5rbGv5U+9XYm=LjuSW&vd+?n9HmI-_BB%3U(%z|O7r^O1PpZ# zySkTC*taKgt&vr^~Fh42(WAKe-StBhM&;ocK(bFu(y&Hs!MTuk8679b>Aj9GXWeO??#yUYu6nxrxzFvV)43Lt*3> zRvATj5&Vhs6LDgatJ8UB`EJ}<-H!Wa)H4x5dEw$FQ&anSA|2d4OD+=-24^#s>E#Zpj807Ll> zLjZc#7^V~^f|fx{1<)To5qcOHffN*+5oCTnIbvyg6>hln4lHm4pFKgso1RJa0*M1> zjSf(jR6x~leQPOuA54vu5(1USUS;}(PL%IwYvWFAYb3I}vzF^1 zV`Has3X72hv@klV=?ypp4U|*$pllxMigOsyNM?9=D)a1!_PyOTJsZL|Cj8^-NX*8l z%Hh7NgTr+>g_U#|94cfg3*h%_H*UtO*Kg#S$%WIW^0lM$%d7F(Z#*9xTb#IOHx9e} zyc?@z0Z+^)47eOj(nurqaK2=5{3-dr5xRNwCHa9|`0j&!F4H|vPa{>C>GWmb8F`e$ z+uY&hLKuUt4#B&kzpEeC74R4=Pktjy@>DwLtGcBLN?fWlyLXg_>t5KjQF2pKFX&Py zLE_M->ruUA>oGVv*^WHH>#`X2IxO3V{-(wcN@haEu z6mJ()p96MzX~c0bFG42t9K9+&fv{ZvICb>tJE3Yez2IZ;PkwbH{)fN653EdJu#Lza4xxvOJs4&$lAI_Mvu(SesXc0d9njlem@pAI6q>K#Ci!^ zg0C)Wc(aW$JdD{fc9rx5ZyVgfgUL~FO-f$%E~dSF<0K91AG~*BTc34c5j?q&#*@l|LBWVP&W8!Rh9?b* zcxHul{i=L^?+Y)6evpH7GW6@OHRf^5W%Y>H3B^O^6kZ^!9j>L}AR zyalg25nY_rAKt-3zvH4u`o17tUrHyE`lK|hD|nQGv!gR3?x7d@ODyOK^oysGV&cuZ z=r^3MFw!8KE{)UyyqZSsmdqSyMUyedBBoaL62aoLoaX;nt1 zRXOP_)n+FrjyjUhzwm%$9r5Vdm(=M~M`c&WXPcY6akh47ZvaLr`;1QHR77+1dKCJt z!q5Ebdi=v*+=zei`K|cu7dGRySGQw%VcVTZ`dpAEblrMpw)~A;vu>r=S!ZPF-_#RG zr_jiXPxu3L;^9}L6Uoyouqi`v=BM$EB(`5A1-?C?UH>?;72< z+)M+cc4TVAgy!!DFP)6@XQ$KHGh!%UeFVeKPN_|%oAkRno&3g$VBpD$uQqZ#<*+d~+Uuy#Tujw5eA5gu^Zfq0~pI8`_B^>VM(wdIX`pZV>T z?HpmSBY+VGBZ!qv?ktk`6EOgVag_>}Dl&?JP@D{FAwGA4P%~*dj`P!FF*!1<^g}T% zPx#8lVLY?CAJ_K#VrlCzHuQaMnfG^hMcH|<}}2mFcT`C z#$O8NMsAG-TxLB=km(bD8U%u@5Dm60j}ZwwF;F55fy-2CeO7(|oX|#{%D@#k%;i?2 z1rG`UFPYNX)kvXFJymvN;YbEptj>3I*eHWM`H=Lr;pi`*ZBUb+GLVHAXL6t(@Qy}3 zo12T?t(1zzwm|Mv)5vjYHEu5~=~Kw-y#-d3`OzWXV)hUS`D| zX&$Y?8x%TePp9gha+_aeKnhCkT|P-h$xs`XcAOvcv3Q#zQMm>XV-PYhf6;)!#~o)I z+bj8GobY67#aTA`m3)Ry%ev?;*%!x49u7k;=x5Puz7WGYOpY*wgHN0adX-^nsHCfU z@!h}4Z`xAkSr3ewe^X}Fo%GO}G&VH98SJyeP)=lfO9pXgE6aC1+IjSy#rXX@e)1d& z#<_U%Mj$&|csW4G^R*XtdQ* zjpePFUu7yH;@r$|yzA0*+)xh!M?}H%>)ZJn+y~~y-)_g00fq!&0gMpng#j>=-d~%O2*P{xX)xR=)9nGh*}Fq9>>gSc+Z5jn@sC3a zM8*z-!ZZL-Wwx(K;cNxQaCcXhRmIc0&TVPTZH|Ih$Cro6)|ow9gKI_gqWGzB?lD>`d*(grtP*V)rJN8^zL@&p)VF?|GXpCeh& zD4eU{9S>iinavW&f-;BYXb1X6QqENl@`4{2ku*HYG__&PUVTUf7Sn@Hdy^i{DVY5iH)=iM16w&7zZbjf|>8 zcAPiB@pz`R$Pd8Vk}*6-hRDZxO1}*r4YV2;B;Erjio##l4jU^)w64YDq6^ks_Z z2|K%(%HlFn@1i_0Gnp@+I)07>=aO6W)wxd`bOseqS)6*uX|T}Fw3!`-9Sb|PSbWE^ zjA~H8SryjDiR*i4X6lo?!Ek1DAWlsV$CivDBaqciIhMVH+};cOw|DTo+>14shYwkt zzq7iNTYfn&!q#CP7`P_JUES}E*EbI0hI$__%`1+*-*PH!-D5XaIV%`S9zHugh30=g zQ5FnZ_M+u1++Z!7ZoGw;0AAvRv-`8FK;SJy<}%&W05@f%kem2St^7=B;bc%$-yZDV z2xaR-|DfAtq_XHqdQ~nDzX>lo$>%T|NB3@(t}%YlV?dduNEja}tka8sO?o(xbyGP1 zxh|H0jUhdKH@H+_%2e;}2sc1#1Oj5fr{BU_a?hJ=7@v;3lAqAvH8qS=Th^8rJQZXf=s2niHI)H= z%C#eMeJmq)Z1_z+HaDQ-=8JIXk95$K5)nH89InUhyPNU3ukFWIp4*FOUfhW< zd}S+c-Snt~Hwm(U?t1G<;>-in+sK??h4Ve@uJ*!+mU>lPD{Z#9ZVsb^2Te#*NCOq> zPs()udNMTQX)~n4N!SL^!SK{$8)NEf>IcOtUn3jR!GG(6!bnG$kRn%T%)Cw5Lzhm* z>64R+b2uY@Z~LVK&31L#l5dNsck?hJ;T)_@?Y`yc;R`x7EYvofGZ_ntpJ@6Y^KtK+$aH*Gj90UWvYBeff<%b0pB@r%a$| zf$M~oBiU20nc?$Z#;bJUa$0C^dTRP}FQ2QS2a2u&ZY z85wgp3_3{H9zA?3j)b&+;Ia*P#g_vB4|VZt06bL%E=JwR$xfC$awP1eYteu)8I;2Y zpcgzMDC^U4Fe(#ccbxVzW>p6s(*LVC5_r0&QTWb|M&o)02SsPUd&Nf}cX%=FwHPnF zx)FcpXM5tGeQH0x{Ppd)Dub}Pv=bX^%{wgw+4kY8+AwQb0Cd8O`d;;Dw<*`1^(5gD zZJmxs_KOMkXO>}(=~L-(L^zOo)h?r(dix^lFqDH1OD7HTLQ8)x+k`bp3IKSO^GQ1U!qkz$=Xb3}(hlfz%v@{&YgIkSN@r0o? zx&L2yfD~9#--Q&Y(zMe7q(yn!;k-9scpRZ?NWQQZ^S77PpnP+tjX&RDs^}B*O%oxa zS)-Zk^>QGqVP%pZCee!VV6hy`~PVr=|uPJ7hielC==D2|MjA0t!s z%_%P$g8t}vdx(Ej;R`62-`u(j9$;%*4F?AZwVcLP9XHqS#Kib$oI8C!9=~)!iaQX` zzV=!=5{BB~p(nJysrnt5rgRV*&A!v@JXZCGXOm1BARc1}ltBQt?T8R_A|6UqU%!_?5wC-FEp0qAdp^&|0;vTX?1 zp2@JE-<&3^+U0%blGB2Jv{Fk2EIu7kc_|dWH7#w2fU{{xbPk*T| z{>#5U6wkfX?2tU94Jb{+|2B=}#K|YlRy+nid6)7f4B41&fHb}(*-kC>s>uT`%gXpj zS2P=th2>f8tpIxAW$_OfpB{*@N%63+Csvl$W8v;5qZvK&X!obsZcp2kXj z7CB}Wx>gR%G}6oe{_lQgeDFOF<)L0i3$^5gs|>BybKG{Vcb^;i)g#5`AZJ zKCfAk6W}FPt`FtY33vSq*zHv>z^X02YW@lkW}TX5Bk$ zTQRC$#`wTM?zke3ylnBZ((+vu({Dx|J7UhZo(&nr{oaEZ7?x7&;XM=ETdIJZ44(+U z7?q&!R&FZP^c0%-5=aTWgh;gYGQ1wr7rugw)8~=AnZ~%=kV-`( z)Wh84=;)9d&u|P4qxj(9QQqTOn<^inliyi3v^5G?WrIl2rHS97AN(j_l1FbAdYbf2 z8jK-EzVq-~kIXFvHszDtjnW3oya(Fh^nx(3JhdO*J!;9hF|53&erI=GO12)WYiqfK zi^GUq@p>TV^;>tW};LLXJW@w8jn$3=GD|hz#o3P}%`{??%mk$8)+)SqD8u zg6XpUbn2_RfeS}*91U{DpfjpShd`~BH)na2AEVBc76-h+$w&sC+X1KyIR~5tQ)8a` z^0|=i*${B4oi_O(+k8=!9%y8=!f&@z^ORHaJ-_FPhYz`2@$w4qJ>Q8x^|5ye$zd$( z`?VDfXEX%el(L+g9*L6^!?C~9t48VDf|vU@VyD*&WWyGL-gVSJ`PCg-F~>XodmuE#4cToqHH&lf173>XKDIl^v3d%-yc zn}VU|&|=CFKp9v>HqF~x^T~(FiSr86sHUkWX=;%gBT7#mI)EIVq$Y41@0fdi`pZ#{ z9t)Cs2zl)*|+8a4*l5!*;3 zBhouvU+y$RXMrqJv~=P-x@bCCaKGJfeutVD|=7F^_2hs|MW>jK~w_R zc7UrZFJwWR&XHo12WaCgs4_irG|o1RPWHuw?7#q5MXhbbwad3+V{IpKk&klWeLDI4 zU0{8MAC@PMMO0Bv%8L;RC)@99(s#k5gRLBa_|PvqDR-hSZ5&tUmIeBq-Tgc)yw3Zi z6;EY=fVz{QYj8(WE=sWYxjArC2dAIXa9$aEwl5pU*GS0 z=fj^oAp$N=j>p5Z(|HdTqmC8zB)A)kHz4wosR31fclR)sVl#v?wt9ABdTJnMCkA6m zJq%6(6o87=Pf8ElcI!)pevj_ z2qhbK{`2&K5@kA+jNevLD+o$jN&`Klvtk=hyG*C=(n>gKG2pv9hq1n@o|r~u<8q$U z(-Sc#K(Ufi#Q|0z&+S56=4l>`3-N^WFx7IX9v?KbP1iKji=~G| zJ#b1Wqm76g5=tceq|7oY+s2yia*k}2pK{py?jE~)?$x`^kx0@Y8s&6&xQ3hs__5KG zQsC*NheM2cgK_Yi6Fm*F#SS+ab1ndgKzF~%EGK*%Cyj%WNDodN0(jj4A(rPb4^dJE`ROD$ zjSZb;V_8`~;+G5;{PdI>y-PP#jF+@4%SvOWywJuLW8O!TXOJ3N*+rJ;5pq3@#m(*5 z++NLxd%(8$a8&Fh^oT~tk_O6x2h_|PLXfd^B6*YAq&V|pa{M5^^CR6$8^5K+_HPl` z)x}l)w7DxcdgIey7>wKV=Bep}@7C#(d4ct~YYTu%3*EGNbQa#4_ug0nZfcUXkuJgC z{L^O!!3T7`aZKdRvf!V9wEQL=GH91se?=?tCDlFIfz37Sz=53Sa7@jN$qtN2Zqg|k z-LymewdzQ|Zdq=VNpHg&!Yz7#p)Q=H`r<2%5ZJl9c!NULmhrkq= z?SXki8zY`_SXUum9a5z3I2+y_#DT=5oly*(M^%*H>&>(nP7VjdXol}N96rI{%L(-R zXU5}gXXfHP=TAylM1bHhrbh*Mfshku8 z8IkbJ$}wf*hmr=e!8HoreIzh_VoVWg8a47DOvRM|GCmci@OUm5#sDewMku69Wz(;8 z6#AYVX`mFS8`6O4T!-R-qODCUg5>2{8_RpKwjkzi9mee3RLq^4irEu>=k>n|sJg=M zdewsh7C2tv`a2HU=u{uBI?2UkeaFyvml=k-_>7_u&NP*r0X<fb!M?$HsC4G+D3MikJ|G4jMVdTIG~jSF?p^D0|Q`EGgmPCT1BHZy$L zPEqyXkmQ;k(&12WG87LksI*R_2kO;}ma2=1#845Z_M^nvei|eBe?5pk@c}vuDAzjK zdiX7xlrZ_n^TxKVXxeoeTpMBJsK6$ix>-Kd+j7JX_zx#iI03%HljFJp*kQQLEaw=l z)FGY7>V9q>*xovj0~w1Gr>0|OZXyPUEc<_1C?|$t$Jq0v6Oip?n=Mmb7;zkcxBJrB zW2YaT7dEHJE^TlyQifXfvx8~bN$DUCfo;5N>mFe+Jq4be%4&c%!S-Jk@BK;pVA|C< zp@B;viw^Mg{Jy6iJ$zV|e}K`1Mj!jTJ$du*mF8U}`#t-yG_an`L-pQzkvF(mF&;lV z6*C&GK(;tSX!svCoMh40}8s9ye0uau8If`nU8mC1blv2;B`(@Y$od30# zZpP{|r`?ZEhtSwMQOt^gE2r(BvcF+A>RAD!AN zI>=9bFlgDB1@IOg0|)9Iv!et$Ml}NAN|xi!8Qx*gJ=y^WPRe1ctJgGKUd%Y04338b zuNZGnBXxNU|EZ_Xcz6n{=xSLo;6cj?3QcvC`VK1k8or3NP~V72kt7f4176I(k6Y4ynGgc zZI!Ds2Bu?JBRZU9LF#%={d!6ShhrzSnmmHGaq$)W$05%-q%7S7hYtAofA`eZGcVG? zvCg1l$bfofn-fhX*MzQGb>i*#ZNTBPdB~`rS%f#A+M=xNXf$DN7YCg(OJ1e-0J3p> z= zcii8EbODFK=mclL{dG8m+WppsxtYm)X9!LJ{A`B7(XimYu(Tq_+mlmaZM#udrj(^R zl!MsZ-p*@M$X9DBOnrfiX)UJJa868j^$y7?u--uhxDtgc<=5m9H#Op5)UnsUuLd<3 zGvh-stA`tX7v*Sp=2y4l&gxb^YaDu!VD+y{3<}T^0)S9qxV?U*BoDndrEMXA38e%X zPageI{}fu{a<9YjUPPs*LD?m^!c$HHKv9keF=?mw)1|4P3<9}fKEK;Q4;7`_T04l% z6+aAhXHHDU?8)gEl~7TdOe_3W{8rud8`@FYdQzcNIiZu(<|BD?_f_K@ely6q=B6=G zHlwaA1LKP!+uck<2V$j$={ zPEdaF87%`$llUcL)s_wAz*8P@rLN&T^*7W%nvRA0s*3l%zl#wg>8mW905ovui^HV8 zwx^sNRdjuDR;q(^8UrxQ-1YcGjE_xaX9=1;3R`WqrrPkq1J~a?NIFF$(_U={{eK6P zenV4%^~f~w6l9&V{>gw0!!~6d&;g}|CzU5hTg{X44bRXQ;waO0Ksq5Fl8!haHB>b4 zR2Y!>GtW^l%`S&O=;t#5av62fn_utR0q&`rnVXC`*@5v1ubDZI>1^|)Xs4&oR$q^d z9gf4`!N9Y9m+32}mN4SHCul^U7<%$Bx?q%nBjL@XI0JTgF{;2u8>c#qN`oWt6qRjS z@}$1B0Gb8|)oEgyO?n6owRq2o`M!_bf9{i9g>q?bJWfvy$8$GUV^PLssb@2`dw25+ zod-`(#EeEOoLGPPmg3j9V|l~p6F9ucZ*KN2Sll>>XRk9AVWHC-ff)$L4VPgUb8aO> zgg}G!{5J}a(^CjepQ%{Mc+ZJecs{}ih7azb=b*Eh7=oLdZ zy(xN*Y*Mh{Y3j$622QKVPucF(kfxv-l7aY~F32Q`6Ro)(lD%rr`&XICXFUfPc?_n) zxEPWtESv$;XOxw_ufz-Q;gNfa$!mB6eF{xE%22X?!WArBy~5S?J2HBC5u$J?QRa6q z)dnYxq3FQCrZW{z%7u67==8_$loNR{z{y)10L7S=h8~`s%Jc4Rt6^oSBGe*#YXu?lqqaaXpF#z+rpw)RXtcrHdzpi}s+>^|1EACnFL@ z2sj45Ps=%RX(Nt@cx)~>nQ~g+@Ky8V1y4?GX^?c6@DW5YhMh-}%@lPYKZ-DdMwBi*aXXBl<^r%uPrsROpvq zTZr>BWAWsL85y919HFqNz9?sMXJsoEG&*5N*W>4=NSQ<19scCFHo}Z>c(9qPV zPyTOZtNbWZQk3~&BEkR;k4amsK*ABshcqV|H>(cq4^K`SfrnM(1K@Yc*+@nY=Hl#kkdOEi1Si zl8vjU5QWZ9^ zg`Fa66E+e`ka8m}h98HCV}c6mz%&p}n%^FgFlw+vB2RXD_){GwM&@!io2L(HeBCuA z(o;8n<2<0*(_nVonXb`q^9xnQrCGD#{&HN=1Q!t zE$Ug(s7^dPU>asnWlu_`I4h=u-BDori_ul`f^x+l>Q7zfrUv7?K63QZ##e5hiC1r5 zQv9AejZ69YuLAdSB=ZYyyD1;>q1qRad>4H@@E|)|$uIO+pL&wlsu%oBJrXW-tLy+! z`E%2Q@?;sJp_~x$0C72**QdkMAav+)*|q~I^Q6^|rvAvoXE`j&$n_aLpd=4K{N8S@ zrT9?b3~!4)H5H>W%IXKGUcw|=Qjcj1lG)qdaxonVMj9YZUYw|+xuOJMWI#N5aUP69 zs1KtO);O>oj37ovIGl)%`X*tVh;5e-Co&pgnv3uP^5Upg*EXf2p1$HDUfw^ouF(xn z1TIh(&LrR6&gF~)13j@RK`rkpQ$Jp>H?NOBIqCPf^KSLbsKUmE2hM_1s>^5uAQbR``@M{!oQUUP78N}p%dY4Q}SJ9OQ8PLFNs5gmu4vAj7O z^Q&`lcV#YCHz!1cjoEJrC@ae|Z}>z_tSbVm3tS>9X|r6rJ@?e>Ob9fzH?kMgoIig% ze`3=Q^V@M<0F&c0jY3LKGQEi;lRuThSOkP+zDCFNms3y~O&rm-f&B4arR_}~)#ad9 zy5F(`GV;7&ip6>Es!Q7ff(DurAx`o|_vkcR=B!!Z1OURY!;n_fRJE%re?wn`w!-XC zoUS10mj2~*F8K6^17WlS4V2^SR6s^rBjuIaJ^$W!JeI%iJ295KttgWhQ$28s*Nk!l z@LqiFs(0OdGq}058Ml_bK!0boC%*JLd*g}mITDD&L*EPN85Y}UTo6gGu@fdwjgaX* z);v88uo^bsVJ`9PT%du`u-q%h>$(>SFq)vD0@-uaQ{@1pFFG+K;3OXA)k$)IMq4;@ zUYlM{3tZ+K8{x=AU(B4=uwD*)YGfpS_xs+fp0B4u0InmLUtW$+f9-4ejt_*lxz!hI z8+~!-&P%bq!_>oJ%uL;{k;9oZRvX*PvAljOc~E8M#e{k~4M3372^dmAmp5KuW2$L= zYgq=$YZvschX*G*bp#l}q_I*tEOIifk`4Krz`e?Zeq=}-#sZ_cwEVjG>jiUwwV9de z2h?ELg4-9jmR?m)$-S{rjcBMFxEQUVAIK47!zhhUWP5sAJ#3}pH+5s{ELZ-cGswq_ z`mUe1;39V#2s*GIC&gp<;P_@cvHte=e^}2Sjp>QMBuD=#rQJ$CQBQEdbLzH#h_2c3 z6~I5rK*w@tmhe)xbxlS_BOqw1X8ok~XVA4vJBeZwDcr7@@O;MJcO=-sg*KR_J$Y6~#BQ==csG;h1Q zel*^?N<4Ift2#9Bwe*`jrQZc5x8pE8)*F*224ZlyH@4OLzxw*ESXyMpp(lR$cYP>6 z@a~6l>WTJGSe_dZT6S^`%89fuZOqZbI@8`B;E(fp^{a$%&DHJOgrWW-OGoX-QY{l{dlR0E$CAVWn84rKbzRBSwJ& z5~sfKFVnUfhr;*`U#cG9;ydS0xWF=dDSZcVeQ7B!&(Ftz>aefjCVRTa)MGh!@Q(klB_7@xvMZWBJ;e?#k2FwT3>Hozc`Vv^k~-?s$cj(+2C@9{ zkGz9}qxyknGH;JgGZ!CF4V0IAD`f(JpbSDiQjy}r~HW=9gmY5mV@A2 zN?&#s`qQz%SL%{y2yl8Thlj=tec5xD(NTy#-K)p2jtdXY2!logq6e4vL-jC{rp}U$ z=(nLp*O0Af5j`%0Yf=2pc7E^df4j>2KGpqWqGwXHJ|`#hwBlD?m8LG%Jv(${5B+>E zeo!9e^Kc@#U%ffvQ(i2*Hx3~?ILRt<0npUwkpA-hKjKGCS6T+%;gwTl(pC6{J}|}6 z*N2`wruYxU=;-6|T_3$37tiuu_M^ZF`?E94@%Y7?(!rg$eD_SeaQ&gUy>c=pN7iJ( zy3YPyaQqy}r@k;4cUtF2%zNvjzq|cGzjR)G0z{NnJMjW&Ubf%`xa-m=8h{yRbDh4+WjcI%E_Whe5eDISP z|NG=f&Z;7law=TFcx8U0^DdG$Ja%>}9S7;!4+S!4;dQ%PyM6KEEzjM-@G{C&&P2i8#ytct!6J-m+%{r_SxJs4Kr(~{{BBPMya=6e$^w5n2!%R-x17pfG*yoV@JPhH z3>+)vw;c&|0PJC}det^tpyI1cdbs&1fuKCxfVw5#ZuD%$t(E1t!J7xi#%1WNh&Yi4 zKCNE&PTZQmnaVIc%saCu)FW7qwe>r)C-B|QfgMBi)TlG`W00V`o>E}19XSjo zB`@FEt4`03wF=Vu4S`Id_+rfj<%bU#fi!w52YeWFri_pqzxAII!)x(DPrFQLupHr$ z4FH!z;z(H3XT5|vZEniB3jd%CIyAv!oQF3hU@YAOKJ^ZBYFoN-e4B#1ao0$cFKPT59R-LdPx3+X>hc!7 z70%S3{FZs(8F*;RB|~J#?=C-G9x+u8Kw5C?KXrh+G`J0!w`eqJ61V<<#M6XlovQ46 z#Jladvzj}rw&+lzyN5A7I~Fr1)B&0ti}PouVp1LYPW1%v20KKDh|vRve`lAg-Pu0g ztYh@`rHz14J0Uv}9D)xqYGl2mQ8j!K?i_u{9I@0+!p)T&wU9EWww9Py zIE*`MeespcLpiktE{57~1TkWNLus~SY;e{gO6w^h8#oL*4W3>XaX6f__f5yxBvTSS zv9`FIujFLW@hF(~G8p_ukl;qp$K$(>NM#`C%+pQhcmx4oY`DM$FqM}0^?%}T_|ygJ zagQOdaPo6wXDN*%W&Y_u_ZvjdUE#NP3L-w?ZX>59I4ZZu4pRY}^BW219o_N2# zdx(SmV`+53z+phww+(H+px{zgIlgR|YV0gNSDqXN$yw}dF2&f`i5MF_t=w$N*h=RH zpBY)?PPy!XZ{>&unwjdF7(1!n^mHRr*n%Mut+Zu1igZBgU9D}X%yfV?b>%6SkzBl< zh@I`fs)X3G4DTwv9si|r844Gosh+T|BmTDcW4g~FISNgfR z^%F7}9~C`Jp`sTD;;BZs{pR<7`)B_njZl6?`MQP`*xTp&A`E;_T$`Vc|NS?fip#gT z-exy`&jT6jV|x?q}tl2J-X>AY@-{M>$!893`V z_>Kd0_c~zdvcVlvJBPFbZFzL!tITTW))S}j|K;F;b{=q%mb$qf9u;)Kw4~l8&Mxhl zR`q1J7VSU?;?%jZn40x;_l48raq-kdOihet@4o;Cz?lw42Zb%~0UY6~l#w*>#Q}6` zesLwQIe~Aa$*D2{PJ_Ggc-IN%Qkb&pOs9=7H7*;aFh(eKqcA`{M~Cc4fWCiABNE;_ zvLw&8xy>}vp&EFf4Afv=+&CenXDSQt1oFiu9ov1cF0AL)U)E7*OyuFZ6cq5~JK&;$ z*+aWlT?PUg-06Q0t?13UccS!}n6!NBQKK7l<f7lgfKT;>d=kL-gnaFdf1UrH_|v=;%n+pO5}sWS3{!SG^UET)6(}UFQ+d|mvLes z?XU=ULo*H@pQy(`&};KL7JOn+o>3M!2*c?z(r|v{W*JjV87ro?ytd&wKnwLW74D(t zC<@1@K+DwzfGx)(1B`%}-g{j1%2@Q?5U;Q5d0n(zmchEFw2TlxtFSxDYk6nA6Bm4_ z9xSC{C~V9y?#$?T+Mp+=4Ysznv!_UpPk~Jyjg)dx6vGd_JXs_Crc=b!A5#wW7HypP z%X)U|+SKjfA?4xR!4=W{y1u`rJU5j0cI@rmiTlrgRtD*Ayml3Ng+^O+Vvs3YpxK5k zBYe=SPUDz55&IgoY^dX~rQhRmXXT`D9>j1z>y%1=Y-)>jlG_WS0mKRBQyZ$j)Xuy^ ze=>q!d~2YIM~3?9cHPCB-xHU^vVHQ$XX=P`o#-a!Nct}RZkdHMzZ2gsgJc;;j%sJn zWd~?X?LR|DM$LoF^-mADq%G>KYXA_uHMU=}NB&GLt#ih2opld&Ahvh2V`O`2ePW9^ zfb9SW9Xk*+nqW%G3-Yu>QLhKQIBPzA!vpP+Hsnu6F*zcKIM+D&mm?hXvX3_D6ZSoS z^+!Hb_3J=;vHduF{*}Ax<-oMaGbO8=9+AA~!4vVG2Tq8vp7`Qx>+$D)=DO0^XaF?u zY(y~^075VgBcVl+qLU1u8@=G2= zyZ%Zx=m&gcV+TfOiIF^wh=?%7%BYL$}Q_^=P?t z>2)4=0L+JytKmogz-s)^_pQh5)KT4T!}e}}{MDcPcs%{e#r*vP@A)~k|1V1yw(q&v z{$?lJ3uqhgp1Ko%92|f2@=4nU4X4RS3gV6@JJA4r{>Ri9&!ISE| z_jfLBq{=yZ(8z?h7c%m|Asl~0B2Ecs!=;R@a~ucOUC5KC-J4Mm=TXRCi}gb)1LuTI zqRfsXIXgX?TYae_=Nm>e8sR(ZC9mK0)hEEym*=I>{{EKp)150)c&B@XKYwF8{`Rjg z#m+t~Gt*!~2^WFGz~M3Gzx!hT?z}4Jo)s@lVUP5M3l}70H^jAN&jA9vTWZ*rbq8uX zAsbUZ>K{n>2nGdBK_i4xE&*`}&S2`JTzUNudgX90Q~TbGO05TCFMij z<_rvcXgiXFT{%q2erB2zbF885r+%cY^yDa|F#u_tJ5GdMTw5_Uc3O^vYeUdy%aeMb zBj^C%>4Bw=h_*7O{PDZwk#!eMMHgvsBs7II_K7hg{HchXmP`K73*@A^<0b zSZ3}$yA4ox!mExC-FF!0&v1WJ#QWa96W{ak%{YDHsBvee;q_Z{v9iI`T*PyiXX5(Z zG1av<-ucMQc<*CZC4=pbBl-FZ@YMQ{b!&9P?X=dajye(!6K0QEP`pbTvtCZGiZto% zK;Q>}BS{|7?puJpwDue|+!KKwk3}nWg>+KeUpU z?}TT(zLz8E$^$)W_@cjpDq~=-mQjcb%U3?I=E3NKhk9byaxF%qW$4K*$ifTlbb#`Z zZ1P=t&Djv1@!|e-3QS`Si^w`3vMon)cXd-kSU=ml7yv)BV}o&C6*)gU8iOi;^%ADK zR^`oLPBAMLKuk*J!`Oyt{rz$z8U{0+z*PeIjt0qw1=3+THJ+i7zTDxoxw@ARFTYO# zv>ucuF{S{m6#h+_Rz^kScWB6TP$;@go4!kiDRkvA5N8GA4 z^H2uHWLrj)g;0!>GIZlxu(q`&r@^~DmgB`M*W>AzUXQQ5_*z`Mv#2aAnE9+i(b=UH zpguIX?gNlEyDCuEsqdSCu2e0HaAOm)D`9fS< zyf2p4XGEusRzZi~Dg%NfJx`OCPhkLy%r*GR8$ zlx~!~j9;;o-8eo)bn^HS@H-!Z8e9#|j=d3(I%}Bm1zDQ^60gDZw(T19Xv8CX%WH`@ zWc#)^_HxJPzHBdtn&+1`V`+6u`X#+mVjM?atAVN^pNbPbyeE$AeGqv4<;bX1u=U;QtBN4GW&+>*n1^|lxOHbvwMH&*gH@FO34 zRJ|S}eD3;ku9>v>k?_slM?&KQ*in)mO_d&FAxYkLbQ0l;(Cz)W{;JAsMmTRa4OP%y zjBliyVU?#1H5~xZ;4lE>E1WdI8k!PTIFrFGoESvDwnr2gN0f&As~d|=UAxT5X{L;? z-YA0T=67%1f*%>0%BhL$NjD63T8s;h+F?v>rP8QQ?rj}4hCnu`7M?a79g*M&%!e{a zZ63lm;KHK&y%~wgdbyiuX7|b6X)MBwbeZ*Qd2&5f*9kZqv0oD9q5<*x}zj| zxpp*Ne&i@hf5e%`M|qRimY}Z+XYrhArtEOQFF8=tzMa#ZTzOJcf;3ZV&f|}aO1$L&w+Wcq%3+O!T2CGXOaX4*2M^DU7 z#@O(1j)MBW|6TWel5M{2hl7RJ7dO;H-qoYR@puiKo*s!?@`PW%x|pMtB8EkIu1R5f zY-6W4zH%9Z@18mj41rNF)54JB7$1z$iNP3V`>_7F>}-cqpd0)7c0~mx*qFJ;1&(U; zW;Xb}{$NrZ2g6lpruUkI>7;yUNkvh9aAY2RrlRN%{H5?@b-JVVpbk9QFu|>Ve!-A> z4LmKUz^8lEU1J4e@bZAe@#YN*j1l(yHsy*I%1g>jf6_$V={012@(OSB{f$qn>^8LX zP?Lrp*H7Oyv~^F8c*vk!(UW=Lt@tQo51lv@9Be5*0DS-rC9i?Nuqq$%mY*GWmPbN; z@_u@N`?;Wr^fmQE8A(IYmvou_bP7tFuf^3U*Nwleb9NBf=##E2G4Yz*hGyYS{^l>Z zo*z_MAB_|eCs`d&Ipnvq;kRV3cz)YhqAM~AN(Ud9_S#~ruD-i-1pTb{Xr$7Fm2NSD z0e6-oJm5q>c!3!sL!3zJNC%+Ybpc3E54+)TH>#oEZNf4q&^&cpNS)VrwHz6zo_OKa zfq444!I+ylh^dLLVZ0aAIg+~z%mOreEXeW_C%lGV@Y6cR_u>^(g3xXoVmhI@!aDV7 zLsp@wgX)y&&=c_TmvvMriqCeezmhlkjKI83TAU(7t#?%q)9i5L1k%>%A+6In4lKso zFke6U=5T$7-X@NGIXbBPnZ&dZXDmv8(398M^J)u{?#S3Lt@`!5BeKWnYn}gqMs%J} zUc<{YmKW3EmCHrXa6sf|S~Xvq)!fa6-W-Q;`%}Wn=Hc8v{fL1UR2uIRoco_vC86_dZ6#j@KEye`l6A!r2 z`0~i2Efqw{k_Hn2DLxyY!mY%mJXH_YMckX9P6&hioG`}_w4De*o*D(E2par2473EA z+>2~_9Wt6MV&h2M!?p0hk&Q{vt6?T}B!AXf;cxor@6Di{mvWE`e1Konk8O$2=J-az zn=+vXK0+h>@KjE7-8ywvntV~2SkI`ToD6k98qm*3h0$s9M}c`lnbY<9>mL|JIQboZ zKoe6zlx-bS{gnsW=uLb7e>sxj>@DWKXL|Cmrbd+ML{gU42kTAKIE4q`H;?&FUAsmO z`KSe}4(#S~XM~<}@<)Hj6J#4i;mvl<%?-i< za9)T%Xh1(WE5jCLMpv$nA$3`gDAX^@-G;ZqsVA6RSLz7w`AvCzro8n>`fO8*U)wm0 zXI>nR&wOP#zVeO1c;7pA;?jkqw&6I`Ig+~z)oDYP@UA;D0>AYiy>OeST;c)C%W|;c zw%l{HKswhb)AGq5WjV}tfO3!*pX34DCL04Q{MZ&gyF7B6sJPCqg4 zFsfiZ!_!r2Z*m0b6o%|b0M<#s&vFI}?W~QEmIZdU1rHAm<;9RJ&eQ&xE@N7Y*(CBD zS!`znl4nBn>%mjw@y<(gnUJl%&)-}Y@vg{gw|Pq&hgA0Cw?B0uod+K5jy&PrRX6e% zZf?i_^63SoBM;L|9&*xqJ$?Up49WQrzP_~QQHTr-ml$$btN=Xi;U4*EkV%E&e3c49 z@{pDY8fq$z{%ROcMDA?Tx6aUA4@L%|kiG<7!U9Mub<6McC~^7KODvoy;`-L2!YK>G z*l6;kn~SOydqf%@{n!TwP~@(*D8q6{gGu7>@T34oh2;1>!BxJT3F zCy)k1@wJ2NI8+#QBpvlBdGa|0}Z4>%q8PP`p@{!F0W*1IyM)+v5lCrbyc*VMh%dPtVh#50v6KIAV2TArSQ z#fjLdGT*%1=+;3@kNX`clv{EjA38{RWq9(lp{e)+;CLvLyahP5iq8>b^1=?%b+tjW zp1T57PyaL@Y8r~LxfvX`%lM=J_F8=BM>k`1xLYRhFFxNB|Jh&ZiLX5m+HwGP3`u+b z!ze4?u`=^DyZQosq)8bm8FA!F-Zi>YkWL4^FPs9tg9F^Q4R-t~E8>L^dfYKo>imuK zvA(q1S@8~;dF`PqWAiA9ODN}P2W9>0*0bw0DHpoPpR!P#h|Kcg0dga}aHO6!@*;0` zYJ#a#(Jy;V{E5?}F>``f6CB2gX>Q0Ei_syqhvqCuUf?+B`dY{QwneU6;X9)eoDk_* z|H8f}Z&beN)N{a%<{Va5Cy)1?eESn;KFPwlQsyE%1 zt}fn;2>MMqm2DL)oMLQmUL*hXW%WRfHb&(k2bGrsSwJUG?rmOYSjGaZn3{4g1+e1s z9SX7%DcGW?(tycvB?t=+!BTi4%`nHS*QK-;yoEv9f-En~g27f$@{ymkHpGpQQ9K5r zwvZw~fV?Q1>(S)zl&P==KTiR{S$fh$qw!Q9=^ci#Q~jGPm6eZdc*z6(YU5XVHW(m3 zS{}3*Oylu*zE|1#TYoqv@L3M#HI0+^U@%paWoKe!>wBWuvRl9!Mj8;sXJe;Ml+(n! zd}^)6s~$PON7d`jgPQ{QTX`wlRW;50PebeLJ1J-KPSHx^-cgyRna6dpe94b4Za8_*BwC2@xbm!wkY9`X?>Pfcz&M@Z(-IL5XDrNg3DFC(Bp7>qA`rpYi^oSmCrp z+VOl*Cvc-DmR5UWS>ss2)8xwx2};dS`q{Qb@*BlrqeP3l1>s@ z&>2?x;<|R_Szrg%;B9c`U&^zUw!VW8^!#O97gv7Eu)^VXoOS>@B}0Cbrt+8UGaHcg z0ifGux*XDx^?12WI1k}^a1(^O{Z|#JR7{~&)RYiqw_$(( z{pwl$nHV4W84cfmPWa~&C-OuN(9U=GRSU5N)!T0O?kN-2vo4dpS#&_(D;^+E<%OOa zSrim+Fg_iAB4g!QS^qJ~?>m*=7en3)dVBB2%*3A+AHFC$Jf(_m!8^;1kuE*?Z(b&} zLuXCH8IMN+9pfY*9GvKk`E)#X3xV)?vu2+T$2ReWq zq|KrOD67kK$CK}s$NaQj!B^}+@=)mj_*dz{m*s%4fj5f>7je|RbhvOrAN(jd9^Zzs zslJ$)(`a^}M|w0Am(ESf@lVLvkK|%Jz-R;~!SvV2@Su32PQFGbT;|xmw2_nVTw{;( z7#$tXvnq7ViR=0MKl<_HG{jW(hUCE*aR7L>wM`6$VO}E=1tm-lb61cX_lg_}3-##S z&Gp{+^!349{NeZhc6T0hTcZ`^YoWQPfPo~g90`(hBk@qO8n+vO84bQP&|+TOk$~6A z!mtoE!kD9^t$6+AG?@~V5CN3$NW8)*)ARla4~K!$`jZ4jS2Y4?N(GZ(Fswy0b%73{ zRvE4zz+9)OP`WHF>r;+|ol7nc1!Z_^!%+tx!Oa{wG#gj9p{6lVIF1BRiTSC(jLR2f zW!>$V3kUQS-zyKYVNTz@Fm4-O$-IFybuFlEZVq@#~svhT?^j#Pk`X$xp zGpf&Lm1jXYU6vx06pZi#NvG=5hN3;& zgLvRDr4j$B&xNy(#-$4<(jYA?y&5mQ_RF!Njt6|jAyYhjE13XpA9h|DE?$8%!}M9a zYdaF4bmC||kJcS1oTNwZI`V4bC<7Ycp=I0Xb+!W=XYv$3_yiZ{a2z4CqL-{<8?e?iC54+$X#=yOI05qAB@wd z$6{iV=`A^x^D{9!$)*(R6J^lh$DuHisdu2TPSW1Jg>&(aFzJi(;COl;J~J7Q=wT$m zU0d^OBvx;*d3n{iMkIF?Ua!>MlNSLwjq=>gXgqw{`H{^g&@!$0O>sM?zAwM&w^w! zxI5qd+jO+!-SEhdB6y(~hvsyJH|a=h$AEA>ZNOphe=4+aL%y9&H-FOA`BvpPebTDB z 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%0A3LFowg*Ew(zB+&5 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 literal 0 HcmV?d00001 diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 5d8dbb6964..9b0a7c2792 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -78,10 +78,27 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. +Yellow cubes and missing sprites +-------------------------------- +If you are seeing yellow cubes in Stonesense, then there is something on the map that +Stonesense does not have a sprite for. + +.. figure:: ../images/stonesense-yellowcubes.png + :align: center + + An example of the yellow cubes. + +If you would like to help us in fixing this, there are two things you can do: + +* Make an issue on `GitHub `_ with what + item is missing and pictures of what it looks like in DF. +* Create the art yourself. For help with this, please see the `stonesense-art-guide`. + Useful links ------------ - Report issues on `Github `_ - `support` +- `Stonesense Subreddit `_ - :forums:`Official Stonesense thread <106497>` - :forums:`Screenshots thread <48172>` - :wiki:`Main wiki page ` From aa6fdb41fc4ee5330ba907a9960dde05cb34d2b1 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 22:48:31 +0800 Subject: [PATCH 0037/1082] Add Military::removeFromSquad --- library/modules/Military.cpp | 64 ++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 23fc15a871..929285a761 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -7,6 +7,10 @@ #include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" +#include "df/histfig_entity_link_former_positionst.h" +#include "df/histfig_entity_link_former_squadst.h" +#include "df/histfig_entity_link_positionst.h" +#include "df/histfig_entity_link_squadst.h" #include "df/historical_figure.h" #include "df/historical_entity.h" #include "df/entity_position.h" @@ -17,6 +21,7 @@ #include "df/squad_schedule_order.h" #include "df/squad_order.h" #include "df/squad_order_trainst.h" +#include "df/unit.h" #include "df/world.h" using namespace DFHack; @@ -297,46 +302,51 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ for (size_t i = 0; i < hf->entity_links.size(); i++) { auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) continue; - if (link->squad_id != squad->id) continue; - + if (link == nullptr || link->squad_id != squad->id) + continue; + hf->entity_links.erase(hf->entity_links.begin() + i); - delete link; - start_year = link->start_year; + + delete link; break; } - if (start_year == -1) return false; + + if (start_year == -1) + return false; - auto former_squad = new df::histfig_entity_link_former_squadst(); + auto former_squad = df::allocate(); former_squad->squad_id = squad->id; former_squad->entity_id = squad->entity_id; former_squad->start_year = start_year; former_squad->end_year = *df::global::cur_year; former_squad->link_strength = 100; - hf->entity_links.push_back(former_pos); + hf->entity_links.push_back(former_squad); return true; } -static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squad) +static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; - if (! Units::getNoblePositions(&nps, hf)) return false; + if (! Units::getNoblePositions(&nps, hf)) + return false; int32_t assignment_id = -1; for (auto& np : nps) { - if (np.entity.id != squad->entity_id) continue; - if (np.assignment.squad_id != squad.id) continue; - + if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) + continue; + np.assignment->histfig = -1; np.assignment->histfig2 = -1; assignment_id = np.assignment->id; break; } - if (assignment_id == -1) return false; + + if (assignment_id == -1) + return false; int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -346,14 +356,16 @@ static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squ if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - delete link; - start_year = link->start_year; + + delete link; break; } - if (start_year == -1) return false; + + if (start_year == -1) + return false; - auto former_pos = new df::histfig_entity_link_former_positionst(); + auto former_pos = df::allocate(); former_pos->assignment_id = assignment_id; former_pos->entity_id = squad->entity_id; former_pos->start_year = start_year; @@ -366,13 +378,16 @@ static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squ bool Military::removeFromSquad(int32_t unit_id) { - df::unit* unit = df::unit::find(unit_id); - if (unit == nullptr) return false; - if (unit->military.squad_id == -1 || unit->military.squad_position == -1) return false; + df::unit *unit = df::unit::find(unit_id); + if (unit == nullptr) + return false; + if (unit->military.squad_id == -1 || unit->military.squad_position == -1) + return false; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); - if (squad == nullptr) return false; + if (squad == nullptr) + return false; // remove from squad information int32_t squad_pos = unit->military.squad_position; @@ -384,9 +399,10 @@ bool Military::removeFromSquad(int32_t unit_id) unit->military.squad_position = -1; df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); - if (hf == nullptr) return false; + if (hf == nullptr) + return false; return squad_pos == 0 // is unit a commander? - ? remove_captain_entity_link(hf, squad) + ? remove_officer_entity_link(hf, squad) : remove_soldier_entity_link(hf, squad); } From 872cc7cba5a2e599c6e2f081b4ee2a8333d5d9ff Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:01:49 +0800 Subject: [PATCH 0038/1082] Fix style --- library/modules/Military.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 929285a761..d569250167 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -312,7 +312,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ break; } - if (start_year == -1) + if (start_year == -1) return false; auto former_squad = df::allocate(); @@ -335,7 +335,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ int32_t assignment_id = -1; for (auto& np : nps) { - if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) + if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) continue; np.assignment->histfig = -1; @@ -352,8 +352,10 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ for (size_t i = 0; i < hf->entity_links.size(); i++) { auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) continue; - if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; + if (link == nullptr) + continue; + if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) + continue; hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; From 84370629900dcfca95a984cae8284d11e3d9678b Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:09:19 +0800 Subject: [PATCH 0039/1082] Attempt to fix trailing whitespace --- library/modules/Military.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index d569250167..d1322735db 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -329,7 +329,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; - if (! Units::getNoblePositions(&nps, hf)) + if (!Units::getNoblePositions(&nps, hf)) return false; int32_t assignment_id = -1; From 7eb27ef350963ebdac9a136bcc291ef989df99aa Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:12:44 +0800 Subject: [PATCH 0040/1082] Remove trailing whitespace --- library/modules/Military.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index d1322735db..8292c6227b 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -304,14 +304,14 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ auto link = strict_virtual_cast(hf->entity_links[i]); if (link == nullptr || link->squad_id != squad->id) continue; - + hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; delete link; break; } - + if (start_year == -1) return false; @@ -337,14 +337,14 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ { if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) continue; - + np.assignment->histfig = -1; np.assignment->histfig2 = -1; assignment_id = np.assignment->id; break; } - + if (assignment_id == -1) return false; @@ -359,11 +359,11 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; - + delete link; break; } - + if (start_year == -1) return false; From 9a06a27222538bf4cd7536453e0d9e851858a8c2 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 02:43:23 +0800 Subject: [PATCH 0041/1082] Add Lua bindings --- docs/dev/Lua API.rst | 8 ++++++++ library/LuaApi.cpp | 1 + 2 files changed, 9 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 000840c76b..4cc2c35d06 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1986,6 +1986,14 @@ Military module Returns the name of a squad as a string. +* ``dfhack.military.removeFromSquad(unit_id)`` + + Removes a unit from its squad and returns true if successful. Unsets the unit's + military information (i.e., ``unit.military.squad_id`` and + ``unit.military.squad_pos``), the squad's position information (i.e., + ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links + to indicate former squad membership or command. + Items module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4b82dddc17..6e251dbfcf 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2341,6 +2341,7 @@ static const LuaWrapper::FunctionReg dfhack_military_module[] = { WRAPM(Military, makeSquad), WRAPM(Military, updateRoomAssignments), WRAPM(Military, getSquadName), + WRAPM(Military, removeFromSquad), { NULL, NULL } }; From c9e2dcc2812247cb4f9d1f3a4e3410f70d2172b3 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 02:44:26 +0800 Subject: [PATCH 0042/1082] Modify Military.cpp --- library/modules/Military.cpp | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 8292c6227b..62d189c4d7 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -301,14 +301,18 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) { - auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr || link->squad_id != squad->id) + df::histfig_entity_link* link = hf->entity_links[i]; + if (link->getType() != df::enums::histfig_entity_link_type::SQUAD) + continue; + + auto squad_link = strict_virtual_cast(link); + if (squad_link == nullptr || squad_link->squad_id != squad->id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - start_year = link->start_year; + start_year = squad_link->start_year; - delete link; + delete squad_link; break; } @@ -351,16 +355,20 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) { - auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) + df::histfig_entity_link* link = hf->entity_links[i]; + if (link->getType() != df::enums::histfig_entity_link_type::POSITION) + continue; + + auto pos_link = strict_virtual_cast(link); + if (pos_link == nullptr) continue; - if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) + if (pos_link->assignment_id != assignment_id && pos_link->entity_id != squad->entity_id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - start_year = link->start_year; + start_year = pos_link->start_year; - delete link; + delete pos_link; break; } @@ -393,9 +401,12 @@ bool Military::removeFromSquad(int32_t unit_id) // remove from squad information int32_t squad_pos = unit->military.squad_position; - df::squad_position* pos = squad->positions.at(squad_pos); - pos->occupant = -1; + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos == nullptr) + return false; + // remove from squad information + pos->occupant = -1; // remove from unit information unit->military.squad_id = -1; unit->military.squad_position = -1; From 76d2af495407cd6cdad1ac9a18854ef3e207e171 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 12:56:43 -0600 Subject: [PATCH 0043/1082] Update Authors.rst --- docs/about/Authors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 2908cae9d6..bfb8aa02a8 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -242,6 +242,7 @@ thurin thurin Tim Siegel softmoth Tim Walberg twalberg Timothy Collett danaris +Timothy Torres timothymtorres Timur Kelman TymurGubayev Tom Jobbins TheBloke Tom Prince From cfe1dbb79b6b53e3d1f4e03bf92a6b2807b6a9de Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 03:14:43 +0800 Subject: [PATCH 0044/1082] Update changelog --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d28decc8..e8340baeac 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,9 +35,11 @@ Template for new versions: ## Documentation ## API +- ``Military::removeFromSquad``: removes unit from any squad assignments ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings +- ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` ## Removed From 71f92c6085625497940d1788f60687228d3b9e12 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 7 Feb 2025 16:32:29 -0800 Subject: [PATCH 0045/1082] Update Gui.cpp - Redundant code --- library/modules/Gui.cpp | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 6dfe7f920e..1d60c335b6 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2962,22 +2962,8 @@ bool Gui::getCursorCoords(int32_t &x, int32_t &y, int32_t &z) bool Gui::getCursorCoords(df::coord &pos) { - using df::global::cursor; - df::coord p; - if (World::isAdventureMode()) - { - if (game) - { - auto &look = game->main_interface.adventure.look; - if (look.open) - p = look.cursor; - } - } - else if (cursor) - p = df::coord(cursor->x, cursor->y, cursor->z); - - pos = p; - return p.isValid(); + pos = getCursorPos(); + return pos.isValid(); } //FIXME: confine writing of coords to map bounds? From 194bde01576571e023ea11255c3aa5a0f80a066a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Feb 2025 07:15:59 +0000 Subject: [PATCH 0046/1082] Auto-update submodules scripts: master plugins/stonesense: master depends/dfhooks: main --- depends/dfhooks | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 5b70cd886f..5bd392890e 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 5b70cd886f619b503aa770008fb79aa39a3be6fa +Subproject commit 5bd392890e92a70622878c0513ef50e72e215435 diff --git a/plugins/stonesense b/plugins/stonesense index 9d851db080..1756e2f3f7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9d851db080012add38631af37ea5c5b60a893e79 +Subproject commit 1756e2f3f704a0d01b24feccd0089972c245c164 diff --git a/scripts b/scripts index ebb22f3adb..fc67fdc5b6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ebb22f3adb6070c931a82b5f6a1749e7bf3ced03 +Subproject commit fc67fdc5b67a25a776eebfa769b289801679e364 From 5a6de1fc928ecc067267064fab2fd443a36643c8 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 19:15:03 +0800 Subject: [PATCH 0047/1082] Remove return val --- docs/dev/Lua API.rst | 2 +- library/include/modules/Military.h | 2 +- library/modules/Military.cpp | 35 ++++++++++++++---------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 4cc2c35d06..66516642a9 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1988,7 +1988,7 @@ Military module * ``dfhack.military.removeFromSquad(unit_id)`` - Removes a unit from its squad and returns true if successful. Unsets the unit's + Removes a unit from its squad. Unsets the unit's military information (i.e., ``unit.military.squad_id`` and ``unit.military.squad_pos``), the squad's position information (i.e., ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index dc69dac52b..37855d94db 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,7 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); -DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); +DFHACK_EXPORT void removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 62d189c4d7..9e507a99aa 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -296,7 +296,7 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s } } -static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) +static void remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) { int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -317,7 +317,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ } if (start_year == -1) - return false; + return; auto former_squad = df::allocate(); former_squad->squad_id = squad->id; @@ -327,14 +327,13 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ former_squad->link_strength = 100; hf->entity_links.push_back(former_squad); - return true; } -static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) +static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; if (!Units::getNoblePositions(&nps, hf)) - return false; + return; int32_t assignment_id = -1; for (auto& np : nps) @@ -350,7 +349,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ } if (assignment_id == -1) - return false; + return; int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -373,7 +372,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ } if (start_year == -1) - return false; + return; auto former_pos = df::allocate(); former_pos->assignment_id = assignment_id; @@ -383,27 +382,24 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->link_strength = 100; hf->entity_links.push_back(former_pos); - return true; } -bool Military::removeFromSquad(int32_t unit_id) +void Military::removeFromSquad(int32_t unit_id) { df::unit *unit = df::unit::find(unit_id); - if (unit == nullptr) - return false; - if (unit->military.squad_id == -1 || unit->military.squad_position == -1) - return false; + if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) + return; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); if (squad == nullptr) - return false; + return; // remove from squad information int32_t squad_pos = unit->military.squad_position; df::squad_position* pos = vector_get(squad->positions, squad_pos); if (pos == nullptr) - return false; + return; // remove from squad information pos->occupant = -1; @@ -413,9 +409,10 @@ bool Military::removeFromSquad(int32_t unit_id) df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); if (hf == nullptr) - return false; + return;; - return squad_pos == 0 // is unit a commander? - ? remove_officer_entity_link(hf, squad) - : remove_soldier_entity_link(hf, squad); + if (squad_pos == 0) // is unit a commander? + remove_officer_entity_link(hf, squad); + else + remove_soldier_entity_link(hf, squad); } From 6d44e8f8f0e1eb0cd7b9a42c29dbfa8309d081f5 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sun, 9 Feb 2025 12:51:12 +0800 Subject: [PATCH 0048/1082] Change return type of Military::removeFromSquad --- library/include/modules/Military.h | 2 +- library/modules/Military.cpp | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 37855d94db..dc69dac52b 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,7 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); -DFHACK_EXPORT void removeFromSquad(int32_t unit_id); +DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 9e507a99aa..3c5708d75e 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -384,22 +384,25 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.push_back(former_pos); } -void Military::removeFromSquad(int32_t unit_id) +bool Military::removeFromSquad(int32_t unit_id) { df::unit *unit = df::unit::find(unit_id); if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) - return; + return false; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); if (squad == nullptr) - return; + return false; - // remove from squad information int32_t squad_pos = unit->military.squad_position; df::squad_position* pos = vector_get(squad->positions, squad_pos); if (pos == nullptr) - return; + return false; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) + return false; // remove from squad information pos->occupant = -1; @@ -407,12 +410,10 @@ void Military::removeFromSquad(int32_t unit_id) unit->military.squad_id = -1; unit->military.squad_position = -1; - df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); - if (hf == nullptr) - return;; - if (squad_pos == 0) // is unit a commander? remove_officer_entity_link(hf, squad); else remove_soldier_entity_link(hf, squad); + + return true; } From c21740929a56691be26401c56cf5ea0ba6df04a7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 9 Feb 2025 07:15:07 +0000 Subject: [PATCH 0049/1082] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 1756e2f3f7..92794aec77 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 1756e2f3f704a0d01b24feccd0089972c245c164 +Subproject commit 92794aec7768bdafd0f5fc23fed8b30655805b32 From 3844957820893c92bd1cf97cac617f7ef5e451bb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 00:55:35 -0800 Subject: [PATCH 0050/1082] refactor announcement settings logic --- plugins/spectate.cpp | 141 +++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index ae33031b14..0871e4754c 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -33,42 +33,8 @@ namespace DFHack { } static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle -static bool was_in_settings = false; // whether we were in the vanilla settings screen last update -static const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); -static std::unique_ptr saved_announcement_settings; - -static void save_announcement_settings(color_ostream &out) { - if (!saved_announcement_settings) - saved_announcement_settings = std::make_unique(new uint32_t[announcement_flag_arr_size]); - DEBUG(control,out).print("saving announcement settings\n"); - for (size_t i = 0; i < announcement_flag_arr_size; ++i) - (*saved_announcement_settings)[i] = d_init->announcements.flags[i].whole; -} - -static void restore_announcement_settings(color_ostream &out) { - if (!saved_announcement_settings) - return; - DEBUG(control,out).print("restoring saved announcement settings\n"); - for (size_t i = 0; i < announcement_flag_arr_size; ++i) - d_init->announcements.flags[i].whole = (*saved_announcement_settings)[i]; -} - -static void scrub_announcements(color_ostream &out) { - if (Gui::matchFocusString("dwarfmode/Settings")) { - DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); - return; - } - - DEBUG(control,out).print("removing PAUSE from announcement settings\n"); - for (auto& flag : d_init->announcements.flags) { - flag.bits.DO_MEGA = false; - flag.bits.PAUSE = false; - flag.bits.RECENTER = false; - } -} - -struct Configuration { +static struct Configuration { bool auto_disengage; bool auto_unpause; bool cinematic_action; @@ -94,6 +60,75 @@ struct Configuration { } } config; +static class AnnouncementSettings { + bool was_in_settings = false; // whether we were in the vanilla settings screen last update + + const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); + std::unique_ptr saved; + + void save_settings(color_ostream &out) { + if (!saved) + saved = std::make_unique(new uint32_t[announcement_flag_arr_size]); + DEBUG(control,out).print("saving announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + (*saved)[i] = d_init->announcements.flags[i].whole; + } + +public: + void reset(color_ostream &out, bool skip_restore = false) { + was_in_settings = false; + + if (saved) { + if (!skip_restore) + restore_settings(out); + delete[] *saved; + saved.reset(); + } + } + + void on_update(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + if (!was_in_settings) { + DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); + restore_settings(out); + was_in_settings = true; + } + } else if (was_in_settings) { + was_in_settings = false; + if (config.auto_unpause) { + DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); + save_and_scrub_settings(out); + } + } + } + + void restore_settings(color_ostream &out) { + if (!saved || was_in_settings) + return; + DEBUG(control,out).print("restoring saved announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + d_init->announcements.flags[i].whole = (*saved)[i]; + } + + // remove pausing, popups, and recentering from all announcements + // saves first so the original settings can be restored + void save_and_scrub_settings(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); + return; + } + + save_settings(out); + + DEBUG(control,out).print("scrubbing announcement settings\n"); + for (auto& flag : d_init->announcements.flags) { + flag.bits.DO_MEGA = false; + flag.bits.PAUSE = false; + flag.bits.RECENTER = false; + } + } +} announcement_settings; + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -108,14 +143,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit = -1; - cleanup(out); + announcement_settings.reset(out); } } else { DEBUG(control,out).print("%s from the API, but already %s; no action\n", @@ -148,7 +175,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_shutdown (color_ostream &out) { DEBUG(control,out).print("shutting down %s\n", plugin_name); - cleanup(out); + announcement_settings.reset(out); return CR_OK; } @@ -162,7 +189,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DEBUG(control,out).print("world unloaded; disabling %s\n", plugin_name); is_enabled = false; - cleanup(out); + announcement_settings.reset(out, true); } break; default: @@ -172,25 +199,12 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (Gui::matchFocusString("dwarfmode/Settings")) { - if (!was_in_settings) { - DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); - restore_announcement_settings(out); - was_in_settings = true; - } - } else if (was_in_settings) { - was_in_settings = false; - if (config.auto_unpause) { - DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); - save_announcement_settings(out); - scrub_announcements(out); - } - } + announcement_settings.on_update(out); if (config.auto_disengage && plotinfo->follow_unit < 0) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; - cleanup(out); + announcement_settings.reset(out); return CR_OK; } @@ -354,10 +368,9 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { config.auto_disengage = val; } else if (name == "auto-unpause") { if (val && !config.auto_unpause) { - save_announcement_settings(out); - scrub_announcements(out); + announcement_settings.save_and_scrub_settings(out); } else if (!val && config.auto_unpause) { - restore_announcement_settings(out); + announcement_settings.restore_settings(out); } config.auto_unpause = val; } else if (name == "cinematic-action") { From 2daa28790b5b3dc3e7a052ec61d4768d5350c380 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 02:28:45 -0800 Subject: [PATCH 0051/1082] implement follow history and zoom to/highlight unit when switching targets when paused --- plugins/spectate.cpp | 92 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 0871e4754c..a3e3c86193 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -2,6 +2,7 @@ #include "LuaTools.h" #include "PluginLua.h" #include "PluginManager.h" +#include "VTableInterpose.h" #include "modules/Gui.h" #include "modules/Units.h" @@ -11,6 +12,7 @@ #include "df/d_init.h" #include "df/plotinfost.h" #include "df/unit.h" +#include "df/viewscreen_dwarfmodest.h" #include "df/world.h" #include @@ -34,6 +36,8 @@ namespace DFHack { static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle +static const size_t MAX_HISTORY = 200; + static struct Configuration { bool auto_disengage; bool auto_unpause; @@ -129,6 +133,77 @@ static class AnnouncementSettings { } } announcement_settings; +static void follow_a_dwarf(color_ostream &out); + +static class UnitHistory { + std::deque history; + size_t offset = 0; + +public: + void reset() { + history.clear(); + offset = 0; + } + + void add(color_ostream &out, df::unit *unit) { + int32_t id = unit->id; + DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = id; + if (offset > 0) { + DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + history.resize(history.size() - offset); + offset = 0; + } + history.push_back(id); + if (history.size() > MAX_HISTORY) { + DEBUG(cycle,out).print("history full, truncating\n"); + history.pop_front(); + } + DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); + } + + void scan_back(color_ostream &out) { + if (history.empty() || offset >= history.size()-1) + return; + int unit_id = history[history.size() - (1 + ++offset)]; + DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); + if (auto unit = df::unit::find(unit_id)) + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = unit_id; + } + + void scan_forward(color_ostream &out) { + if (history.empty() || offset == 0) { + DEBUG(cycle,out).print("already at most recent unit; following new unit\n"); + follow_a_dwarf(out); + return; + } + + int unit_id = history[history.size() - (1 + --offset)]; + DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); + if (auto unit = df::unit::find(unit_id)) + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = unit_id; + } +} unit_history; + +struct forward_back_interceptor : df::viewscreen_dwarfmodest { + typedef df::viewscreen_dwarfmodest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { + bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default", Gui::getDFViewscreen()); + if (is_at_default_view && input->count(df::interface_key::CUSTOM_LEFT)) + unit_history.scan_back(Core::getInstance().getConsole()); + else if (is_at_default_view && input->count(df::interface_key::CUSTOM_RIGHT)) + unit_history.scan_forward(Core::getInstance().getConsole()); + else { + INTERPOSE_NEXT(feed)(input); + } + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -143,6 +218,11 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit = -1; - announcement_settings.reset(out); + on_disable(out); + // don't reset the unit history since we may want to re-enable } } else { DEBUG(control,out).print("%s from the API, but already %s; no action\n", @@ -175,7 +257,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_shutdown (color_ostream &out) { DEBUG(control,out).print("shutting down %s\n", plugin_name); - announcement_settings.reset(out); + on_disable(out); return CR_OK; } @@ -190,6 +272,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan plugin_name); is_enabled = false; announcement_settings.reset(out, true); + unit_history.reset(); } break; default: @@ -204,7 +287,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (config.auto_disengage && plotinfo->follow_unit < 0) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; - announcement_settings.reset(out); + on_disable(out); return CR_OK; } @@ -353,8 +436,7 @@ static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); } - DEBUG(cycle,out).print("now following unit %d: %s\n", unit->id, Units::getReadableName(unit).c_str()); - plotinfo->follow_unit = unit->id; + unit_history.add(out, unit); } ///////////////////////////////////////////////////// From 82e014dd6791d2e8046ea9a15c84f258876a407b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 02:55:03 -0800 Subject: [PATCH 0052/1082] auto-disengage when the squads menu is opened --- docs/plugins/spectate.rst | 6 +++--- plugins/lua/spectate.lua | 15 +++++++++++++++ plugins/spectate.cpp | 23 +++++++++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index f49849c850..d0b5c8f04e 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -13,9 +13,9 @@ to following a different dwarf. It can also switch to following animals, hostiles, or visiting units. You can switch to the next target (or a previous target) immediately with the left/right arrow keys. -`spectate` will disengage and turn itself off when you move the map, just like -the vanilla follow mechanic. It will also disengage immediately if you open the -squads menu for military action. +By default, `spectate` will disengage and turn itself off when you move the +map, just like the vanilla follow mechanic. It will also disengage immediately +if you open the squads menu for military action. It can also annotate your dwarves on the map with their name, job, and other information, either as floating tooltips or in a panel that comes up when you diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index ba7e9ba7ba..e21016329f 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -1,6 +1,7 @@ local _ENV = mkmodule('plugins.spectate') local argparse = require('argparse') +local dlg = require('gui.dialogs') local json = require('json') local overlay = require('plugins.overlay') local utils = require('utils') @@ -55,6 +56,20 @@ function refresh_cpp_config() end end +function show_squads_warning() + local message = { + 'Cannot start spectate mode while auto-disengage is enabled and', + 'the squads panel is open. The auto-disengage feature automatically', + 'stops spectate mode when you open the squads panel.', + '', + 'Please either close the squads panel or disable auto-disengage by', + 'running the following command:', + '', + 'spectate set auto-disengage false', + } + dlg.showMessage("Spectate", table.concat(message, '\n')) +end + ----------------------------- -- commandline interface diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index a3e3c86193..67a841ed6a 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -79,7 +79,7 @@ static class AnnouncementSettings { } public: - void reset(color_ostream &out, bool skip_restore = false) { + void reset(color_ostream &out, bool skip_restore) { was_in_settings = false; if (saved) { @@ -218,9 +218,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit < 0) { + if (config.auto_disengage && (plotinfo->follow_unit < 0 || is_squads_open())) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; + plotinfo->follow_unit = -1; on_disable(out); return CR_OK; } From 8b28be61602c7c86baff4960024069253b9f2744 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:29:50 -0800 Subject: [PATCH 0053/1082] first part of prefer recent units feature --- plugins/spectate.cpp | 80 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 67a841ed6a..c7f07b94ce 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -38,6 +38,17 @@ static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; +static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; +static const float JOB_WEIGHT = 3.0f; +static const float OTHER_WEIGHT = 1.0f; + +static const float RECENT_UNIT_MULTIPLIER = 2.0f; +static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes + +///////////////////////////////////////////////////// +// Configuration + static struct Configuration { bool auto_disengage; bool auto_unpause; @@ -64,6 +75,9 @@ static struct Configuration { } } config; +///////////////////////////////////////////////////// +// AnnouncementSettings + static class AnnouncementSettings { bool was_in_settings = false; // whether we were in the vanilla settings screen last update @@ -133,6 +147,9 @@ static class AnnouncementSettings { } } announcement_settings; +///////////////////////////////////////////////////// +// UnitHistory + static void follow_a_dwarf(color_ostream &out); static class UnitHistory { @@ -204,6 +221,37 @@ struct forward_back_interceptor : df::viewscreen_dwarfmodest { }; IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); +///////////////////////////////////////////////////// +// RecentUnits + +static class RecentUnits { + std::unordered_map units; // unit id -> time seen +public: + void add(int32_t unit_id) { + units[unit_id] = Core::getInstance().getUnpausedMs(); + } + + bool contains(int32_t unit_id) { + return units.contains(unit_id); + } + + void trim() { + uint32_t unpaused_ms = Core::getInstance().getUnpausedMs(); + if (unpaused_ms < RECENT_UNIT_MS) + return; + uint32_t cutoff = unpaused_ms - RECENT_UNIT_MS; + for (auto it = units.begin(); it != units.end();) { + if (it->second < cutoff) + it = units.erase(it); + else + ++it; + } + } +} recent_units; + +///////////////////////////////////////////////////// +// plugin API + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -321,7 +369,6 @@ static command_result do_command(color_ostream &out, vector ¶meters) ///////////////////////////////////////////////////// // cycle logic -// static bool is_in_combat(df::unit *unit) { return false; @@ -370,17 +417,18 @@ static void get_dwarf_buckets(color_ostream &out, static std::default_random_engine rng; -static uint32_t get_next_cycle_unpaused_ms(bool has_active_combat) { +static uint32_t get_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat) { int32_t delay_ms = config.follow_ms; if (has_active_combat) { std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); int32_t delay_ms = distribution(rng); delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } + DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); return Core::getInstance().getUnpausedMs() + delay_ms; } -static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { +static void add_bucket_to_vectors(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { if (bucket.empty()) return; intervals.push_back(units.size() + bucket.size()); @@ -388,6 +436,24 @@ static void add_bucket(const vector &bucket, vector &units units.insert(units.end(), bucket.begin(), bucket.end()); } +static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { + if (bucket.empty()) + return; + if (config.prefer_new_arrivals) { + vector new_bucket, old_bucket; + for (auto unit : bucket) { + if (recent_units.contains(unit->id)) + new_bucket.push_back(unit); + else + old_bucket.push_back(unit); + } + add_bucket_to_vectors(new_bucket, units, intervals, weights, weight * RECENT_UNIT_MULTIPLIER); + add_bucket_to_vectors(old_bucket, units, intervals, weights, weight); + } else { + add_bucket_to_vectors(bucket, units, intervals, weights, weight); + } +} + #define DUMP_BUCKET(name) \ DEBUG(cycle,out).print("bucket: " #name ", size: %zd\n", name.size()); \ if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ @@ -402,11 +468,6 @@ static void add_bucket(const vector &bucket, vector &units DEBUG(cycle,out).print(" %d\n", (int)f); \ } -static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; -static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; -static const float JOB_WEIGHT = 3.0f; -static const float OTHER_WEIGHT = 1.0f; - static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("choosing a unit to follow\n"); @@ -416,7 +477,7 @@ static void follow_a_dwarf(color_ostream &out) { vector other_units; get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); - next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(!active_combat_units.empty()); + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !active_combat_units.empty()); // coalesce the buckets and add weights vector units; @@ -452,7 +513,6 @@ static void follow_a_dwarf(color_ostream &out) { ///////////////////////////////////////////////////// // Lua API -// static void spectate_setSetting(color_ostream &out, string name, int val) { DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); From ae305971aad0eba85af8f5760e1a39d8ab7c0e85 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:31:04 -0800 Subject: [PATCH 0054/1082] only intercept arrow keys when the map has focus --- plugins/spectate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index c7f07b94ce..94c5adb23f 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -209,7 +209,7 @@ struct forward_back_interceptor : df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { - bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default", Gui::getDFViewscreen()); + bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default"); if (is_at_default_view && input->count(df::interface_key::CUSTOM_LEFT)) unit_history.scan_back(Core::getInstance().getConsole()); else if (is_at_default_view && input->count(df::interface_key::CUSTOM_RIGHT)) From ea4b2a2e7a3fd498b986019ba21d219688328fe3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:39:55 -0800 Subject: [PATCH 0055/1082] collapse two overlays to one since we don't have a good method to control which one gets rendered first --- docs/plugins/spectate.rst | 43 ++++++++++++++++----------------------- plugins/lua/spectate.lua | 29 +++++++------------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index d0b5c8f04e..3d20b1e665 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -37,7 +37,7 @@ Usage spectate [status] spectate toggle spectate set - spectate overlay enable|disable + spectate overlay enable|disable Examples -------- @@ -59,8 +59,9 @@ Examples ``spectate set follow-seconds 30`` Configure `spectate` to switch targets every 30 seconds when in follow mode. -``spectate overlay follow enable`` - Show informative tooltips that follow each unit on the map. +``spectate overlay enable`` + Show informative tooltips that follow each unit on the map. Note that this + can be enabled independently of `spectate` itself. Settings -------- @@ -68,7 +69,8 @@ Settings ``auto-disengage`` (default: enabled) Toggle automatically disabling the plugin when the player moves the map or opens the squad panel. If this is disabled, you will need to manually - disable the plugin to turn off follow mode. + disable the plugin to turn off follow mode. You can still interact normally + with the DF UI. ``auto-unpause`` (default: disabled) Toggle auto-dismissal of announcements that pause the game, like sieges, @@ -102,47 +104,36 @@ Settings arrived on the map. ``tooltip-follow-job`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. ``tooltip-follow-name`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the tooltip. ``tooltip-follow-stress`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the tooltip. ``tooltip-hover-job`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the hover panel. ``tooltip-hover-name`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the hover panel. ``tooltip-hover-stress`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the hover panel. Overlays -------- -``spectate`` provides two overlays via the `overlay` framework to add -information and functionality to the main map. These overlays can be controlled -via the ``spectate overlay`` command or the ``Overlays`` tab in -`gui/control-panel`. +``spectate.tooltip`` -The information displayed by these overlays can be configured via the -``spectate set`` command or the `gui/spectate` interface. +``spectate`` can show informative tooltips that follow each unit on the map +and/or a popup panel with information when your mouse cursor hovers over a unit. -``spectate.follow`` - Show informative tooltips that follow each unit on the map. You can enable - this overlay by running ``spectate overlay follow enable`` or, - equivalently, ``overlay enable spectate.follow``. - -``spectate.hover`` - Show a popup panel with selected information when your mouse cursor hovers - over a unit. You can enable this overlay by running - ``spectate overlay hover enable`` or, equivalently, - ``overlay enable spectate.hover``. +This overlay is managed via the `overlay` framework. It can be controlled via +the ``spectate overlay`` command or the ``Overlays`` tab in `gui/control-panel`. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index e21016329f..d44957dcb1 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -109,15 +109,9 @@ local function set_setting(key, value) end end -local function set_overlay(name, value) - if not name:startswith('spectate.') then - name = 'spectate.' .. name - end - if name ~= 'spectate.follow' and name ~= 'spectate.hover' then - qerror('unknown overlay: ' .. name) - end +local function set_overlay(value) value = argparse.boolean(value, name) - dfhack.run_command('overlay', value and 'enable' or 'disable', name) + dfhack.run_command('overlay', value and 'enable' or 'disable', 'spectate.tooltip') end function parse_commandline(args) @@ -129,7 +123,7 @@ function parse_commandline(args) elseif command == 'set' then set_setting(args[1], args[2]) elseif command == 'overlay' then - set_overlay(args[1], args[2]) + set_overlay(args[1]) else return false end @@ -140,25 +134,16 @@ end ----------------------------- -- overlays -FollowOverlay = defclass(FollowOverlay, overlay.OverlayWidget) -FollowOverlay.ATTRS{ - desc='Adds info tooltips that follow units on the map.', - default_pos={x=1,y=1}, - fullscreen=true, - viewscreens='dwarfmode/Default', -} - -HoverOverlay = defclass(HoverOverlay, overlay.OverlayWidget) -HoverOverlay.ATTRS{ - desc='Shows info popup when hovering the mouse over units on the map.', +TooltipOverlay = defclass(TooltipOverlay, overlay.OverlayWidget) +TooltipOverlay.ATTRS{ + desc='Adds info tooltips that follow units or appear when you hover the mouse.', default_pos={x=1,y=1}, fullscreen=true, viewscreens='dwarfmode/Default', } OVERLAY_WIDGETS = { - follow=FollowOverlay, - hover=HoverOverlay, + tooltip=TooltipOverlay, } return _ENV From 77bbe8c87815e218d5f9bdc2f175572c9c27bc72 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:59:30 -0800 Subject: [PATCH 0056/1082] implement recent units preference --- plugins/spectate.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 94c5adb23f..abe0fffaf1 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -4,6 +4,7 @@ #include "PluginManager.h" #include "VTableInterpose.h" +#include "modules/EventManager.h" #include "modules/Gui.h" #include "modules/Units.h" #include "modules/World.h" @@ -32,6 +33,7 @@ REQUIRE_GLOBAL(world); namespace DFHack { DBG_DECLARE(spectate, control, DebugCategory::LINFO); DBG_DECLARE(spectate, cycle, DebugCategory::LINFO); + DBG_DECLARE(spectate, event, DebugCategory::LINFO); } static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle @@ -43,6 +45,7 @@ static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; static const float JOB_WEIGHT = 3.0f; static const float OTHER_WEIGHT = 1.0f; +static const int32_t RECENT_UNITS_SCAN_CYCLE = 51; static const float RECENT_UNIT_MULTIPLIER = 2.0f; static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes @@ -226,7 +229,12 @@ IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); static class RecentUnits { std::unordered_map units; // unit id -> time seen + public: + void reset() { + units.clear(); + } + void add(int32_t unit_id) { units[unit_id] = Core::getInstance().getUnpausedMs(); } @@ -249,6 +257,14 @@ static class RecentUnits { } } recent_units; +static void on_new_active_unit(color_ostream& out, void* data) { + int32_t unit_id = reinterpret_cast(data); + DEBUG(event,out).print("unit %d has arrived on map\n", unit_id); + recent_units.add(unit_id); +} + +static EventManager::EventHandler new_unit_handler(plugin_self, on_new_active_unit, RECENT_UNITS_SCAN_CYCLE); + ///////////////////////////////////////////////////// // plugin API @@ -267,6 +283,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) + if ((!config.auto_disengage && plotinfo->follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { + recent_units.trim(); follow_a_dwarf(out); + } return CR_OK; } From a8ebea3c96af3a4909b6a34e817cb01cedb6cf93 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 04:43:10 -0800 Subject: [PATCH 0057/1082] implement conflict detection and cinematic camera --- plugins/spectate.cpp | 67 ++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index abe0fffaf1..b27db51460 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -13,6 +13,7 @@ #include "df/d_init.h" #include "df/plotinfost.h" #include "df/unit.h" +#include "df/activity_entry.h" #include "df/viewscreen_dwarfmodest.h" #include "df/world.h" @@ -40,14 +41,22 @@ static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; -static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; -static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; -static const float JOB_WEIGHT = 3.0f; +static const float CITIZEN_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float OTHER_COMBAT_PREFERRED_WEIGHT = 10.0f; +static const float JOB_WEIGHT = 5.0f; static const float OTHER_WEIGHT = 1.0f; static const int32_t RECENT_UNITS_SCAN_CYCLE = 51; -static const float RECENT_UNIT_MULTIPLIER = 2.0f; -static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes +static const float RECENT_UNIT_MULTIPLIER = 2.0f; // weight multiplier for recent units +static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes + +// jobs that get "other" weight instad of "job" weight +static const std::unordered_set boring_jobs = { + df::job_type::Eat, + df::job_type::Drink, + df::job_type::Sleep, +}; + ///////////////////////////////////////////////////// // Configuration @@ -392,25 +401,22 @@ static command_result do_command(color_ostream &out, vector ¶meters) // cycle logic static bool is_in_combat(df::unit *unit) { - return false; -} - -static bool is_fleeing(df::unit *unit) { + if (Units::isCrazed(unit) || unit->mood == df::mood_type::Berserk) + return true; + for (auto activity_id : unit->activities) { + auto activity = df::activity_entry::find(activity_id); + if (activity && activity->type == df::activity_entry_type::Conflict) + return true; + } return false; } static void get_dwarf_buckets(color_ostream &out, - vector &active_combat_units, - vector &passive_combat_units, + vector &citizen_combat_units, + vector &other_combat_units, vector &job_units, vector &other_units) { - static const std::unordered_set boring_jobs = { - df::job_type::Eat, - df::job_type::Drink, - df::job_type::Sleep, - }; - for (auto unit : world->units.active) { if (Units::isDead(unit) || !Units::isActive(unit) || unit->flags1.bits.caged || unit->flags1.bits.chained || Units::isHidden(unit)) continue; @@ -424,10 +430,11 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - if (is_fleeing(unit)) - passive_combat_units.push_back(unit); + INFO(cycle).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) + citizen_combat_units.push_back(unit); else - active_combat_units.push_back(unit); + other_combat_units.push_back(unit); } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { job_units.push_back(unit); } else { @@ -440,9 +447,9 @@ static std::default_random_engine rng; static uint32_t get_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat) { int32_t delay_ms = config.follow_ms; - if (has_active_combat) { + if (config.cinematic_action && has_active_combat) { std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); - int32_t delay_ms = distribution(rng); + delay_ms = distribution(rng); delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); @@ -492,21 +499,21 @@ static void add_bucket(const vector &bucket, vector &units static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("choosing a unit to follow\n"); - vector active_combat_units; - vector passive_combat_units; + vector citizen_combat_units; + vector other_combat_units; vector job_units; vector other_units; - get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); + get_dwarf_buckets(out, citizen_combat_units, other_combat_units, job_units, other_units); - next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !active_combat_units.empty()); + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); // coalesce the buckets and add weights vector units; vector intervals; vector weights; intervals.push_back(0); - add_bucket(active_combat_units, units, intervals, weights, config.prefer_conflict ? ACTIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); - add_bucket(passive_combat_units, units, intervals, weights, config.prefer_conflict ? PASSIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(citizen_combat_units, units, intervals, weights, config.prefer_conflict ? CITIZEN_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(other_combat_units, units, intervals, weights, config.prefer_conflict ? OTHER_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); @@ -520,8 +527,8 @@ static void follow_a_dwarf(color_ostream &out) { df::unit *unit = units[unit_idx]; if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { - DUMP_BUCKET(active_combat_units); - DUMP_BUCKET(passive_combat_units); + DUMP_BUCKET(citizen_combat_units); + DUMP_BUCKET(other_combat_units); DUMP_BUCKET(job_units); DUMP_BUCKET(other_units); DUMP_FLOAT_VECTOR(intervals); From ab263e4755bd39aafa8ed5795860f689dd7f2e0f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 04:47:47 -0800 Subject: [PATCH 0058/1082] clarify function of include-animals setting --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 3d20b1e665..b4d913d9e0 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -84,7 +84,7 @@ Settings include time that the game is paused. ``include-animals`` (default: disabled) - Toggle whether to sometimes follow fort animals. + Toggle whether to sometimes follow fort animals and wildlife. ``include-hostiles`` (default: disabled) Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, From fa8550427d3516bffe2f646c204938022db64326 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 12:30:24 -0800 Subject: [PATCH 0059/1082] protect chained units from autobutcher --- docs/changelog.txt | 1 + docs/plugins/autobutcher.rst | 1 + plugins/autobutcher.cpp | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 36315052f7..3e5fa6ad90 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort +- `autobutcher`: treat animals on restraints as unavailable for slaughter ## Documentation - `stonesense-art-guide`: new guide for making sprite art for Stonesense diff --git a/docs/plugins/autobutcher.rst b/docs/plugins/autobutcher.rst index b429f57542..f7bea4d6d9 100644 --- a/docs/plugins/autobutcher.rst +++ b/docs/plugins/autobutcher.rst @@ -15,6 +15,7 @@ Units are protected from being automatically butchered if they are: * Untamed * Named or nicknamed * Caged, if and only if the cage is in a zone (to protect zoos) +* On a restraint * Trained for war or hunting * Females who are pregnant or brooding a clutch of fertile eggs diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 9e93018cc9..8fde1cc347 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -666,9 +666,10 @@ static bool isProtectedUnit(df::unit *unit) { || Units::isHunter(unit) // ignore hunting dogs etc || Units::isMarkedForWarTraining(unit) // ignore units marked for any kind of training || Units::isMarkedForHuntTraining(unit) - // ignore creatures in built cages which are defined as rooms to leave zoos alone + || unit->flags1.bits.chained // ignore chained animals + // ignore creatures in built cages which are members of zones to leave zoos alone // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) || (unit->pregnancy_timer != 0) // do not butcher pregnant animals (which includes brooding female egglayers) || Units::isAvailableForAdoption(unit) || unit->name.has_name From eed96caeeea7f8ce1729c41ab7c3b4f89d1199f4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 07:34:55 -0800 Subject: [PATCH 0060/1082] update to great reorg structures --- library/LuaApi.cpp | 5 +- library/RemoteTools.cpp | 2 +- .../df/custom/creature_handler.methods.inc | 2 +- library/include/modules/Buildings.h | 5 +- library/include/modules/Items.h | 2 +- library/include/modules/Job.h | 2 +- library/include/modules/MapCache.h | 2 +- library/modules/Buildings.cpp | 119 +++++++++--------- library/modules/Gui.cpp | 5 +- library/modules/Items.cpp | 21 ++-- library/modules/Job.cpp | 10 +- library/modules/Maps.cpp | 16 +-- library/modules/Materials.cpp | 18 +-- library/modules/Military.cpp | 35 +++--- library/modules/Units.cpp | 13 +- plugins/3dveins.cpp | 2 +- plugins/blueprint.cpp | 2 +- plugins/buildingplan/buildingplan.cpp | 4 +- plugins/buildingplan/buildingplan_cycle.cpp | 2 +- plugins/cursecheck.cpp | 1 - plugins/devel/eventExample.cpp | 1 + plugins/devel/stripcaged.cpp | 1 - plugins/dwarfmonitor.cpp | 3 +- plugins/embark-assistant/embark-assistant.cpp | 1 - plugins/embark-assistant/finder_ui.cpp | 2 - plugins/embark-assistant/matcher.cpp | 1 - plugins/embark-assistant/overlay.cpp | 1 - plugins/embark-assistant/survey.cpp | 2 - plugins/forceequip.cpp | 2 +- plugins/generated-creature-renamer.cpp | 1 - plugins/infinite-sky.cpp | 4 +- plugins/logistics.cpp | 1 + plugins/nestboxes.cpp | 1 + plugins/orders.cpp | 10 +- plugins/probe.cpp | 2 +- plugins/prospector.cpp | 14 ++- .../remotefortressreader/building_reader.cpp | 4 +- plugins/remotefortressreader/item_reader.cpp | 19 ++- .../remotefortressreader.cpp | 70 +++++------ plugins/stockpiles/OrganicMatLookup.cpp | 6 +- plugins/stockpiles/StockpileSerializer.cpp | 22 ++-- plugins/strangemood.cpp | 20 +-- plugins/tailor.cpp | 4 +- plugins/timestream.cpp | 5 +- 44 files changed, 236 insertions(+), 229 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 6e251dbfcf..637df32211 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -99,6 +99,7 @@ distribution. #include "df/report_zoom_type.h" #include "df/specific_ref.h" #include "df/specific_ref_type.h" +#include "df/squad_use_flags.h" #include "df/squad.h" #include "df/unit.h" #include "df/unit_misc_trait.h" @@ -2443,7 +2444,7 @@ static int items_moveToBuilding(lua_State *state) static int items_moveToInventory(lua_State *state) { auto item = Lua::CheckDFObject(state, 1); auto unit = Lua::CheckDFObject(state, 2); - auto use_mode = (df::unit_inventory_item::T_mode)luaL_optint(state, 3, 0); + auto use_mode = (df::inv_item_role_type)luaL_optint(state, 3, 0); int body_part = luaL_optint(state, 4, -1); lua_pushboolean(state, Items::moveToInventory(item, unit, use_mode, body_part)); return 1; @@ -2816,7 +2817,7 @@ int buildings_setSize(lua_State *state) lua_pushinteger(state, size.x); lua_pushinteger(state, size.y); lua_pushinteger(state, area); - lua_pushinteger(state, Buildings::countExtentTiles(&bld->room, area)); + lua_pushinteger(state, Buildings::countExtentTiles(bld, area)); return 5; } else diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index fbace1ae2e..3df55b847a 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -458,7 +458,7 @@ static command_result ListEnums(color_ostream &stream, BITFIELD(cie_add_tag_mask1); BITFIELD(cie_add_tag_mask2); - describe_bitfield(out->mutable_death_info_flags()); + describe_bitfield(out->mutable_death_info_flags()); ENUM(profession); diff --git a/library/include/df/custom/creature_handler.methods.inc b/library/include/df/custom/creature_handler.methods.inc index 811128d058..14639bd889 100644 --- a/library/include/df/custom/creature_handler.methods.inc +++ b/library/include/df/custom/creature_handler.methods.inc @@ -1 +1 @@ -friend struct world_raws; +friend struct world; diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index ee7fcdf698..80fe443396 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -44,7 +44,6 @@ namespace df { struct building; struct building_cagest; struct building_civzonest; - struct building_extents; struct building_stockpilest; struct item; struct job_item; @@ -112,7 +111,7 @@ DFHACK_EXPORT bool getCorrectSize(df::coord2d &size, df::coord2d ¢er, * Checks if the tiles are free to be built upon. */ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, - df::building_extents *ext = NULL, + df::building *bld, bool create_ext = false, bool allow_occupied = false, bool allow_wall = false, @@ -121,7 +120,7 @@ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, /** * Returns the number of tiles included by the extent, or defval. */ -DFHACK_EXPORT int countExtentTiles(df::building_extents *ext, int defval = -1); +DFHACK_EXPORT int countExtentTiles(df::building *bld, int defval = -1); /** * Checks if the building contains the specified tile. If the building has diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 79aed92c36..6d068e8786 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -157,7 +157,7 @@ DFHACK_EXPORT bool moveToContainer(df::item *item, df::item *container); DFHACK_EXPORT bool moveToBuilding(df::item *item, df::building_actual *building, df::building_item_role_type use_mode = df::building_item_role_type::TEMP, bool force_in_building = false); DFHACK_EXPORT bool moveToInventory(df::item *item, df::unit *unit, - df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Hauled, int body_part = -1); + df::inv_item_role_type mode = df::inv_item_role_type::Hauled, int body_part = -1); /// Remove item from jobs and inventories, hide and forbid. /// Unless no_uncat, item is marked for garbage collection. diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index f0d3e89b97..a0754c4781 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -105,7 +105,7 @@ namespace DFHack DFHACK_EXPORT bool listNewlyCreated(std::vector *pvec, int *id_var); DFHACK_EXPORT bool attachJobItem(df::job *job, df::item *item, - df::job_item_ref::T_role role, + df::job_role_type role, int filter_idx = -1, int insert_idx = -1); DFHACK_EXPORT bool isSuitableItem(const df::job_item *item, df::item_type itype, int isubtype); diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h index 04fddd38bd..0d63627389 100644 --- a/library/include/modules/MapCache.h +++ b/library/include/modules/MapCache.h @@ -84,7 +84,7 @@ class BlockInfo }; static GroundType getGroundType(int material); - typedef df::block_square_event_mineralst::T_flags DFVeinFlags; + typedef df::mineral_event_flag DFVeinFlags; t_veintype veintype; t_blockmaterials veinmats; diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index a560a6fc99..672464ceba 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -48,18 +48,21 @@ distribution. #include "df/building_coffinst.h" #include "df/building_def.h" #include "df/building_design.h" +#include "df/building_extents_type.h" #include "df/building_floodgatest.h" #include "df/building_furnacest.h" #include "df/building_grate_floorst.h" #include "df/building_grate_wallst.h" #include "df/building_rollersst.h" #include "df/building_screw_pumpst.h" +#include "df/building_squad_infost.h" #include "df/building_stockpilest.h" #include "df/building_trapst.h" #include "df/building_water_wheelst.h" #include "df/building_weaponst.h" #include "df/building_wellst.h" #include "df/building_workshopst.h" +#include "df/buildingitemst.h" #include "df/buildings_other_id.h" #include "df/d_init.h" #include "df/dfhack_room_quality_level.h" @@ -77,6 +80,7 @@ distribution. #include "df/plotinfost.h" #include "df/punishment.h" #include "df/squad.h" +#include "df/squad_barracks_infost.h" #include "df/unit.h" #include "df/unit_relationship_type.h" #include "df/world.h" @@ -112,15 +116,15 @@ struct CoordHash { static unordered_map locationToBuilding; -static df::building_extents_type *getExtentTile(df::building_extents &extent, df::coord2d tile) +static df::building_extents_type *getExtentTile(const df::building::T_room &room, df::coord2d tile) { - if (!extent.extents) + if (!room.extents) return NULL; - int dx = tile.x - extent.x; - int dy = tile.y - extent.y; - if (dx < 0 || dy < 0 || dx >= extent.width || dy >= extent.height) + int dx = tile.x - room.x; + int dy = tile.y - room.y; + if (dx < 0 || dy < 0 || dx >= room.width || dy >= room.height) return NULL; - return &extent.extents[dx + dy*extent.width]; + return &room.extents[dx + dy*room.width]; } /* @@ -162,14 +166,14 @@ void buildings_onUpdate(color_ostream &out) for (size_t i = 0; i < job->items.size(); i++) { df::job_item_ref *iref = job->items[i]; - if (iref->role != df::job_item_ref::Reagent) + if (iref->role != df::job_role_type::Reagent) continue; df::job_item *item = vector_get(job->job_items.elements, iref->job_item_idx); if (!item) continue; // Convert Reagent to Hauled, while decrementing quantity item->quantity = std::max(0, item->quantity-1); - iref->role = df::job_item_ref::Hauled; + iref->role = df::job_role_type::Hauled; iref->job_item_idx = -1; } } @@ -219,7 +223,7 @@ static void add_building_to_all_zones(df::building* bld) static void add_zone_to_all_buildings(df::building* zone_as_building) { - if (zone_as_building->getType() != building_type::Civzone) + if (zone_as_building->getType() != df::building_type::Civzone) return; auto zone = strict_virtual_cast(zone_as_building); @@ -259,7 +263,7 @@ static void remove_building_from_zone(df::building* bld, df::building_civzonest* static void remove_zone_from_all_buildings(df::building* zone_as_building) { - if (zone_as_building->getType() != building_type::Civzone) + if (zone_as_building->getType() != df::building_type::Civzone) return; auto zone = strict_virtual_cast(zone_as_building); @@ -478,19 +482,20 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in // Type specific init switch (type) { - case building_type::Well: + using namespace df::enums::building_type; + case Well: { if (VIRTUAL_CAST_VAR(obj, df::building_wellst, bld)) obj->bucket_z = bld->z; break; } - case building_type::Workshop: + case Workshop: { if (VIRTUAL_CAST_VAR(obj, df::building_workshopst, bld)) obj->profile.max_general_orders = 5; break; } - case building_type::Furnace: + case Furnace: { if (VIRTUAL_CAST_VAR(obj, df::building_furnacest, bld)) { @@ -507,7 +512,7 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in break; } */ - case building_type::Trap: + case Trap: { if (VIRTUAL_CAST_VAR(obj, df::building_trapst, bld)) { @@ -516,46 +521,46 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in } break; } - case building_type::Floodgate: + case Floodgate: { if (VIRTUAL_CAST_VAR(obj, df::building_floodgatest, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::GrateWall: + case GrateWall: { if (VIRTUAL_CAST_VAR(obj, df::building_grate_wallst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::GrateFloor: + case GrateFloor: { if (VIRTUAL_CAST_VAR(obj, df::building_grate_floorst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::BarsVertical: + case BarsVertical: { if (VIRTUAL_CAST_VAR(obj, df::building_bars_verticalst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::BarsFloor: + case BarsFloor: { if (VIRTUAL_CAST_VAR(obj, df::building_bars_floorst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::Bridge: + case Bridge: { if (VIRTUAL_CAST_VAR(obj, df::building_bridgest, bld)) - obj->gate_flags.bits.closed = false; + obj->gate_flags.bits.raised = false; break; } - case building_type::Weapon: + case Weapon: { if (VIRTUAL_CAST_VAR(obj, df::building_weaponst, bld)) - obj->gate_flags.bits.closed = false; + obj->gate_flags.bits.retracted = false; break; } default: @@ -711,26 +716,29 @@ bool Buildings::getCorrectSize(df::coord2d &size, df::coord2d ¢er, } } -static void init_extents(df::building_extents *ext, const df::coord &pos, +static void init_extents(df::building::T_room &room, const df::coord &pos, const df::coord2d &size) { - ext->extents = new df::building_extents_type[size.x*size.y]; - ext->x = pos.x; - ext->y = pos.y; - ext->width = size.x; - ext->height = size.y; + room.extents = new df::building_extents_type[size.x*size.y]; + room.x = pos.x; + room.y = pos.y; + room.width = size.x; + room.height = size.y; - memset(ext->extents, 1, size.x*size.y); + memset(room.extents, 1, size.x*size.y); } bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, - df::building_extents *ext, + df::building *bld, bool create_ext, bool allow_occupied, bool allow_wall, bool allow_flow) { + CHECK_NULL_POINTER(bld); + bool found_any = false; + auto & room = bld->room; for (int dx = 0; dx < size.x; dx++) { @@ -740,9 +748,9 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, df::building_extents_type *etile = NULL; // Exclude using extents - if (ext && ext->extents) + if (room.extents) { - etile = getExtentTile(*ext, tile); + etile = getExtentTile(room, tile); if (!etile || !*etile) continue; } @@ -776,13 +784,13 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, found_any = true; else { - if (!ext || !create_ext) + if (!create_ext) return false; - if (!ext->extents) + if (!room.extents) { - init_extents(ext, pos, size); - etile = getExtentTile(*ext, tile); + init_extents(room, pos, size); + etile = getExtentTile(room, tile); } if (!etile) @@ -813,10 +821,10 @@ static bool checkBuildingTiles(df::building *bld, bool can_change, if (force_extents && !bld->room.extents) { // populate the room structure if it's not set already - init_extents(&bld->room, psize.first, psize.second); + init_extents(bld->room, psize.first, psize.second); } - return Buildings::checkFreeTiles(psize.first, psize.second, &bld->room, + return Buildings::checkFreeTiles(psize.first, psize.second, bld, can_change && bld->isExtentShaped(), !bld->isSettingOccupancy(), bld->getType() == @@ -824,14 +832,18 @@ static bool checkBuildingTiles(df::building *bld, bool can_change, !bld->isActual()); } -int Buildings::countExtentTiles(df::building_extents *ext, int defval) +int Buildings::countExtentTiles(df::building *bld, int defval) { - if (!ext || !ext->extents) + CHECK_NULL_POINTER(bld); + + auto & room = bld->room; + + if (!room.extents) return defval; int cnt = 0; - for (int i = 0; i < ext->width * ext->height; i++) - if (ext->extents[i]) + for (int i = 0; i < room.width * room.height; i++) + if (room.extents[i]) cnt++; return cnt; } @@ -881,7 +893,7 @@ static int computeMaterialAmount(df::building *bld) int cnt = size.x * size.y; if (bld->room.extents && bld->isExtentShaped()) - cnt = Buildings::countExtentTiles(&bld->room, cnt); + cnt = Buildings::countExtentTiles(bld, cnt); return cnt/4 + 1; } @@ -911,10 +923,9 @@ bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) auto type = bld->getType(); - using namespace df::enums::building_type; - switch (type) { + using namespace df::enums::building_type; case WaterWheel: { auto obj = (df::building_water_wheelst*)bld; @@ -953,7 +964,7 @@ bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) bool ok = checkBuildingTiles(bld, true); - if (type != Construction) + if (type != df::building_type::Construction) bld->setMaterialAmount(computeMaterialAmount(bld)); return ok; @@ -1158,7 +1169,7 @@ bool Buildings::constructWithItems(df::building *bld, std::vector ite for (size_t i = 0; i < items.size(); i++) { - Job::attachJobItem(job, items[i], df::job_item_ref::Hauled); + Job::attachJobItem(job, items[i], df::job_role_type::Hauled); if (items[i]->getType() == item_type::BOULDER) rough = true; @@ -1224,15 +1235,9 @@ bool Buildings::constructWithFilters(df::building *bld, std::vectorsquad_room_info) + for (auto room_info : zone->squad_room_info) { - int32_t squad_id = room_info->squad_id; - - df::squad* squad = df::squad::find(squad_id); - - //if this is null, something has gone just *terribly* wrong - if (squad) - { + if (auto squad = df::squad::find(room_info->squad_id)) { for (int i=(int)squad->rooms.size() - 1; i >= 0; i--) { if (squad->rooms[i]->building_id == zone->id) @@ -1406,7 +1411,7 @@ bool Buildings::deconstruct(df::building *bld) for (int i = ui_look_list->size()-1; i >= 0; --i) { auto item = (*ui_look_list)[i]; - if (item->type == df::lookinfost::Building && + if (item->type == df::look_info_type::Building && item->data.building.bld_id == id) { vector_erase_at(*ui_look_list, i); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 951482d23f..853d42f3b8 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -84,7 +84,6 @@ distribution. #include "df/route_stockpile_link.h" #include "df/soundst.h" #include "df/stop_depart_condition.h" -#include "df/ui_unit_view_mode.h" #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/viewscreen_choose_start_sitest.h" @@ -1877,7 +1876,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce new_report->text = message; new_report->color = color; new_report->bright = bright; - new_report->flags.whole = adv_unconscious ? df::report::T_flags::mask_unconscious : 0x0; + new_report->flags.whole = adv_unconscious ? df::announcement_flag::mask_unconscious : 0x0; new_report->pos = pos; new_report->id = world->status.next_report_id++; new_report->year = *df::global::cur_year; @@ -2197,7 +2196,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) new_report->text = message; new_report->color = info.color; new_report->bright = info.bright; - new_report->flags.whole = adv_unconscious ? df::report::T_flags::mask_unconscious : 0x0; + new_report->flags.whole = adv_unconscious ? df::announcement_flag::mask_unconscious : 0x0; new_report->zoom_type = info.zoom_type; new_report->pos = info.pos; new_report->zoom_type2 = info.zoom_type2; diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 47d68b1654..a99c2bb761 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -44,6 +44,7 @@ distribution. #include "df/building.h" #include "df/building_actual.h" #include "df/building_tradedepotst.h" +#include "df/buildingitemst.h" #include "df/builtin_mats.h" #include "df/caravan_state.h" #include "df/creature_raw.h" @@ -765,13 +766,13 @@ static bool removeItemOnGround(df::item *item) } static void resetUnitInvFlags(df::unit *unit, df::unit_inventory_item *inv_item) { - if (inv_item->mode == df::unit_inventory_item::Worn || - inv_item->mode == df::unit_inventory_item::WrappedAround) + if (inv_item->mode == df::inv_item_role_type::Worn || + inv_item->mode == df::inv_item_role_type::WrappedAround) { unit->flags2.bits.calculated_inventory = false; unit->flags2.bits.calculated_insulation = false; } - else if (inv_item->mode == df::unit_inventory_item::StuckIn) + else if (inv_item->mode == df::inv_item_role_type::StuckIn) unit->flags3.bits.stuck_weapon_computed = false; } @@ -815,7 +816,7 @@ static bool detachItem(df::item *item) } if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) - return linked_list_remove(&world->proj_list, ref->projectile_id) + return linked_list_remove(&world->projectiles.all, ref->projectile_id) && removeRef(item->general_refs, general_ref_type::PROJECTILE, ref->getID()); if (item->flags.bits.on_ground) { @@ -960,7 +961,7 @@ bool DFHack::Items::moveToBuilding(df::item *item, df::building_actual *building ref->building_id = building->id; item->general_refs.push_back(ref); - auto con = new df::building_actual::T_contained_items; + auto con = new df::buildingitemst; con->item = item; con->use_mode = use_mode; building->contained_items.push_back(con); @@ -969,7 +970,7 @@ bool DFHack::Items::moveToBuilding(df::item *item, df::building_actual *building } bool DFHack::Items::moveToInventory(df::item *item, df::unit *unit, - df::unit_inventory_item::T_mode mode, int body_part) + df::inv_item_role_type mode, int body_part) { CHECK_NULL_POINTER(item); CHECK_NULL_POINTER(unit); @@ -1063,7 +1064,7 @@ df::proj_itemst *Items::makeProjectile(df::item *item) ref->projectile_id = proj->id; item->general_refs.push_back(ref); - linked_list_append(&world->proj_list, proj->link); + linked_list_append(&world->projectiles.all, proj->link); return proj; } @@ -1799,8 +1800,8 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t bool Items::checkMandates(df::item *item) { CHECK_NULL_POINTER(item); - for (auto mandate : world->mandates) { - if ((mandate->mode == df::mandate::Export) && + for (auto mandate : world->mandates.all) { + if ((mandate->mode == df::mandate_type::Export) && (item->getType() == mandate->item_type) && (mandate->item_subtype == -1 || item->getSubtype() == mandate->item_subtype) && (mandate->mat_type == -1 || item->getMaterial() == mandate->mat_type) && @@ -1881,7 +1882,7 @@ bool Items::markForTrade(df::item *item, df::building_tradedepotst *depot) { job->pos = df::coord(depot->centerx, depot->centery, depot->z); // job <-> item link - if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) { + if (!Job::attachJobItem(job, item, df::job_role_type::Hauled)) { delete job; delete href; return false; diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 86809ba5ba..96344ddd6f 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -46,6 +46,8 @@ distribution. #include "df/job.h" #include "df/job_item.h" #include "df/job_list_link.h" +#include "df/job_postingst.h" +#include "df/job_restrictionst.h" #include "df/plotinfost.h" #include "df/specific_ref.h" #include "df/unit.h" @@ -323,11 +325,11 @@ void DFHack::Job::setJobCooldown(df::building *workshop, df::unit *worker, int c if (cooldown <= 0) return; - int idx = linear_index(workshop->job_claim_suppress, &df::building::T_job_claim_suppress::unit, worker); + int idx = linear_index(workshop->job_claim_suppress, &df::job_restrictionst::unit, worker); if (idx < 0) { - auto obj = new df::building::T_job_claim_suppress; + auto obj = new df::job_restrictionst; obj->unit = worker; obj->timer = cooldown; workshop->job_claim_suppress.push_back(obj); @@ -575,7 +577,7 @@ bool DFHack::Job::listNewlyCreated(std::vector *pvec, int *id_var) } bool DFHack::Job::attachJobItem(df::job *job, df::item *item, - df::job_item_ref::T_role role, + df::job_role_type role, int filter_idx, int insert_idx) { CHECK_NULL_POINTER(job); @@ -585,7 +587,7 @@ bool DFHack::Job::attachJobItem(df::job *job, df::item *item, * Functionality 100% reverse-engineered from DF code. */ - if (role != df::job_item_ref::TargetContainer) + if (role != df::job_role_type::TargetContainer) { if (item->flags.bits.in_job) return false; diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index c207360ac4..54fb43c810 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -47,6 +47,8 @@ distribution. #include "df/builtin_mats.h" #include "df/burrow.h" #include "df/feature_init.h" +#include "df/feature_map_shellst.h" +#include "df/feature_mapst.h" #include "df/flow_info.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -515,7 +517,7 @@ df::feature_init *Maps::getLocalInitFeature(df::coord2d rgn_pos, int32_t index) df::coord2d sub = rgn_pos & 15; - vector &features = fptr->feature_init[sub.x][sub.y]; + vector &features = fptr->feature_init[sub.x][sub.y]; return vector_get(features, index); } @@ -947,13 +949,13 @@ static int16_t basic_wet_dry_effect(int16_t region_y, int16_t rain) auto dimy = world->world_data->world_height; auto pole = world->world_data->flip_latitude; - if (dimy > 65 && pole != df::world_data::T_flip_latitude::None) + if (dimy > 65 && pole != df::pole_type::None) { // Medium and Large worlds with at least one pole auto latitude = region_y; - if (pole == df::world_data::T_flip_latitude::South) + if (pole == df::pole_type::South) latitude = dimy - region_y - 1; - else if (pole == df::world_data::T_flip_latitude::Both) + else if (pole == df::pole_type::Both) { if (region_y < dimy / 2) latitude = region_y * 2; @@ -993,7 +995,7 @@ df::enums::biome_type::biome_type Maps::getBiomeTypeWithRef(int16_t region_x, in bool tropical; // Determine tropicality - if (pole == df::world_data::T_flip_latitude::None) + if (pole == df::pole_type::None) { potential_tropical = region->temperature > 74; tropical = region->temperature > 84; @@ -1002,9 +1004,9 @@ df::enums::biome_type::biome_type Maps::getBiomeTypeWithRef(int16_t region_x, in { auto latitude = region_ref_y; // DF just uses region_y, but embark assistant needs region_ref_y - if (pole == df::world_data::T_flip_latitude::South) + if (pole == df::pole_type::South) latitude = dimy - region_ref_y - 1; - else if (pole == df::world_data::T_flip_latitude::Both) + else if (pole == df::pole_type::Both) { if (region_ref_y < dimy / 2) latitude = region_ref_y * 2; diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 341ddeabc4..18dd73b09a 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -99,7 +99,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; } - df::world_raws &raws = world->raws; + auto &raws = world->raws; if (size_t(type) >= sizeof(raws.mat_table.builtin)/sizeof(void*)) return false; @@ -216,7 +216,7 @@ bool MaterialInfo::findBuiltin(const std::string &token) return true; } - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (int i = 0; i < NUM_BUILTIN; i++) { auto obj = raws.mat_table.builtin[i]; @@ -236,10 +236,10 @@ bool MaterialInfo::findInorganic(const std::string &token) return true; } - df::world_raws &raws = world->raws; - for (size_t i = 0; i < raws.inorganics.size(); i++) + auto &raws = world->raws; + for (size_t i = 0; i < raws.inorganics.all.size(); i++) { - df::inorganic_raw *p = raws.inorganics[i]; + df::inorganic_raw *p = raws.inorganics.all[i]; if (p->id == token) return decode(0, i); } @@ -250,7 +250,7 @@ bool MaterialInfo::findPlant(const std::string &token, const std::string &subtok { if (token.empty()) return decode(-1); - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (size_t i = 0; i < raws.plants.all.size(); i++) { df::plant_raw *p = raws.plants.all[i]; @@ -274,7 +274,7 @@ bool MaterialInfo::findCreature(const std::string &token, const std::string &sub { if (token.empty() || subtoken.empty()) return decode(-1); - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (size_t i = 0; i < raws.creatures.all.size(); i++) { df::creature_raw *p = raws.creatures.all[i]; @@ -645,12 +645,12 @@ bool t_matglossInorganic::isGem() bool Materials::CopyInorganicMaterials (std::vector & inorganic) { - size_t size = world->raws.inorganics.size(); + size_t size = world->raws.inorganics.all.size(); inorganic.clear(); inorganic.reserve (size); for (size_t i = 0; i < size;i++) { - df::inorganic_raw *orig = world->raws.inorganics[i]; + df::inorganic_raw *orig = world->raws.inorganics.all[i]; t_matglossInorganic mat; mat.id = orig->id; mat.name = orig->material.stone_name; diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 3c5708d75e..cbb4295562 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -7,6 +7,7 @@ #include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" +#include "df/building_squad_infost.h" #include "df/histfig_entity_link_former_positionst.h" #include "df/histfig_entity_link_former_squadst.h" #include "df/histfig_entity_link_positionst.h" @@ -16,11 +17,16 @@ #include "df/entity_position.h" #include "df/entity_position_assignment.h" #include "df/plotinfost.h" +#include "df/military_routinest.h" #include "df/squad.h" -#include "df/squad_position.h" -#include "df/squad_schedule_order.h" +#include "df/squad_barracks_infost.h" +#include "df/squad_month_positionst.h" #include "df/squad_order.h" #include "df/squad_order_trainst.h" +#include "df/squad_position.h" +#include "df/squad_routine_schedulest.h" +#include "df/squad_schedule_entry.h" +#include "df/squad_schedule_order.h" #include "df/unit.h" #include "df/world.h" @@ -96,7 +102,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) result->id = *df::global::squad_next_id; result->uniform_priority = result->id + 1; //no idea why, but seems to hold result->supplies.carry_food = 2; - result->supplies.carry_water = df::squad::T_supplies::Water; + result->supplies.carry_water = df::squad_water_level_type::Water; result->entity_id = df::global::plotinfo->group_id; result->leader_position = corresponding_position->id; result->leader_assignment = found_assignment->id; @@ -116,7 +122,8 @@ df::squad* Military::makeSquad(int32_t assignment_id) for (const auto& routine : routines) { - df::squad_schedule_entry* asched = (df::squad_schedule_entry*)malloc(sizeof(df::squad_schedule_entry) * 12); + df::squad_routine_schedulest* schedule = new df::squad_routine_schedulest[12]; + auto &asched = schedule->month; for(int kk=0; kk < 12; kk++) { @@ -124,14 +131,14 @@ df::squad* Military::makeSquad(int32_t assignment_id) for(int jj=0; jj < squad_size; jj++) { - int32_t* order_assignments = new int32_t(); - *order_assignments = -1; + df::squad_month_positionst* order_assignments = new df::squad_month_positionst(); + order_assignments->assigned_order_idx = -1; asched[kk].order_assignments.push_back(order_assignments); } } - auto insert_training_order = [asched, squad_size](int month) + auto insert_training_order = [&](int month) { df::squad_schedule_order* order = new df::squad_schedule_order(); order->min_count = squad_size; @@ -212,7 +219,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) } } - result->schedule.push_back(reinterpret_cast(asched)); + result->schedule.routine.push_back(schedule); } //Modify necessary world state @@ -234,8 +241,8 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s if (squad == nullptr || zone == nullptr) return; - df::squad::T_rooms* room_from_squad = nullptr; - df::building_civzonest::T_squad_room_info* room_from_building = nullptr; + df::squad_barracks_infost* room_from_squad = nullptr; + df::building_squad_infost* room_from_building = nullptr; for (auto room : squad->rooms) { @@ -263,18 +270,18 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s if (!avoiding_squad_roundtrip && room_from_squad == nullptr) { - room_from_squad = new df::squad::T_rooms(); + room_from_squad = new df::squad_barracks_infost(); room_from_squad->building_id = civzone_id; - insert_into_vector(squad->rooms, &df::squad::T_rooms::building_id, room_from_squad); + insert_into_vector(squad->rooms, &df::squad_barracks_infost::building_id, room_from_squad); } if (room_from_building == nullptr) { - room_from_building = new df::building_civzonest::T_squad_room_info(); + room_from_building = new df::building_squad_infost(); room_from_building->squad_id = squad_id; - insert_into_vector(zone->squad_room_info, &df::building_civzonest::T_squad_room_info::squad_id, room_from_building); + insert_into_vector(zone->squad_room_info, &df::building_squad_infost::squad_id, room_from_building); } if (room_from_squad) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 933ea53754..1af93c3b7d 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -61,16 +61,20 @@ distribution. #include "df/historical_kills.h" #include "df/history_event_hist_figure_diedst.h" #include "df/identity.h" +#include "df/interaction_profilest.h" #include "df/item.h" #include "df/job.h" #include "df/nemesis_record.h" +#include "df/personality_goalst.h" #include "df/plotinfost.h" +#include "df/reputation_profilest.h" #include "df/syndrome.h" #include "df/tile_occupancy.h" #include "df/training_assignment.h" #include "df/unit.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" +#include "df/unit_active_animationst.h" #include "df/unit_inventory_item.h" #include "df/unit_misc_trait.h" #include "df/unit_path_goal.h" @@ -79,6 +83,7 @@ distribution. #include "df/unit_soul.h" #include "df/unit_syndrome.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" #include "df/world_site.h" @@ -325,7 +330,7 @@ bool Units::isNaked(df::unit *unit, bool no_items) { for (auto inv_item : unit->inventory) { // TODO: Check for proper coverage (bad thought) - if (inv_item->mode == df::unit_inventory_item::Worn) + if (inv_item->mode == df::inv_item_role_type::Worn) return false; } return true; @@ -1247,7 +1252,7 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) { // This is 100% reverse-engineered from DF code int rating = getNominalSkill(unit, skill_id, true); // Apply special states - if (unit->counters.soldier_mood == df::unit::T_counters::None) { + if (unit->counters.soldier_mood == df::soldier_mood_type::None) { if (unit->counters.nausea > 0) rating >>= 1; if (unit->counters.winded > 0) @@ -1260,7 +1265,7 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) { rating >>= 1; } - if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance) { + if (unit->counters.soldier_mood != df::soldier_mood_type::MartialTrance) { if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle && !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged && !hasExtravision(unit)) @@ -1608,7 +1613,7 @@ float Units::computeSlowdownFactor(df::unit *unit) { if (!unit->flags1.bits.marauder && casteFlagSet(unit->race, unit->caste, caste_raw_flags::MEANDERER) && !(unit->following && isCitizen(unit)) && - linear_index(unit->inventory, &df::unit_inventory_item::mode, df::unit_inventory_item::Hauled) < 0) + linear_index(unit->inventory, &df::unit_inventory_item::mode, df::inv_item_role_type::Hauled) < 0) { coeff *= 4.0f; } diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp index 3ed5455d0f..8f75e0549e 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -568,7 +568,7 @@ struct VeinGenerator bool VeinGenerator::init_biomes() { auto &mats = df::inorganic_raw::get_vector(); - materials.resize(world->raws.inorganics.size()); + materials.resize(world->raws.inorganics.all.size()); for (size_t i = 0; i < mats.size(); i++) { diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index c7ce4114e7..b82442a66d 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -679,7 +679,7 @@ static const char * if_pretty(const tile_context &ctx, const char *c) { } static bool is_rectangular(const df::building *bld) { - const df::building_extents &room = bld->room; + const df::building::T_room &room = bld->room; if (!room.extents) return true; for (int32_t y = 0; y < room.height; ++y) { diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index fe317e4705..64ddf89e34 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -166,12 +166,12 @@ static void load_organic_material_cache(df::organic_mat_category cat) { } static void load_material_cache() { - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) if (raws.mat_table.builtin[i]) cache_matched(i, -1); - for (size_t i = 0; i < raws.inorganics.size(); i++) + for (size_t i = 0; i < raws.inorganics.all.size(); i++) cache_matched(0, i); load_organic_material_cache(df::organic_mat_category::Wood); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index d55b5f5da4..1a97a801de 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -337,7 +337,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, } auto item = closest->second; // some item must be closest. - if (Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { + if (Job::attachJobItem(job, item, df::job_role_type::Hauled, filter_idx)) { MaterialInfo material; material.decode(item); ItemTypeInfo item_type; diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 9c41d73912..50b0c7f138 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -29,7 +29,6 @@ #include "df/unit_soul.h" #include "df/unit_syndrome.h" #include "df/world.h" -#include "df/world_raws.h" using std::vector; using std::string; diff --git a/plugins/devel/eventExample.cpp b/plugins/devel/eventExample.cpp index ceaec29921..c0944249f1 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -13,6 +13,7 @@ #include "df/job.h" #include "df/unit.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" #include diff --git a/plugins/devel/stripcaged.cpp b/plugins/devel/stripcaged.cpp index 14189bdc30..fdb11741ca 100644 --- a/plugins/devel/stripcaged.cpp +++ b/plugins/devel/stripcaged.cpp @@ -21,7 +21,6 @@ using namespace std; #include #include "df/world.h" -#include "df/world_raws.h" #include "df/building_def.h" #include "df/unit_inventory_item.h" #include diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index d4fddb4846..35f0238123 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -46,7 +46,6 @@ #include "df/unit_preference.h" #include "df/unit_soul.h" #include "df/viewscreen_unitst.h" -#include "df/world_raws.h" using std::deque; @@ -1184,7 +1183,7 @@ struct preference_map label = ""; typedef df::unit_preference::T_type T_type; - df::world_raws &raws = world->raws; + auto &raws = world->raws; switch (pref.type) { case (T_type::LikeCreature): diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp index ee418363e7..db04308d0b 100644 --- a/plugins/embark-assistant/embark-assistant.cpp +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -18,7 +18,6 @@ #include "df/world.h" #include "df/world_data.h" #include "df/world_geo_biome.h" -#include "df/world_raws.h" #include "defs.h" #include "embark-assistant.h" diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp index b4c55dd823..bb90435343 100644 --- a/plugins/embark-assistant/finder_ui.cpp +++ b/plugins/embark-assistant/finder_ui.cpp @@ -14,9 +14,7 @@ #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" #include "df/world_data.h" -#include "df/world_raws.h" #include "df/world_region_type.h" -#include "df/world_raws.h" #include "embark-assistant.h" #include "finder_ui.h" diff --git a/plugins/embark-assistant/matcher.cpp b/plugins/embark-assistant/matcher.cpp index b540ae80b4..9f3900f0c9 100644 --- a/plugins/embark-assistant/matcher.cpp +++ b/plugins/embark-assistant/matcher.cpp @@ -10,7 +10,6 @@ #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" #include "df/world_data.h" -#include "df/world_raws.h" #include "df/world_region.h" #include "df/world_region_details.h" #include "df/world_region_type.h" diff --git a/plugins/embark-assistant/overlay.cpp b/plugins/embark-assistant/overlay.cpp index d5b50294a9..37130c6897 100644 --- a/plugins/embark-assistant/overlay.cpp +++ b/plugins/embark-assistant/overlay.cpp @@ -8,7 +8,6 @@ #include "df/viewscreen.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" -#include "df/world_raws.h" #include "finder_ui.h" #include "help_ui.h" diff --git a/plugins/embark-assistant/survey.cpp b/plugins/embark-assistant/survey.cpp index 84994b2869..ddfc6d8d49 100644 --- a/plugins/embark-assistant/survey.cpp +++ b/plugins/embark-assistant/survey.cpp @@ -30,7 +30,6 @@ #include "df/interaction_source_type.h" #include "df/interaction_target.h" #include "df/interaction_target_materialst.h" -#include "df/material_common.h" #include "df/reaction.h" #include "df/reaction_product_itemst.h" #include "df/reaction_product_type.h" @@ -42,7 +41,6 @@ #include "df/world_data.h" #include "df/world_geo_biome.h" #include "df/world_geo_layer.h" -#include "df/world_raws.h" #include "df/world_region.h" #include "df/world_region_details.h" #include "df/world_region_feature.h" diff --git a/plugins/forceequip.cpp b/plugins/forceequip.cpp index df716831db..fa8cb8ef12 100644 --- a/plugins/forceequip.cpp +++ b/plugins/forceequip.cpp @@ -238,7 +238,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * return false; } - if (!Items::moveToInventory(item, unit, df::unit_inventory_item::Worn, bpIndex)) + if (!Items::moveToInventory(item, unit, df::inv_item_role_type::Worn, bpIndex)) { if (verbose) { WARN(log).print("\nEquipping failed - failed to retrieve item from its current location/container/inventory. Please move it to the ground and try again.\n"); } return false; diff --git a/plugins/generated-creature-renamer.cpp b/plugins/generated-creature-renamer.cpp index b0a00b3f21..0ea3227b1b 100644 --- a/plugins/generated-creature-renamer.cpp +++ b/plugins/generated-creature-renamer.cpp @@ -3,7 +3,6 @@ #include "Export.h" #include "PluginManager.h" #include "df/world.h" -#include "df/world_raws.h" #include "df/creature_raw.h" #include "df/caste_raw.h" #include "modules/World.h" diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 9b41c2e2ca..ffa62ed830 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -8,6 +8,7 @@ #include "modules/Maps.h" #include "modules/World.h" +#include "df/block_column_print_infost.h" #include "df/construction.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -190,8 +191,7 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { __LINE__, bpos.x, bpos.y); continue; } - df::map_block_column::T_unmined_glyphs *glyphs = - new df::map_block_column::T_unmined_glyphs; + df::block_column_print_infost *glyphs = new df::block_column_print_infost; glyphs->x[0] = 0; glyphs->x[1] = 1; glyphs->x[2] = 2; diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index 52b9bf43a4..bcb59b7ad5 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -10,6 +10,7 @@ #include "modules/World.h" #include "df/building.h" +#include "df/buildingitemst.h" #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" #include "df/caravan_state.h" diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp index 64cfcc301b..b935c34f81 100644 --- a/plugins/nestboxes.cpp +++ b/plugins/nestboxes.cpp @@ -6,6 +6,7 @@ #include "modules/Persistence.h" #include "modules/World.h" +#include "df/buildingitemst.h" #include "df/building_nest_boxst.h" #include "df/item.h" #include "df/item_eggst.h" diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 89df783245..49558c1b88 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -765,8 +765,8 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) if (it2.isMember("bearing")) { std::string bearing(it2["bearing"].asString()); - auto found = std::find_if(world->raws.inorganics.begin(), world->raws.inorganics.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); - if (found == world->raws.inorganics.end()) + auto found = std::find_if(world->raws.inorganics.all.begin(), world->raws.inorganics.all.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); + if (found == world->raws.inorganics.all.end()) { delete condition; @@ -774,7 +774,7 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) continue; } - condition->metal_ore = found - world->raws.inorganics.begin(); + condition->metal_ore = found - world->raws.inorganics.all.begin(); } if (it2.isMember("reaction_class")) @@ -1008,8 +1008,8 @@ static bool orders_compare(df::manager_order *a, df::manager_order *b) return a->workshop_id >= 0; } - if (a->frequency == df::manager_order::T_frequency::OneTime - || b->frequency == df::manager_order::T_frequency::OneTime) + if (a->frequency == df::workquota_frequency_type::OneTime + || b->frequency == df::workquota_frequency_type::OneTime) return a->frequency < b->frequency; return a->frequency > b->frequency; } diff --git a/plugins/probe.cpp b/plugins/probe.cpp index 31c2df5edc..eff0c98c86 100644 --- a/plugins/probe.cpp +++ b/plugins/probe.cpp @@ -46,7 +46,7 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) for (auto inv_item : unit->inventory) { df::item* item = inv_item->item; - if (inv_item->mode == df::unit_inventory_item::T_mode::Worn) { + if (inv_item->mode == df::inv_item_role_type::Worn) { out << " wears item: #" << item->id; if (item->flags.bits.owned) out << " (owned)"; diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index 159b557e4f..e8883fde59 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -223,9 +223,11 @@ void printVeins(color_ostream &con, MatMap &mat_map, MatMap gems; MatMap rest; + auto & inorganics = world->raws.inorganics.all; + for (const auto &kv : mat_map) { - df::inorganic_raw *gloss = vector_get(world->raws.inorganics, kv.first); + df::inorganic_raw *gloss = vector_get(inorganics, kv.first); if (!gloss) { con.printerr("invalid material gloss: %hi\n", kv.first); @@ -242,17 +244,17 @@ void printVeins(color_ostream &con, MatMap &mat_map, if (options.ores) { con << "Ores:" << std::endl; - printMats(con, ores, world->raws.inorganics, options); + printMats(con, ores, inorganics, options); } if (options.gems) { con << "Gems:" << std::endl; - printMats(con, gems, world->raws.inorganics, options); + printMats(con, gems, inorganics, options); } if (options.veins) { con << "Other vein stone:" << std::endl; - printMats(con, rest, world->raws.inorganics, options); + printMats(con, rest, inorganics, options); } } @@ -604,7 +606,7 @@ static command_result embark_prospector(color_ostream &out, // Print the report if (options.layers) { out << "Layer materials:" << std::endl; - printMats(out, layerMats, world->raws.inorganics, options); + printMats(out, layerMats, world->raws.inorganics.all, options); } if (options.hidden) { @@ -837,7 +839,7 @@ static command_result map_prospector(color_ostream &con, if (options.layers) { con << "Layer materials:" << std::endl; - printMats(con, layerMats, world->raws.inorganics, options); + printMats(con, layerMats, world->raws.inorganics.all, options); } if (options.features) { diff --git a/plugins/remotefortressreader/building_reader.cpp b/plugins/remotefortressreader/building_reader.cpp index 7cc715147b..ebf0359afd 100644 --- a/plugins/remotefortressreader/building_reader.cpp +++ b/plugins/remotefortressreader/building_reader.cpp @@ -529,7 +529,7 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re default: break; } - if (actual->gate_flags.bits.closed) + if (actual->gate_flags.bits.raised) remote_build->set_active(1); else remote_build->set_active(0); @@ -671,7 +671,7 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re auto actual = strict_virtual_cast(local_build); if (actual) { - if (actual->gate_flags.bits.closed) + if (actual->gate_flags.bits.retracted) remote_build->set_active(1); else remote_build->set_active(0); diff --git a/plugins/remotefortressreader/item_reader.cpp b/plugins/remotefortressreader/item_reader.cpp index 1353243b2b..58c84b9075 100644 --- a/plugins/remotefortressreader/item_reader.cpp +++ b/plugins/remotefortressreader/item_reader.cpp @@ -14,7 +14,6 @@ #include "df/art_image_property.h" #include "df/art_image_property_intransitive_verbst.h" #include "df/art_image_property_transitive_verbst.h" -#include "df/art_image_ref.h" #include "df/descriptor_shape.h" #include "df/instrument_piece.h" #include "df/instrument_register.h" @@ -240,19 +239,19 @@ void CopyItem(RemoteFortressReader::Item * NetItem, df::item * DfItem) GET_ART_IMAGE_CHUNK GetArtImageChunk = reinterpret_cast(Core::getInstance().vinfo->getAddress("get_art_image_chunk")); if (GetArtImageChunk) { - chunk = GetArtImageChunk(&(world->art_image_chunks), statue->image.id); + chunk = GetArtImageChunk(&(world->art_image_chunks.all), statue->image.id); } else { - for (size_t i = 0; i < world->art_image_chunks.size(); i++) + for (size_t i = 0; i < world->art_image_chunks.all.size(); i++) { - if (world->art_image_chunks[i]->id == statue->image.id) - chunk = world->art_image_chunks[i]; + if (world->art_image_chunks.all[i]->id == statue->image.id) + chunk = world->art_image_chunks.all[i]; } } - if (chunk && chunk->images[statue->image.subid]) + if (chunk && chunk->images[statue->image.subid].art_image) { - CopyImage(chunk->images[statue->image.subid], NetItem->mutable_image()); + CopyImage(chunk->images[statue->image.subid].art_image, NetItem->mutable_image()); } @@ -663,11 +662,11 @@ DFHack::command_result GetItemList(DFHack::color_ostream &stream, const DFHack:: { send_instrument->add_tuning_parm(*(instrument->tuning_parm[j])); } - for (size_t j = 0; j < instrument->registers.size(); j++) + for (size_t j = 0; j < instrument->timbre.registers.size(); j++) { auto reg = send_instrument->add_registers(); - reg->set_pitch_range_min(instrument->registers[j]->pitch_range_min); - reg->set_pitch_range_max(instrument->registers[j]->pitch_range_max); + reg->set_pitch_range_min(instrument->timbre.registers[j]->pitch_range_min); + reg->set_pitch_range_max(instrument->timbre.registers[j]->pitch_range_max); } send_instrument->set_description(DF2UTF(instrument->description)); } diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index ae6683dee9..697eb3fe92 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -42,6 +42,7 @@ #include "df/body_part_layer_raw.h" #include "df/body_part_raw.h" #include "df/bp_appearance_modifier.h" +#include "df/buildingitemst.h" #include "df/builtin_mats.h" #include "df/building_wellst.h" @@ -64,6 +65,7 @@ #include "df/inorganic_raw.h" #include "df/item.h" #include "df/job.h" +#include "df/job_postingst.h" #include "df/job_type.h" #include "df/job_item.h" #include "df/job_material_category.h" @@ -97,6 +99,7 @@ #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_savegamest.h" @@ -194,7 +197,6 @@ const char* growth_locations[] = { #include "df/art_image.h" #include "df/art_image_chunk.h" -#include "df/art_image_ref.h" command_result loadArtImageChunk(color_ostream &out, std::vector & parameters) { if (parameters.size() != 1) @@ -210,7 +212,7 @@ command_result loadArtImageChunk(color_ostream &out, std::vector & if (GetArtImageChunk) { int index = atoi(parameters[0].c_str()); - auto chunk = GetArtImageChunk(&(world->art_image_chunks), index); + auto chunk = GetArtImageChunk(&(world->art_image_chunks.all), index); out.print("Loaded chunk id: %d\n", chunk->id); } return CR_OK; @@ -809,10 +811,10 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage return CR_OK; } - df::world_raws *raws = &world->raws; + auto *raws = &world->raws; // df::world_history *history = &world->history; MaterialInfo mat; - for (size_t i = 0; i < raws->inorganics.size(); i++) + for (size_t i = 0; i < raws->inorganics.all.size(); i++) { mat.decode(0, i); MaterialDefinition *mat_def = out->add_material_list(); @@ -820,9 +822,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; - if (size_t(raws->inorganics[i]->material.state_color[GetState(&raws->inorganics[i]->material)]) < raws->descriptors.colors.size()) + if (size_t(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)]) < raws->descriptors.colors.size()) { - ConvertDFColorDescriptor(raws->inorganics[i]->material.state_color[GetState(&raws->inorganics[i]->material)], mat_def->mutable_state_color()); + ConvertDFColorDescriptor(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)], mat_def->mutable_state_color()); } } for (int i = 0; i < 19; i++) @@ -890,7 +892,7 @@ static command_result GetGrowthList(color_ostream &stream, const EmptyMessage *i - df::world_raws *raws = &world->raws; + auto *raws = &world->raws; if (!raws) return CR_OK;//'. @@ -1154,7 +1156,7 @@ void CopyDesignation(df::map_block * DfBlock, RemoteFortressReader::MapBlock * N void CopyProjectiles(RemoteFortressReader::MapBlock * NetBlock) { - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(projectile, df::proj_itemst, proj->item); if (projectile == NULL) @@ -1187,7 +1189,7 @@ void CopyProjectiles(RemoteFortressReader::MapBlock * NetBlock) { bool isProj = false; auto vehicle = world->vehicles.active[i]; - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(projectile, df::proj_itemst, proj->item); if (!projectile) @@ -1515,14 +1517,14 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in GET_ART_IMAGE_CHUNK GetArtImageChunk = reinterpret_cast(Core::getInstance().vinfo->getAddress("get_art_image_chunk")); if (GetArtImageChunk) { - chunk = GetArtImageChunk(&(world->art_image_chunks), engraving->art_id); + chunk = GetArtImageChunk(&(world->art_image_chunks.all), engraving->art_id); } else { - for (size_t i = 0; i < world->art_image_chunks.size(); i++) + for (size_t i = 0; i < world->art_image_chunks.all.size(); i++) { - if (world->art_image_chunks[i]->id == engraving->art_id) - chunk = world->art_image_chunks[i]; + if (world->art_image_chunks.all[i]->id == engraving->art_id) + chunk = world->art_image_chunks.all[i]; } } if (!chunk) @@ -1534,8 +1536,8 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in ConvertDFCoord(engraving->pos, netEngraving->mutable_pos()); netEngraving->set_quality(engraving->quality); netEngraving->set_tile(engraving->tile); - if (chunk->images[engraving->art_subid]) { - CopyImage(chunk->images[engraving->art_subid], netEngraving->mutable_image()); + if (chunk->images[engraving->art_subid].art_image) { + CopyImage(chunk->images[engraving->art_subid].art_image, netEngraving->mutable_image()); } netEngraving->set_floor(engraving->flags.bits.floor); netEngraving->set_west(engraving->flags.bits.west); @@ -1772,7 +1774,7 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques if (unit->flags1.bits.projectile) { - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(item, df::proj_unitst, proj->item); if (item == NULL) @@ -1985,27 +1987,23 @@ static command_result GetWorldMap(color_ostream &stream, const EmptyMessage *in, out->set_name(DF2UTF(Translation::translateName(&(data->name), false))); out->set_name_english(DF2UTF(Translation::translateName(&(data->name), true))); auto poles = data->flip_latitude; -#if DF_VERSION_INT > 34011 switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif for (int yy = 0; yy < height; yy++) for (int xx = 0; xx < width; xx++) { @@ -2133,28 +2131,24 @@ static command_result GetWorldMapNew(color_ostream &stream, const EmptyMessage * out->set_world_height(height); out->set_name(DF2UTF(Translation::translateName(&(data->name), false))); out->set_name_english(DF2UTF(Translation::translateName(&(data->name), true))); -#if DF_VERSION_INT > 34011 auto poles = data->flip_latitude; switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif for (int yy = 0; yy < height; yy++) for (int xx = 0; xx < width; xx++) { @@ -2272,28 +2266,24 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w sprintf(name, "Region %d, %d", pos_x, pos_y); out->set_name_english(name); out->set_name(name); -#if DF_VERSION_INT > 34011 auto poles = worldData->flip_latitude; switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif df::world_region_details * south = NULL; df::world_region_details * east = NULL; diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 1efdd4df60..05cb776487 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -23,7 +23,7 @@ DBG_EXTERN(stockpiles, log); void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat) { DEBUG(log, out).print("food_lookup: food_idx(%zd)\n", food_idx); - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; int32_t main_idx = table.organic_indexes[mat_category][food_idx]; int16_t type = table.organic_types[mat_category][food_idx]; @@ -58,7 +58,7 @@ size_t OrganicMatLookup::food_max_size(organic_mat_category::organic_mat_categor void OrganicMatLookup::food_build_map() { if (index_built) return; - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; @@ -73,7 +73,7 @@ void OrganicMatLookup::food_build_map() { } int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_category::organic_mat_category mat_category, const std::string& token) { - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; DEBUG(log, out).print("food_idx_by_token:\n"); if (mat_category == organic_mat_category::Fish || diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index f315e7d674..1d11db41e5 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -566,7 +566,7 @@ static void unserialize_list_material(color_ostream& out, const char* subcat, bo vector& pile_list) { // we initialize all disallowed values to 1 // why? because that's how the memory is in DF before we muck with it. - size_t num_elems = world->raws.inorganics.size(); + size_t num_elems = world->raws.inorganics.all.size(); pile_list.resize(num_elems, 0); for (size_t i = 0; i < pile_list.size(); ++i) { MaterialInfo mi(0, i); @@ -814,9 +814,9 @@ void StockpileSerializer::read(color_ostream &out, DeserializeMode mode, const v void StockpileSerializer::write_containers(color_ostream& out) { DEBUG(log, out).print("writing container settings\n"); - mBuffer.set_max_bins(mPile->max_bins); - mBuffer.set_max_barrels(mPile->max_barrels); - mBuffer.set_max_wheelbarrows(mPile->max_wheelbarrows); + mBuffer.set_max_bins(mPile->storage.max_bins); + mBuffer.set_max_barrels(mPile->storage.max_barrels); + mBuffer.set_max_wheelbarrows(mPile->storage.max_wheelbarrows); } template @@ -869,21 +869,21 @@ void StockpileSerializer::read_containers(color_ostream& out, DeserializeMode mo read_elem(out, "max_bins", mode, std::bind(&StockpileSettings::has_max_bins, mBuffer), std::bind(&StockpileSettings::max_bins, mBuffer), - mPile->max_bins); + mPile->storage.max_bins); read_elem(out, "max_barrels", mode, std::bind(&StockpileSettings::has_max_barrels, mBuffer), std::bind(&StockpileSettings::max_barrels, mBuffer), - mPile->max_barrels); + mPile->storage.max_barrels); read_elem(out, "max_wheelbarrows", mode, std::bind(&StockpileSettings::has_max_wheelbarrows, mBuffer), std::bind(&StockpileSettings::max_wheelbarrows, mBuffer), - mPile->max_wheelbarrows); + mPile->storage.max_wheelbarrows); } void StockpileSettingsSerializer::write_general(color_ostream& out) { DEBUG(log, out).print("writing general settings\n"); - mBuffer.set_allow_inorganic(mSettings->allow_inorganic); - mBuffer.set_allow_organic(mSettings->allow_organic); + mBuffer.set_allow_inorganic(mSettings->misc.allow_inorganic); + mBuffer.set_allow_organic(mSettings->misc.allow_organic); } void StockpileSerializer::write_general(color_ostream& out) { @@ -895,11 +895,11 @@ void StockpileSettingsSerializer::read_general(color_ostream& out, DeserializeMo read_elem(out, "allow_inorganic", mode, std::bind(&StockpileSettings::has_allow_inorganic, mBuffer), std::bind(&StockpileSettings::allow_inorganic, mBuffer), - mSettings->allow_inorganic); + mSettings->misc.allow_inorganic); read_elem(out, "allow_organic", mode, std::bind(&StockpileSettings::has_allow_organic, mBuffer), std::bind(&StockpileSettings::allow_organic, mBuffer), - mSettings->allow_organic); + mSettings->misc.allow_organic); } void StockpileSerializer::read_general(color_ostream& out, DeserializeMode mode) { diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 4955d49e96..98b8b4b725 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -644,8 +644,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && pref->mattype == builtin_mats::INORGANIC) { item->mat_type = pref->mattype; @@ -722,8 +722,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial) + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial) { MaterialInfo mat(pref->mattype, pref->matindex); if (mat.material->flags.is_set(material_flags::SILK)) @@ -805,8 +805,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && pref->mattype == 0 && getCreatedMetalBars(pref->matindex) > 0) mats.push_back(pref->matindex); } @@ -835,8 +835,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && ((pref->mattype == builtin_mats::GLASS_GREEN) || (pref->mattype == builtin_mats::GLASS_CLEAR && have_glass[1]) || (pref->mattype == builtin_mats::GLASS_CRYSTAL && have_glass[2]))) @@ -867,8 +867,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial) + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial) { MaterialInfo mat(pref->mattype, pref->matindex); if (mat.material->flags.is_set(material_flags::BONE)) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index ad96302887..ef9b77c67a 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -276,7 +276,7 @@ class Tailor { for (auto inv : u->inventory) { - if (inv->mode != df::unit_inventory_item::Worn) + if (inv->mode != df::inv_item_role_type::Worn) continue; // skip non-clothing if (!inv->item->isClothing()) @@ -456,7 +456,7 @@ class Tailor { order->mat_index == -1 && order->specdata.hist_figure_id == hfid && order->material_category.whole == mcat.whole && - order->frequency == df::manager_order::T_frequency::OneTime) + order->frequency == df::workquota_frequency_type::OneTime) return order; } return NULL; diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index a472c2efec..ea75ced359 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -32,6 +32,7 @@ #include "df/activity_event_worshipst.h" #include "df/building_nest_boxst.h" #include "df/building_trapst.h" +#include "df/buildingitemst.h" #include "df/init.h" #include "df/item_eggst.h" #include "df/unit.h" @@ -409,7 +410,9 @@ static void adjust_activities(color_ostream &out, int32_t timeskip) { for (auto act : world->activities.all) { for (auto ev : act->events) { switch (ev->getType()) { - using namespace df::enums::activity_event_type; + using namespace df::enums::activity_event_type; + case NONE: + break; case TrainingSession: // no counters From 3583334b7aa0039f78dd852f32c2482ec5886672 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 14:55:38 -0800 Subject: [PATCH 0061/1082] update core/plugin lua for great reorg --- library/lua/gui/dwarfmode.lua | 2 +- library/lua/syndrome-util.lua | 2 +- plugins/lua/buildingplan/planneroverlay.lua | 2 +- plugins/lua/sort/diplomacy.lua | 2 +- test/library/print.lua | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 793acbd520..61317f9ebc 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -249,7 +249,7 @@ end function get_hotkey_target(key) local hk = HOTKEY_KEYS[key] - if hk and hk.cmd == df.ui_hotkey.T_cmd.Zoom then + if hk and hk.cmd == df.hotkey_type.Zoom then return xyz2pos(hk.x, hk.y, hk.z) end end diff --git a/library/lua/syndrome-util.lua b/library/lua/syndrome-util.lua index efbaa5deaa..c0af9f8a72 100644 --- a/library/lua/syndrome-util.lua +++ b/library/lua/syndrome-util.lua @@ -98,7 +98,7 @@ function infectWithSyndrome(target,syndrome,resetPolicy) unitSyndrome.ticks = 0 unitSyndrome.wound_id = -1 for k,v in ipairs(syndrome.ce) do - local symptom = df.unit_syndrome.T_symptoms:new() + local symptom = df.active_creature_interaction_effectst:new() symptom.quantity = 0 symptom.delay = 0 symptom.ticks = 0 diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 62e1a6c49c..f92dc95d84 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -1348,7 +1348,7 @@ function PlannerOverlay:place_building(placement_data, chosen_items) dfhack.printerr(('item no longer available: %d'):format(item_id)) break end - if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then + if not dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, idx-1, -1) then dfhack.printerr(('cannot attach item: %d'):format(item_id)) break end diff --git a/plugins/lua/sort/diplomacy.lua b/plugins/lua/sort/diplomacy.lua index c0ec818505..3e78593862 100644 --- a/plugins/lua/sort/diplomacy.lua +++ b/plugins/lua/sort/diplomacy.lua @@ -64,7 +64,7 @@ local function get_preferences(unit) if not unit then return {} end local preferences = {} for _, pref in ipairs(unit.status.current_soul.preferences) do - if pref.type == df.unit_preference.T_type.LikeItem and pref.active then + if pref.type == df.unitpref_type.LikeItem and pref.active then table.insert(preferences, make_item_description(pref.item_type, pref.item_subtype)) end end diff --git a/test/library/print.lua b/test/library/print.lua index 28a2e70370..79243d8989 100644 --- a/test/library/print.lua +++ b/test/library/print.lua @@ -47,7 +47,7 @@ end local function new_int_vector() -- create a vector of integers by cloning one from world. we do it this way -- because we can't allocate typed vectors from lua directly. - local vector = df.global.world.busy_buildings:new() + local vector = df.global.world.building_uses.buildings:new() vector:resize(0) return vector end From 17be93cb594379622ec8309ef1490c4d35ce2889 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 16:58:00 -0800 Subject: [PATCH 0062/1082] ignore missing viewscreens unused viewscreens are optimized out of windows builds but not linux builds, so this is necessary to get this test to pass on Windows --- test/structures/misc.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/structures/misc.lua b/test/structures/misc.lua index 13febff566..33d386b832 100644 --- a/test/structures/misc.lua +++ b/test/structures/misc.lua @@ -28,8 +28,10 @@ function test.viewscreenDtors() for name, type in pairs(df) do if name:startswith('viewscreen') then print('testing', name) - local v = type:new() - expect.true_(v:delete(), "destructor returned false: " .. name) + local ok, v = pcall(function() return type:new() end) + if ok then + expect.true_(v:delete(), "destructor returned false: " .. name) + end end end end From df46a55a523606ba005ac61639cbac3b31974989 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 23:13:44 -0800 Subject: [PATCH 0063/1082] add unit test for allocating and deallocating all types --- test/structures/misc.lua | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/test/structures/misc.lua b/test/structures/misc.lua index 33d386b832..ab58187b31 100644 --- a/test/structures/misc.lua +++ b/test/structures/misc.lua @@ -24,14 +24,39 @@ function test.overlappingGlobals() end end -function test.viewscreenDtors() +local known_bad_types = { + -- renderer base class has non-destructible padding declared + renderer_2d_base=true, + renderer_2d=true, + renderer_offscreen=true, + + -- abstract base classes that aren't instantiable + active_script_varst=true, + widget_sheet_button=true, +} + +if dfhack.getOSType() == 'linux' then + -- empty destructors are declared inline for these types, + -- and gcc appears to optimize them out + known_bad_types.mental_picture_propertyst = true + known_bad_types.region_block_eventst = true +end + +function test.destructors() + local count = 1 for name, type in pairs(df) do - if name:startswith('viewscreen') then - print('testing', name) - local ok, v = pcall(function() return type:new() end) - if ok then - expect.true_(v:delete(), "destructor returned false: " .. name) - end + if known_bad_types[name] then + goto continue + end + print(('testing constructor %5d: %s'):format(count, name)) + local ok, v = pcall(function() return type:new() end) + if not ok then + print(' constructor failed; skipping destructor test') + else + print(' destructor ok') + expect.true_(v:delete(), "destructor returned false: " .. name) end + count = count + 1 + ::continue:: end end From 6518abd6d6384105d5d534e3a56c464ddc0c03aa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 07:34:29 -0800 Subject: [PATCH 0064/1082] update submodule refs --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index b3202b06e0..2a84adf2fe 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b3202b06e04c03433565e8248f2638dddde4ee22 +Subproject commit 2a84adf2fe1e76f5244f51cd4536442ebce10b5d diff --git a/plugins/stonesense b/plugins/stonesense index 92794aec77..8d445c72d5 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 92794aec7768bdafd0f5fc23fed8b30655805b32 +Subproject commit 8d445c72d5bc797bc2e8f6d0c42091fc068d0893 diff --git a/scripts b/scripts index fc67fdc5b6..fc3f7d044b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit fc67fdc5b67a25a776eebfa769b289801679e364 +Subproject commit fc3f7d044b1b45f3ad7b9f7ba3bcc5a4bb24b09b From aedfa33bc99c918eab848f5ddac5d9414933f97c Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 9 Feb 2025 22:01:35 +0100 Subject: [PATCH 0065/1082] spectate.lua: implement unit banners and mouse tooltips --- plugins/lua/spectate.lua | 373 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 360 insertions(+), 13 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d44957dcb1..36045c7a48 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -6,9 +6,15 @@ local json = require('json') local overlay = require('plugins.overlay') local utils = require('utils') +local gui = require('gui') +local widgets = require('gui.widgets') + -- settings starting with 'tooltip-' are not passed to the C++ plugin local lua_only_settings_prefix = 'tooltip-' +-- how many lines the text following unit is allowed to be moved down to avoid overlapping +local max_banner_y_offset = 4 + local function get_default_state() return { ['auto-disengage']=true, @@ -21,32 +27,69 @@ local function get_default_state() ['include-wildlife']=false, ['prefer-conflict']=true, ['prefer-new-arrivals']=true, + ['tooltip-follow-blink-milliseconds']=2000, ['tooltip-follow-job']=true, - ['tooltip-follow-name']=true, + ['tooltip-follow-job-shortenings'] = { + ["Store item in stockpile"] = "Store item", + }, + ['tooltip-follow-name']=false, ['tooltip-follow-stress']=true, + ['tooltip-follow-stress-levels']={ + [0] = + true, -- Miserable + true, + false, + false, + false, + true, + true, -- Ecstatic + }, ['tooltip-hover-job']=true, ['tooltip-hover-name']=true, ['tooltip-hover-stress']=true, + ['tooltip-hover-stress-levels']={ + [0] = + true, -- Miserable + true, + false, + false, + false, + true, + true, -- Ecstatic + }, + ['tooltip-stress-levels']={ + -- keep in mind, the text will look differently with game's font + -- colors are same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE + [0] = + {text = "=C", pen = COLOR_RED, name = "Miserable"}, + {text = ":C", pen = COLOR_LIGHTRED, name = "Unhappy"}, + {text = ":(", pen = COLOR_YELLOW, name = "Displeased"}, + {text = ":]", pen = COLOR_GREY, name = "Content"}, + {text = ":)", pen = COLOR_GREEN, name = "Pleased"}, + {text = ":D", pen = COLOR_LIGHTGREEN, name = "Happy"}, + {text = "=D", pen = COLOR_LIGHTCYAN, name = "Ecstatic"}, + } } end local function load_state() local state = get_default_state() - local config = json.open('dfhack-config/spectate.json') - for key in pairs(config.data) do + local config_file = json.open('dfhack-config/spectate.json') + for key in pairs(config_file.data) do if state[key] == nil then - config.data[key] = nil + config_file.data[key] = nil end end - utils.assign(state, config.data) - config.data = state - return config + utils.assign(state, config_file.data) + config_file.data = state + return config_file.data, + function() config_file:write() end end -local config = load_state() +local config, save_state = load_state() function refresh_cpp_config() - for name,value in pairs(config.data) do + for name,value in pairs(config) do if not name:startswith(lua_only_settings_prefix) then if type(value) == 'boolean' then value = value and 1 or 0 @@ -77,7 +120,7 @@ local function print_status() print('spectate is:', isEnabled() and 'enabled' or 'disabled') print() print('settings:') - for key, value in pairs(config.data) do + for key, value in pairs(config) do print(' ' .. key .. ': ' .. tostring(value)) end end @@ -91,7 +134,7 @@ local function do_toggle() end local function set_setting(key, value) - if config.data[key] == nil then + if config[key] == nil then qerror('unknown setting: ' .. key) end if key == 'follow-seconds' then @@ -99,8 +142,8 @@ local function set_setting(key, value) else value = argparse.boolean(value, key) end - config.data[key] = value - config:write() + config[key] = value + save_state() if not key:startswith(lua_only_settings_prefix) then if type(value) == 'boolean' then value = value and 1 or 0 @@ -131,6 +174,80 @@ function parse_commandline(args) return true end +----------------------------- +-- info functions + +local function GetUnitStress(unit, stress_levels) + local stressCat = dfhack.units.getStressCategory(unit) + if stressCat > 6 then stressCat = 6 end + if not stress_levels[stressCat] then return end + + local level_cfg = config['tooltip-stress-levels'][stressCat] + return {text=level_cfg.text, pen=level_cfg.pen} +end + +local function GetUnitName(unit) + return dfhack.units.getReadableName(unit) +end + +local function GetUnitJob(unit) + local job = unit.job.current_job + return job and dfhack.job.getName(job) +end + +local function GetRelevantSettings(key) + return config['tooltip-' .. key .. '-name'], + config['tooltip-' .. key .. '-job'], + config['tooltip-' .. key .. '-stress'], + config['tooltip-' .. key .. '-stress-levels'], + config['tooltip-' .. key .. '-job-shortenings'] +end + +local function GetUnitInfoText(unit, settings_group_name) + local show_name, show_job, show_stress, stress_levels, job_shortenings = GetRelevantSettings(settings_group_name) + + local stress = show_stress and GetUnitStress(unit, stress_levels) or nil + local name = show_name and GetUnitName(unit) or nil + local job = show_job and GetUnitJob(unit) or nil + if job_shortenings then job = job_shortenings[job] or job end + + local txt = {} + if stress then + txt[#txt+1] = stress + if name or job then txt[#txt+1] = ' ' end + end + if name then + txt[#txt+1] = name + end + if job then + if name then txt[#txt+1] = ": " end + txt[#txt+1] = job + end + + return txt +end + +local function GetHoverText(pos) + if not pos then return end + + local txt = {} + local units = dfhack.units.getUnitsInBox(pos, pos) or {} -- todo: maybe (optionally) use filter parameter here? + + for _,unit in ipairs(units) do + local info = GetUnitInfoText(unit, 'hover') + if not next(info) then goto continue end + + for _,t in ipairs(info) do + txt[#txt+1] = t + end + txt[#txt+1] = NEWLINE + + ::continue:: + end + + return txt +end + ----------------------------- -- overlays @@ -142,6 +259,236 @@ TooltipOverlay.ATTRS{ viewscreens='dwarfmode/Default', } +function TooltipOverlay:init() + self:addviews{MouseTooltip{view_id = 'tooltip'}} +end + +function TooltipOverlay:preUpdateLayout(parent_rect) + -- this is required, otherwise there is no room to draw child widgets in + self.frame.w = parent_rect.width + self.frame.h = parent_rect.height +end + +function TooltipOverlay:render(dc) + self:render_unit_banners(dc) + TooltipOverlay.super.render(self, dc) +end + +local function AnyFollowOptionOn() + return config['tooltip-follow-job'] + or config['tooltip-follow-name'] + or config['tooltip-follow-stress'] +end + +-- map coordinates -> interface layer coordinates +local function GetScreenCoordinates(map_coord) + -- -> map viewport offset + local vp = df.global.world.viewport + local vp_Coord = vp.corner + local map_offset_by_vp = { + x = map_coord.x - vp_Coord.x, + y = map_coord.y - vp_Coord.y, + z = map_coord.z - vp_Coord.z, + } + + if not dfhack.screen.inGraphicsMode() then + return map_offset_by_vp + else + -- -> pixel offset + local gps = df.global.gps + local map_tile_pixels = gps.viewport_zoom_factor // 4; + local screen_coord_px = { + x = map_tile_pixels * map_offset_by_vp.x, + y = map_tile_pixels * map_offset_by_vp.y, + } + -- -> interface layer coordinates + local screen_coord_text = { + x = math.ceil( screen_coord_px.x / gps.tile_pixel_x ), + y = math.ceil( screen_coord_px.y / gps.tile_pixel_y ), + } + + return screen_coord_text + end +end + +local function GetString(tokens) + local sb = {} + for _, tok in ipairs(tokens) do + if type(tok) == "string" then + sb[#sb+1] = tok + else -- must be a table token + sb[#sb+1] = tok.text + end + end + if not next(sb) then return nil end + return table.concat(sb) +end + +local once = false +function TooltipOverlay:render_unit_banners(dc) + if not AnyFollowOptionOn() then return end + + local blink_duration = config['tooltip-follow-blink-milliseconds'] + if blink_duration > 0 and not gui.blink_visible(blink_duration) then + return + end + + if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + return + end + + local vp = df.global.world.viewport + local topleft = vp.corner + local width = vp.max_x + local height = vp.max_y + local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} + + local units = dfhack.units.getUnitsInBox(topleft, bottomright) + if not units or #units == 0 then return end + + local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) + local pen = COLOR_WHITE + + local used_tiles = {} + -- reverse order yields better offsets for overlapping texts + for i = #units, 1, -1 do + local unit = units[i] + + local posX, posY, posZ = dfhack.units.getPosition(unit) + if not posX then goto continue end + local pos = xyz2pos(posX, posY, posZ) + + local info = GetUnitInfoText(unit, 'follow') + if not info or not next(info) then goto continue end + + local str = GetString(info) + if not str then goto continue end + + local scrPos = GetScreenCoordinates(pos) + local y = scrPos.y - 1 -- subtract 1 to move the text over the heads + local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile + + -- to resolve overlaps, we'll mark every coordinate we write anything in, + -- and then check if the new tooltip will overwrite any used coordinate. + -- if it will, try the next row, to a maximum offset of 4. + local row + local dy = 0 + -- todo: search for the "best" offset instead, f.e. max `usedAt` value, with `-1` the best + local usedAt = -1 + for yOffset = 0, max_banner_y_offset do + dy = yOffset + + row = used_tiles[y + dy] + if not row then + row = {} + used_tiles[y + dy] = row + end + + usedAt = -1 + for j = 0, #str - 1 do + if row[x + j] then + usedAt = j + break + end + end + + if usedAt == -1 then break end + end -- for dy + -- if other text starts at the same position, or even 2 to the right, + -- we can't place any useful information, and will ignore it instead. + if 0 <= usedAt and usedAt <= 2 then goto continue end + + local writer = dc:seek(x, y + dy) + local ix = 0 + for _, tok in ipairs(info) do + local s + if type(tok) == "string" then + writer = writer:pen(pen) + s = tok + else + writer = writer:pen(tok.pen) + s = tok.text + end + + -- in case there isn't enough space, cut the text off + local len = #s + if usedAt > 0 and ix + len + 1 >= usedAt then + -- last position we can write is `usedAt - len - ix - 1` + -- we want to replace it with an `_`, so we need another `- 1` + s = s:sub(1, usedAt - len - ix - 1 - 1) .. '_' + + writer = writer:string(s) + break -- nothing more will fit + else + writer = writer:string(s) + end + + ix = ix + len + end + + -- mark coordinates as used + for j = 0, #str - 1 do + row[x + j] = true + end + + ::continue:: + end +end + +-- MouseTooltip is an almost copy&paste of the DimensionsTooltip +MouseTooltip = defclass(MouseTooltip, widgets.ResizingPanel) + +MouseTooltip.ATTRS{ + frame_style=gui.FRAME_THIN, + frame_background=gui.CLEAR_PEN, + no_force_pause_badge=true, + auto_width=true, + display_offset={x=3, y=3}, +} + +function MouseTooltip:init() + ensure_key(self, 'frame').w = 17 + self.frame.h = 4 + + self.label = widgets.Label{ + frame={t=0}, + auto_width=true, + } + + self:addviews{ + widgets.Panel{ + -- set minimum size for tooltip frame so the DFHack frame badge fits + frame={t=0, l=0, w=7, h=2}, + }, + self.label, + } +end + +local function AnyHoverOptionOn() + return config['tooltip-hover-job'] + or config['tooltip-hover-name'] + or config['tooltip-hover-stress'] +end + +function MouseTooltip:render(dc) + if not AnyHoverOptionOn() then return end + + local x, y = dfhack.screen.getMousePos() + if not x then return end + + local pos = dfhack.gui.getMousePos() + local text = GetHoverText(pos) + if not text or not next(text) then return end + self.label:setText(text) + + local sw, sh = dfhack.screen.getWindowSize() + local frame_width = math.max(9, self.label:getTextWidth() + 2) + self.frame.l = math.min(x + self.display_offset.x, sw - frame_width) + self.frame.t = math.min(y + self.display_offset.y, sh - self.frame.h) + self:updateLayout() + MouseTooltip.super.render(self, dc) +end + OVERLAY_WIDGETS = { tooltip=TooltipOverlay, } From 5a016ceacd5e4d9f7eb6c37277188d648ed1dbe9 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 9 Feb 2025 22:36:00 +0100 Subject: [PATCH 0066/1082] remove debug remnant --- plugins/lua/spectate.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 36045c7a48..96fc0770c4 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -324,7 +324,6 @@ local function GetString(tokens) return table.concat(sb) end -local once = false function TooltipOverlay:render_unit_banners(dc) if not AnyFollowOptionOn() then return end From 47792ce67181d476c14123232e61d4de2192890a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 17:06:51 -0800 Subject: [PATCH 0067/1082] finish updating modules for the Great Reorg --- library/lua/custom-raw-tokens.lua | 2 +- library/lua/dfhack/workshops.lua | 2 +- library/lua/gui/materials.lua | 6 +++--- plugins/lua/sort.lua | 4 ++-- plugins/lua/stockflow.lua | 2 +- plugins/rendermax/rendermax.lua | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/library/lua/custom-raw-tokens.lua b/library/lua/custom-raw-tokens.lua index 6c19deb72a..7975ba5e39 100644 --- a/library/lua/custom-raw-tokens.lua +++ b/library/lua/custom-raw-tokens.lua @@ -300,7 +300,7 @@ local function getTokenArg1Else(userdata, token) elseif df.is_instance(df.building_workshopst, userdata) or df.is_instance(df.building_furnacest, userdata) then rawStruct = df.building_def.find(userdata.custom_type) elseif df.is_instance(df.interaction_instance, userdata) then - rawStruct = df.global.world.raws.interactions[userdata.interaction_id] + rawStruct = df.global.world.raws.interactions.all[userdata.interaction_id] else -- Assume raw struct *is* argument 1 rawStruct = userdata diff --git a/library/lua/dfhack/workshops.lua b/library/lua/dfhack/workshops.lua index a09849a57d..f43429642f 100644 --- a/library/lua/dfhack/workshops.lua +++ b/library/lua/dfhack/workshops.lua @@ -509,7 +509,7 @@ local function addReactionJobs(ret,bid,wid,cid) end local function scanRawsOres() local ret={} - for idx,ore in ipairs(df.global.world.raws.inorganics) do + for idx,ore in ipairs(df.global.world.raws.inorganics.all) do if #ore.metal_ore.mat_index~=0 then ret[idx]=ore end diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua index d3a9408a51..7429a78237 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -116,7 +116,7 @@ end function MaterialDialog:initInorganicMode() local choices = {} - for i,mat in ipairs(df.global.world.raws.inorganics) do + for i,mat in ipairs(df.global.world.raws.inorganics.all) do self:addMaterial(choices, mat.material, 0, i, false, mat) end @@ -378,14 +378,14 @@ function ItemTraitsDialog(args) end -------------------------------------- local set_ore_ix = {} - for i, raw in ipairs(df.global.world.raws.inorganics) do + for i, raw in ipairs(df.global.world.raws.inorganics.all) do for _, ix in ipairs(raw.metal_ore.mat_index) do set_ore_ix[ix] = true end end local ores = {} for ix in pairs(set_ore_ix) do - local raw = df.global.world.raws.inorganics[ix] + local raw = df.global.world.raws.inorganics.all[ix] ores[#ores+1] = {mat_index = ix, name = raw.material.state_name.Solid} end table.sort(ores, function(a,b) return a.name < b.name end) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index fe42cbb1ed..5777d2b174 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -43,7 +43,7 @@ local function get_active_idx_cache() end local function is_original_dwarf(unit) - return df.global.plotinfo.fortress_age == unit.curse.time_on_site // 10 + return df.global.plotinfo.fortress_age == unit.curse.interaction.time_on_site // 10 end local WAVE_END_GAP = 10000 @@ -53,7 +53,7 @@ local function get_most_recent_wave_oldest_active_idx(cache) for idx=#active_units-1,0,-1 do local unit = active_units[idx] if not dfhack.units.isCitizen(unit) then goto continue end - if oldest_unit and unit.curse.time_on_site - oldest_unit.curse.time_on_site > WAVE_END_GAP then + if oldest_unit and unit.curse.interaction.time_on_site - oldest_unit.curse.interaction.time_on_site > WAVE_END_GAP then return cache[oldest_unit.id] else oldest_unit = unit diff --git a/plugins/lua/stockflow.lua b/plugins/lua/stockflow.lua index 3b2b0fe669..dc44d23045 100644 --- a/plugins/lua/stockflow.lua +++ b/plugins/lua/stockflow.lua @@ -283,7 +283,7 @@ function collect_reactions() reaction_entry(result, job_types.CatchLiveFish) -- Cutting, encrusting, and metal extraction. - local rock_types = df.global.world.raws.inorganics + local rock_types = df.global.world.raws.inorganics.all for rock_id = #rock_types-1, 0, -1 do local material = rock_types[rock_id].material local rock_name = material.state_adj.Solid diff --git a/plugins/rendermax/rendermax.lua b/plugins/rendermax/rendermax.lua index f1cfa90203..6aacbde64f 100644 --- a/plugins/rendermax/rendermax.lua +++ b/plugins/rendermax/rendermax.lua @@ -164,7 +164,7 @@ function colorFrom16(col16) return {col[0],col[1],col[2]} end function addGems() - for k,v in pairs(df.global.world.raws.inorganics) do + for k,v in pairs(df.global.world.raws.inorganics.all) do if v.material.flags.IS_GEM then addMaterial("INORGANIC:"..v.id,colorFrom16(v.material.tile_color[0]+v.material.tile_color[2]*8)) end From 82c6551ebcb135f73ddb49e7d0d4533b2f794ccf Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 01:35:43 +0000 Subject: [PATCH 0068/1082] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 2a84adf2fe..9bf00722a7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2a84adf2fe1e76f5244f51cd4536442ebce10b5d +Subproject commit 9bf00722a739e6c00fd2a2c19ac7208e6f337ed3 diff --git a/plugins/stonesense b/plugins/stonesense index 8d445c72d5..9f5aa0f22e 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 8d445c72d5bc797bc2e8f6d0c42091fc068d0893 +Subproject commit 9f5aa0f22e70d3c9c0145fe2c4b4047a54dddca9 diff --git a/scripts b/scripts index fc3f7d044b..795cba2474 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit fc3f7d044b1b45f3ad7b9f7ba3bcc5a4bb24b09b +Subproject commit 795cba2474b928771cf59d6fc3690f4f5622bf99 From db49f6d5873e543ad0f72c0050bd34175de3b7a4 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 9 Feb 2025 21:57:50 -0800 Subject: [PATCH 0069/1082] Update docs/changelog.txt Co-authored-by: Myk --- docs/changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index aee713f373..6d930a3045 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,7 +59,6 @@ Template for new versions: - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S ## Fixes -- `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command - `createitem`: output items will now end up at look cursor if active - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself From b9f7882a3e310ef8b966ec39284bc5f44fbb91ef Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 9 Feb 2025 22:56:01 -0800 Subject: [PATCH 0070/1082] Update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6d930a3045..b36fad7284 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -76,6 +76,7 @@ Template for new versions: ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` +- ``gui.dwarfmode`` module: ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` now operate on the adventure mode look cursor, if active. Clearing the cursor sets it to the active adventurer's position. ## Removed From 57d850f2653af01c1bd32bc3733d6e820fe4b473 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:18:09 +0000 Subject: [PATCH 0071/1082] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 9f5aa0f22e..9bc40021b3 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9f5aa0f22e70d3c9c0145fe2c4b4047a54dddca9 +Subproject commit 9bc40021b3673f59393e4c718949b5575599f01f From 928f2435fec9b0d80f84cc9dd482c30253e3da81 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 02:46:14 -0800 Subject: [PATCH 0072/1082] add property filters for brewable, millable, and processable --- docs/changelog.txt | 1 + docs/plugins/stockpiles.rst | 6 +++++ plugins/stockpiles/OrganicMatLookup.cpp | 8 +++--- plugins/stockpiles/OrganicMatLookup.h | 2 +- plugins/stockpiles/StockpileSerializer.cpp | 31 +++++++++++++++++++--- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3e5fa6ad90..e8bfa535c4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,7 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort - `autobutcher`: treat animals on restraints as unavailable for slaughter +- `stockpiles`: add property filters for brewable, millable, and processable organic materials ## Documentation - `stonesense-art-guide`: new guide for making sprite art for Stonesense diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index 739cac5b8d..506b9d783c 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -378,6 +378,12 @@ Flags and subcategory prefixes:: paste/ pressed/ +Properties:: + + brewable + millable + processable + Settings files:: preparedmeals diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 05cb776487..879353f3b0 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -39,9 +39,7 @@ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category: DEBUG(log, out).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str()); } } -std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx) { - FoodMat food_mat; - food_mat_by_idx(out, mat_category, idx, food_mat); +std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, const FoodMat& food_mat) { if (food_mat.material.isValid()) { return food_mat.material.getToken(); } @@ -62,11 +60,11 @@ void OrganicMatLookup::food_build_map() { df::special_mat_table table = raws.mat_table; using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; - for (int32_t mat_category = traits::first_item_value; mat_category <= traits::last_item_value; ++mat_category) { + for (int32_t mat_category = 0; mat_category <= traits::last_item_value; ++mat_category) { for (size_t i = 0; i < table.organic_indexes[mat_category].size(); ++i) { int16_t type = table.organic_types[mat_category].at(i); int32_t index = table.organic_indexes[mat_category].at(i); - food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); // wtf.. only in c++ + food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); } } index_built = true; diff --git a/plugins/stockpiles/OrganicMatLookup.h b/plugins/stockpiles/OrganicMatLookup.h index f585ef2de4..15f9838df2 100644 --- a/plugins/stockpiles/OrganicMatLookup.h +++ b/plugins/stockpiles/OrganicMatLookup.h @@ -31,7 +31,7 @@ class OrganicMatLookup { }; static void food_mat_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat); - static std::string food_token_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx); + static std::string food_token_by_idx(DFHack::color_ostream& out, const FoodMat& food_mat); static size_t food_max_size(df::enums::organic_mat_category::organic_mat_category mat_category); static void food_build_map(); diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 1d11db41e5..afd0a6dbc8 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -449,7 +449,9 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v all = false; continue; } - string token = OrganicMatLookup::food_token_by_idx(out, cat, i); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, i, food_mat); + string token = OrganicMatLookup::food_token_by_idx(out, food_mat); if (token.empty()) { DEBUG(log, out).print("food mat invalid :(\n"); continue; @@ -460,6 +462,24 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v return all; } +static string get_filter_string(color_ostream& out, const OrganicMatLookup::FoodMat& food_mat) { + auto str = OrganicMatLookup::food_token_by_idx(out, food_mat); + if (auto plant = food_mat.material.plant) { + if (plant->flags.is_set(df::plant_raw_flags::DRINK)) + str += "/brewable"; + if (plant->flags.is_set(df::plant_raw_flags::MILL)) + str += "/millable"; + if (auto mat = food_mat.material.material) { + if (mat->flags.is_set(df::material_flags::STRUCTURAL_PLANT_MAT) && + (plant->flags.is_set(df::plant_raw_flags::THREAD) || + plant->flags.is_set(df::plant_raw_flags::EXTRACT_VIAL) || + plant->flags.is_set(df::plant_raw_flags::EXTRACT_BARREL))) + str += "/processable"; + } + } + return str; +} + static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, bool all, char val, const vector& filters, FuncReadImport read_value, size_t list_size, vector& pile_list, organic_mat_category::organic_mat_category cat) { @@ -467,8 +487,9 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, pile_list.resize(num_elems, '\0'); if (all) { for (size_t idx = 0; idx < num_elems; ++idx) { - string token = OrganicMatLookup::food_token_by_idx(out, cat, idx); - set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, idx, food_mat); + set_filter_elem(out, subcat, filters, val, get_filter_string(out, food_mat), idx, pile_list.at(idx)); } return; } @@ -480,7 +501,9 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, WARN(log, out).print("organic mat index too large! idx[%d] max_size[%zd]\n", idx, num_elems); continue; } - set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, idx, food_mat); + set_filter_elem(out, subcat, filters, val, get_filter_string(out, food_mat), idx, pile_list.at(idx)); } } From 803cddce9ff40e5b8d17568d719a7bc377f0bfd4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:53:22 +0000 Subject: [PATCH 0073/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 795cba2474..5890968648 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 795cba2474b928771cf59d6fc3690f4f5622bf99 +Subproject commit 589096864804e5d6758c555851ef12228003f51c From ef3c9785c560057e26bf6d908c2a2411435c1f55 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 08:04:05 -0800 Subject: [PATCH 0074/1082] changelog editing pass --- docs/changelog.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9586c36d6a..aba2146d48 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,9 +54,10 @@ Template for new versions: ## New Tools ## New Features -- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit +- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S +- `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units ## Fixes - `createitem`: output items will now end up at look cursor if active @@ -66,10 +67,10 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort - `autobutcher`: treat animals on restraints as unavailable for slaughter -- `stockpiles`: add property filters for brewable, millable, and processable organic materials +- `stockpiles`: add property filters for brewable, millable, and processable (e.g. at a Farmer's workshop) organic materials ## Documentation -- `stonesense-art-guide`: new guide for making sprite art for Stonesense +- `stonesense-art-guide`: guide for making sprite art for Stonesense ## API - ``Military::removeFromSquad``: removes unit from any squad assignments @@ -77,7 +78,7 @@ Template for new versions: ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` -- ``gui.dwarfmode`` module: ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` now operate on the adventure mode look cursor, if active. Clearing the cursor sets it to the active adventurer's position. +- ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons ## Removed From 07b43723b7bc39a43faebae3b9faa555b529d34c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:30:55 +0000 Subject: [PATCH 0075/1082] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 9bf00722a7..13cd875ce9 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9bf00722a739e6c00fd2a2c19ac7208e6f337ed3 +Subproject commit 13cd875ce99fec984b7d654d68c31ef265d05bdd diff --git a/plugins/stonesense b/plugins/stonesense index 9bc40021b3..760d36ec92 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9bc40021b3673f59393e4c718949b5575599f01f +Subproject commit 760d36ec92d8d250ea6a08e588ae8dcc1bbd2367 diff --git a/scripts b/scripts index 5890968648..273494caa5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 589096864804e5d6758c555851ef12228003f51c +Subproject commit 273494caa52ddff73f8eeaed3c037166c70796ca From 537727be276ffd1cc4ac2fe99be245113594ddb4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 11 Feb 2025 07:16:50 +0000 Subject: [PATCH 0076/1082] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 760d36ec92..17c6be1669 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 760d36ec92d8d250ea6a08e588ae8dcc1bbd2367 +Subproject commit 17c6be1669b2bce22864892892722c625e1d66e3 diff --git a/scripts b/scripts index 273494caa5..70c54c4e48 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 273494caa52ddff73f8eeaed3c037166c70796ca +Subproject commit 70c54c4e48c8005f2a07b68646e1f5b3855a0373 From 3f817a04ef07698c8b0b700466ead859a25555f2 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:03:57 +0800 Subject: [PATCH 0077/1082] Add missing history event --- library/modules/Military.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index cbb4295562..356c606681 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -14,6 +14,7 @@ #include "df/histfig_entity_link_squadst.h" #include "df/historical_figure.h" #include "df/historical_entity.h" +#include "df/history_event_remove_hf_entity_linkst.h" #include "df/entity_position.h" #include "df/entity_position_assignment.h" #include "df/plotinfost.h" @@ -334,6 +335,8 @@ static void remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ former_squad->link_strength = 100; hf->entity_links.push_back(former_squad); + + // Unassigning a normal soldier does not seem to generate an event record. } static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) @@ -343,6 +346,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ return; int32_t assignment_id = -1; + int32_t pos_id = -1; for (auto& np : nps) { if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) @@ -352,6 +356,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ np.assignment->histfig2 = -1; assignment_id = np.assignment->id; + pos_id = np.position->id; break; } @@ -387,8 +392,21 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->start_year = start_year; former_pos->end_year = *df::global::cur_year; former_pos->link_strength = 100; - + hf->entity_links.push_back(former_pos); + + int32_t event_id = *df::global::hist_event_next_id; + auto former_pos_event = df::allocate(); + former_pos_event->year = *df::global::cur_year; + former_pos_event->seconds = *df::global::cur_year_tick; + former_pos_event->id = event_id; + former_pos_event->civ = squad->entity_id; + former_pos_event->histfig = hf->id; + former_pos_event->position_id = pos_id; + former_pos_event->link_type = df::histfig_entity_link_type::POSITION; + + df::global::world->history.events.push_back(former_pos_event); + *df::global::hist_event_next_id = event_id + 1; } bool Military::removeFromSquad(int32_t unit_id) From 25196c0281487f35b606a197676e94cd95ea6bc9 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:07:38 +0800 Subject: [PATCH 0078/1082] Trim whitespace --- library/modules/Military.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 356c606681..2f44e04824 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -392,7 +392,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->start_year = start_year; former_pos->end_year = *df::global::cur_year; former_pos->link_strength = 100; - + hf->entity_links.push_back(former_pos); int32_t event_id = *df::global::hist_event_next_id; @@ -404,7 +404,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos_event->histfig = hf->id; former_pos_event->position_id = pos_id; former_pos_event->link_type = df::histfig_entity_link_type::POSITION; - + df::global::world->history.events.push_back(former_pos_event); *df::global::hist_event_next_id = event_id + 1; } From 72819ad3a290ee399d8f86462793e4370c5b2817 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:16:14 +0800 Subject: [PATCH 0079/1082] Update docs/dev/Lua API.rst --- docs/dev/Lua API.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 66516642a9..bbab5a6c38 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1991,8 +1991,9 @@ Military module Removes a unit from its squad. Unsets the unit's military information (i.e., ``unit.military.squad_id`` and ``unit.military.squad_pos``), the squad's position information (i.e., - ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links - to indicate former squad membership or command. + ``squad.positions[squad_pos].occupant``), modifies the unit's entity links + to indicate former squad membership or command, and creates a corresponding + world history event. Items module ------------ From 4aa4fe9468a6f4175cde36861caa520df94c5d12 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:48:16 +0800 Subject: [PATCH 0080/1082] Change increment style --- docs/about/Authors.rst | 1 + library/modules/Military.cpp | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index bfb8aa02a8..d806d92aaa 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -158,6 +158,7 @@ NotRexButCaesar NotRexButCaesar Nuno Fernandes UnknowableCoder nuvu vallode Omniclasm +Ong Ying Gao ong-yinggao98 oorzkws oorzkws OwnageIsMagic OwnageIsMagic palenerd dlmarquis diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 2f44e04824..a5946acca5 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -395,7 +395,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.push_back(former_pos); - int32_t event_id = *df::global::hist_event_next_id; + int32_t event_id = (*df::global::hist_event_next_id)++; auto former_pos_event = df::allocate(); former_pos_event->year = *df::global::cur_year; former_pos_event->seconds = *df::global::cur_year_tick; @@ -406,7 +406,6 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos_event->link_type = df::histfig_entity_link_type::POSITION; df::global::world->history.events.push_back(former_pos_event); - *df::global::hist_event_next_id = event_id + 1; } bool Military::removeFromSquad(int32_t unit_id) From 3752608ebd00c426aaad634cbaf7ac81ee9d2ea9 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Tue, 11 Feb 2025 15:34:09 -0600 Subject: [PATCH 0081/1082] Vastly Improved Slider --- library/lua/gui/widgets/range_slider.lua | 35 ++++-- library/lua/gui/widgets/slider.lua | 138 +++-------------------- 2 files changed, 38 insertions(+), 135 deletions(-) diff --git a/library/lua/gui/widgets/range_slider.lua b/library/lua/gui/widgets/range_slider.lua index bd1f9cb3de..f55f0e00f0 100644 --- a/library/lua/gui/widgets/range_slider.lua +++ b/library/lua/gui/widgets/range_slider.lua @@ -29,6 +29,7 @@ RangeSlider.ATTRS{ get_right_idx_fn=DEFAULT_NIL, on_left_change=DEFAULT_NIL, on_right_change=DEFAULT_NIL, + is_single=DEFAULT_NIL } function RangeSlider:preinit(init_table) @@ -43,7 +44,8 @@ function RangeSlider:init() end local function rangeslider_get_width_per_idx(self) - return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) + local min_value = (self.is_single) and 3 or 5 -- Single slider = 3, else 5 + return math.max(min_value, (self.frame_body.width-7) // (self.num_stops-1)) end function RangeSlider:onInput(keys) @@ -142,16 +144,27 @@ function RangeSlider:onRenderBody(dc, rect) end dc:char(nil, SLIDER_TRACK) dc:char(nil, SLIDER_RIGHT_END) - -- draw tabs - dc:seek(width_per_idx*(left_idx-1)) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - dc:seek(width_per_idx*(right_idx-1)+4) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - -- manage dragging + + -- Draw tab(s) + if self.is_single then + -- Single slider: Draw one centered tab + dc:seek(width_per_idx * (left_idx-1) + 2) -- Center the tab + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + else + -- Dual slider: Draw left and right tabs separately + dc:seek(width_per_idx * (left_idx-1)) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + dc:seek(width_per_idx*(right_idx-1)+4) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + end + + -- Manage dragging if self.is_dragging_target then rangeslider_do_drag(self, width_per_idx) end diff --git a/library/lua/gui/widgets/slider.lua b/library/lua/gui/widgets/slider.lua index 92263d2924..23bf6f6202 100644 --- a/library/lua/gui/widgets/slider.lua +++ b/library/lua/gui/widgets/slider.lua @@ -1,131 +1,21 @@ -local Widget = require('gui.widgets.widget') - -local to_pen = dfhack.pen.parse - --------------------------------- --- Slider --------------------------------- - ----@class widgets.Slider.attrs: widgets.Widget.attrs ----@field num_stops integer ----@field get_idx_fn? function ----@field on_change? fun(index: integer) - ----@class widgets.Slider.attrs.partial: widgets.Slider.attrs - ----@class widgets.Slider.initTable: widgets.Slider.attrs ----@field num_stops integer - ----@class widgets.Slider: widgets.Widget, widgets.Slider.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) ----@overload fun(init_table: widgets.Slider.initTable): self -Slider = defclass(Slider, Widget) -Slider.ATTRS{ - num_stops=DEFAULT_NIL, - get_idx_fn=DEFAULT_NIL, - on_change=DEFAULT_NIL, +-- slider.lua +local RangeSlider = require('gui.widgets.range_slider') + +local Slider = defclass(Slider, RangeSlider) +Slider.ATTRS { + get_idx_fn = DEFAULT_NIL, -- Function to get the current index + on_change = DEFAULT_NIL, -- Function to handle index change + is_single = true } -function Slider:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - function Slider:init() - if self.num_stops < 2 then error('too few Slider stops') end - self.is_dragging_target = nil -- 'left', 'right', or 'both' - self.is_dragging_idx = nil -- offset from leftmost dragged tile -end - -local function Slider_get_width_per_idx(self) - return math.max(3, (self.frame_body.width-7) // (self.num_stops-1)) -end - -function Slider:onInput(keys) - if not keys._MOUSE_L then return false end - local x = self:getMousePos() - if not x then return false end - local left_idx = self.get_idx_fn() - local width_per_idx = Slider_get_width_per_idx(self) - local left_pos = width_per_idx*(left_idx-1) - local right_pos = width_per_idx*(left_idx-1) + 4 - if x < left_pos then - self.on_change(self.get_idx_fn() - 1) - else - self.is_dragging_target = 'both' - self.is_dragging_idx = x - right_pos - end - return true -end - -local function Slider_do_drag(self, width_per_idx) - local x = self.frame_body:localXY(dfhack.screen.getMousePos()) - local cur_pos = x - self.is_dragging_idx - cur_pos = math.max(0, cur_pos) - cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) - local offset = 1 - local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 - if self.is_dragging_target == 'both' then - if new_idx > self.num_stops then - return - end - end - if new_idx and new_idx ~= self.get_idx_fn() then - self.on_change(new_idx) - end -end - -local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} - -function Slider:onRenderBody(dc, rect) - local left_idx = self.get_idx_fn() - local width_per_idx = Slider_get_width_per_idx(self) - -- draw track - dc:seek(1,0) - dc:char(nil, SLIDER_LEFT_END) - dc:char(nil, SLIDER_TRACK) - for stop_idx=1,self.num_stops-1 do - local track_stop_pen = SLIDER_TRACK_STOP_SELECTED - local track_pen = SLIDER_TRACK_SELECTED - if left_idx ~= stop_idx then - track_stop_pen = SLIDER_TRACK_STOP - track_pen = SLIDER_TRACK - elseif left_idx == stop_idx then - track_pen = SLIDER_TRACK - end - dc:char(nil, track_stop_pen) - for i=2,width_per_idx do - dc:char(nil, track_pen) - end - end - if left_idx >= self.num_stops then - dc:char(nil, SLIDER_TRACK_STOP_SELECTED) - else - dc:char(nil, SLIDER_TRACK_STOP) - end - dc:char(nil, SLIDER_TRACK) - dc:char(nil, SLIDER_RIGHT_END) - -- draw tab - dc:seek(width_per_idx*(left_idx-1)+2) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - -- manage dragging - if self.is_dragging_target then - Slider_do_drag(self, width_per_idx) + self.get_left_idx_fn = self.get_idx_fn + self.get_right_idx_fn = self.get_idx_fn + self.on_left_change = function(index) + if self.on_change then self.on_change(index) end end - if df.global.enabler.mouse_lbut_down == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil + self.on_right_change = function(index) + if self.on_change then self.on_change(index) end end end From 2d33265846786767c942e26789752c5623aac80f Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Tue, 11 Feb 2025 15:38:27 -0600 Subject: [PATCH 0082/1082] Update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index aba2146d48..f68ca870d2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -79,6 +79,7 @@ Template for new versions: - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons +- ``widgets.Slider``: new mouse-controlled single-headed slider widget ## Removed From 2a00e78d7addb83cf400bcca9e8c37edf4d48946 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 12 Feb 2025 07:16:57 +0000 Subject: [PATCH 0083/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 70c54c4e48..a76a39a204 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 70c54c4e48c8005f2a07b68646e1f5b3855a0373 +Subproject commit a76a39a2048ea47607794c1ba2f5f0807f5d7a92 From cfb2cac22246972bc6e3ec703d65e03e3af3db1b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 12 Feb 2025 14:15:33 -0500 Subject: [PATCH 0084/1082] Fix regression in checkFreeTiles --- library/include/modules/Buildings.h | 2 +- library/modules/Buildings.cpp | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index 80fe443396..c323958e17 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -111,7 +111,7 @@ DFHACK_EXPORT bool getCorrectSize(df::coord2d &size, df::coord2d ¢er, * Checks if the tiles are free to be built upon. */ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, - df::building *bld, + df::building *bld = nullptr, bool create_ext = false, bool allow_occupied = false, bool allow_wall = false, diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 672464ceba..1135fb5307 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -735,10 +735,7 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, bool allow_wall, bool allow_flow) { - CHECK_NULL_POINTER(bld); - bool found_any = false; - auto & room = bld->room; for (int dx = 0; dx < size.x; dx++) { @@ -748,9 +745,9 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, df::building_extents_type *etile = NULL; // Exclude using extents - if (room.extents) + if (bld && bld->room.extents) { - etile = getExtentTile(room, tile); + etile = getExtentTile(bld->room, tile); if (!etile || !*etile) continue; } @@ -784,13 +781,13 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, found_any = true; else { - if (!create_ext) + if (!bld || !create_ext) return false; - if (!room.extents) + if (!bld->room.extents) { - init_extents(room, pos, size); - etile = getExtentTile(room, tile); + init_extents(bld->room, pos, size); + etile = getExtentTile(bld->room, tile); } if (!etile) From 52a6b891104fff68ac4d2e08f6de7b66e5d81a9f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 12 Feb 2025 14:57:14 -0600 Subject: [PATCH 0085/1082] do it right this time --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 345d8967db..1e61d7f5f3 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 345d8967db88ff33283111fdfecd0eaebbf09584 +Subproject commit 1e61d7f5f36a0bc937985c5b859d8ee152789bde From ba448949ffac1d933ce4089871f19ce0f1c54ee3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Feb 2025 18:44:14 -0800 Subject: [PATCH 0086/1082] update docs for checkFreeTiles --- docs/changelog.txt | 2 ++ docs/dev/Lua API.rst | 17 +++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 20e65538d2..20d11103bc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -74,11 +74,13 @@ Template for new versions: ## API - ``Military::removeFromSquad``: removes unit from any squad assignments +- ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons +- ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index bbab5a6c38..f8108e8b2c 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2477,14 +2477,15 @@ General using width and height for flexible dimensions. Returns *is_flexible, width, height, center_x, center_y*. -* ``dfhack.buildings.checkFreeTiles(pos,size[,extents[,change_extents[,allow_occupied[,allow_wall[,allow_flow]]]]])`` - - Checks if the rectangle defined by ``pos`` and ``size``, and possibly extents, - can be used for placing a building. If ``change_extents`` is true, bad tiles - are removed from extents. If ``allow_occupied``, the occupancy test is skipped. - Set ``allow_wall`` to true if the building is unhindered by walls (such as an - activity zone). Set ``allow_flow`` to true if the building can be built even - if there is deep water or any magma on the tile (such as abstract buildings). +* ``dfhack.buildings.checkFreeTiles(pos,size[,bld[,change_extents[,allow_occupied[,allow_wall[,allow_flow]]]]])`` + + Checks if the rectangle defined by ``pos`` and ``size``, and possibly the + extents associated with bld, can be used for placing a building. If + ``change_extents`` is true, bad tiles are removed from extents. If + ``allow_occupied``, the occupancy test is skipped. Set ``allow_wall`` to true + if the building is unhindered by walls (such as an activity zone). Set + ``allow_flow`` to true if the building can be built even if there is deep + water or any magma on the tile (such as abstract buildings). * ``dfhack.buildings.countExtentTiles(extents,defval)`` From 66c2af8677505069471d6d60aa3c979cbecaa214 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 13 Feb 2025 15:07:48 -0600 Subject: [PATCH 0087/1082] Remove the segment size and overlay stuff --- docs/plugins/stonesense.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index f0b9c3f9ce..57eccee1a2 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -16,11 +16,7 @@ Usage The viewer window has read-only access to the game, and can follow the game view or be moved independently. Configuration for stonesense can be set in the -``dfhack-config/stonesense/init.txt`` file in your DF game directory. If the window refresh -rate is too low, change ``SEGMENTSIZE_Z`` to ``2`` in this file, and if you are -unable to see the edges of the map with the overlay active, try decreasing the -value for ``SEGMENTSIZE_X`` and ``SEGMENTSIZE_Y`` -- normal values are ``50`` to ``80``, depending -on your screen resolution. +``dfhack-config/stonesense/init.txt`` file in your DF game directory. .. figure:: ../images/stonesense-roadtruss.jpg :align: center From f944852c209cc0bc46b73f09422fafa80b772eb6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 14 Feb 2025 20:01:41 -0800 Subject: [PATCH 0088/1082] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index a76a39a204..4af65c03bf 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit a76a39a2048ea47607794c1ba2f5f0807f5d7a92 +Subproject commit 4af65c03bf6f0028e5f9e03af125bee0266688b0 From 02652911d3e4e6c7b75c0d22b62c1092f8c213bb Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Feb 2025 12:51:57 +0100 Subject: [PATCH 0089/1082] make it possible to set all settings Also: * print settings in key order * use string keys to simplify using json for persistence * change default tooltip-follow-blink-milliseconds to 3000ms --- plugins/lua/spectate.lua | 135 +++++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 40 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 96fc0770c4..647f022bf2 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -27,7 +27,7 @@ local function get_default_state() ['include-wildlife']=false, ['prefer-conflict']=true, ['prefer-new-arrivals']=true, - ['tooltip-follow-blink-milliseconds']=2000, + ['tooltip-follow-blink-milliseconds']=3000, ['tooltip-follow-job']=true, ['tooltip-follow-job-shortenings'] = { ["Store item in stockpile"] = "Store item", @@ -35,39 +35,36 @@ local function get_default_state() ['tooltip-follow-name']=false, ['tooltip-follow-stress']=true, ['tooltip-follow-stress-levels']={ - [0] = - true, -- Miserable - true, - false, - false, - false, - true, - true, -- Ecstatic + ["0"] = true, -- Miserable + ["1"] = true, + ["2"] = false, + ["3"] = false, + ["4"] = false, + ["5"] = true, + ["6"] = true, -- Ecstatic }, ['tooltip-hover-job']=true, ['tooltip-hover-name']=true, ['tooltip-hover-stress']=true, ['tooltip-hover-stress-levels']={ - [0] = - true, -- Miserable - true, - false, - false, - false, - true, - true, -- Ecstatic + ["0"] = true, -- Miserable + ["1"] = true, + ["2"] = false, + ["3"] = false, + ["4"] = false, + ["5"] = true, + ["6"] = true, -- Ecstatic }, ['tooltip-stress-levels']={ -- keep in mind, the text will look differently with game's font -- colors are same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE - [0] = - {text = "=C", pen = COLOR_RED, name = "Miserable"}, - {text = ":C", pen = COLOR_LIGHTRED, name = "Unhappy"}, - {text = ":(", pen = COLOR_YELLOW, name = "Displeased"}, - {text = ":]", pen = COLOR_GREY, name = "Content"}, - {text = ":)", pen = COLOR_GREEN, name = "Pleased"}, - {text = ":D", pen = COLOR_LIGHTGREEN, name = "Happy"}, - {text = "=D", pen = COLOR_LIGHTCYAN, name = "Ecstatic"}, + ["0"] = {text = "=C", pen = COLOR_RED, name = "Miserable"}, + ["1"] = {text = ":C", pen = COLOR_LIGHTRED, name = "Unhappy"}, + ["2"] = {text = ":(", pen = COLOR_YELLOW, name = "Displeased"}, + ["3"] = {text = ":]", pen = COLOR_GREY, name = "Content"}, + ["4"] = {text = ":)", pen = COLOR_GREEN, name = "Pleased"}, + ["5"] = {text = ":D", pen = COLOR_LIGHTGREEN, name = "Happy"}, + ["6"] = {text = "=D", pen = COLOR_LIGHTCYAN, name = "Ecstatic"}, } } end @@ -116,13 +113,39 @@ end ----------------------------- -- commandline interface +local function pairsByKeys(t, f) + local a = {} + for n in pairs(t) do table.insert(a, n) end + table.sort(a, f) + local i = 0 -- iterator variable + local iter = function () -- iterator function + i = i + 1 + if a[i] == nil then return nil + else return a[i], t[a[i]] + end + end + return iter +end + +-- no recursion protection, but it shouldn't be needed for a config... +local function print_table(t, indent) + indent = indent or '' + for key, value in pairsByKeys(t) do + if type(value) == 'table' then + print(indent .. key .. ':') + print_table(value, indent .. ' ') + else + print(indent .. key .. ': ' .. tostring(value)) + end + end +end + local function print_status() print('spectate is:', isEnabled() and 'enabled' or 'disabled') print() print('settings:') - for key, value in pairs(config) do - print(' ' .. key .. ': ' .. tostring(value)) - end + + print_table(config, ' ') end local function do_toggle() @@ -133,23 +156,54 @@ local function do_toggle() end end -local function set_setting(key, value) +local function set_setting(args) + local key = table.remove(args, 1) if config[key] == nil then qerror('unknown setting: ' .. key) end - if key == 'follow-seconds' then - value = argparse.positiveInt(value, 'follow-seconds') - else - value = argparse.boolean(value, key) + local n = #args + if n == 0 then + qerror('missing value') end - config[key] = value - save_state() - if not key:startswith(lua_only_settings_prefix) then - if type(value) == 'boolean' then - value = value and 1 or 0 + + if n == 1 then + local value = args[1] + if key == 'follow-seconds' then + value = argparse.positiveInt(value, 'follow-seconds') + elseif key == 'tooltip-follow-blink-milliseconds' then + value = argparse.nonnegativeInt(value, 'tooltip-follow-blink-milliseconds') + else + value = argparse.boolean(value, key) end - spectate_setSetting(key, value) + + config[key] = value + + if not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(key, value) + end + else + local t = config[key] + for i = 1, n - 2 do + t = t[args[i]] + end + local k = args[n-1] + local v = args[n] + if key ~= 'tooltip-follow-job-shortenings' then + -- user should be able to add new shortenings, but not other things + if t[k] == nil then + table.remove(args) + qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) + elseif key:endswith('-stress-levels') then + v = argparse.boolean(v, key .. '/' .. k) + end + end + t[k] = v end + + save_state() end local function set_overlay(value) @@ -164,7 +218,7 @@ function parse_commandline(args) elseif command == 'toggle' then do_toggle() elseif command == 'set' then - set_setting(args[1], args[2]) + set_setting(args) elseif command == 'overlay' then set_overlay(args[1]) else @@ -180,6 +234,7 @@ end local function GetUnitStress(unit, stress_levels) local stressCat = dfhack.units.getStressCategory(unit) if stressCat > 6 then stressCat = 6 end + stressCat = tostring(stressCat) if not stress_levels[stressCat] then return end local level_cfg = config['tooltip-stress-levels'][stressCat] From 82ce1be6c97dabceefd9ac10aa2d1debbb0b73bc Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Feb 2025 12:59:33 +0100 Subject: [PATCH 0090/1082] add options to enable/disable tooltips at once --- plugins/lua/spectate.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 647f022bf2..a1d23c1f0d 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -27,6 +27,7 @@ local function get_default_state() ['include-wildlife']=false, ['prefer-conflict']=true, ['prefer-new-arrivals']=true, + ['tooltip-follow']=true, ['tooltip-follow-blink-milliseconds']=3000, ['tooltip-follow-job']=true, ['tooltip-follow-job-shortenings'] = { @@ -43,6 +44,7 @@ local function get_default_state() ["5"] = true, ["6"] = true, -- Ecstatic }, + ['tooltip-hover']=true, ['tooltip-hover-job']=true, ['tooltip-hover-name']=true, ['tooltip-hover-stress']=true, @@ -380,7 +382,7 @@ local function GetString(tokens) end function TooltipOverlay:render_unit_banners(dc) - if not AnyFollowOptionOn() then return end + if not (config['tooltip-follow'] and AnyFollowOptionOn()) then return end local blink_duration = config['tooltip-follow-blink-milliseconds'] if blink_duration > 0 and not gui.blink_visible(blink_duration) then @@ -525,7 +527,7 @@ local function AnyHoverOptionOn() end function MouseTooltip:render(dc) - if not AnyHoverOptionOn() then return end + if not (config['tooltip-hover'] and AnyHoverOptionOn()) then return end local x, y = dfhack.screen.getMousePos() if not x then return end From 9f103d1bc4c477c822caaf5fa265ecb7af455aaa Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Feb 2025 16:25:47 +0100 Subject: [PATCH 0091/1082] add documentation for new options Also, make it possible to set `tooltip-stress-levels`. --- docs/plugins/spectate.rst | 31 +++++++++++++++++++++++++++++++ plugins/lua/spectate.lua | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index b4d913d9e0..c93e2140b6 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -103,10 +103,22 @@ Settings Toggle whether to prefer following (non-siege) units that have newly arrived on the map. +``tooltip-follow`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + tooltip. + +``tooltip-follow-blink-milliseconds`` (default: 3000) + If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration + in milliseconds. Set to 0 to always show. + ``tooltip-follow-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. +``tooltip-follow-job-shortenings`` + If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to + shorten some job names, f.e. "Store item in stockpile" becomes "Store item". + ``tooltip-follow-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the tooltip. @@ -115,6 +127,15 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the tooltip. +``tooltip-follow-stress-levels`` (default: Displeased, Content, Pleased are disabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + specific happiness level (stress) of the dwarf in the tooltip. F.e. + ``tooltip-follow-stress-levels 2 true`` would show Displeased emoticon. + +``tooltip-hover`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + hover panel. + ``tooltip-hover-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the hover panel. @@ -127,6 +148,16 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the hover panel. +``tooltip-hover-stress-levels`` (default: Displeased, Content, Pleased are disabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + specific happiness level (stress) of the dwarf in the hover panel. F.e. + ``tooltip-hover-stress-levels 2 true`` would show Displeased emoticon. + +``tooltip-stress-levels`` + Controls how happiness levels (stress) are displayed (emoticon and color). + F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for + Ecstatic dwarves to ``XD``. + Overlays -------- diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index a1d23c1f0d..2f8ade03cf 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -198,7 +198,7 @@ local function set_setting(args) if t[k] == nil then table.remove(args) qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) - elseif key:endswith('-stress-levels') then + elseif key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then v = argparse.boolean(v, key .. '/' .. k) end end From 1d79fd05e4290584c9deee349a3ecbf2ca1f80f2 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Feb 2025 16:28:44 +0100 Subject: [PATCH 0092/1082] remove tabs --- docs/plugins/spectate.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index c93e2140b6..8cd40276b6 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -104,20 +104,20 @@ Settings arrived on the map. ``tooltip-follow`` (default: enabled) - If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the tooltip. ``tooltip-follow-blink-milliseconds`` (default: 3000) - If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration - in milliseconds. Set to 0 to always show. + If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration + in milliseconds. Set to 0 to always show. ``tooltip-follow-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. ``tooltip-follow-job-shortenings`` - If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to - shorten some job names, f.e. "Store item in stockpile" becomes "Store item". + If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to + shorten some job names, f.e. "Store item in stockpile" becomes "Store item". ``tooltip-follow-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -128,12 +128,12 @@ Settings happiness level (stress) of the dwarf in the tooltip. ``tooltip-follow-stress-levels`` (default: Displeased, Content, Pleased are disabled) - If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the tooltip. F.e. - ``tooltip-follow-stress-levels 2 true`` would show Displeased emoticon. + ``tooltip-follow-stress-levels 2 true`` would show Displeased emoticon. ``tooltip-hover`` (default: enabled) - If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the hover panel. ``tooltip-hover-job`` (default: enabled) @@ -149,14 +149,14 @@ Settings happiness level (stress) of the dwarf in the hover panel. ``tooltip-hover-stress-levels`` (default: Displeased, Content, Pleased are disabled) - If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the hover panel. F.e. - ``tooltip-hover-stress-levels 2 true`` would show Displeased emoticon. + ``tooltip-hover-stress-levels 2 true`` would show Displeased emoticon. ``tooltip-stress-levels`` - Controls how happiness levels (stress) are displayed (emoticon and color). - F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for - Ecstatic dwarves to ``XD``. + Controls how happiness levels (stress) are displayed (emoticon and color). + F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for + Ecstatic dwarves to ``XD``. Overlays -------- From 4f016d4874a7daf21eac415741eba433ca3393c6 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 19:43:58 -0500 Subject: [PATCH 0093/1082] Binpatches to remove the vanilla dimensions tooltip --- CMakeLists.txt | 1 + patches/CMakeLists.txt | 3 +++ .../disable-vanilla-dimensions.dif | 6 ++++++ .../v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif | 7 +++++++ .../disable-vanilla-dimensions.dif | 9 +++++++++ .../disable-vanilla-dimensions.dif | 6 ++++++ .../v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif | 6 ++++++ .../v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif | 6 ++++++ 8 files changed, 44 insertions(+) create mode 100644 patches/CMakeLists.txt create mode 100644 patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c58b36bf2..2d73cd996b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,6 +451,7 @@ if(INSTALL_DATA_FILES) install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/depends/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches DESTINATION ${DFHACK_DATA_DESTINATION}/patches) endif() if(INSTALL_SCRIPTS) diff --git a/patches/CMakeLists.txt b/patches/CMakeLists.txt new file mode 100644 index 0000000000..37ee7c9440 --- /dev/null +++ b/patches/CMakeLists.txt @@ -0,0 +1,3 @@ +install(DIRECTORY ${patchdirectories} + DESTINATION ${DFHACK_DATA_DESTINATION}/patches +) diff --git a/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif new file mode 100644 index 0000000000..f8c95a2af6 --- /dev/null +++ b/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif @@ -0,0 +1,6 @@ +Dwarf_Fortress +011ba9eb: e8 90 +011ba9ec: c0 90 +011ba9ed: 4a 90 +011ba9ee: e6 90 +011ba9ef: fe 90 diff --git a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif new file mode 100644 index 0000000000..0c8cd61ad6 --- /dev/null +++ b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif @@ -0,0 +1,7 @@ +Dwarf_Fortress +011c605b: e8 90 +011c605c: 50 90 +011c605d: a5 90 +011c605e: e5 90 +011c605f: fe 90 + diff --git a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif new file mode 100644 index 0000000000..59af3301c7 --- /dev/null +++ b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif @@ -0,0 +1,9 @@ +---8<--- +This difference file is created by The Interactive Disassembler + +Dwarf_Fortress +011C9C3B: e8 90 +011c9c3c: 80 90 +011c9c3d: 6a 90 +011c9c3e: e5 90 +011c9c3f: fe 90 diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif new file mode 100644 index 0000000000..b097959e80 --- /dev/null +++ b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif @@ -0,0 +1,6 @@ +Dwarf_Fortress +377d5d: e8 90 +377d5e: ee 90 +377d5f: 93 90 +377d60: 68 90 +377d61: 00 90 \ No newline at end of file diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif new file mode 100644 index 0000000000..b0eea33793 --- /dev/null +++ b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif @@ -0,0 +1,6 @@ +Dwarf_Fortress +37af8d: e8 90 +37af8e: 1e 90 +37af8f: b7 90 +37af90: 68 90 +37af91: 00 90 \ No newline at end of file diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif new file mode 100644 index 0000000000..22fc267369 --- /dev/null +++ b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif @@ -0,0 +1,6 @@ +Dwarf_Fortress +37d77d: e8 90 +37d77e: 6e 90 +37d77f: bb 90 +37d780: 68 90 +37d781: 00 90 \ No newline at end of file From db459e41da2489c8383e157ebac25334fec51c9c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 19:47:46 -0500 Subject: [PATCH 0094/1082] Changelog for new binpatches --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 20d11103bc..700a776ed8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S - `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units +- disable-vanilla-dimensions: new binpatch to disable the vanilla dimensions tooltip ## Fixes - `createitem`: output items will now end up at look cursor if active From fb9fb5f235db45fe1dd0252f680f5cf0fc08f2db Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 19:51:48 -0500 Subject: [PATCH 0095/1082] Cleanup line endings and formatting --- patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif | 1 - .../v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif | 5 +---- .../v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif | 2 +- patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif | 2 +- patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif index 0c8cd61ad6..1784576881 100644 --- a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif @@ -4,4 +4,3 @@ Dwarf_Fortress 011c605d: a5 90 011c605e: e5 90 011c605f: fe 90 - diff --git a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif index 59af3301c7..142e53e4fa 100644 --- a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif @@ -1,8 +1,5 @@ ----8<--- -This difference file is created by The Interactive Disassembler - Dwarf_Fortress -011C9C3B: e8 90 +011c9c3b: e8 90 011c9c3c: 80 90 011c9c3d: 6a 90 011c9c3e: e5 90 diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif index b097959e80..2cd08dfd0e 100644 --- a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif @@ -3,4 +3,4 @@ Dwarf_Fortress 377d5e: ee 90 377d5f: 93 90 377d60: 68 90 -377d61: 00 90 \ No newline at end of file +377d61: 00 90 diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif index b0eea33793..f1d36354d3 100644 --- a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif @@ -3,4 +3,4 @@ Dwarf_Fortress 37af8e: 1e 90 37af8f: b7 90 37af90: 68 90 -37af91: 00 90 \ No newline at end of file +37af91: 00 90 diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif index 22fc267369..82d103ad18 100644 --- a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif @@ -3,4 +3,4 @@ Dwarf_Fortress 37d77e: 6e 90 37d77f: bb 90 37d780: 68 90 -37d781: 00 90 \ No newline at end of file +37d781: 00 90 From c64f8969b0e3323f484462cf9d4a38311e0c6d19 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 20:07:31 -0500 Subject: [PATCH 0096/1082] Fix lineendings for real this time --- .../disable-vanilla-dimensions.dif | 12 ++++++------ .../disable-vanilla-dimensions.dif | 12 ++++++------ .../disable-vanilla-dimensions.dif | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif index 2cd08dfd0e..2f5cf9e0ae 100644 --- a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif @@ -1,6 +1,6 @@ -Dwarf_Fortress -377d5d: e8 90 -377d5e: ee 90 -377d5f: 93 90 -377d60: 68 90 -377d61: 00 90 +Dwarf_Fortress +377d5d: e8 90 +377d5e: ee 90 +377d5f: 93 90 +377d60: 68 90 +377d61: 00 90 diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif index f1d36354d3..e254650761 100644 --- a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif @@ -1,6 +1,6 @@ -Dwarf_Fortress -37af8d: e8 90 -37af8e: 1e 90 -37af8f: b7 90 -37af90: 68 90 -37af91: 00 90 +Dwarf_Fortress +37af8d: e8 90 +37af8e: 1e 90 +37af8f: b7 90 +37af90: 68 90 +37af91: 00 90 diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif index 82d103ad18..44dd5ebb81 100644 --- a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif @@ -1,6 +1,6 @@ -Dwarf_Fortress -37d77d: e8 90 -37d77e: 6e 90 -37d77f: bb 90 -37d780: 68 90 -37d781: 00 90 +Dwarf_Fortress +37d77d: e8 90 +37d77e: 6e 90 +37d77f: bb 90 +37d780: 68 90 +37d781: 00 90 From 87bfb088399bc07a8fd1c52a744e1aec1e73fe68 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 20:09:56 -0500 Subject: [PATCH 0097/1082] Improve formatting of changelog entry Co-authored-by: Myk --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 700a776ed8..9ee8fdb513 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,7 +58,7 @@ Template for new versions: - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S - `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units -- disable-vanilla-dimensions: new binpatch to disable the vanilla dimensions tooltip +- ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip ## Fixes - `createitem`: output items will now end up at look cursor if active From db1039718d5c4a59c9b64fccf3add2e766d416c8 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 21:31:47 -0500 Subject: [PATCH 0098/1082] Correct path to not create "hack/patches/patches" --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d73cd996b..e133c87acb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,7 +451,7 @@ if(INSTALL_DATA_FILES) install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/depends/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches DESTINATION ${DFHACK_DATA_DESTINATION}/patches) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ DESTINATION ${DFHACK_DATA_DESTINATION}/patches) endif() if(INSTALL_SCRIPTS) From c1380835b70397a5eb05bab9a95a4b6872de94cc Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 16 Feb 2025 21:55:37 +0000 Subject: [PATCH 0099/1082] 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 17c6be1669..f312d35324 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 17c6be1669b2bce22864892892722c625e1d66e3 +Subproject commit f312d353240f45922084f901ade84f7479fde53d diff --git a/scripts b/scripts index 4af65c03bf..654720a2d6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4af65c03bf6f0028e5f9e03af125bee0266688b0 +Subproject commit 654720a2d608dfd26a9596182a70ca1ad1ea3053 From bed64583b05293452af78dc3c335b2225ad91666 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 16 Feb 2025 19:05:21 -0600 Subject: [PATCH 0100/1082] fix bad logic in windows console handler prompt refresh logic would underflow and infinite-loop if window size was smaller than the prompt size replaced with direct math instead of while loops --- library/Console-windows.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 85934b2f37..83ed0f64ee 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -36,6 +36,7 @@ POSSIBILITY OF SUCH DAMAGE. */ +#define NOMINMAX #include #include #include @@ -232,16 +233,13 @@ namespace DFHack size_t len = raw_buffer.size(); int cooked_cursor = raw_cursor; - while ((plen + cooked_cursor) >= cols) - { - buf++; - len--; - cooked_cursor--; - } - while (plen + len > cols) - { - len--; - } + int adj = std::min(plen + cooked_cursor - cols, len); + buf += adj; + len -= adj; + cooked_cursor -= adj; + + int adj2 = std::min(plen + len - cols, len); + len -= adj2; CONSOLE_SCREEN_BUFFER_INFO inf = { 0 }; GetConsoleScreenBufferInfo(console_out, &inf); From d7100be8f73dbc5e1255d2e585b574f59e84f15d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 16 Feb 2025 19:07:03 -0600 Subject: [PATCH 0101/1082] add changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 20d11103bc..ad953a7e81 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: - `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units ## Fixes +- Windows console: fix possible hang if the console returns a too-small window width (for any reason) - `createitem`: output items will now end up at look cursor if active - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself From 6011cb375e769e3bb317ec0a2f7f98500fe63339 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 17 Feb 2025 20:51:22 +0100 Subject: [PATCH 0102/1082] Update docs/plugins/spectate.rst Co-authored-by: Myk --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 8cd40276b6..8eb38729e9 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -105,7 +105,7 @@ Settings ``tooltip-follow`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the - tooltip. + tooltips that follow onscreen dwarves around the map. ``tooltip-follow-blink-milliseconds`` (default: 3000) If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration From 594e1debb8eae45be4b7939d6b77bbc8417e2c5e Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 17 Feb 2025 20:51:37 +0100 Subject: [PATCH 0103/1082] Update docs/plugins/spectate.rst Co-authored-by: Myk --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 8eb38729e9..5a9ba0b2e5 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -134,7 +134,7 @@ Settings ``tooltip-hover`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the - hover panel. + hover popup panel when your mouse cursor is over a unit. ``tooltip-hover-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the From 3093a3f59d5796e7e1fd6a7bc4d173c770fa0185 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 17 Feb 2025 22:10:34 +0100 Subject: [PATCH 0104/1082] expand documentation --- docs/plugins/spectate.rst | 54 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 5a9ba0b2e5..69c626d56c 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -63,6 +63,9 @@ Examples Show informative tooltips that follow each unit on the map. Note that this can be enabled independently of `spectate` itself. +``spectate set tooltip-follow-job-shortenings "Store item in stockpile" "Store item"`` + Set a (see ``tooltip-follow-job-shortenings`` for details). + Settings -------- @@ -108,8 +111,8 @@ Settings tooltips that follow onscreen dwarves around the map. ``tooltip-follow-blink-milliseconds`` (default: 3000) - If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration - in milliseconds. Set to 0 to always show. + If the ``spectate.tooltip`` overlay is enabled, set the tooltip's blink + duration in milliseconds. Set to 0 to always show. ``tooltip-follow-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -118,6 +121,7 @@ Settings ``tooltip-follow-job-shortenings`` If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to shorten some job names, f.e. "Store item in stockpile" becomes "Store item". + See examples for an example. ``tooltip-follow-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -130,7 +134,8 @@ Settings ``tooltip-follow-stress-levels`` (default: Displeased, Content, Pleased are disabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the tooltip. F.e. - ``tooltip-follow-stress-levels 2 true`` would show Displeased emoticon. + ``tooltip-follow-stress-levels 2 true`` would show the Displeased emoticon. + See ``tooltip-stress-levels`` for details. ``tooltip-hover`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -151,12 +156,53 @@ Settings ``tooltip-hover-stress-levels`` (default: Displeased, Content, Pleased are disabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the hover panel. F.e. - ``tooltip-hover-stress-levels 2 true`` would show Displeased emoticon. + ``tooltip-hover-stress-levels 2 true`` would show the Displeased emoticon. + See ``tooltip-stress-levels`` for details. ``tooltip-stress-levels`` Controls how happiness levels (stress) are displayed (emoticon and color). F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for Ecstatic dwarves to ``XD``. + Default values are: + +.. list-table:: + :widths: 25 25 25 25 + :header-rows: 1 + + * - Level + - name + - text + - pen + * - 0 + - Miserable + - =C + - COLOR_RED + * - 1 + - Unhappy + - :C + - COLOR_LIGHTRED + * - 2 + - Displeased + - :( + - COLOR_YELLOW + * - 3 + - Content + - :] + - COLOR_GREY + * - 4 + - Pleased + - :) + - COLOR_GREEN + * - 5 + - Happy + - :D + - COLOR_LIGHTGREEN + * - 6 + - Ecstatic + - =D + - COLOR_LIGHTCYAN + +Keep in mind that the text will look different with the game's font. Overlays -------- From 2c87cf82d393c4c8e01eaa4617e496508348382e Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 17 Feb 2025 18:56:50 -0800 Subject: [PATCH 0105/1082] doc clarifications --- docs/plugins/spectate.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 69c626d56c..65d7cefe28 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -63,8 +63,10 @@ Examples Show informative tooltips that follow each unit on the map. Note that this can be enabled independently of `spectate` itself. -``spectate set tooltip-follow-job-shortenings "Store item in stockpile" "Store item"`` - Set a (see ``tooltip-follow-job-shortenings`` for details). +``spectate set tooltip-follow-job-shortenings "Store item in stockpile" "Store"`` + Abbreviate the names of "Store item in stockpile" jobs to just "Store" when the + job is displayed in the `spectate` tooltips. See the + ``tooltip-follow-job-shortenings`` setting below for details. Settings -------- @@ -118,10 +120,11 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. -``tooltip-follow-job-shortenings`` +``tooltip-follow-job-shortenings`` (default: "Store item in stockpile" -> "Store item") If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to shorten some job names, f.e. "Store item in stockpile" becomes "Store item". - See examples for an example. + You can pass two parameters to ``spectate set tooltip-follow-job-shortenings`` to + add or change elements in the dictionary. See the Examples section for an example. ``tooltip-follow-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -135,7 +138,7 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the tooltip. F.e. ``tooltip-follow-stress-levels 2 true`` would show the Displeased emoticon. - See ``tooltip-stress-levels`` for details. + See ``tooltip-stress-levels`` below for details. ``tooltip-hover`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -157,7 +160,7 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the hover panel. F.e. ``tooltip-hover-stress-levels 2 true`` would show the Displeased emoticon. - See ``tooltip-stress-levels`` for details. + See ``tooltip-stress-levels`` below for details. ``tooltip-stress-levels`` Controls how happiness levels (stress) are displayed (emoticon and color). @@ -202,7 +205,7 @@ Settings - =D - COLOR_LIGHTCYAN -Keep in mind that the text will look different with the game's font. +Keep in mind that the text may look different when rendered in the game's font. Overlays -------- From ca3af5e9318838cc6b5ff3b3ded07292926f8aee Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 18 Feb 2025 07:17:09 +0000 Subject: [PATCH 0106/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 654720a2d6..8639a8f17e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 654720a2d608dfd26a9596182a70ca1ad1ea3053 +Subproject commit 8639a8f17e43e0caba48137a407c2ac60abc3d15 From 7b6bfc856a1ed982408aaea23d898b8eac65bb10 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Feb 2025 17:25:31 -0600 Subject: [PATCH 0107/1082] add type identity for `std::filesystem::path` --- library/DataIdentity.cpp | 1 + library/LuaTypes.cpp | 18 ++++++++++++++++++ library/LuaWrapper.cpp | 2 +- library/include/DataIdentity.h | 24 ++++++++++++++++++++++++ library/include/LuaWrapper.h | 2 +- 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index e6b4889cf8..a385915d92 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -38,6 +38,7 @@ namespace df { const bool_identity identity_traits::identity; const stl_string_identity identity_traits::identity; + const path_identity identity_traits::identity; const ptr_string_identity identity_traits::identity; const ptr_string_identity identity_traits::identity; const pointer_identity identity_traits::identity; diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index 1aa7711e68..81914a7185 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -188,6 +188,24 @@ void df::stl_string_identity::lua_write(lua_State *state, int fname_idx, void *p *(std::string*)ptr = std::string(bytes, size); } +void df::path_identity::lua_read(lua_State* state, int fname_idx, void* ptr) const +{ + auto ppath = (std::filesystem::path*)ptr; + auto str = ppath->u8string(); + lua_pushlstring(state, (char*)str.data(), str.size()); +} + +void df::path_identity::lua_write(lua_State* state, int fname_idx, void* ptr, int val_index) const +{ + size_t size; + const char* bytes = lua_tolstring(state, val_index, &size); + if (!bytes) + field_error(state, fname_idx, "path expected", "write"); + + std::u8string str((char8_t*)bytes, size); + *(std::filesystem::path*)ptr = std::filesystem::path(str); +} + void df::pointer_identity::lua_read(lua_State *state, int fname_idx, void *ptr, const type_identity *target) { push_object_internal(state, target, *(void**)ptr); diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 608ef95683..892a6784cf 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -49,7 +49,7 @@ using namespace DFHack::LuaWrapper; /** * Report an error while accessing a field (index = field name). */ -void LuaWrapper::field_error(lua_State *state, int index, const char *err, const char *mode) +[[noreturn]] void LuaWrapper::field_error(lua_State *state, int index, const char *err, const char *mode) { if (lua_islightuserdata(state, UPVAL_METATABLE)) lua_pushstring(state, "(global)"); diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 02bc8b3201..7e1f8eaaea 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -34,6 +34,7 @@ distribution. #include #include #include +#include #include "DataDefs.h" @@ -298,6 +299,24 @@ namespace df virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; }; + class DFHACK_EXPORT path_identity : public DFHack::constructed_identity { + public: + path_identity() + : constructed_identity(sizeof(std::filesystem::path), &allocator_fn) + { + }; + + const std::string getFullName() const { return "path"; } + + virtual DFHack::identity_type type() const { return DFHack::IDTYPE_PRIMITIVE; } + + virtual bool isPrimitive() const { return true; } + + virtual void lua_read(lua_State* state, int fname_idx, void* ptr) const; + virtual void lua_write(lua_State* state, int fname_idx, void* ptr, int val_index) const; + }; + + class DFHACK_EXPORT stl_ptr_vector_identity : public ptr_container_identity { public: typedef std::vector container; @@ -616,6 +635,11 @@ namespace df static const stl_string_identity *get() { return &identity; } }; + template<> struct DFHACK_EXPORT identity_traits { + static const bool is_primitive = true; + static const path_identity identity; + static const path_identity* get() { return &identity; } + }; template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; static const ptr_string_identity identity; diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 347945e896..44bc19938c 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -142,7 +142,7 @@ namespace LuaWrapper { /** * Report an error while accessing a field (index = field name). */ - void field_error(lua_State *state, int index, const char *err, const char *mode); + [[noreturn]] void field_error(lua_State *state, int index, const char *err, const char *mode); /* * If is_method is true, these use UPVAL_TYPETABLE to save a hash lookup. From cb7b4cdc484d2a20c6147c2a415fade252d14122 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Feb 2025 17:26:43 -0600 Subject: [PATCH 0108/1082] really fix windows console --- library/Console-windows.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 83ed0f64ee..12a3e0c2eb 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -233,13 +233,16 @@ namespace DFHack size_t len = raw_buffer.size(); int cooked_cursor = raw_cursor; - int adj = std::min(plen + cooked_cursor - cols, len); - buf += adj; - len -= adj; - cooked_cursor -= adj; + if (plen + cooked_cursor > cols) + { + int adj = std::min(plen + cooked_cursor - cols, len); + buf += adj; + len -= adj; + cooked_cursor -= adj; + } - int adj2 = std::min(plen + len - cols, len); - len -= adj2; + if (len + plen > cols) + len = cols - plen; CONSOLE_SCREEN_BUFFER_INFO inf = { 0 }; GetConsoleScreenBufferInfo(console_out, &inf); From b7424cc5223b7b05a4d815404797c2cd082f48ec Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Feb 2025 14:37:11 -0800 Subject: [PATCH 0109/1082] add spectate features to vanilla follow mode --- docs/changelog.txt | 2 +- docs/plugins/spectate.rst | 24 +++++--- plugins/lua/spectate.lua | 65 ++++++++++++++++++++- plugins/spectate.cpp | 117 +++++++++++++++++++++++--------------- 4 files changed, 150 insertions(+), 58 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index eb90bcf861..3d6da9045c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S -- `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units +- `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) - ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip ## Fixes diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 65d7cefe28..a09d0170b9 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -178,31 +178,31 @@ Settings - pen * - 0 - Miserable - - =C + - ``=C`` - COLOR_RED * - 1 - Unhappy - - :C + - ``:C`` - COLOR_LIGHTRED * - 2 - Displeased - - :( + - ``:(`` - COLOR_YELLOW * - 3 - Content - - :] + - ``:]`` - COLOR_GREY * - 4 - Pleased - - :) + - ``:)`` - COLOR_GREEN * - 5 - Happy - - :D + - ``:D`` - COLOR_LIGHTGREEN * - 6 - Ecstatic - - =D + - ``=D`` - COLOR_LIGHTCYAN Keep in mind that the text may look different when rendered in the game's font. @@ -217,3 +217,13 @@ and/or a popup panel with information when your mouse cursor hovers over a unit. This overlay is managed via the `overlay` framework. It can be controlled via the ``spectate overlay`` command or the ``Overlays`` tab in `gui/control-panel`. + +``spectate.followpanel`` + +This overlay adds widgets to the vanilla follow panel -- the one that appears +in the lower left corner of the screen when you are following a unit. When you +are following a unit, regardless of whether the `spectate` plugin is enabled, +you can use the keyboard cursor left/right keys to switch which unit you are +following. There is also an indicator for whether spectate mode is enabled +(that is, whether the `spectate` plugin is enabled), and there is a button for +launching the `gui/spectate` configuration UI. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 2f8ade03cf..d90eb3e70f 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -2,11 +2,10 @@ local _ENV = mkmodule('plugins.spectate') local argparse = require('argparse') local dlg = require('gui.dialogs') +local gui = require('gui') local json = require('json') local overlay = require('plugins.overlay') local utils = require('utils') - -local gui = require('gui') local widgets = require('gui.widgets') -- settings starting with 'tooltip-' are not passed to the C++ plugin @@ -306,7 +305,7 @@ local function GetHoverText(pos) end ----------------------------- --- overlays +-- TooltipOverlay TooltipOverlay = defclass(TooltipOverlay, overlay.OverlayWidget) TooltipOverlay.ATTRS{ @@ -545,7 +544,67 @@ function MouseTooltip:render(dc) MouseTooltip.super.render(self, dc) end +----------------------------- +-- FollowPanelOverlay + +local plotinfo = df.global.plotinfo +local mi = df.global.game.main_interface + +local function follow_panel_is_visible() + return plotinfo.follow_unit > -1 and + mi.current_hover == -1 and + not mi.hover_instructions_on and + not mi.current_hover_alert +end + +FollowPanelOverlay = defclass(FollowPanelOverlay, overlay.OverlayWidget) +FollowPanelOverlay.ATTRS{ + desc='Adds spectate widgets to the vanilla follow panel.', + default_pos={x=6,y=-5}, + viewscreens='dwarfmode/Default', + default_enabled=true, + frame={w=39, h=1}, + visible=follow_panel_is_visible, +} + +function FollowPanelOverlay:init() + self:addviews{ + widgets.Label{ + frame={l=0, t=0, w=3}, + text=(' %s '):format(string.char(27)), + on_click=spectate_followPrev, + }, + widgets.Label{ + frame={l=5, t=0, w=3}, + text=(' %s '):format(string.char(26)), + on_click=spectate_followNext, + }, + widgets.Label{ + frame={l=10, t=0, w=25}, + text={ + ' spectate mode: ', + {text=function() return isEnabled() and 'enabled ' or 'disabled ' end}, + }, + on_click=function() dfhack.run_command(isEnabled() and 'disable' or 'enable', 'spectate') end, + }, + widgets.ConfigureButton{ + frame={l=36, t=0}, + on_click=function() dfhack.run_script('gui/spectate') end, + } + } +end + +function FollowPanelOverlay:onInput(keys) + if keys.KEYBOARD_CURSOR_LEFT then + spectate_followPrev() + elseif keys.KEYBOARD_CURSOR_RIGHT then + spectate_followNext() + end + return FollowPanelOverlay.super.onInput(self, keys) +end + OVERLAY_WIDGETS = { + followpanel=FollowPanelOverlay, tooltip=TooltipOverlay, } diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index b27db51460..7b2b24f946 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -37,6 +37,7 @@ namespace DFHack { DBG_DECLARE(spectate, event, DebugCategory::LINFO); } +static std::default_random_engine rng; static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; @@ -174,28 +175,27 @@ static class UnitHistory { offset = 0; } - void add(color_ostream &out, df::unit *unit) { + void add_and_follow(color_ostream &out, df::unit *unit) { + // if we're currently following a unit, add it to the history if it's not already there + if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { + DEBUG(cycle,out).print("currently following unit %d that is not in history; adding\n", plotinfo->follow_unit); + update_history(out, plotinfo->follow_unit); + } + int32_t id = unit->id; + update_history(out, id); DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_unit = id; - if (offset > 0) { - DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); - history.resize(history.size() - offset); - offset = 0; - } - history.push_back(id); - if (history.size() > MAX_HISTORY) { - DEBUG(cycle,out).print("history full, truncating\n"); - history.pop_front(); - } - DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); } void scan_back(color_ostream &out) { - if (history.empty() || offset >= history.size()-1) + if (history.empty() || offset >= history.size()-1) { + DEBUG(cycle,out).print("already at beginning of history\n"); return; - int unit_id = history[history.size() - (1 + ++offset)]; + } + ++offset; + int unit_id = get_cur_unit_id(); DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); @@ -209,29 +209,39 @@ static class UnitHistory { return; } - int unit_id = history[history.size() - (1 + --offset)]; + --offset; + int unit_id = get_cur_unit_id(); DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_unit = unit_id; } -} unit_history; -struct forward_back_interceptor : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { - bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default"); - if (is_at_default_view && input->count(df::interface_key::CUSTOM_LEFT)) - unit_history.scan_back(Core::getInstance().getConsole()); - else if (is_at_default_view && input->count(df::interface_key::CUSTOM_RIGHT)) - unit_history.scan_forward(Core::getInstance().getConsole()); - else { - INTERPOSE_NEXT(feed)(input); + int32_t get_cur_unit_id() { + if (offset >= history.size()) + return -1; + return history[history.size() - (1 + offset)]; + } + +private: + void update_history(color_ostream &out, int32_t unit_id) { + if (offset > 0) { + DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + history.resize(history.size() - offset); + offset = 0; + } + if (history.size() && history.back() == unit_id) { + DEBUG(cycle,out).print("unit %d is already current unit; not adding to history\n", unit_id); + } else { + history.push_back(unit_id); + if (history.size() > MAX_HISTORY) { + DEBUG(cycle,out).print("history full, truncating\n"); + history.pop_front(); + } } + DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); } -}; -IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); +} unit_history; ///////////////////////////////////////////////////// // RecentUnits @@ -293,7 +303,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector distribution(config.follow_ms / 2, config.follow_ms / 6); + delay_ms = distribution(rng); + delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); + } + DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); + next_cycle_unpaused_ms = Core::getInstance().getUnpausedMs() + delay_ms; +} + DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); @@ -323,9 +343,11 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { return CR_FAILURE; } INFO(control,out).print("Spectate mode enabled!\n"); - INTERPOSE_HOOK(forward_back_interceptor, feed).apply(); EventManager::registerListener(EventManager::EventType::UNIT_NEW_ACTIVE, new_unit_handler); - follow_a_dwarf(out); + if (plotinfo->follow_unit > -1) + set_next_cycle_unpaused_ms(out); + else + follow_a_dwarf(out); } else { INFO(control,out).print("Spectate mode disabled!\n"); plotinfo->follow_unit = -1; @@ -443,19 +465,6 @@ static void get_dwarf_buckets(color_ostream &out, } } -static std::default_random_engine rng; - -static uint32_t get_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat) { - int32_t delay_ms = config.follow_ms; - if (config.cinematic_action && has_active_combat) { - std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); - delay_ms = distribution(rng); - delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); - } - DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); - return Core::getInstance().getUnpausedMs() + delay_ms; -} - static void add_bucket_to_vectors(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { if (bucket.empty()) return; @@ -505,7 +514,7 @@ static void follow_a_dwarf(color_ostream &out) { vector other_units; get_dwarf_buckets(out, citizen_combat_units, other_combat_units, job_units, other_units); - next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); + set_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); // coalesce the buckets and add weights vector units; @@ -536,7 +545,7 @@ static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); } - unit_history.add(out, unit); + unit_history.add_and_follow(out, unit); } ///////////////////////////////////////////////////// @@ -579,7 +588,21 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { } } +static void spectate_followPrev(color_ostream &out) { + DEBUG(control,out).print("entering spectate_followPrev\n"); + unit_history.scan_back(out); + set_next_cycle_unpaused_ms(out); +}; + +static void spectate_followNext(color_ostream &out) { + DEBUG(control,out).print("entering spectate_followNext\n"); + unit_history.scan_forward(out); + set_next_cycle_unpaused_ms(out); +}; + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(spectate_setSetting), + DFHACK_LUA_FUNCTION(spectate_followPrev), + DFHACK_LUA_FUNCTION(spectate_followNext), DFHACK_LUA_END }; From d32c52bc2c8f4bdd4334c07b7b8a455f089f2c0c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Feb 2025 15:37:02 -0800 Subject: [PATCH 0110/1082] add global keybinding for gui/sitemap --- data/init/dfhack.keybindings.init | 1 + docs/changelog.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index 1fa7b9facc..d7ce5c8662 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -41,6 +41,7 @@ keybinding add Ctrl-A@choose_start_site gui/embark-anywhere # in the game root directory keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeonmode/ViewSheets/UNIT|dungeonmode/ViewSheets/ITEM markdown +keybinding add Ctrl-G@dwarfmode/Default|dungeonmode/Default gui/sitemap ###################### # dwarfmode bindings # diff --git a/docs/changelog.txt b/docs/changelog.txt index 3d6da9045c..166f55c45f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S - `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) - ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip +- `gui/sitemap`: is now the official "go to" tool. new global hotkey for fort and adventure mode: Ctrl-G ## Fixes - Windows console: fix possible hang if the console returns a too-small window width (for any reason) From 7d92f181f3935be0859de0ab926358b9b7e1e85a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Feb 2025 15:37:14 -0800 Subject: [PATCH 0111/1082] add infrastructure for use by gui/sitemap --- plugins/lua/spectate.lua | 9 ++++--- plugins/spectate.cpp | 56 ++++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d90eb3e70f..4b36bee693 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -563,7 +563,7 @@ FollowPanelOverlay.ATTRS{ default_pos={x=6,y=-5}, viewscreens='dwarfmode/Default', default_enabled=true, - frame={w=39, h=1}, + frame={w=29, h=1}, visible=follow_panel_is_visible, } @@ -582,13 +582,14 @@ function FollowPanelOverlay:init() widgets.Label{ frame={l=10, t=0, w=25}, text={ - ' spectate mode: ', - {text=function() return isEnabled() and 'enabled ' or 'disabled ' end}, + ' spectate:', + {text=function() return isEnabled() and ' on ' or 'off ' end, + pen=function() return isEnabled() and COLOR_GREEN or COLOR_LIGHTRED end}, }, on_click=function() dfhack.run_command(isEnabled() and 'disable' or 'enable', 'spectate') end, }, widgets.ConfigureButton{ - frame={l=36, t=0}, + frame={l=26, t=0}, on_click=function() dfhack.run_script('gui/spectate') end, } } diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 7b2b24f946..45512a2042 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -175,17 +175,36 @@ static class UnitHistory { offset = 0; } + void add_to_history(color_ostream &out, int32_t unit_id) { + if (offset > 0) { + DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + history.resize(history.size() - offset); + offset = 0; + } + if (history.size() && history.back() == unit_id) { + DEBUG(cycle,out).print("unit %d is already current unit; not adding to history\n", unit_id); + } else { + history.push_back(unit_id); + if (history.size() > MAX_HISTORY) { + DEBUG(cycle,out).print("history full, truncating\n"); + history.pop_front(); + } + } + DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); + } + void add_and_follow(color_ostream &out, df::unit *unit) { // if we're currently following a unit, add it to the history if it's not already there if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { DEBUG(cycle,out).print("currently following unit %d that is not in history; adding\n", plotinfo->follow_unit); - update_history(out, plotinfo->follow_unit); + add_to_history(out, plotinfo->follow_unit); } int32_t id = unit->id; - update_history(out, id); + add_to_history(out, id); DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; plotinfo->follow_unit = id; } @@ -199,6 +218,7 @@ static class UnitHistory { DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; plotinfo->follow_unit = unit_id; } @@ -214,6 +234,7 @@ static class UnitHistory { DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; plotinfo->follow_unit = unit_id; } @@ -222,25 +243,6 @@ static class UnitHistory { return -1; return history[history.size() - (1 + offset)]; } - -private: - void update_history(color_ostream &out, int32_t unit_id) { - if (offset > 0) { - DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); - history.resize(history.size() - offset); - offset = 0; - } - if (history.size() && history.back() == unit_id) { - DEBUG(cycle,out).print("unit %d is already current unit; not adding to history\n", unit_id); - } else { - history.push_back(unit_id); - if (history.size() > MAX_HISTORY) { - DEBUG(cycle,out).print("history full, truncating\n"); - history.pop_front(); - } - } - DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); - } } unit_history; ///////////////////////////////////////////////////// @@ -392,7 +394,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_onupdate(color_ostream &out) { announcement_settings.on_update(out); - if (config.auto_disengage && (plotinfo->follow_unit < 0 || is_squads_open())) { + if (config.auto_disengage && (plotinfo->follow_unit < 0 || plotinfo->follow_item > -1 || is_squads_open())) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; plotinfo->follow_unit = -1; @@ -600,9 +602,19 @@ static void spectate_followNext(color_ostream &out) { set_next_cycle_unpaused_ms(out); }; +static void spectate_addToHistory(color_ostream &out, int32_t unit_id) { + DEBUG(control,out).print("entering spectate_addToHistory; unit_id=%d\n", unit_id); + if (!df::unit::find(unit_id)) { + WARN(control,out).print("unit with id %d not found; not adding to history\n", unit_id); + return; + } + unit_history.add_to_history(out, unit_id); +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(spectate_setSetting), DFHACK_LUA_FUNCTION(spectate_followPrev), DFHACK_LUA_FUNCTION(spectate_followNext), + DFHACK_LUA_FUNCTION(spectate_addToHistory), DFHACK_LUA_END }; From 872ba28e02e842a6a8fc6409cc4c504fd969f857 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 21 Feb 2025 00:04:04 +0000 Subject: [PATCH 0112/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8639a8f17e..cc421f03ee 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8639a8f17e43e0caba48137a407c2ac60abc3d15 +Subproject commit cc421f03eebed861cdf3a5cd925dd01887373020 From fa911ff0152fb49979ddbf9087c7592f6c3bf0e4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Feb 2025 09:52:39 -0800 Subject: [PATCH 0113/1082] exclude inactive units from inBox functions --- docs/changelog.txt | 1 + library/modules/Units.cpp | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 166f55c45f..7954d9c6a0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -78,6 +78,7 @@ Template for new versions: ## API - ``Military::removeFromSquad``: removes unit from any squad assignments - ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``Units::isUnitInBox``, ``Units::getUnitsInBox``: don't include inactive units ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 1af93c3b7d..21f39932fe 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -665,6 +665,8 @@ bool Units::isGreatDanger(df::unit *unit) { bool Units::isUnitInBox(df::unit *u, const cuboid &box) { CHECK_NULL_POINTER(u); + if (!isActive(u)) + return false; return box.containsPos(getPosition(u)); } @@ -674,7 +676,7 @@ bool Units::getUnitsInBox(vector &units, const cuboid &box, std::fun units.clear(); for (auto unit : world->units.active) - if (filter(unit) && isUnitInBox(unit, box)) + if (isUnitInBox(unit, box) && filter(unit)) units.push_back(unit); return true; } From ea65827025ac39ad02163c34ae55dcfbdf47fd3d Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Fri, 21 Feb 2025 21:04:49 +0100 Subject: [PATCH 0114/1082] spectate.lua: make config public Also improve error messages when missing command arguments --- plugins/lua/spectate.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 4b36bee693..0bd2d66920 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -84,7 +84,8 @@ local function load_state() function() config_file:write() end end -local config, save_state = load_state() +-- global variables to make the UI script simpler +_ENV.config, _ENV.save_state = load_state() function refresh_cpp_config() for name,value in pairs(config) do @@ -158,11 +159,15 @@ local function do_toggle() end local function set_setting(args) + local n = #args + if n == 0 then + qerror('missing key') + end local key = table.remove(args, 1) if config[key] == nil then qerror('unknown setting: ' .. key) end - local n = #args + n = #args if n == 0 then qerror('missing value') end @@ -221,6 +226,7 @@ function parse_commandline(args) elseif command == 'set' then set_setting(args) elseif command == 'overlay' then + if #args == 0 then qerror('missing option') end set_overlay(args[1]) else return false From 20f7fd346aaa63c97b02b47777fe9613f2dc22c7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Feb 2025 17:18:54 -0600 Subject: [PATCH 0115/1082] flush cache after mempatch This adds code to flush cache after applying a hotpatch. While it's unlikely that a patched location will be in cache at the time we apply a patch, it is better to be safe than sorry Also fixed the spelling error in `setPermissions` while I was here --- library/Core.cpp | 6 ++++-- library/Process-linux.cpp | 9 ++++++++- library/Process-windows.cpp | 7 ++++++- library/include/MemAccess.h | 5 ++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 9dc2d2e68a..be53fe58fd 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2987,7 +2987,7 @@ bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write) save.push_back(perms); perms.write = perms.read = true; - if (!p->setPermisions(perms, perms)) + if (!p->setPermissions(perms, perms)) return false; } @@ -3000,13 +3000,15 @@ bool MemoryPatcher::write(void *target, const void *src, size_t size) return false; memmove(target, src, size); + + p->flushCache(target, size); return true; } void MemoryPatcher::close() { for (size_t i = 0; i < save.size(); i++) - p->setPermisions(save[i], save[i]); + p->setPermissions(save[i], save[i]); save.clear(); ranges.clear(); diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 85e83f54f6..321db80550 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -32,6 +32,7 @@ distribution. #include #include +#include #include #include #include @@ -205,7 +206,7 @@ int Process::getPID() return getpid(); } -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) { int result; int protect=0; @@ -217,6 +218,12 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) return result==0; } +bool Process::flushCache(const void* target, size_t count) +{ + return cacheflush(target, count, BCACHE); +} + + // returns -1 on error void* Process::memAlloc(const int length) { diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index 2450eb87bd..0098c8c167 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -399,7 +399,7 @@ int Process::getPID() } -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) { DWORD newprotect=0; if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; @@ -414,6 +414,11 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) return result; } +bool Process::flushCache(const void* target, size_t count) +{ + return 0 == FlushInstructionCache(d->my_handle, (LPCVOID)target, count); +} + void* Process::memAlloc(const int length) { void *ret; diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 083d94434c..bcbab75bd3 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -270,11 +270,14 @@ namespace DFHack uint32_t getTickCount(); /// modify permisions of memory range - bool setPermisions(const t_memrange & range,const t_memrange &trgrange); + bool setPermissions(const t_memrange & range,const t_memrange &trgrange); /// write a possibly read-only memory area bool patchMemory(void *target, const void* src, size_t count); + /// flush cache + bool flushCache(const void* target, size_t count); + /// allocate new memory pages for code or stuff /// returns -1 on error (0 is a valid address) void* memAlloc(const int length); From 3b0d1ea40817e72ebf1ce3ea0728630f7eca6385 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Feb 2025 17:32:08 -0600 Subject: [PATCH 0116/1082] use __builtin___clear_cache on linux apparently the documentation i found was... incomplete --- library/Process-linux.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 321db80550..1583b9d619 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -32,7 +32,6 @@ distribution. #include #include -#include #include #include #include @@ -220,7 +219,7 @@ bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange bool Process::flushCache(const void* target, size_t count) { - return cacheflush(target, count, BCACHE); + __builtin___clear_cache((char*)target, (char*)target + count - 1); } From a78f2a70e5e89a95a9569233cbbead221e19a500 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Feb 2025 17:37:05 -0600 Subject: [PATCH 0117/1082] assume the intrinsic always succeeds --- library/Process-linux.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 1583b9d619..5e2458ec47 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -220,6 +220,7 @@ bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange bool Process::flushCache(const void* target, size_t count) { __builtin___clear_cache((char*)target, (char*)target + count - 1); + return false; /* assume always succeeds, as the builtin has no return type */ } From 970bfc6e5ba9a4f42278b27055508b4f918e4aa7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 22 Feb 2025 07:15:20 +0000 Subject: [PATCH 0118/1082] 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 f312d35324..7badeff963 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f312d353240f45922084f901ade84f7479fde53d +Subproject commit 7badeff963b0494e2e79e49a0dd3555961df5f04 From acd6ec97fd359969369c37ba8c4b9855eb0bc9e2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Feb 2025 23:22:12 -0800 Subject: [PATCH 0119/1082] actually disable timestream when unloading world --- plugins/timestream.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index ea75ced359..eeb4d1115c 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -157,6 +157,7 @@ static void reset_ephemeral_state() { static void do_disable() { EventManager::unregisterAll(plugin_self); + is_enabled = false; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { From 0676850f670ee0a3d0673f8a684087478b1b88da Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Feb 2025 23:58:52 -0800 Subject: [PATCH 0120/1082] add API for querying whether an overlay is enabled --- docs/changelog.txt | 1 + docs/dev/overlay-dev-guide.rst | 17 +++++++++++++++++ plugins/lua/overlay.lua | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7954d9c6a0..7c2b587bda 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -85,6 +85,7 @@ Template for new versions: - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons - ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``overlay.isOverlayEnabled``: new API for querying whether a given overlay is enabled ## Removed diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 7b8198c78a..4302d5d4c8 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -228,6 +228,23 @@ If you need to improve performance, here are some potential options: 3. Move hotspots into C++ code, either in a new core library function or in a dedicated plugin +Overlay framework API +--------------------- + +The overlay plugin Lua interface provides a few functions for interacting with +the framework. You can get a reference to the API via:: + + local overlay = require('plugins.overlay') + +* ``overlay.rescan()`` + + Rescans all module-loadable Lua scripts for registered overlays and loads + updated widget definitions. + +* ``overlay.isOverlayEnabled(name)`` + + Returns whether the overlay with the given name is enabled. + Development workflows --------------------- diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 8d7c9a7e2d..77df7d47a4 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -24,6 +24,7 @@ local overlay_config = {} -- map of widget name to persisted state local active_hotspot_widgets = {} -- map of widget names to the db entry local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db +-- for use by gui/overlay function get_state() return {index=widget_index, config=overlay_config, db=widget_db} end @@ -73,6 +74,11 @@ local function save_config() end end +function isOverlayEnabled(name) + if not overlay_config[name] then return false end + return overlay_config[name].enabled +end + -- ----------- -- -- utility fns -- -- ----------- -- From 7cf972279bab990bf549c36fc05775c9bb8c3d77 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 22 Feb 2025 00:22:59 -0800 Subject: [PATCH 0121/1082] add state query function intending to modify state via the public API so it gets synced to cpp --- plugins/lua/spectate.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 0bd2d66920..d7a2081802 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -84,8 +84,18 @@ local function load_state() function() config_file:write() end end --- global variables to make the UI script simpler -_ENV.config, _ENV.save_state = load_state() +local config, save_state = load_state() + +-- called by gui/spectate +function get_config_elem(name, key) + local elem = config[name] + if not elem then return end + if type(elem) == 'table' then + return elem[key] + end + return elem +end + function refresh_cpp_config() for name,value in pairs(config) do From c0252824359d27cd230147a21c9b111eb477a2d7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 00:25:50 -0800 Subject: [PATCH 0122/1082] improve error message when boolean arg is missing --- library/lua/argparse.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index a76e9d3c6d..e5337afa11 100644 --- a/library/lua/argparse.lua +++ b/library/lua/argparse.lua @@ -244,6 +244,9 @@ local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true,["ena ---@param arg_name? string ---@return boolean function boolean(arg, arg_name) + if arg == nil then + arg_error(arg_name, 'missing value; expected "true", "yes", "false", or "no"') + end local arg_lower = string.lower(arg) if toBool[arg_lower] == nil then arg_error(arg_name, From 5e835fa27ca881bdef47229cefca0c192cef1544 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 00:38:20 -0800 Subject: [PATCH 0123/1082] fix typo --- plugins/lua/spectate.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d7a2081802..b50ab399dc 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -96,7 +96,6 @@ function get_config_elem(name, key) return elem end - function refresh_cpp_config() for name,value in pairs(config) do if not name:startswith(lua_only_settings_prefix) then From 2b7b1b1a25d90a8a5256924c55792bd34fcbb067 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 02:37:29 -0800 Subject: [PATCH 0124/1082] get rid of auto-disengage option and make it always on otherwise the UI can fight with the player --- docs/plugins/spectate.rst | 10 ++-------- plugins/lua/spectate.lua | 13 ++++--------- plugins/spectate.cpp | 14 +++++--------- scripts | 2 +- 4 files changed, 12 insertions(+), 27 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index a09d0170b9..89aef2b9f4 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -13,7 +13,7 @@ to following a different dwarf. It can also switch to following animals, hostiles, or visiting units. You can switch to the next target (or a previous target) immediately with the left/right arrow keys. -By default, `spectate` will disengage and turn itself off when you move the +`spectate` will automatically disengage and turn itself off when you move the map, just like the vanilla follow mechanic. It will also disengage immediately if you open the squads menu for military action. @@ -36,7 +36,7 @@ Usage enable spectate spectate [status] spectate toggle - spectate set + spectate set [] spectate overlay enable|disable Examples @@ -71,12 +71,6 @@ Examples Settings -------- -``auto-disengage`` (default: enabled) - Toggle automatically disabling the plugin when the player moves the map or - opens the squad panel. If this is disabled, you will need to manually - disable the plugin to turn off follow mode. You can still interact normally - with the DF UI. - ``auto-unpause`` (default: disabled) Toggle auto-dismissal of announcements that pause the game, like sieges, forgotten beasts, etc. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index b50ab399dc..d9e5f5e566 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -16,7 +16,6 @@ local max_banner_y_offset = 4 local function get_default_state() return { - ['auto-disengage']=true, ['auto-unpause']=false, ['cinematic-action']=true, ['follow-seconds']=10, @@ -89,7 +88,7 @@ local config, save_state = load_state() -- called by gui/spectate function get_config_elem(name, key) local elem = config[name] - if not elem then return end + if elem == nil then return end if type(elem) == 'table' then return elem[key] end @@ -109,14 +108,10 @@ end function show_squads_warning() local message = { - 'Cannot start spectate mode while auto-disengage is enabled and', - 'the squads panel is open. The auto-disengage feature automatically', - 'stops spectate mode when you open the squads panel.', + 'Cannot start spectate mode while the squads panel is open. Spectate', + 'automatically disengages when you open the squads panel.', '', - 'Please either close the squads panel or disable auto-disengage by', - 'running the following command:', - '', - 'spectate set auto-disengage false', + 'Please close the squads panel before enabling spectate mode.', } dlg.showMessage("Spectate", table.concat(message, '\n')) end diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 45512a2042..517d8a29db 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -63,7 +63,6 @@ static const std::unordered_set boring_jobs = { // Configuration static struct Configuration { - bool auto_disengage; bool auto_unpause; bool cinematic_action; bool include_animals; @@ -75,7 +74,6 @@ static struct Configuration { int32_t follow_ms; void reset() { - auto_disengage = true; auto_unpause = false; cinematic_action = true; include_animals = false; @@ -338,8 +336,8 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "refresh_cpp_config")) { WARN(control,out).print("Failed to refresh config\n"); } - if (config.auto_disengage && is_squads_open()) { - out.printerr("Cannot enable %s while auto-disengage is enabled and the squads screen is open.\n", plugin_name); + if (is_squads_open()) { + out.printerr("Cannot enable %s while the squads screen is open.\n", plugin_name); Lua::CallLuaModuleFunction(out, "plugins.spectate", "show_squads_warning"); is_enabled = false; return CR_FAILURE; @@ -394,7 +392,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_onupdate(color_ostream &out) { announcement_settings.on_update(out); - if (config.auto_disengage && (plotinfo->follow_unit < 0 || plotinfo->follow_item > -1 || is_squads_open())) { + if (plotinfo->follow_unit < 0 || plotinfo->follow_item > -1 || is_squads_open()) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; plotinfo->follow_unit = -1; @@ -402,7 +400,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } - if ((!config.auto_disengage && plotinfo->follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { + if (Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { recent_units.trim(); follow_a_dwarf(out); } @@ -556,9 +554,7 @@ static void follow_a_dwarf(color_ostream &out) { static void spectate_setSetting(color_ostream &out, string name, int val) { DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); - if (name == "auto-disengage") { - config.auto_disengage = val; - } else if (name == "auto-unpause") { + if (name == "auto-unpause") { if (val && !config.auto_unpause) { announcement_settings.save_and_scrub_settings(out); } else if (!val && config.auto_unpause) { diff --git a/scripts b/scripts index cc421f03ee..637eb47ca9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit cc421f03eebed861cdf3a5cd925dd01887373020 +Subproject commit 637eb47ca98e9188fe7cdd73c73b0d994c4bada8 From a4d2e0809f516ae31553c197629d58de6dd98675 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 22 Feb 2025 10:55:34 +0000 Subject: [PATCH 0125/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 637eb47ca9..bbf75a749c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 637eb47ca98e9188fe7cdd73c73b0d994c4bada8 +Subproject commit bbf75a749c510a7baf5bafbcd4bb8f2bfc4fa46d From 83dbb4dd3fdd15b47d3f14055f44188720d81632 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 03:25:48 -0800 Subject: [PATCH 0126/1082] remove leftover debug log --- plugins/spectate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 517d8a29db..54bd4944a0 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -452,7 +452,7 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - INFO(cycle).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + TRACE(cycle,out).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) citizen_combat_units.push_back(unit); else From c9a7537ecc6a1f6684942832dba12dfb6e8628e5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 03:53:59 -0800 Subject: [PATCH 0127/1082] don't modify state with bad coords --- library/modules/Gui.cpp | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 6b1976d34f..ff635ac418 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2831,33 +2831,31 @@ bool Gui::revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center, boo unfollow(); + if (!Maps::isValidTilePos(x, y, z)) + return false; + auto dims = getDwarfmodeViewDims(); int32_t w = dims.map_x2 - dims.map_x1 + 1; int32_t h = dims.map_y2 - dims.map_y1 + 1; int32_t new_win_x, new_win_y, new_win_z; getViewCoords(new_win_x, new_win_y, new_win_z); - if (Maps::isValidTilePos(x, y, z)) - { - if (center) - { - new_win_x = x - w / 2; - new_win_y = y - h / 2; - } - else // just bring it on screen - { - if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" - new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; - if (new_win_y > (y - 5)) - new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; - if (new_win_x < (x + 5 - w)) - new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; - if (new_win_y < (y + 5 - h)) - new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; - } - - new_win_z = z; - } + if (center) { + new_win_x = x - w / 2; + new_win_y = y - h / 2; + } else { + // just bring it on screen + if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" + new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; + if (new_win_y > (y - 5)) + new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; + if (new_win_x < (x + 5 - w)) + new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; + if (new_win_y < (y + 5 - h)) + new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; + } + + new_win_z = z; *window_x = new_win_x; *window_y = new_win_y; From 4c5a461759fddd10a2bb2184c5c3f95f245abf99 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 03:54:16 -0800 Subject: [PATCH 0128/1082] ensure highlight appears when scanning through history --- plugins/lua/spectate.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d9e5f5e566..c0858ee536 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -608,8 +608,10 @@ end function FollowPanelOverlay:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then spectate_followPrev() + return true elseif keys.KEYBOARD_CURSOR_RIGHT then spectate_followNext() + return true end return FollowPanelOverlay.super.onInput(self, keys) end From d5167e19daac69ce039ae3db5e2bbf79dc5c4aae Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Feb 2025 14:12:15 -0600 Subject: [PATCH 0129/1082] switch `flushCache` to return `true` on success --- library/Process-linux.cpp | 2 +- library/Process-windows.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 5e2458ec47..a3fa9f2d67 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -220,7 +220,7 @@ bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange bool Process::flushCache(const void* target, size_t count) { __builtin___clear_cache((char*)target, (char*)target + count - 1); - return false; /* assume always succeeds, as the builtin has no return type */ + return true; /* assume always succeeds, as the builtin has no return type */ } diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index 0098c8c167..79bbea8002 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -416,7 +416,7 @@ bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange bool Process::flushCache(const void* target, size_t count) { - return 0 == FlushInstructionCache(d->my_handle, (LPCVOID)target, count); + return 0 != FlushInstructionCache(d->my_handle, (LPCVOID)target, count); } void* Process::memAlloc(const int length) From 0d4912f3c2679fd02a319ed51ec277b649879370 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:57:14 +0000 Subject: [PATCH 0130/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index bbf75a749c..03fc4b2348 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit bbf75a749c510a7baf5bafbcd4bb8f2bfc4fa46d +Subproject commit 03fc4b2348e71b929a90cff429be151e9a7c1b49 From 65595afe4a580220d5e1832fa1b729073d034907 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 23 Feb 2025 07:15:49 +0000 Subject: [PATCH 0131/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 03fc4b2348..557ac5732e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 03fc4b2348e71b929a90cff429be151e9a7c1b49 +Subproject commit 557ac5732e87d62f3b7970295322ae0c5f7eae2c From 941c2762a52c779e1238487b3a52abf929311c11 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 23 Feb 2025 09:20:07 +0100 Subject: [PATCH 0132/1082] spectate.lua: prevent users from destroying their config --- plugins/lua/spectate.lua | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index c0858ee536..b3b81ef79a 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -172,7 +172,9 @@ local function set_setting(args) qerror('unknown setting: ' .. key) end n = #args - if n == 0 then + if n == 0 + or (n == 1 and type(config[key]) == 'table') + then qerror('missing value') end @@ -195,21 +197,30 @@ local function set_setting(args) spectate_setSetting(key, value) end else + local errorUnknownSettingIfNil = function(t) + if t == nil then + table.remove(args) + qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) + end + end + local t = config[key] for i = 1, n - 2 do + errorUnknownSettingIfNil(t) t = t[args[i]] end local k = args[n-1] local v = args[n] if key ~= 'tooltip-follow-job-shortenings' then -- user should be able to add new shortenings, but not other things - if t[k] == nil then - table.remove(args) - qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) - elseif key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then + errorUnknownSettingIfNil(t[k]) + if key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then v = argparse.boolean(v, key .. '/' .. k) end end + if type(t[k]) == 'table' then + qerror('missing value') + end t[k] = v end From d05b2df74862d3eae54552f29ec5f5c537820768 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 23 Feb 2025 13:33:36 -0600 Subject: [PATCH 0133/1082] Fix spectate.lua (#5300) fix the follow panel hitbox --- 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 b3b81ef79a..dbe80ac958 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -601,7 +601,7 @@ function FollowPanelOverlay:init() on_click=spectate_followNext, }, widgets.Label{ - frame={l=10, t=0, w=25}, + frame={l=10, t=0, w=14}, text={ ' spectate:', {text=function() return isEnabled() and ' on ' or 'off ' end, From a7a55f4b0eb3765296ea0dbe67c51bd650d5d9bf Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 23 Feb 2025 23:10:33 +0100 Subject: [PATCH 0134/1082] spectate.lua: refactor `set_setting()` --- plugins/lua/spectate.lua | 97 +++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index dbe80ac958..d70efc6238 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -167,61 +167,64 @@ local function set_setting(args) if n == 0 then qerror('missing key') end - local key = table.remove(args, 1) - if config[key] == nil then - qerror('unknown setting: ' .. key) - end - n = #args - if n == 0 - or (n == 1 and type(config[key]) == 'table') - then - qerror('missing value') - end - if n == 1 then - local value = args[1] - if key == 'follow-seconds' then - value = argparse.positiveInt(value, 'follow-seconds') - elseif key == 'tooltip-follow-blink-milliseconds' then - value = argparse.nonnegativeInt(value, 'tooltip-follow-blink-milliseconds') + local cfg = config + local v + for i = 1, n do + v = cfg[args[i]] + if v == nil then + -- probably an unknown option, but we may allow adding new keys + break + elseif type(v) == 'table' then + if i == n then + -- arrived at the very last argument, but have a table + qerror('missing value for ' .. table.concat(args, '/', 1, i)) + end + cfg = v else - value = argparse.boolean(value, key) - end - - config[key] = value - - if not key:startswith(lua_only_settings_prefix) then - if type(value) == 'boolean' then - value = value and 1 or 0 + -- arrived at something that's not a table + if i == n-1 then + -- if there is exactly 1 argument left, we're good + break + elseif i == n then + qerror('missing value for ' .. table.concat(args, '/', 1, i)) + else -- i < n-1 then + qerror('too many arguments for ' .. table.concat(args, '/', 1, i)) end - spectate_setSetting(key, value) end - else - local errorUnknownSettingIfNil = function(t) - if t == nil then - table.remove(args) - qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) - end + end + if v == nil then + if n == 3 and args[1] == 'tooltip-follow-job-shortenings' then + -- user should be able to add new shortenings, but not other things + else + qerror('unknown option: ' .. table.concat(args, '/', 1, i)) end + end - local t = config[key] - for i = 1, n - 2 do - errorUnknownSettingIfNil(t) - t = t[args[i]] - end - local k = args[n-1] - local v = args[n] - if key ~= 'tooltip-follow-job-shortenings' then - -- user should be able to add new shortenings, but not other things - errorUnknownSettingIfNil(t[k]) - if key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then - v = argparse.boolean(v, key .. '/' .. k) - end + local path = table.concat(args, '/', 1, n-1) + local key = args[n-1] + local value = args[n] + local entry_type = type(cfg[key]) + if entry_type == 'table' then + -- here just in case, is already checked in the loop above + qerror('missing value for ' .. path) + elseif entry_type == 'boolean' then + value = argparse.boolean(value, path) + elseif entry_type == 'number' then + if path == 'follow-seconds' then + value = argparse.positiveInt(value, path) + else + value = argparse.nonnegativeInt(value, path) end - if type(t[k]) == 'table' then - qerror('missing value') + end + + cfg[key] = value + + if n == 2 and not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 end - t[k] = v + spectate_setSetting(key, value) end save_state() From 7652b346370b1c167111af4eae18ea9ca9681181 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 26 Feb 2025 07:17:39 +0000 Subject: [PATCH 0135/1082] 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 7badeff963..af8cf85f0c 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 7badeff963b0494e2e79e49a0dd3555961df5f04 +Subproject commit af8cf85f0c2a772439a441120eac246123afdc6a From e71649449edb2b470cf15199d126db44233ff42f Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Wed, 26 Feb 2025 17:14:47 +0100 Subject: [PATCH 0136/1082] spectate.lua: minor simplification of `render_unit_banners()` --- plugins/lua/spectate.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d70efc6238..e255989117 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -482,10 +482,10 @@ function TooltipOverlay:render_unit_banners(dc) for _, tok in ipairs(info) do local s if type(tok) == "string" then - writer = writer:pen(pen) + writer:pen(pen) s = tok else - writer = writer:pen(tok.pen) + writer:pen(tok.pen) s = tok.text end @@ -496,14 +496,15 @@ function TooltipOverlay:render_unit_banners(dc) -- we want to replace it with an `_`, so we need another `- 1` s = s:sub(1, usedAt - len - ix - 1 - 1) .. '_' - writer = writer:string(s) + writer:string(s) break -- nothing more will fit else - writer = writer:string(s) + writer:string(s) end ix = ix + len end + writer:pen(pen) -- just in case a different dc isn't resetting it -- mark coordinates as used for j = 0, #str - 1 do From 28cb1a0d19cce49b62b1862b9a1b169fd058fd46 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Wed, 26 Feb 2025 17:17:31 +0100 Subject: [PATCH 0137/1082] Update spectate.lua --- plugins/lua/spectate.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index e255989117..7be670802f 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -504,7 +504,6 @@ function TooltipOverlay:render_unit_banners(dc) ix = ix + len end - writer:pen(pen) -- just in case a different dc isn't resetting it -- mark coordinates as used for j = 0, #str - 1 do From c96f1080f3ceb94af14afa94b0591f29d674abf3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 26 Feb 2025 11:46:35 -0600 Subject: [PATCH 0138/1082] add `std::abort` falllthrough to `field_error` this should not be required, but `luaL_error` is not marked `[[noreturn]]` even though it reliably does not return, and i don't want to alter the lua module any more than absolutely necessary, so we do this here instead --- library/LuaWrapper.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 892a6784cf..f9fd0e7832 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -59,6 +59,7 @@ using namespace DFHack::LuaWrapper; const char *fname = index ? lua_tostring(state, index) : "*"; luaL_error(state, "Cannot %s field %s.%s: %s.", mode, (cname ? cname : "?"), (fname ? fname : "?"), err); + std::abort(); // should never be reached but makes gcc happy } /* */ From f3e3570b990b6298fc760802434aa1f5484483c4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Feb 2025 11:00:42 -0800 Subject: [PATCH 0139/1082] add experimental as a symbols target --- .github/workflows/generate-symbols.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 85a0067c85..9f4e32b146 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -36,6 +36,7 @@ on: default: default options: - default + - experimental - testing - adventure_test - beta From 4de298941b2904cee57e590212313951bcabbeca Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Feb 2025 12:27:00 -0800 Subject: [PATCH 0140/1082] turn on autopush for df experimental branch --- .github/workflows/watch-df-release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index 6e72333034..77f4947781 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -15,13 +15,17 @@ jobs: fail-fast: false matrix: # df_steam_branch: which DF Steam branch to watch - # platform: leave blank to default to all + # platform: for symbols generation; leave blank to default to all # structures_ref: leave blank to default to master # dfhack_ref: leave blank if no structures update is desired # steam_branch: leave blank if no DFHack steam push is desired include: - df_steam_branch: public - df_steam_branch: beta + - df_steam_branch: experimental + structures_ref: experimental + dfhack_ref: experimental + steam_branch: experimental steps: - name: Fetch state uses: actions/cache/restore@v4 From 15b72512252d00eee2c22b26a73ce0394f5095c7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Feb 2025 18:35:54 -0800 Subject: [PATCH 0141/1082] update xml ref --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 36117efc6b..79154f3e8f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 36117efc6b233ade089dcf053fe996a04e06e7ef +Subproject commit 79154f3e8f7699f1630c9b9601ebd22f96116024 From 66f58df8fad5b7fc38e6d2a6ddb3a167560e4bcf Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:17:49 +0000 Subject: [PATCH 0142/1082] 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 79154f3e8f..f5ca9135f0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 79154f3e8f7699f1630c9b9601ebd22f96116024 +Subproject commit f5ca9135f09d0b13a23e5394b563609417d568c5 From 12cb12506d0857fc1dda0ffaf8e78352e69fa56b Mon Sep 17 00:00:00 2001 From: Quietust Date: Fri, 28 Feb 2025 10:30:48 -0600 Subject: [PATCH 0143/1082] Update item value functions. Fixes #5313 --- docs/changelog.txt | 2 ++ library/modules/Items.cpp | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1995e9d1d3..612d5f9f70 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -79,6 +79,8 @@ Template for new versions: - ``Military::removeFromSquad``: removes unit from any squad assignments - ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer - ``Units::isUnitInBox``, ``Units::getUnitsInBox``: don't include inactive units +- ``Items::getItemBaseValue``: DF 51.06 reduced the base value of prepared meals +- ``Items::getValue``: magical powers now correctly contribute to item value ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index a99c2bb761..ff92b3cece 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -64,6 +64,7 @@ distribution. #include "df/historical_entity.h" #include "df/item.h" #include "df/item_bookst.h" +#include "df/item_magicalst.h" #include "df/item_plant_growthst.h" #include "df/item_toolst.h" #include "df/item_type.h" @@ -1238,7 +1239,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, value = craw->misc.petvalue; return value; case FOOD: - return 10; + return 1; case CORPSE: case CORPSEPIECE: case REMAINS: @@ -1645,7 +1646,10 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { case 3: value = value / 4; break; // (XX) tattered } - // Ignore value bonuses from magic, since that never actually happens + // Magical powers have 500 value each + auto magic = item->getMagic(); + if (magic != NULL) + value += magic->power.size() * 500; // Artifacts have 10x value if (item->flags.bits.artifact_mood) From 88f23737dfcd9a11d12f9ff977f22f7d3529d393 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 10:10:10 -0800 Subject: [PATCH 0144/1082] remove now-unused binpatches but update docs and keep structure for future binpatches --- CMakeLists.txt | 1 - data/CMakeLists.txt | 4 ++++ data/patches/README.md | 13 +++++++++++++ docs/changelog.txt | 1 - docs/dev/Binpatches.rst | 14 ++++++++++++-- patches/CMakeLists.txt | 3 --- .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ 12 files changed, 29 insertions(+), 43 deletions(-) create mode 100644 data/patches/README.md delete mode 100644 patches/CMakeLists.txt delete mode 100644 patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e927a295d..f102c35554 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,7 +451,6 @@ if(INSTALL_DATA_FILES) install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/depends/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ DESTINATION ${DFHACK_DATA_DESTINATION}/patches) endif() if(INSTALL_SCRIPTS) diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 355182bfb7..75146071b8 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -24,6 +24,10 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/ FILES_MATCHING PATTERN "*" PATTERN blueprints/test EXCLUDE) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ + DESTINATION ${DFHACK_DATA_DESTINATION}/patches +) + if(BUILD_TESTS) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/test/ DESTINATION "${DFHACK_DATA_DESTINATION}/data/blueprints/test") diff --git a/data/patches/README.md b/data/patches/README.md new file mode 100644 index 0000000000..599bbf47fb --- /dev/null +++ b/data/patches/README.md @@ -0,0 +1,13 @@ +Place IDA-exported `.dif` files for use by `binpatch` in subdirectories of this +directory. Each `.dif` file must be in a subdirectory named after the full +symbol table version string. For example, for DF version 51.05, you would use +these subdirectories: + +- "v0.51.05 linux64 CLASSIC" +- "v0.51.05 linux64 ITCH" +- "v0.51.05 linux64 STEAM" +- "v0.51.05 win64 CLASSIC" +- "v0.51.05 win64 ITCH" +- "v0.51.05 win64 STEAM" + +See https://docs.dfhack.org/en/stable/docs/dev/Binpatches.html for more details. diff --git a/docs/changelog.txt b/docs/changelog.txt index 612d5f9f70..f67755852f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,7 +58,6 @@ Template for new versions: - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S - `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) -- ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip - `gui/sitemap`: is now the official "go to" tool. new global hotkey for fort and adventure mode: Ctrl-G ## Fixes diff --git a/docs/dev/Binpatches.rst b/docs/dev/Binpatches.rst index a566576168..d59b1f8482 100644 --- a/docs/dev/Binpatches.rst +++ b/docs/dev/Binpatches.rst @@ -50,10 +50,20 @@ directly in memory at runtime:: If the name of the patch has no extension or directory separators, the script uses :file:`hack/patches//.dif`, thus auto-selecting -the version appropriate for the currently loaded executable. +the version appropriate for the currently loaded executable. The ``df-version`` +is the version string in the loaded symbol table. For example, if you want to +make a patch for all distributed verisons of DF 51.05, you'd provide a ``dif`` +file in each of the following directories: + +- :file:`hack/patches/v0.51.05 linux64 CLASSIC/mypatch.dif` +- :file:`hack/patches/v0.51.05 linux64 ITCH/mypatch.dif` +- :file:`hack/patches/v0.51.05 linux64 STEAM/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 CLASSIC/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 ITCH/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 STEAM/mypatch.dif` This is the preferred method; it's easier to debug, does not cause persistent -problems, and leaves file checksums alone. As with many other commands, users +problems, and leaves file checksums alone. As with many other commands, users can simply add it to `dfhack.init` to reapply the patch every time DF is run. diff --git a/patches/CMakeLists.txt b/patches/CMakeLists.txt deleted file mode 100644 index 37ee7c9440..0000000000 --- a/patches/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -install(DIRECTORY ${patchdirectories} - DESTINATION ${DFHACK_DATA_DESTINATION}/patches -) diff --git a/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif deleted file mode 100644 index f8c95a2af6..0000000000 --- a/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011ba9eb: e8 90 -011ba9ec: c0 90 -011ba9ed: 4a 90 -011ba9ee: e6 90 -011ba9ef: fe 90 diff --git a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif deleted file mode 100644 index 1784576881..0000000000 --- a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011c605b: e8 90 -011c605c: 50 90 -011c605d: a5 90 -011c605e: e5 90 -011c605f: fe 90 diff --git a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif deleted file mode 100644 index 142e53e4fa..0000000000 --- a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011c9c3b: e8 90 -011c9c3c: 80 90 -011c9c3d: 6a 90 -011c9c3e: e5 90 -011c9c3f: fe 90 diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif deleted file mode 100644 index 2f5cf9e0ae..0000000000 --- a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -377d5d: e8 90 -377d5e: ee 90 -377d5f: 93 90 -377d60: 68 90 -377d61: 00 90 diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif deleted file mode 100644 index e254650761..0000000000 --- a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -37af8d: e8 90 -37af8e: 1e 90 -37af8f: b7 90 -37af90: 68 90 -37af91: 00 90 diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif deleted file mode 100644 index 44dd5ebb81..0000000000 --- a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -37d77d: e8 90 -37d77e: 6e 90 -37d77f: bb 90 -37d780: 68 90 -37d781: 00 90 From f4ff0cea2fabd579d182d6a6fb32164cd4e683bf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 10:30:40 -0800 Subject: [PATCH 0145/1082] add enable/disable hooks for widgets and update docs --- docs/changelog.txt | 1 + docs/dev/overlay-dev-guide.rst | 5 +++++ plugins/lua/overlay.lua | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f67755852f..254f3ea9e5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -87,6 +87,7 @@ Template for new versions: - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons - ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer - ``overlay.isOverlayEnabled``: new API for querying whether a given overlay is enabled +- ``overlay``: widgets can now declare ``overlay_onenable`` and ``overlay_ondisable`` functions to hook enable/disable ## Removed diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 4302d5d4c8..d87178bfff 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -86,6 +86,11 @@ beyond your everyday `widgets.Widget `: end This allows for dynamic updates to UI overlays directly from the CLI. +- If an ``overlay_onenable()`` function is defined, it is called when the + overlay is enabled (including when the persisted state is reloaded at DF + startup). +- If an ``overlay_ondisable()`` function is defined, it is called when the + overlay is disabled. If the widget can take up a variable amount of space on the screen, and you want the widget to adjust its position according to the size of its contents, you can diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 77df7d47a4..c93f8ed7bd 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -171,6 +171,9 @@ local function do_enable(args, quiet, skip_save) vs_name = normalize_viewscreen_name(vs_name) ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry end + if db_entry.widget.overlay_onenable then + db_entry.widget.overlay_onenable() + end if not quiet then print(('enabled widget %s'):format(name)) end @@ -202,6 +205,9 @@ local function do_disable(args, quiet) active_viewscreen_widgets[vs_name] = nil end end + if db_entry.widget.overlay_ondisable then + db_entry.widget.overlay_ondisable() + end if not quiet then print(('disabled widget %s'):format(name)) end From a224c5958a7effae46e8c2dfb04e8bd823e5d5fe Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 10:47:41 -0800 Subject: [PATCH 0146/1082] reset draw context between rendering widgets --- docs/changelog.txt | 1 + plugins/lua/overlay.lua | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 254f3ea9e5..2352db036b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,6 +65,7 @@ Template for new versions: - `createitem`: output items will now end up at look cursor if active - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself +- `overlay`: reset draw context between rendering widgets so context changes can't propagate from widget to widget ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index c93f8ed7bd..c1d1bcb434 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -538,16 +538,16 @@ function feed_viewscreen_widgets(vs_name, vs, keys) return true end -local function _render_viewscreen_widgets(vs_name, vs, full_dc, scaled_dc) +local function _render_viewscreen_widgets(vs_name, vs) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return end local full, scaled = get_interface_rects() - full_dc = full_dc or gui.Painter.new(full) - scaled_dc = scaled_dc or gui.Painter.new(scaled) for _,db_entry in pairs(vs_widgets) do local w = db_entry.widget if (not vs or matches_focus_strings(db_entry, vs_name, vs)) and utils.getval(w.visible) then - detect_frame_change(w, function() w:render(w.fullscreen and full_dc or scaled_dc) end) + detect_frame_change(w, function() + w:render(w.fullscreen and gui.Painter.new(full) or gui.Painter.new(scaled)) + end) end end return full_dc, scaled_dc @@ -556,8 +556,8 @@ end local force_refresh function render_viewscreen_widgets(vs_name, vs) - local full_dc, scaled_dc = _render_viewscreen_widgets(vs_name, vs, nil, nil) - _render_viewscreen_widgets('all', nil, full_dc, scaled_dc) + _render_viewscreen_widgets(vs_name, vs) + _render_viewscreen_widgets('all', nil) if force_refresh then force_refresh = nil df.global.gps.force_full_display_count = 1 From eee505b4a556a7459f94016a015ddf2c156eadea Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 28 Feb 2025 18:57:14 +0000 Subject: [PATCH 0147/1082] 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 af8cf85f0c..30b3d1f1ce 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit af8cf85f0c2a772439a441120eac246123afdc6a +Subproject commit 30b3d1f1cefd5773fb9e10c49c89dba937c0f58c diff --git a/scripts b/scripts index 557ac5732e..72b6578e90 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 557ac5732e87d62f3b7970295322ae0c5f7eae2c +Subproject commit 72b6578e90f9abc91d31faf92d7aa9d4c6f978ae From b300afcb07dc9633f8f4d45add1d9a4528068d1d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 11:21:39 -0800 Subject: [PATCH 0148/1082] changelog editing pass and update refs --- docs/changelog.txt | 8 ++++---- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2352db036b..496ef9febe 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,7 +62,7 @@ Template for new versions: ## Fixes - Windows console: fix possible hang if the console returns a too-small window width (for any reason) -- `createitem`: output items will now end up at look cursor if active +- `createitem`: produced items will now end up at the look cursor position (if it is active) - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself - `overlay`: reset draw context between rendering widgets so context changes can't propagate from widget to widget @@ -77,16 +77,16 @@ Template for new versions: ## API - ``Military::removeFromSquad``: removes unit from any squad assignments -- ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``Buildings::checkFreeTiles``: now takes a building instead of a pointer to the building extents - ``Units::isUnitInBox``, ``Units::getUnitsInBox``: don't include inactive units -- ``Items::getItemBaseValue``: DF 51.06 reduced the base value of prepared meals +- ``Items::getItemBaseValue``: adjust to the reduced value of prepared meals (changed in DF 51.06) - ``Items::getValue``: magical powers now correctly contribute to item value ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons -- ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``dfhack.buildings.checkFreeTiles``: now takes a building pointer instead of an extents parameter - ``overlay.isOverlayEnabled``: new API for querying whether a given overlay is enabled - ``overlay``: widgets can now declare ``overlay_onenable`` and ``overlay_ondisable`` functions to hook enable/disable diff --git a/library/xml b/library/xml index f5ca9135f0..f5a63e87b3 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f5ca9135f09d0b13a23e5394b563609417d568c5 +Subproject commit f5a63e87b3d96c5c435d8737e95a70151bd218c1 diff --git a/plugins/stonesense b/plugins/stonesense index 30b3d1f1ce..8fdc2fdb49 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 30b3d1f1cefd5773fb9e10c49c89dba937c0f58c +Subproject commit 8fdc2fdb4939fda074b50626381a43581b0b8479 diff --git a/scripts b/scripts index 72b6578e90..108e5e9a37 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 72b6578e90f9abc91d31faf92d7aa9d4c6f978ae +Subproject commit 108e5e9a3774a57758404704c706361d2b1745d9 From 10e680794350bde96c4bf1a32e4193f2ccc813dc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 11:28:29 -0800 Subject: [PATCH 0149/1082] bump version to 51.06-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f102c35554..22f04fe085 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.06") -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 8eb563a889d7bb4117d105003a47e13f9332c7cb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 13:00:39 -0800 Subject: [PATCH 0150/1082] remove MakeCheese job from basic orders list defer to autocheese --- data/orders/basic.json | 21 --------------------- docs/changelog.txt | 1 + docs/plugins/orders.rst | 11 +++++++++-- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/data/orders/basic.json b/data/orders/basic.json index 688bdaa7ab..e1cd5ac8ff 100644 --- a/data/orders/basic.json +++ b/data/orders/basic.json @@ -114,27 +114,6 @@ "job" : "CustomReaction", "reaction" : "BREW_DRINK_FROM_PLANT_GROWTH" }, - { - "amount_left" : 1, - "amount_total" : 1, - "frequency" : "Daily", - "id" : 3, - "is_active" : false, - "is_validated" : false, - "item_conditions" : - [ - { - "condition" : "AtLeast", - "flags" : - [ - "unrotten", - "milk" - ], - "value" : 2 - } - ], - "job" : "MakeCheese" - }, { "amount_left" : 1, "amount_total" : 1, diff --git a/docs/changelog.txt b/docs/changelog.txt index 496ef9febe..b4a39ba389 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -91,6 +91,7 @@ Template for new versions: - ``overlay``: widgets can now declare ``overlay_onenable`` and ``overlay_ondisable`` functions to hook enable/disable ## Removed +- `orders`: MakeCheese job removed from library/basic orders set. Please use `autocheese` instead! # 51.06-r1 diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 9ac3009af8..a56e527466 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -145,7 +145,14 @@ This collection of orders handles basic fort necessities: You should import it as soon as you have enough dwarves to perform the tasks. Right after the first migration wave is usually a good time. -Note that the jugs are specifically made out of wood. This is so, as long as you don't may any other "Tools" out of wood, you can have a stockpile just for jugs by restricting a finished goods stockpile to only take wooden tools. +These orders do not contain milking, shearing, or cheesemaking jobs since the +game does not provide sufficient order conditions. Please enable `automilk`, +`autoshear`, and `autocheese` on the DFHack `gui/control-panel` for these types +of jobs. + +Note that the jugs are specifically made out of wood. This is so, as long as +you don't may any other "Tools" out of wood, you can have a stockpile just for +jugs by restricting a finished goods stockpile to only take wooden tools. Armok's additional note: "shleggings? Yes, `shleggings `__." @@ -155,7 +162,7 @@ Armok's additional note: "shleggings? Yes, This collection creates basic items that require heat. It is separated out from ``library/basic`` to give players the opportunity to set up magma furnaces first -in order to save resources. It handles: +(if desired) in order to save resources. It handles: - charcoal (including smelting of bituminous coal and lignite) - pearlash From 989359fb41e9a160fb5a163ba70c123f7c7a4e66 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Fri, 28 Feb 2025 22:33:50 +0100 Subject: [PATCH 0151/1082] spectate.lua: do not write into top and bottom 4 rows over DF's UI This is a quick workaround. A proper solution would involve calculating sizes and locations of the DF's widgets, as well as visibility for some of them. Also, remove `local writer = dc`, just use `dc`. --- plugins/lua/spectate.lua | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 7be670802f..0bf46cef2b 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -428,6 +428,8 @@ function TooltipOverlay:render_unit_banners(dc) local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) local pen = COLOR_WHITE + local _, screenHeight = dfhack.screen:getWindowSize() + local used_tiles = {} -- reverse order yields better offsets for overlapping texts for i = #units, 1, -1 do @@ -447,6 +449,10 @@ function TooltipOverlay:render_unit_banners(dc) local y = scrPos.y - 1 -- subtract 1 to move the text over the heads local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile + -- do not write anything in the top rows, where DF's interface is. + -- todo: use precise rectangles + if y < 4 then goto continue end + -- to resolve overlaps, we'll mark every coordinate we write anything in, -- and then check if the new tooltip will overwrite any used coordinate. -- if it will, try the next row, to a maximum offset of 4. @@ -477,15 +483,19 @@ function TooltipOverlay:render_unit_banners(dc) -- we can't place any useful information, and will ignore it instead. if 0 <= usedAt and usedAt <= 2 then goto continue end - local writer = dc:seek(x, y + dy) + -- do not write anything over DF's interface + -- todo: use precise rectangles + if y + dy > screenHeight - 4 then goto continue end + + dc:seek(x, y + dy) local ix = 0 for _, tok in ipairs(info) do local s if type(tok) == "string" then - writer:pen(pen) + dc:pen(pen) s = tok else - writer:pen(tok.pen) + dc:pen(tok.pen) s = tok.text end @@ -496,10 +506,10 @@ function TooltipOverlay:render_unit_banners(dc) -- we want to replace it with an `_`, so we need another `- 1` s = s:sub(1, usedAt - len - ix - 1 - 1) .. '_' - writer:string(s) + dc:string(s) break -- nothing more will fit else - writer:string(s) + dc:string(s) end ix = ix + len From d161360944b8b73ffb2f2581036e34949b0d9804 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:54:15 +0000 Subject: [PATCH 0152/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 108e5e9a37..e927734288 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 108e5e9a3774a57758404704c706361d2b1745d9 +Subproject commit e927734288e1676ccb6f25b96228719b79b48f13 From edbce6a8ff93375c50f62a331e3af059b3123b9f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 16:56:12 -0800 Subject: [PATCH 0153/1082] double quote not-commands --- docs/plugins/orders.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index a56e527466..378881d12c 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -146,9 +146,9 @@ You should import it as soon as you have enough dwarves to perform the tasks. Right after the first migration wave is usually a good time. These orders do not contain milking, shearing, or cheesemaking jobs since the -game does not provide sufficient order conditions. Please enable `automilk`, -`autoshear`, and `autocheese` on the DFHack `gui/control-panel` for these types -of jobs. +game does not provide sufficient order conditions. Please enable ``automilk``, +``autoshear``, and `autocheese` on the DFHack `gui/control-panel` for these +types of jobs. Note that the jugs are specifically made out of wood. This is so, as long as you don't may any other "Tools" out of wood, you can have a stockpile just for From e0889266c44f8d53ebec616056b24811de593dae Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 18:17:09 -0800 Subject: [PATCH 0154/1082] fix changelog typo --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b4a39ba389..c883d4868b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S -- `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) +- `spectate`: new overlay panel that allows you to cycle through following next/previous units (regardless of whether spectate mode is enabled) - `gui/sitemap`: is now the official "go to" tool. new global hotkey for fort and adventure mode: Ctrl-G ## Fixes From 3e96c601553689a4b9d789a60ea4b57ecfadd0e3 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 11:21:13 +0100 Subject: [PATCH 0155/1082] spectate.lua: add "hold-to-show" option (f.e. `ctrl`) --- plugins/lua/spectate.lua | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 0bf46cef2b..01e4cf32ac 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -27,6 +27,7 @@ local function get_default_state() ['prefer-new-arrivals']=true, ['tooltip-follow']=true, ['tooltip-follow-blink-milliseconds']=3000, + ['tooltip-follow-hold-to-show']='none', -- one of none, ctrl, alt, or shift ['tooltip-follow-job']=true, ['tooltip-follow-job-shortenings'] = { ["Store item in stockpile"] = "Store item", @@ -407,13 +408,20 @@ end function TooltipOverlay:render_unit_banners(dc) if not (config['tooltip-follow'] and AnyFollowOptionOn()) then return end - local blink_duration = config['tooltip-follow-blink-milliseconds'] - if blink_duration > 0 and not gui.blink_visible(blink_duration) then - return - end + local hold_to_show = config['tooltip-follow-hold-to-show'] + if hold_to_show and hold_to_show ~= 'none' then + if not dfhack.internal.getModifiers()[hold_to_show] then + return + end + else + local blink_duration = config['tooltip-follow-blink-milliseconds'] + if blink_duration > 0 and not gui.blink_visible(blink_duration) then + return + end - if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then - return + if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + return + end end local vp = df.global.world.viewport From 768b3d6b173f1a389cd43d8913094956a374e45f Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 11:44:34 +0100 Subject: [PATCH 0156/1082] add a 'toggle' command line option for any boolean value --- plugins/lua/spectate.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 01e4cf32ac..de005ae784 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -210,7 +210,11 @@ local function set_setting(args) -- here just in case, is already checked in the loop above qerror('missing value for ' .. path) elseif entry_type == 'boolean' then - value = argparse.boolean(value, path) + if value == 'toggle' then + value = not cfg[key] + else + value = argparse.boolean(value, path) + end elseif entry_type == 'number' then if path == 'follow-seconds' then value = argparse.positiveInt(value, path) @@ -241,7 +245,12 @@ function parse_commandline(args) if not command or command == 'status' then print_status() elseif command == 'toggle' then - do_toggle() + if #args == 0 then + do_toggle() + else + args[#args+1] = 'toggle' + set_setting(args) + end elseif command == 'set' then set_setting(args) elseif command == 'overlay' then From 0ecda6088a2d845c14a9e91c0a57267ebea7a921 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 11:53:35 +0100 Subject: [PATCH 0157/1082] update docs --- docs/plugins/spectate.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 89aef2b9f4..bf1349b8ad 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -37,6 +37,7 @@ Usage spectate [status] spectate toggle spectate set [] + spectate toggle spectate overlay enable|disable Examples @@ -68,6 +69,9 @@ Examples job is displayed in the `spectate` tooltips. See the ``tooltip-follow-job-shortenings`` setting below for details. +``spectate toggle tooltip-follow`` + Toggle the follow tooltips on or off. + Settings -------- @@ -110,6 +114,12 @@ Settings If the ``spectate.tooltip`` overlay is enabled, set the tooltip's blink duration in milliseconds. Set to 0 to always show. +``tooltip-follow-hold-to-show`` (default: none) + If the ``spectate.tooltip`` overlay is enabled, specifies a modifier key + (one of none, ctrl, alt, or shift) that has to be hold to show the tooltips + that follow onscreen dwarves around the map. This supersedes the + ``tooltip-follow-blink-milliseconds`` option. + ``tooltip-follow-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. From ccdfaa9924fd3a4fcedc75b1cc2cae209e3f02a2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Mar 2025 07:27:27 -0800 Subject: [PATCH 0158/1082] document dfhack.internal.getModstate function --- docs/dev/Lua API.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index f8108e8b2c..5cc5024abc 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3428,6 +3428,11 @@ and are only documented here for completeness: Sets the system clipboard text from a CP437 string. Character 0x10 is interpreted as a newline instead of the usual CP437 glyph. +* ``dfhack.internal.getModifiers()`` + + Returns the state of the keyboard modifier keys in a table of string -> + boolean. The keys are ``ctrl``, ``shift``, and ``alt``. + * ``dfhack.internal.getSuppressDuplicateKeyboardEvents()`` * ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)`` From d6dd79c95d122f5dd0313b569b02fc4497f6470b Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 23:23:40 +0100 Subject: [PATCH 0159/1082] Update spectate.rst --- docs/plugins/spectate.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index bf1349b8ad..181c85a521 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -35,9 +35,8 @@ Usage enable spectate spectate [status] - spectate toggle - spectate set [] spectate toggle + spectate set [] spectate overlay enable|disable Examples @@ -116,7 +115,7 @@ Settings ``tooltip-follow-hold-to-show`` (default: none) If the ``spectate.tooltip`` overlay is enabled, specifies a modifier key - (one of none, ctrl, alt, or shift) that has to be hold to show the tooltips + (one of none, ctrl, alt, or shift) that has to be held to show the tooltips that follow onscreen dwarves around the map. This supersedes the ``tooltip-follow-blink-milliseconds`` option. From 4ef8d2240f95fdc0f0be2cc23a47e45e4f95216b Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 23:24:14 +0100 Subject: [PATCH 0160/1082] Update spectate.rst --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 181c85a521..e57456ac83 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -35,7 +35,7 @@ Usage enable spectate spectate [status] - spectate toggle + spectate toggle [] spectate set [] spectate overlay enable|disable From 2a116f9794ba3ae6f400506c41948529f9f94bc5 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 2 Mar 2025 22:32:54 +0000 Subject: [PATCH 0161/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e927734288..eb4ea8175d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e927734288e1676ccb6f25b96228719b79b48f13 +Subproject commit eb4ea8175df8ade8494ba8d63189021680949e5c From e944a38c3365c608b77925f4322e012a2f6e4a45 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 2 Mar 2025 17:10:18 -0600 Subject: [PATCH 0162/1082] fix make squad from the great reorg --- library/modules/Military.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index a5946acca5..2e86fb958d 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -123,7 +123,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) for (const auto& routine : routines) { - df::squad_routine_schedulest* schedule = new df::squad_routine_schedulest[12]; + df::squad_routine_schedulest* schedule = new df::squad_routine_schedulest(); auto &asched = schedule->month; for(int kk=0; kk < 12; kk++) From 0f782ce4a00a058c6f0f5e821b0ed6848cd7b66c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:32:45 +0000 Subject: [PATCH 0163/1082] [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.31.1 → 0.31.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.1...0.31.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 ae54f7cf0c..6904eb760c 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.31.1 + rev: 0.31.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 2668082ef84f17cfa71cc096f5b3cd8e9d6a3b5a Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 6 Mar 2025 18:15:58 -0800 Subject: [PATCH 0164/1082] Update Lua API.rst - Fix outdated enum names --- docs/dev/Lua API.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 5cc5024abc..0a2fdc894a 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1395,9 +1395,9 @@ Job module Attach a real item to this job. If the item is intended to satisfy a job_item filter, the index of that filter should be passed in ``filter_idx``; otherwise, pass ``-1``. Similarly, if you don't care where the item is inserted, pass - ``-1`` for ``insert_idx``. The ``role`` param is a ``df.job_item_ref.T_role``. + ``-1`` for ``insert_idx``. The ``role`` param is a ``df.job_role_type``. If the item needs to be brought to the job site, then the value should be - ``df.job_item_ref.T_role.Hauled``. + ``df.job_role_type.Hauled``. * ``dfhack.job.isSuitableItem(job_item, item_type, item_subtype)`` @@ -2108,7 +2108,7 @@ Items module * ``dfhack.items.moveToInventory(item,unit[,use_mode[,body_part]])`` Move the item to the unit inventory. Returns *false* if impossible. - ``use_mode`` defaults to ``df.unit_inventory_item.T_mode.Hauled``. + ``use_mode`` defaults to ``df.inv_item_role_type.Hauled``. ``body_part`` defaults to ``-1``. * ``dfhack.items.remove(item[,no_uncat])`` From 9d44e1f67e6ddaa8df3dc8ce967067a7bda8ee4c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:18:03 +0000 Subject: [PATCH 0165/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index eb4ea8175d..dcd3612f4a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit eb4ea8175df8ade8494ba8d63189021680949e5c +Subproject commit dcd3612f4a08020436fb62cdc0339fb9498e695d From 0d9f62bc4ac861fec1c6b6a8e4a5977ca0ed2c5f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:28:35 +0000 Subject: [PATCH 0166/1082] 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 5bd392890e..364f71f745 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 5bd392890e92a70622878c0513ef50e72e215435 +Subproject commit 364f71f74544c37f47a4ddfebcb9d6b9e4cdfcce diff --git a/library/xml b/library/xml index f5a63e87b3..e97f8d46fe 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f5a63e87b3d96c5c435d8737e95a70151bd218c1 +Subproject commit e97f8d46febe9156ef737eff63ffc8c09a0ad26c diff --git a/plugins/stonesense b/plugins/stonesense index 8fdc2fdb49..675aee7a6d 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 8fdc2fdb4939fda074b50626381a43581b0b8479 +Subproject commit 675aee7a6de151261f83e45049bad0fcfa7bb92c diff --git a/scripts b/scripts index dcd3612f4a..e9987efcb8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit dcd3612f4a08020436fb62cdc0339fb9498e695d +Subproject commit e9987efcb8444dab606edea50f245166c4417b90 From 2259cd3e7f04496a0ef0b84299b14057aac4d104 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 17:14:55 -0800 Subject: [PATCH 0167/1082] fix dead link --- docs/guides/quickfort-user-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 03b26d56a5..a5b39c529b 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -1919,7 +1919,7 @@ Links DF maps. - DFHack's `buildingplan plugin ` sets material and quality constraints for quickfort-placed buildings. -- `Python Quickfort `__ is the previous, +- `Python Quickfort `__ is the previous, Python-based implementation that DFHack's quickfort script was inspired by. .. _quickfort_guide_appendix: From e40cd40b86044577ca3b05d2a3e124ea4b2fb276 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 17:18:20 -0800 Subject: [PATCH 0168/1082] check for null in fprintf --- library/VersionInfoFactory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index b1f0472c61..776362855a 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -209,7 +209,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "md5-hash") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): MD5: %s\n", cstr_name, cstr_os, cstr_value); + fprintf(stderr, "%s (%s): MD5: %s\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addMD5(cstr_value); @@ -217,7 +217,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "binary-timestamp") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): PE: %s\n", cstr_name, cstr_os, cstr_value); + fprintf(stderr, "%s (%s): PE: %s\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addPE(strtol(cstr_value, 0, 16)); From b01fa4d0bd78070ed1951197d579284e9df9e564 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 17:41:07 -0800 Subject: [PATCH 0169/1082] add toggle-kbd-cursor keybindings for adv mode --- data/init/dfhack.keybindings.init | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index d7ce5c8662..776a8425b3 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -41,8 +41,13 @@ keybinding add Ctrl-A@choose_start_site gui/embark-anywhere # in the game root directory keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeonmode/ViewSheets/UNIT|dungeonmode/ViewSheets/ITEM markdown +# gui/sitemap keybinding add Ctrl-G@dwarfmode/Default|dungeonmode/Default gui/sitemap +# toggle keyboard cursor +keybinding add Alt-K@dwarfmode|dungeonmode/Default|dungeonmode/Look toggle-kbd-cursor + + ###################### # dwarfmode bindings # ###################### @@ -72,9 +77,6 @@ keybinding add Ctrl-Shift-T@dwarfmode gui/teleport # apply blueprints to the map keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort -# toggle keyboard cursor -keybinding add Alt-K@dwarfmode toggle-kbd-cursor - # Stocks plugin #keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show" From 94a26e1a810c3d415e3b8d1a98d9daca2ac03317 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Mar 2025 01:57:32 +0000 Subject: [PATCH 0170/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e9987efcb8..43a4e221ed 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e9987efcb8444dab606edea50f245166c4417b90 +Subproject commit 43a4e221ed1144b918a3c37ee44994cd55a8c6a6 From 346b3f9a2cde40c46b8b2b0699092efb51178b03 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 01:15:06 -0800 Subject: [PATCH 0171/1082] add autocheese to the dreamfort recommended list --- data/blueprints/dreamfort.csv | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/blueprints/dreamfort.csv b/data/blueprints/dreamfort.csv index 72013fd487..c1266edb6e 100644 --- a/data/blueprints/dreamfort.csv +++ b/data/blueprints/dreamfort.csv @@ -113,6 +113,7 @@ You can save some time by setting up your settings in gui/control-panel before y - autobutcher - autobutcher target 10 10 14 2 BIRD_GOOSE - autochop +- autocheese - autofarm - autofarm threshold 150 grass_tail_pig - autofish @@ -134,6 +135,7 @@ You can save some time by setting up your settings in gui/control-panel before y """Gameplay"" subtab:" - combine - dwarfvet +- immortal-cravings - timestream - work-now "Note that if you've already started your fort and have missed the ""new fort"" trigger, you can enable these tools on the ""Enabled"" tabs instead. You can run the one-time commands (like ban-cooking all) manually from gui/launcher." @@ -283,11 +285,11 @@ Surface Walkthrough: Sieges and Prisoner Processing: Here are some tips and procedures for handling seiges -- including how to clean up afterwards! "" -"- Your ""Inside+"" burrow will automatically grow with your fort and should include only safe areas of your fort. In particular, it should not include the ""atrium"" area (where the ""siege bait"" pasture is) or the trapped hallways." +"- Your ""Inside+"" burrow will automatically grow with your fort and should include only safe areas. In particular, it does not include the ""atrium"" area (where the ""siege bait"" pasture is) or the trapped hallways." "" "- When a siege begins, set your civilian alert (attach the alert to your ""Inside+"" burrow if it isn't already) to ensure all your civilians stay out of danger. Immediately pull the lever to close the outer main gate. It is also wise to close the trade depot and inner main gate as well. That way, if enemies get past the traps, they'll have to go through the soldiers in your barracks (assuming you have a military)." "" -"- During a siege, you can use the levers to control how attackers path through the trapped corridors. If there are more enemies than cage traps, time your lever pulling so that the inner gates snap closed before your last cage trap is sprung. Then the remaining attackers will have to backtrack and go through the other trap-filled hallway. You can also choose *not* to use the trap hallways and meet the siege with your military. It's up to you!" +"- During a siege, you can use the levers to control how attackers path through the trapped corridors. If there are more enemies than cage traps, time your lever pulling so that the inner gates snap closed before your last cage trap is sprung. Then the remaining attackers will have to backtrack and go through the other trap-filled hallway. You can also choose *not* to use the trap hallways and instead meet the siege head-on with your military. It's up to you!" "" "- If your cage traps fill up, ensure your hallways are free of uncaged attackers, then close the trap hallway outer gates and open the inner gates. Clear the civilian alert and allow your dwarves to reset all the traps -- make some extra cages in preparation for this! Then re-enable the civilian alert and open the trap hallway outer gates." "" From 1e8790dd9bb6baf478b961ed435847e76b6ab129 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Mar 2025 09:31:27 +0000 Subject: [PATCH 0172/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 43a4e221ed..2f1873d55f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 43a4e221ed1144b918a3c37ee44994cd55a8c6a6 +Subproject commit 2f1873d55fb4b7780fa02acdb0ff30569551b0d1 From 210f1c6362653a511b8b463f2d07adaad807a35d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 18:21:15 -0800 Subject: [PATCH 0173/1082] document github workflows and release process --- docs/dev/github-workflows.rst | 204 ++++++++++++++++++++++++++++++++++ docs/dev/index.rst | 3 + docs/dev/release-process.rst | 192 ++++++++++++++++++++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 docs/dev/github-workflows.rst create mode 100644 docs/dev/release-process.rst diff --git a/docs/dev/github-workflows.rst b/docs/dev/github-workflows.rst new file mode 100644 index 0000000000..06249f3f18 --- /dev/null +++ b/docs/dev/github-workflows.rst @@ -0,0 +1,204 @@ +GitHub workflows +================ + +We run our continuous integration (CI) validation and our release automation +via GitHub workflows. This allows us to merge PRs with confidence that they +won't catestrophically break DFHack functionality. GitHub workflows also allow +us to quickly produce stable release builds with fewer manual steps. Reducing +manual steps for releases is important since it is easy for a person to forget +a small but impactful step and therefore produce a bad release that causes +trouble for our users. + +Background +---------- + +`GitHub workflows `_ run +on provisioned VMs in the cloud with stable environments that we specify. They +are free to use since DFHack is an open source project. They have proven to be +reliably available within a few seconds when our workflows are triggered. The +logic for the workflows is written in yaml, and the files that control our +workflows are stored in the :file:`.github/workflows/` directory in each of our +repos. Example: :source:`.github/workflows`. + +Each workflow contains metadata that specifies: +- when it `triggers `_ +- what `base environment `_ it uses (OS, pre-installed dependencies, etc.) +- what additional dependencies should be installed (if any) +- custom business logic + +Workflows run in the context of a single repo, but workflows defined in one +repo can inherit logic from workflows in other repos. All our common CI logic +is in the main DFHack/dfhack repo, but our submodules, like our ``scripts`` and +``df-structures`` repos, have CI workflows defined that inherit from the logic +in DFHack/dfhack. That way we can fix bugs and extend functionality in one +place and have it benefit the entire org tree. + +Caches +~~~~~~ + +GitHub also provides 10GB per repository for `caches `_. +We utilize the cache system to keep state between workflow runs, cache +downloads, and keep compiler output to speed up subsequent builds. Efficient +use of the cache system is a critical part of our workflow design. It allows us +to iterate on test failures in PRs in one minute instead of 20. It allows us to +put out an entire emergency release build in 5 minutes instead of 45. We have +tuned our build and test workflows to minimize spurious cache misses and keep +the fast path fast. + +Caches are namespaced by key prefixes, and we have one key prefix per build +context. For example, release builds on gcc-10 are kept in one cache namespace, +whereas test builds on gcc-10 are kept separate. MSVC release and test builds +similarly have their own namespaces. Each cache has a maximum size that is +enforced by the business logic that writes the cache data. + +In order to maintain consistency in a distributed environment, caches are +versioned. A workflow will read the latest version of the cache with its key +prefix, maybe modify the cache with new data, then write back a new version. +Caches that are not used for 2 weeks are purged from GitHub storage, but if a +repo goes over the 10GB limit, caches are deleted in LRU order until the repo +is under the storage limit again. + +CI workflows +------------ + +Build +~~~~~ + +The Build workflow is the main CI workflow. It runs on every PR and push to a +branch. The ``build.yml`` file is essentially an orchestration layer for the +logic in several other .yml files: + +- ``test.yml`` builds DFHack with the test suite enabled (but stonesense and + windows pdb files disabled) and runs the test suite. It is optimized for + speed and is intended to give PR authors quick feedback on their changes. + The test suite is executed in a real running DF game on both Linux and + Windows. The ``test`` job populates the ``test`` cache, which is used by many + other workflows for non-distributed builds. +- ``package.yml`` builds DFHack as it would be released: test suite disabled + but stonesense and windows pdb files enabled. The ``package`` job populates the ``release`` cache, which is used to build all distributed binaries. +- The ``docs`` target does a docs-only build of DFHack and reports any errors. + Doc errors would show up in the ``test`` and ``package`` builds anyway, but + the ``docs`` target runs very fast and can identify doc errors in less than + 1m. +- ``lint.yml`` runs the verification scripts in the ``ci`` directory. These + scripts check for common errors in the codebase that are not caught by the + compiler. The lint scripts are written in Python and shell script and are + intended to be run quickly and catch common errors. + +Check type sizes +~~~~~~~~~~~~~~~~ + +``check-type-sizes.yml`` is a df-structures-only workflow that checks for +changes in the sizes of types in the xml structures. It builds the +``xml-dump-type-sizes`` binary on both Linux and Windows for both the +structures in this PR and for the structures in the target merge branch. It +then runs the built binary on its native OS and compares the output. If any +type sizes have changed, the workflow generates a PR comment (via the +``comment-pr.yml`` workflow) with details. + +.. _workflows-release-automation: + +Release automation workflows +---------------------------- + +Watch DF Releases +~~~~~~~~~~~~~~~~~ + +This workflow runs every 8 minutes and checks the Steam metadata, the Itch +website, and the Bay 12 website for evidence of new releases. If a new release +is found, it generates an announcement in a private channel on the DFHack +Discord server. + +Inside the ``watch-df-releases.yml`` workflow, there are separate jobs for +watching Steam branches and watching the websites. For the Steam watcher, it +takes configuration for: + +- which branches to watch +- whether to kick off the Generate symbols workflow when a new release is found +- whether to autodeploy to Steam when the Generate symbols workflow completes + +The workflow has protections against concurrent runs, so if you suspect a new +release is out, you can manually trigger the workflow to check. If the cron +trigger happens to run the workflow at the same time, the second run will be +paused while the first run completes. + +Generate symbols +~~~~~~~~~~~~~~~~ + +This workflow can be triggered manually or by the Watch DF Releases workflow. +It downloads the specified DF version for the selected distribution platform(s) +and OS target(s), then updates the ``symbol-table`` entries in ``symbols.xml``. +If the distribution platform is Steam, it can also autodetect the DF version by +extracting the version string from the DF title screen data. + +For Linux, it always builds DFHack -- just the core library (no plugins) -- and +generates symbols via the `devel/dump-offsets` and `devel/scan-vtables` scripts. + +For Windows, we extract symbol data via static analysis, so the workflow only +builds DFHack if it needs to autodetect the DF version. + +Once the symbols.xml file is updated, the workflow commits the changes to the +specified df-structures branch and updates the xml submodule ref in the +specified DFHack/dfhack branch. If a deploy Steam branch is specified, it also +launches the Deploy to Steam workflow. + +Deploy to GitHub +~~~~~~~~~~~~~~~~ + +`github-release.yml `_ +can be triggered manually or automatically by creating a new release version +tag in git. It builds DFHack with the release configuration, packages the +aritifacts for GitHub, creates a new GitHub release, and uploads the packages +to the GitHub release page. + +It uses text in :source:`.github/release_template.md` to generate the release +notes, and appends the changelog contents for the tagged version. + +If you need to re-tag the release to fix a mistake, it will automatically run +again and replace the binaries attached to the GitHub release for the tagged +version. It will not overwrite the release notes, though, to preserve any edits +you may have made in the GitHub UI. If you *want* it to completely regenerate +the release notes, you can delete the release before you re-tag the version. + +GitHub releases end up here: https://github.com/DFHack/dfhack/releases. + +Deploy to Steam +~~~~~~~~~~~~~~~ + +`steam-deploy.yml `_ +can be triggered manually or automatically by creating a new release version +tag in git. It builds DFHack with the release configuration, packages the +aritifacts for Steam, and uploads them to the specified Steam branch. + +The workflow caches steamcmd to speed the deployment up by 30s or so. +Otherwise, steamcmd would have to be downloaded and updated every time the +workflow runs. + +Steam releases end up here: +https://partner.steamgames.com/apps/builds/2346660. The "version" you +specified for the workflow is used as the "description" for the build. + +Maintenance workflows +--------------------- + +Update submodules +~~~~~~~~~~~~~~~~~ + +`update-submodules.yml `_ +runs daily, or can be run manually as needed. It checks DFHack submodules for +new commits on the main branches and updates the submodule refs in the DFHack +develop branch. + +You generally should not run this workflow for anything other than the develop +branch, as it will overwrite any changes you have made to the submodule refs in +other branches. + +Clean up PR caches +~~~~~~~~~~~~~~~~~~ + +This workflow runs automatically whenever a PR is closed or merged. It removes +caches created for the PR so they don't take up quota. + +Note that if you merge a PR before all the workflows have completed, the caches +may be created after this workflow runs. In that case, the caches will be +orphaned and will be purged by GitHub's cache eviction policy after 2 weeks. diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 4992015d86..006fe06da1 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -18,7 +18,10 @@ These are pages relevant to people developing for DFHack. /docs/dev/overlay-dev-guide /docs/dev/Structures-intro /docs/dev/data-identity + /docs/dev/github-workflows + /docs/dev/release-process /docs/dev/Memory-research /docs/dev/Binpatches /docs/dev/Remote /docs/NEWS-dev + diff --git a/docs/dev/release-process.rst b/docs/dev/release-process.rst new file mode 100644 index 0000000000..51c0ac61b0 --- /dev/null +++ b/docs/dev/release-process.rst @@ -0,0 +1,192 @@ +Release process +=============== + +This page details the process we follow for beta and stable releases. + +For documentation on the related GitHub workflows, see +`workflows-release-automation`. + +Beta release +------------ + +This process pushes a pre-release build to GitHub and Steam. It is intended to +be lower-toil than the stable release process and allows us to facilitate +frequent public testing and feedback without compromising the stability of our +"stable" releases. + +1. Run the `Update submodules `_ GitHub action on the ``develop`` branch to ensure that all submodules are up to date. +1. Update version strings in :source:`CMakeLists.txt` as appropriate. Set ``DFHACK_RELEASE`` to the *next* stable release version with an "rc#" suffix. For example, if the last stable release was "r1" then set the string to "r2rc1". If we do a second beta release before the final stable "r2" then the string would be "r2rc2". + + - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``TRUE``. + - Commit and push to ``develop`` + - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r2rc1`` for bash) + +1. Tag ``develop`` (no need to tag the submodules) and push: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin`` + + - This will automatically trigger `Deploy to Steam `_ and `Deploy to GitHub `_ build workflows. + +1. Write release notes highlights and any requests for feedback in the `draft GitHub release `_ and publish the draft. +1. Associate release notes with the build on Steam + + - Go to the `announcement creation page `_ + - Select "A game update" + - Select "Small update / Patch notes" + - Set the "Event title" to the release version (e.g. "DFHack 51.07-r2rc1") + - Set the "Subtitle" to "DFHack pre-release (beta channel)" + - Mention release highlights in the "Summary" field + - Transcode release notes from the GitHub release into the "Event description" field + - patch notes must be in BBcode; see `converting-markdown-to-bbcode` below for how to convert our release notes to BBcode + - Click "Link to build" and select the staging branch (be sure the build has been deployed to the branch first. By the time you're done with the patch notes the build will likely be ready for you) + - Go to the Artwork tab, select "Previously uploaded images", and search for and double-click on dfhack_logo.png. Click "Upload" (even though it has already been uploaded). + - Switch to the "Publish" tab and publish! + - `Promote `_ the build to the "beta" branch (and the "testing" branch if it's newer than what is on the "testing" branch) + +1. Monitor for beta channel subscriber feedback on the Steam `community page `_ +1. *Maybe* also post to Reddit and other announcement channels if we feel like we need to recruit more beta testers into the pool, but we should avoid posting so often that it is annoying for those who don't use Steam or just want announcements for stable releases. + +Stable release +-------------- + +This process creates a stable DFHack release meant for widespread distribution. +Stable releases come in two forms: straight from ``develop`` or from a point +release branch. + +During "normal" times, we will test out new features in beta releases until we +reach a point of stability. Then, after the ``develop`` branch is feature frozen +while we polish and fix bugs, we tag a release directly from ``develop`` +``HEAD``. + +However, if we have already started committing beta features to ``develop`` and +it becomes necessary to put out a bugfix release for a problem in an +already-released stable release, then we will create a new branch from the +stable tag, cherry-pick fixes from ``develop`` onto that branch, and spin a +release from there. After the point release is published, we'll merge the +branch back into ``develop`` and remove the release branch to clean up. + +1. Triage remaining issues/PRs in the `release project `_ + + - Don't feel pressure to merge anything risky just before a stable release. That's what beta releases are for. + +1. In your local clone of the ``DFHack/develop`` branch, make sure your checkout and all submodules (listed in :source:`.gitmodules`) are up to date with their latest public commits and have no uncommitted/unpushed local changes. +1. Ensure that CI has not failed unexpectedly on the latest online changes: + + - https://github.com/DFHack/dfhack/commits/develop + - https://github.com/DFHack/scripts/commits/master + - https://github.com/DFHack/df-structures/commits/master + +1. Update version strings in :source:`CMakeLists.txt` as appropriate + + - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``FALSE``. + - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r1``) + +1. Replace "Future" with the version number and clean up changelog entries; add new "Future" section (with headers pre-populated from the template at the top of the file): + + - ``docs/changelog.txt`` + - ``scripts/changelog.txt`` + - ``library/xml/changelog.txt`` + - ``plugins/stonesense/docs/changelog.txt`` + +1. Do a top-level build to ensure the docs build cleanly +1. Commit/push changes to submodules and tag (``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin master``) + + - ``scripts`` + - ``library/xml`` + - ``plugins/stonesense`` + +1. Commit and push changes to ``develop`` + + - Ensure that any updates you pushed to submodules are tracked in the commit to ``DFHack/develop`` + +1. Tag ``dfhack``: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin develop`` + + - This will automatically trigger a `Deploy to Steam `_ GitHub action to the "staging" Steam branch and a `Deploy to GitHub `_ GitHub action to create a draft `release `_ from a template and attach the built artifacts. + +1. Switch to the Steam ``staging`` release channel in the Steam client (password: ``stagingstagingstaging``) and download/test the update. + + - Ensure DFHack starts DF when run from the Steam client + - Ensure the DFHack version string is accurate on the title page (should just be the release number, e.g. ``DFHack 51.07-r1``, with no git hash or warnings) + - Run `devel/check-release` + - If something goes wrong with this step, fix it, delete the tag (both from `GitHub `_ and locally (``git tag -d $RELEASE``)), re-tag, re-push, and re-test. Note that you do *not* need to remove the GitHub draft release -- the existing one will just get updated with the new tag and binaries. You *can* remove the draft release, though, if you want the release notes to get regenerated. + +1. Prep release on GitHub + + - Go to the draft `release `_ on GitHub + - Add announcements, highlights (with demo videos), etc. to the description + +1. Push develop to master (``git push origin develop:master``) + + - This will start the documentation build process and update the published "stable" docs + - Note that if this is a -r1 release, you won't be able to complete this step until a classic build is available on the Bay 12 website so the DFHack Test workflow can pass, which is a prerequisite for being able to push to ``master``. + +1. Post release notes on Steam + + - Go to the `announcement creation page `_ + - Select "A game update" + - Select "Regular update" + - Set the "Event title" to the release version (e.g. "DFHack 51.07-r1") + - Set the "Subtitle" to "DFHack stable release" + - Add list of highlights (and maybe announcements, if significant) to the "Summary" field + - Upload screenshots and demo videos via the button at the bottom of the "Previously uploaded videos" area + - Add release notes to the "Event description" field (must be in BBcode; see `converting-markdown-to-bbcode` below for how to convert our release notes to BBcode) + - Drag uploaded images/videos into their appropriate places in the announcement text (replace the GitHub URL tags, which won't work from Steam) + - If the generated release notes exceed the announcement length limits, add a link to the GitHub release page at the bottom of the announcement instead + - Click "Link to build" and select the staging branch (be sure the build has been deployed to the branch first. by the time you're done with the patch notes the build will likely be ready for you) + - the release notes will travel with the build when we promote it to other branches + - Go to the Artwork tab, select "Previously uploaded images", and search for and double-click on STABLEannouncement6.png. Click "Upload" (even though it has already been uploaded). + - Switch to the "Publish" tab and publish! + +1. Go to the `Steam builds page `_ and promote the build to the "default" branch + + - For the build that you just pushed to "staging", click the "-- Select an app branch --" drop-down and select "default" + - Click on "Preview Change" + - Commit the change (you may need to verify with 2FA) + - If the release is newer than what's on the ``beta`` and/or ``testing`` branches, set it live on those branches as well + +1. Publish the prepped GitHub release +1. Send out release announcements + + - Announce new version in r/dwarffortress. Example: https://www.reddit.com/r/dwarffortress/comments/1i3l5xl/dfhack_5015r2_released_highlights_stonesense/ + - Create the post in the Reddit web interface; the mobile app is extremely painful to use for posting + - Do an "Images & Video" post, sample title: "DFHack 51.07-r1 released! Hilights: Open legends mode directly from an active fort, Dig through warm or damp tiles without interruption, Unlink buildings from levers" + - Add the animated gifs to the post (with appropriate captions naming the relevant tool and what is being demonstrated) + - Add the "DFHack Official" flair to the post. If you're not a r/dwarffortress mod, ask Myk to do this after posting. + - After posting, add each section of the release notes as its own comment, splitting out individual announcements and highlights. This gives people the opportunity to respond directly to the portion of the release notes that interests them; it also helps us avoid size limits for comments. You can include a single still shot (.png file) per comment, but you have to switch to "Fancy Pants Editor" to do it. You can only switch editors once, or the image will get messed up (that is, the image will turn into a hyperlink to an image). Suggested procedure is to prepare the comment in markdown, switch to Fancy Pants Editor, and add images just before submitting the comment. + - Announce new version in forum thread. Example: http://www.bay12forums.com/smf/index.php?topic=164123.msg8567134#msg8567134 + - Update latest version text and link in `first post `_ (if you are not Lethosor, ping Lethosor for this) + - Announce in `#announcements `_ on DFHack Discord + - Announce in `#mod-releases `_ on Kitfox Discord + - Change the name of the release thread on Kitfox Discord to match the release version (if you are not Myk, ping Myk for this) + +1. Monitor all announcement channels for feedback and respond to questions/complaints +1. Create a `project `_ on GitHub in the DFHack org for the next release + + - Copy the previous project (3 dot menu, "Make a copy"), set DFHack as the owner + - In the new project, select settings and set the visibility to Public + - Move any remaining To Do or In Progress items from last release project to next release project + - Close project for last release + +1. If this is a -r2 release or later, go to https://readthedocs.org/projects/dfhack/versions/ and "Edit" previous DFHack releases for the same DF version and mark them "Hidden" (keep the "Active" flag set) so they no longer appear on the docs version selector. + +.. _converting-markdown-to-bbcode: + +Converting Markdown to BBcode +----------------------------- + +Hopefully we can `automate `_ this in the future, but for now, here is the procedure: + +1. Get the markdown that you want to convert into some field on GitHub (can be a temporary text field that you then preview without saving) +1. View the rendered release notes in your browser (these instructions are for Chrome, but other browsers probably have similar capabilities) +1. Right click on the rendered text and inspect the DOM +1. Copy the HTML element that contains the release notes +1. Click on the "Import HTML" button on the Steam announcement form; paste in the HTML and click "Overwrite" +1. Copy the generated BBCode out from the description field and into a text editor +1. Fix it up: + + - Remove the "How do I download DFHack?" section -- people on Steam don't need it + - Some ``

`` elements aren't converted properly and need to be rewritten with square brackets + - Any monospaced text gets HTML tags instead of BBCode ``[code]`` tags, but you can't use them either since they force newlines. ``[tt]`` isn't supported. Any ```` tags just need to be removed entirely. + - Any ``
`` and ```` tags need to be removed + +1. Copy it all back into the description field for the announcement +1. Click on "Preview event" to double check that it renders sanely +1. You're done. From c953144c94ec21cab8cf40b9085a84c136ac80b0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 03:59:13 -0800 Subject: [PATCH 0174/1082] formatting --- docs/dev/index.rst | 1 - docs/dev/release-process.rst | 81 +++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 006fe06da1..931b9bb006 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -24,4 +24,3 @@ These are pages relevant to people developing for DFHack. /docs/dev/Binpatches /docs/dev/Remote /docs/NEWS-dev - diff --git a/docs/dev/release-process.rst b/docs/dev/release-process.rst index 51c0ac61b0..c2aa7b1cc1 100644 --- a/docs/dev/release-process.rst +++ b/docs/dev/release-process.rst @@ -15,18 +15,20 @@ frequent public testing and feedback without compromising the stability of our "stable" releases. 1. Run the `Update submodules `_ GitHub action on the ``develop`` branch to ensure that all submodules are up to date. -1. Update version strings in :source:`CMakeLists.txt` as appropriate. Set ``DFHACK_RELEASE`` to the *next* stable release version with an "rc#" suffix. For example, if the last stable release was "r1" then set the string to "r2rc1". If we do a second beta release before the final stable "r2" then the string would be "r2rc2". + +2. Update version strings in :source:`CMakeLists.txt` as appropriate. Set ``DFHACK_RELEASE`` to the *next* stable release version with an "rc#" suffix. For example, if the last stable release was "r1" then set the string to "r2rc1". If we do a second beta release before the final stable "r2" then the string would be "r2rc2". - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``TRUE``. - Commit and push to ``develop`` - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r2rc1`` for bash) -1. Tag ``develop`` (no need to tag the submodules) and push: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin`` +3. Tag ``develop`` (no need to tag the submodules) and push: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin`` - This will automatically trigger `Deploy to Steam `_ and `Deploy to GitHub `_ build workflows. -1. Write release notes highlights and any requests for feedback in the `draft GitHub release `_ and publish the draft. -1. Associate release notes with the build on Steam +4. Write release notes highlights and any requests for feedback in the `draft GitHub release `_ and publish the draft. + +5. Associate release notes with the build on Steam - Go to the `announcement creation page `_ - Select "A game update" @@ -41,8 +43,9 @@ frequent public testing and feedback without compromising the stability of our - Switch to the "Publish" tab and publish! - `Promote `_ the build to the "beta" branch (and the "testing" branch if it's newer than what is on the "testing" branch) -1. Monitor for beta channel subscriber feedback on the Steam `community page `_ -1. *Maybe* also post to Reddit and other announcement channels if we feel like we need to recruit more beta testers into the pool, but we should avoid posting so often that it is annoying for those who don't use Steam or just want announcements for stable releases. +6. Monitor for beta channel subscriber feedback on the Steam `community page `_ + +7. *Maybe* also post to Reddit and other announcement channels if we feel like we need to recruit more beta testers into the pool, but we should avoid posting so often that it is annoying for those who don't use Steam or just want announcements for stable releases. Stable release -------------- @@ -67,58 +70,60 @@ branch back into ``develop`` and remove the release branch to clean up. - Don't feel pressure to merge anything risky just before a stable release. That's what beta releases are for. -1. In your local clone of the ``DFHack/develop`` branch, make sure your checkout and all submodules (listed in :source:`.gitmodules`) are up to date with their latest public commits and have no uncommitted/unpushed local changes. -1. Ensure that CI has not failed unexpectedly on the latest online changes: +2. In your local clone of the ``DFHack/develop`` branch, make sure your checkout and all submodules (listed in :source:`.gitmodules`) are up to date with their latest public commits and have no uncommitted/unpushed local changes. + +3. Ensure that CI has not failed unexpectedly on the latest online changes: - https://github.com/DFHack/dfhack/commits/develop - https://github.com/DFHack/scripts/commits/master - https://github.com/DFHack/df-structures/commits/master -1. Update version strings in :source:`CMakeLists.txt` as appropriate +4. Update version strings in :source:`CMakeLists.txt` as appropriate - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``FALSE``. - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r1``) -1. Replace "Future" with the version number and clean up changelog entries; add new "Future" section (with headers pre-populated from the template at the top of the file): +5. Replace "Future" with the version number and clean up changelog entries; add new "Future" section (with headers pre-populated from the template at the top of the file): - ``docs/changelog.txt`` - ``scripts/changelog.txt`` - ``library/xml/changelog.txt`` - ``plugins/stonesense/docs/changelog.txt`` -1. Do a top-level build to ensure the docs build cleanly -1. Commit/push changes to submodules and tag (``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin master``) +6. Do a top-level build to ensure the docs build cleanly + +7. Commit/push changes to submodules and tag (``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin master``) - ``scripts`` - ``library/xml`` - ``plugins/stonesense`` -1. Commit and push changes to ``develop`` +8. Commit and push changes to ``develop`` - Ensure that any updates you pushed to submodules are tracked in the commit to ``DFHack/develop`` -1. Tag ``dfhack``: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin develop`` +9. Tag ``dfhack``: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin develop`` - This will automatically trigger a `Deploy to Steam `_ GitHub action to the "staging" Steam branch and a `Deploy to GitHub `_ GitHub action to create a draft `release `_ from a template and attach the built artifacts. -1. Switch to the Steam ``staging`` release channel in the Steam client (password: ``stagingstagingstaging``) and download/test the update. +10. Switch to the Steam ``staging`` release channel in the Steam client (password: ``stagingstagingstaging``) and download/test the update. - Ensure DFHack starts DF when run from the Steam client - Ensure the DFHack version string is accurate on the title page (should just be the release number, e.g. ``DFHack 51.07-r1``, with no git hash or warnings) - Run `devel/check-release` - If something goes wrong with this step, fix it, delete the tag (both from `GitHub `_ and locally (``git tag -d $RELEASE``)), re-tag, re-push, and re-test. Note that you do *not* need to remove the GitHub draft release -- the existing one will just get updated with the new tag and binaries. You *can* remove the draft release, though, if you want the release notes to get regenerated. -1. Prep release on GitHub +11. Prep release on GitHub - Go to the draft `release `_ on GitHub - Add announcements, highlights (with demo videos), etc. to the description -1. Push develop to master (``git push origin develop:master``) +12. Push develop to master (``git push origin develop:master``) - This will start the documentation build process and update the published "stable" docs - Note that if this is a -r1 release, you won't be able to complete this step until a classic build is available on the Bay 12 website so the DFHack Test workflow can pass, which is a prerequisite for being able to push to ``master``. -1. Post release notes on Steam +13. Post release notes on Steam - Go to the `announcement creation page `_ - Select "A game update" @@ -135,15 +140,16 @@ branch back into ``develop`` and remove the release branch to clean up. - Go to the Artwork tab, select "Previously uploaded images", and search for and double-click on STABLEannouncement6.png. Click "Upload" (even though it has already been uploaded). - Switch to the "Publish" tab and publish! -1. Go to the `Steam builds page `_ and promote the build to the "default" branch +14. Go to the `Steam builds page `_ and promote the build to the "default" branch - For the build that you just pushed to "staging", click the "-- Select an app branch --" drop-down and select "default" - Click on "Preview Change" - Commit the change (you may need to verify with 2FA) - If the release is newer than what's on the ``beta`` and/or ``testing`` branches, set it live on those branches as well -1. Publish the prepped GitHub release -1. Send out release announcements +15. Publish the prepped GitHub release + +16. Send out release announcements - Announce new version in r/dwarffortress. Example: https://www.reddit.com/r/dwarffortress/comments/1i3l5xl/dfhack_5015r2_released_highlights_stonesense/ - Create the post in the Reddit web interface; the mobile app is extremely painful to use for posting @@ -157,15 +163,16 @@ branch back into ``develop`` and remove the release branch to clean up. - Announce in `#mod-releases `_ on Kitfox Discord - Change the name of the release thread on Kitfox Discord to match the release version (if you are not Myk, ping Myk for this) -1. Monitor all announcement channels for feedback and respond to questions/complaints -1. Create a `project `_ on GitHub in the DFHack org for the next release +17. Monitor all announcement channels for feedback and respond to questions/complaints + +18. Create a `project `_ on GitHub in the DFHack org for the next release - Copy the previous project (3 dot menu, "Make a copy"), set DFHack as the owner - In the new project, select settings and set the visibility to Public - Move any remaining To Do or In Progress items from last release project to next release project - Close project for last release -1. If this is a -r2 release or later, go to https://readthedocs.org/projects/dfhack/versions/ and "Edit" previous DFHack releases for the same DF version and mark them "Hidden" (keep the "Active" flag set) so they no longer appear on the docs version selector. +19. If this is a -r2 release or later, go to https://readthedocs.org/projects/dfhack/versions/ and "Edit" previous DFHack releases for the same DF version and mark them "Hidden" (keep the "Active" flag set) so they no longer appear on the docs version selector. .. _converting-markdown-to-bbcode: @@ -175,18 +182,26 @@ Converting Markdown to BBcode Hopefully we can `automate `_ this in the future, but for now, here is the procedure: 1. Get the markdown that you want to convert into some field on GitHub (can be a temporary text field that you then preview without saving) -1. View the rendered release notes in your browser (these instructions are for Chrome, but other browsers probably have similar capabilities) -1. Right click on the rendered text and inspect the DOM -1. Copy the HTML element that contains the release notes -1. Click on the "Import HTML" button on the Steam announcement form; paste in the HTML and click "Overwrite" -1. Copy the generated BBCode out from the description field and into a text editor -1. Fix it up: + +2. View the rendered release notes in your browser (these instructions are for Chrome, but other browsers probably have similar capabilities) + +3. Right click on the rendered text and inspect the DOM + +4. Copy the HTML element that contains the release notes + +5. Click on the "Import HTML" button on the Steam announcement form; paste in the HTML and click "Overwrite" + +6. Copy the generated BBCode out from the description field and into a text editor + +7. Fix it up: - Remove the "How do I download DFHack?" section -- people on Steam don't need it - Some ``

`` elements aren't converted properly and need to be rewritten with square brackets - Any monospaced text gets HTML tags instead of BBCode ``[code]`` tags, but you can't use them either since they force newlines. ``[tt]`` isn't supported. Any ```` tags just need to be removed entirely. - Any ``
`` and ```` tags need to be removed -1. Copy it all back into the description field for the announcement -1. Click on "Preview event" to double check that it renders sanely -1. You're done. +8. Copy it all back into the description field for the announcement + +9. Click on "Preview event" to double check that it renders sanely + +10. You're done. From e2c3bca2695c41000d504d86a7279cc9abcb0977 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 8 Mar 2025 07:18:19 -0800 Subject: [PATCH 0175/1082] spelling --- docs/dev/github-workflows.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/github-workflows.rst b/docs/dev/github-workflows.rst index 06249f3f18..fa1c36b66c 100644 --- a/docs/dev/github-workflows.rst +++ b/docs/dev/github-workflows.rst @@ -3,7 +3,7 @@ GitHub workflows We run our continuous integration (CI) validation and our release automation via GitHub workflows. This allows us to merge PRs with confidence that they -won't catestrophically break DFHack functionality. GitHub workflows also allow +won't catastrophically break DFHack functionality. GitHub workflows also allow us to quickly produce stable release builds with fewer manual steps. Reducing manual steps for releases is important since it is easy for a person to forget a small but impactful step and therefore produce a bad release that causes From 9936e584b6899b316ef4ec4d0890b02e287ec254 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 08:44:49 -0800 Subject: [PATCH 0176/1082] use template for project creation --- docs/dev/release-process.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/release-process.rst b/docs/dev/release-process.rst index c2aa7b1cc1..bef09be412 100644 --- a/docs/dev/release-process.rst +++ b/docs/dev/release-process.rst @@ -167,7 +167,9 @@ branch back into ``develop`` and remove the release branch to clean up. 18. Create a `project `_ on GitHub in the DFHack org for the next release - - Copy the previous project (3 dot menu, "Make a copy"), set DFHack as the owner + - Open the `project template `_ + - Click "Use this template" + - Name the project according to the version, e.g. "51.07-r2" and click "Use template" - In the new project, select settings and set the visibility to Public - Move any remaining To Do or In Progress items from last release project to next release project - Close project for last release From 03c8001f7749b3b22e407c1761063299594953e7 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 8 Mar 2025 13:43:47 -0600 Subject: [PATCH 0177/1082] Fix capitalization The proper name of Stonesense should be capitalized when its usage does not refer to the case-sensitive command `stonesense` --- docs/plugins/stonesense.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 57eccee1a2..23fbaf2b6f 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -15,7 +15,7 @@ Usage Open the visualiser in a new window. The viewer window has read-only access to the game, and can follow the game view -or be moved independently. Configuration for stonesense can be set in the +or be moved independently. Configuration for Stonesense can be set in the ``dfhack-config/stonesense/init.txt`` file in your DF game directory. .. figure:: ../images/stonesense-roadtruss.jpg @@ -53,14 +53,14 @@ views, fog, and rotation. Here's the important section: :end-before: VALID ACTIONS: -Streaming stonesense on Windows +Streaming Stonesense on Windows ------------------------------- -If you wish to stream stonesense thru a broadcasting software such as `OBS `_ -then you may find that opening stonesense causes your main DF window to flicker -between DF and stonesense. While it is unknown exactly what causes this, a fix -does exist. Simply make sure that both DF and stonesense are using ``Window Capture`` +If you wish to stream Stonesense thru a broadcasting software such as `OBS `_ +then you may find that opening Stonesense causes your main DF window to flicker +between DF and Stonesense. While it is unknown exactly what causes this, a fix +does exist. Simply make sure that both DF and Stonesense are using ``Window Capture`` and NOT ``Game Capture``. This will stop the flickering from happening and enable -you to stream stonesense for all to enjoy. This has been tested in OBS on Windows 10 but +you to stream Stonesense for all to enjoy. This has been tested in OBS on Windows 10 but should work on Windows 11 and in `Streamlabs `_. Linux, having no ``Game Capture`` option should be unaffected by this issue. From 83e476d6542c7797dfd62d4986ecb64b6ba1f604 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 8 Mar 2025 13:53:36 -0600 Subject: [PATCH 0178/1082] Update follow mode stuff --- docs/plugins/stonesense.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 23fbaf2b6f..88fc2f40c7 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -33,10 +33,9 @@ Mouse controls are hard-coded and cannot be changed. :Scrollwheel: Move up and down :Ctrl-Scroll: Increase/decrease Z depth shown -Follow mode makes the Stonesense view follow the location of the DF -window. The offset can be adjusted by holding :kbd:`Ctrl` while using the -keyboard window movement keys. When you turn on cursor follow mode, the -Stonesense debug cursor will follow the DF cursor when the latter exists. +Follow mode makes the Stonesense view follow the location and zoom level of the DF +window. The offset can be adjusted by holding :kbd:`Alt` while using the +keyboard window movement keys. You can take screenshots with :kbd:`F5`, larger screenshots with :kbd:`Ctrl`:kbd:`F5`, and screenshot the whole map at full resolution with From da2762982ca3ae7ca69505af3cd02518f14b02f8 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 8 Mar 2025 13:59:01 -0600 Subject: [PATCH 0179/1082] Add some more known issues --- docs/plugins/stonesense.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 88fc2f40c7..7127dc71b4 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -70,6 +70,13 @@ If Stonesense gives an error saying that it can't load detail sprites used. Either open :file:`creatures/init.txt` and remove the line containing that folder, or :dffd:`use these smaller sprites <6096>`. +Sometimes if you have opened Stonesense and then resize the DF window, DF will appear to be +unresponsive. This bug is graphical only and if you hit :kbd:`Ctrl`:kbd:`Alt`:kbd:`S` and wait +a minute or so (since you can't see when the game finishes saving) the game should quicksave. + +If you have Stonesense open in a fort and want to load a new fort, you MUST close Stonesense before +loading the new fort or the game will crash. + Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. From 7bbca5f3915816d5404aa3c404dd97478cc2b108 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 01:06:49 -0800 Subject: [PATCH 0180/1082] redesign aquifer tap for better fill rate and update walkthrough --- data/blueprints/aquifer_tap.csv | 96 +++++++++++++++------------------ 1 file changed, 42 insertions(+), 54 deletions(-) diff --git a/data/blueprints/aquifer_tap.csv b/data/blueprints/aquifer_tap.csv index b6675cee28..dc79e523d4 100644 --- a/data/blueprints/aquifer_tap.csv +++ b/data/blueprints/aquifer_tap.csv @@ -7,9 +7,9 @@ Here's the procedure: "" 2) Dig a one-tile-wide tunnel from where you want the water to end up (e.g. your well cistern) to an area on the same z-level directly below the target light aquifer. Dig a one-tile-wide diagonal segment in this tunnel near the cistern side so water that will eventually flow through the tunnel is depressurized. "" -"3) Pause the game. From the end of that tunnel, go down one z-level, enable damp dig mode in the dig toolbar, then designate for digging a staircase straight up so that the top is in the lowest aquifer level (a tile with a two-drop icon). If you only have one layer of aquifer, you should end the staircase one level below the aquifer so when we dig the tap, it will extend up into the aquifer level. Your tunnel should connect to the staircase one z-level above the bottom of the staircase." +"3) Pause the game. From the end of that tunnel, go down one z-level then designate for digging a staircase straight up so that the top is in the lowest aquifer level (a tile with a two-drop icon). Your original tunnel should connect to the staircase one z-level above the bottom of the staircase." "" -"4) Apply this blueprint (gui/quickfort aquifer_tap /dig) to the z-level at the top of the staircase. The tiles will be designated in ""damp dig"" mode so your miners can dig it out without the damp tiles canceling the digging designations. This blueprint designates ramps for digging so two layers of aquifer can contribute to the water collector. It also changes the staircase tile below the tap to a ""blueprint"" tile so your miners don't dig the tap before your drainage tunnel is ready." +"4) Apply this blueprint (gui/quickfort aquifer_tap /dig) to the z-level at the top of the staircase. The tiles will be designated in ""damp dig"" mode so your miners can dig it out without the damp tiles canceling the digging designations. This blueprint also changes the staircase tile below the tap to a vanilla ""blueprint"" tile (shaded in blue) so your miners don't dig the tap before your drainage tunnel is ready." "" "5) You can now unpause the game. From the bottom of the staircase (the z-level below where the water will flow to your cisterns), dig a straight, one-tile wide tunnel to the closest edge of the map. This is your emergency drainage tunnel. Smooth the map edge tile and carve a fortification. The water can flow through the fortification and off the map, allowing the dwarves to dig out the aquifer tap without drowning." "" @@ -17,7 +17,7 @@ Here's the procedure: "" "7) If you want, haul away any boulders in the tunnels and/or smooth the tiles (e.g. mark them for dumping -- hotkey i-p -- and wait for them to be dumped). Enable prioritize in gui/control-panel to focus dwarves on dumping tasks and make it go faster. You won't be able to access any of this area once it fills up with water!" "" -"8) Convert the ""blueprint"" stairway tile to a regular up/down stair dig designation to allow your miners to dig out the tap. You can haul away any boulders and remove the ramps if you like. There is no rush. The water will safely flow down the staircase, through the open floodgate, down the drainage tunnel, and off the map as long as the floodgate is open." +"8) Convert the ""blueprint"" stairway tile to a regular up/down stair dig designation to allow your miners to dig out the tap. You can haul away any boulders if you like. There is no rush. The water will safely flow down the staircase, through the open floodgate, down the drainage tunnel, and off the map as long as the floodgate is open." "8b) Sometimes, DF gets into a bad state with mining designations and miners will refuse to dig the stairway tile. If this happens to you, enter mining mode, enable the keyboard cursor if it's not already enabled (hotkey: Alt-k), highlight the undug stair designation, and run dig-now here in gui/launcher. You might also have to do this for the down stair designation in the center of the aquifer tap. Your miners should be able to handle the rest without assistance." "9) Once everything is dug out and all dwarves are out of the waterways, close the floodgate. Your cisterns will fill with water. Since the waterway to your cisterns is depressurized (due to the diagonal tunnel you dug), the cisterns will stay forever full, but will not flood." @@ -32,54 +32,42 @@ i <- cistern outlet level with diagonal tunnel to depressurize "u <- up stairs, drainage level" "" "Good luck! If done right, this method is the safest way to supply your fort with clean water." -#dig label(dig) start(13 13 center of tap) light aquifer water collector -,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,mdr3,mdr3,mdr3,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,, -,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdj3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3, -,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,mdr3,mdr3,mdr3,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,, -#>,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,,,`,`,`,`,`,`,`,`,`,,,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,,,`,`,`,,,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,`,,,`,,,`,,,`,,,`,,, -,`,`,`,`,`,`,`,`,`,`,`,mbmdi3,`,`,`,`,`,`,`,`,`,`,`, -,,,`,,,`,,,`,,,`,,,`,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,,,`,`,`,,,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,,,`,`,`,`,`,`,`,`,`,,,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, +#dig label(dig) start(10 10 center of tap) light aquifer water collector +,,,,,,,,,,,,,,,,,, +,,,,,,,,mdd3,mdd3,mdd3,,,,,,,, +,,,,,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,,,,, +,,,,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,,,, +,,,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,,, +,,mdd3,,,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,,,mdd3,, +,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,, +,,,mdd3,mdd3,,,mdd3,mdd3,,mdd3,mdd3,,,mdd3,mdd3,,, +,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3, +,mdd3,mdd3,,,mdd3,mdd3,,,mdj3,,,mdd3,mdd3,,,mdd3,mdd3, +,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3, +,,,mdd3,mdd3,,,mdd3,mdd3,,mdd3,mdd3,,,mdd3,mdd3,,, +,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,, +,,mdd3,,,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,,,mdd3,, +,,,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,,, +,,,,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,,,, +,,,,,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,,,,, +,,,,,,,,mdd3,mdd3,mdd3,,,,,,,, +,,,,,,,,,,,,,,,,,, +#>,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,, +,,,,,,,,`,`,`,,,,,,,, +,,,,,,`,`,`,,`,`,`,,,,,, +,,,,`,,`,`,`,,`,`,`,,`,,,, +,,,`,`,`,`,,`,`,`,,`,`,`,`,,, +,,`,,,`,`,,`,`,`,,`,`,,,`,, +,,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,, +,,,`,`,,,`,`,,`,`,,,`,`,,, +,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`, +,`,`,,,`,`,,,mbmdi3,,,`,`,,,`,`, +,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`, +,,,`,`,,,`,`,,`,`,,,`,`,,, +,,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,, +,,`,,,`,`,,`,`,`,,`,`,,,`,, +,,,`,`,`,`,,`,`,`,,`,`,`,`,,, +,,,,`,,`,`,`,,`,`,`,,`,,,, +,,,,,,`,`,`,,`,`,`,,,,,, +,,,,,,,,`,`,`,,,,,,,, From 72f8afcff89a52f1d43a4423d7eee9ebdcc08308 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 01:07:08 -0800 Subject: [PATCH 0181/1082] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index c883d4868b..84cf4e6eaf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -71,6 +71,7 @@ Template for new versions: - `spectate`: player-set configuration is now stored globally instead of per-fort - `autobutcher`: treat animals on restraints as unavailable for slaughter - `stockpiles`: add property filters for brewable, millable, and processable (e.g. at a Farmer's workshop) organic materials +- `quickfort`: redesigned ``library/aquifer_tap.cav`` to improve the water fill rate ## Documentation - `stonesense-art-guide`: guide for making sprite art for Stonesense From 0c5484e5a0ff56648fa7272227f03a000ad496ff Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 9 Mar 2025 07:13:54 +0000 Subject: [PATCH 0182/1082] 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 675aee7a6d..82088731a7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 675aee7a6de151261f83e45049bad0fcfa7bb92c +Subproject commit 82088731a7843973582fd1e1fd7d8ceb6f21b2a6 From 48fdd07c4a5565daf83bd300758143784969ce9f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 11 Mar 2025 01:13:01 +0000 Subject: [PATCH 0183/1082] 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 82088731a7..8087647e3d 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 82088731a7843973582fd1e1fd7d8ceb6f21b2a6 +Subproject commit 8087647e3daf50ddb479fcfba51dd2702525673f From 2b1a36e4e9761c07b0b420afd7965593284df9f0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Mar 2025 19:07:41 -0700 Subject: [PATCH 0184/1082] don't show suspend overlay when other ui panels are up --- docs/changelog.txt | 1 + plugins/lua/suspendmanager.lua | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 84cf4e6eaf..73329f7d31 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself - `overlay`: reset draw context between rendering widgets so context changes can't propagate from widget to widget +- `suspendmanager`: in ASCII mode, building planning mode overlay now only displays when viewing the default map, reducing issues with showing through the UI ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort diff --git a/plugins/lua/suspendmanager.lua b/plugins/lua/suspendmanager.lua index 594fb31155..c15be19402 100644 --- a/plugins/lua/suspendmanager.lua +++ b/plugins/lua/suspendmanager.lua @@ -140,12 +140,18 @@ if not ok then buildingplan = nil end +local function show_suspend_overlay() + return dfhack.screen.inGraphicsMode() or + dfhack.gui.matchFocusString('dwarfmode/Default', dfhack.gui.getDFViewscreen(true)) +end + SuspendOverlay = defclass(SuspendOverlay, overlay.OverlayWidget) SuspendOverlay.ATTRS{ desc='Annotates suspended buildings with a visible marker.', viewscreens='dwarfmode', default_enabled=true, frame={w=0, h=0}, + visible=show_suspend_overlay, overlay_onupdate_max_freq_seconds=30, } From b719eefe28cdcc488ed73a2f9397ae1c9dd16f7e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 11 Mar 2025 11:05:00 -0700 Subject: [PATCH 0185/1082] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 2f1873d55f..d399dd3227 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 2f1873d55fb4b7780fa02acdb0ff30569551b0d1 +Subproject commit d399dd322781b3f3c453d6d6c7f8cc196cdbffc5 From beb1b23e89cc66ac169d7e7fd8975f849c341910 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:11:55 +0000 Subject: [PATCH 0186/1082] Auto-update structures ref for 51.07 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index e97f8d46fe..19dbe08f62 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e97f8d46febe9156ef737eff63ffc8c09a0ad26c +Subproject commit 19dbe08f627aa1d0143b81b12f308873b34c6ec6 From 85d385c70cb5e8e2b427598441f75e329f2a41f3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Mar 2025 10:04:25 -0700 Subject: [PATCH 0187/1082] update version to 51.07 --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 22f04fe085..e67b695b4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.06") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DF_VERSION "51.07") +set(DFHACK_RELEASE "r1") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 7a449436d52b32ce33c95b89a29c8f8aae6daaab Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Mar 2025 10:10:12 -0700 Subject: [PATCH 0188/1082] update changelog and refs --- docs/changelog.txt | 16 ++++++++++++++++ library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 73329f7d31..bf4eb7d112 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -53,6 +53,22 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 51.07-r1 + ## New Features - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict diff --git a/library/xml b/library/xml index 19dbe08f62..a5286b3d70 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 19dbe08f627aa1d0143b81b12f308873b34c6ec6 +Subproject commit a5286b3d70249abe4d45d9f707b826ec306a1c33 diff --git a/plugins/stonesense b/plugins/stonesense index 8087647e3d..93496b510f 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 8087647e3daf50ddb479fcfba51dd2702525673f +Subproject commit 93496b510fcfe691f8baeb54d885a8ac811cae1e diff --git a/scripts b/scripts index d399dd3227..90431556c4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d399dd322781b3f3c453d6d6c7f8cc196cdbffc5 +Subproject commit 90431556c4c2924c206403232ea1c24491355d07 From 7cf6799bbb919e2196e57dc06cfd13e71c0dc9e1 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:28:12 +0000 Subject: [PATCH 0189/1082] Auto-update structures ref for 51.08 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a5286b3d70..860017b193 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a5286b3d70249abe4d45d9f707b826ec306a1c33 +Subproject commit 860017b193c5cce1ee6c16321301b0a1d4788569 From 487df2ce4ad051015ea75e764f59372b1738b54c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 13 Mar 2025 07:55:32 -0700 Subject: [PATCH 0190/1082] update version --- CMakeLists.txt | 2 +- docs/changelog.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e67b695b4d..11e95c6d8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.07") +set(DF_VERSION "51.08") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf4eb7d112..6327a843b7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,11 @@ Template for new versions: ## Removed +# 51.08-r1 + +## Misc Improvements +- Compatibility update for DF 51.08 + # 51.07-r1 ## New Features From 05a35ed66a89ef2bf843c56fda75d1145fe85af0 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Mar 2025 14:46:14 +0100 Subject: [PATCH 0191/1082] spectate.lua: display unit activity if no job --- plugins/lua/spectate.lua | 52 ++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index de005ae784..fd2f736d0e 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -29,6 +29,7 @@ local function get_default_state() ['tooltip-follow-blink-milliseconds']=3000, ['tooltip-follow-hold-to-show']='none', -- one of none, ctrl, alt, or shift ['tooltip-follow-job']=true, + ['tooltip-follow-activity']=true, ['tooltip-follow-job-shortenings'] = { ["Store item in stockpile"] = "Store item", }, @@ -45,6 +46,7 @@ local function get_default_state() }, ['tooltip-hover']=true, ['tooltip-hover-job']=true, + ['tooltip-hover-activity']=true, ['tooltip-hover-name']=true, ['tooltip-hover-stress']=true, ['tooltip-hover-stress-levels']={ @@ -285,33 +287,59 @@ local function GetUnitJob(unit) return job and dfhack.job.getName(job) end +-- there is no equivalent of `dfhack.job.GetName` yet +-- this function is by no means a replacement, it's +-- here to show something at least +local activityNames = { + [df.activity_entry_type.TrainingSession] = "Training Session", + [df.activity_entry_type.IndividualSkillDrill] = "Individual Skill Drill", + [df.activity_entry_type.FillServiceOrder] = "Fill Service Order", + [df.activity_entry_type.StoreObject] = "Store Object", + -- other types are single words +} +local function activity_GetName(activity) + local t = activity.type + local n = df.activity_entry_type[t] + n = activityNames[n] or n + return {text = n, pen = COLOR_LIGHTGREEN} +end + +local function GetUnitActivity(unit) + local activity = dfhack.units.getMainSocialActivity(unit) + return activity and activity_GetName(activity) +end + local function GetRelevantSettings(key) return config['tooltip-' .. key .. '-name'], config['tooltip-' .. key .. '-job'], + config['tooltip-' .. key .. '-activity'], config['tooltip-' .. key .. '-stress'], config['tooltip-' .. key .. '-stress-levels'], config['tooltip-' .. key .. '-job-shortenings'] end local function GetUnitInfoText(unit, settings_group_name) - local show_name, show_job, show_stress, stress_levels, job_shortenings = GetRelevantSettings(settings_group_name) + local show_name, show_job, show_activity, show_stress, stress_levels, job_shortenings = GetRelevantSettings(settings_group_name) local stress = show_stress and GetUnitStress(unit, stress_levels) or nil local name = show_name and GetUnitName(unit) or nil local job = show_job and GetUnitJob(unit) or nil if job_shortenings then job = job_shortenings[job] or job end + local activity = show_activity and GetUnitActivity(unit) or nil + + local job_or_activity = job or activity local txt = {} if stress then txt[#txt+1] = stress - if name or job then txt[#txt+1] = ' ' end + if name or job_or_activity then txt[#txt+1] = ' ' end end if name then txt[#txt+1] = name end - if job then + if job_or_activity then if name then txt[#txt+1] = ": " end - txt[#txt+1] = job + txt[#txt+1] = job_or_activity end return txt @@ -364,12 +392,6 @@ function TooltipOverlay:render(dc) TooltipOverlay.super.render(self, dc) end -local function AnyFollowOptionOn() - return config['tooltip-follow-job'] - or config['tooltip-follow-name'] - or config['tooltip-follow-stress'] -end - -- map coordinates -> interface layer coordinates local function GetScreenCoordinates(map_coord) -- -> map viewport offset @@ -415,7 +437,7 @@ local function GetString(tokens) end function TooltipOverlay:render_unit_banners(dc) - if not (config['tooltip-follow'] and AnyFollowOptionOn()) then return end + if not config['tooltip-follow'] then return end local hold_to_show = config['tooltip-follow-hold-to-show'] if hold_to_show and hold_to_show ~= 'none' then @@ -570,14 +592,8 @@ function MouseTooltip:init() } end -local function AnyHoverOptionOn() - return config['tooltip-hover-job'] - or config['tooltip-hover-name'] - or config['tooltip-hover-stress'] -end - function MouseTooltip:render(dc) - if not (config['tooltip-hover'] and AnyHoverOptionOn()) then return end + if not config['tooltip-hover'] then return end local x, y = dfhack.screen.getMousePos() if not x then return end From 3a89f6120ea724073584d52ed9e4e8e16057c038 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Mar 2025 15:35:23 +0100 Subject: [PATCH 0192/1082] update docs --- docs/plugins/spectate.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index e57456ac83..68d1013f64 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -123,6 +123,10 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. +``tooltip-follow-activity`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + activity of the dwarf in the tooltip. + ``tooltip-follow-job-shortenings`` (default: "Store item in stockpile" -> "Store item") If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to shorten some job names, f.e. "Store item in stockpile" becomes "Store item". @@ -151,6 +155,10 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the hover panel. +``tooltip-hover-activity`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + activity of the dwarf in the hover panel. + ``tooltip-hover-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the hover panel. From 1058f839e5cb71c7cd1961328c8fc5f177387ba0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 09:27:42 -0500 Subject: [PATCH 0193/1082] use `std::filesystem::path` Recode most uses of `std::string` to represent a path name to use `std::filesystem::path` instead. Eliminate most of old crunchy filesystem module with `std::filesystem` equivalent methods some modules have also cleaned to remove `using filesystem std` and/or missing or unnecessary `#include`s note: the `atime`, `ctime`, and `filetype` methods of the `Filesystem` module have been removed because they're POSIX-specific, not supported by `std::filesystem`, and unused in DFHack --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 6 - library/Core.cpp | 129 +++++++------- library/LuaApi.cpp | 25 ++- library/PlugLoad.cpp | 6 +- library/PluginManager.cpp | 60 +++---- library/Process-linux.cpp | 2 +- library/Process-windows.cpp | 4 +- library/Types.cpp | 14 +- library/include/Console.h | 5 +- library/include/Core.h | 8 +- library/include/MemAccess.h | 3 +- library/include/PluginManager.h | 6 +- library/include/Types.h | 4 +- library/include/modules/Filesystem.h | 122 ++----------- library/modules/Filesystem.cpp | 251 ++++++++++----------------- library/modules/Persistence.cpp | 20 +-- plugins/orders.cpp | 24 +-- 18 files changed, 253 insertions(+), 437 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6327a843b7..ed65524207 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: ## Documentation ## API +- `Filesystem` module: rewritten to use C++ standard library components, for better portability ## Lua diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 0a2fdc894a..d449c8810b 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3168,12 +3168,6 @@ unless otherwise noted. specified by ``path``, or -1 if ``path`` does not exist. This depends on the system clock and should only be used locally. -* ``dfhack.filesystem.atime(path)`` -* ``dfhack.filesystem.ctime(path)`` - - Return values vary across operating systems - return the ``st_atime`` and - ``st_ctime`` fields of a C++ stat struct, respectively. - * ``dfhack.filesystem.listdir(path)`` Lists files/directories in a directory. Returns ``{}`` if ``path`` does not exist. diff --git a/library/Core.cpp b/library/Core.cpp index be53fe58fd..c4c611ee9d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -83,8 +83,14 @@ distribution. #include #include #include +#include #include +#ifdef _WIN32 +#define NOMINMAX +#include +#endif + #ifdef LINUX_BUILD #include #endif @@ -98,15 +104,15 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = NULL); -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder); +size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder); namespace DFHack { DBG_DECLARE(core,keybinding,DebugCategory::LINFO); DBG_DECLARE(core,script,DebugCategory::LINFO); -static const std::string CONFIG_PATH = "dfhack-config/"; -static const std::string CONFIG_DEFAULTS_PATH = "hack/data/dfhack-config-defaults/"; +static const std::filesystem::path CONFIG_PATH{ std::filesystem::path{} / "dfhack-config" }; +static const std::filesystem::path CONFIG_DEFAULTS_PATH{ std::filesystem::path{} / "hack" / "data" / "dfhack-config-defaults" }; class MainThread { public: @@ -511,33 +517,33 @@ bool Core::removeScriptPath(std::string path) return found; } -void Core::getScriptPaths(std::vector *dest) +void Core::getScriptPaths(std::vector *dest) { std::lock_guard lock(script_path_mutex); dest->clear(); - std::string df_path = this->p->getPath() + "/"; + std::filesystem::path df_path = this->p->getPath(); for (auto & path : script_paths[0]) dest->emplace_back(path); - dest->push_back(df_path + CONFIG_PATH + "scripts"); + dest->push_back(df_path / CONFIG_PATH / "scripts"); if (df::global::world && isWorldLoaded()) { std::string save = World::ReadWorldFolder(); if (save.size()) - dest->emplace_back(df_path + "save/" + save + "/scripts"); + dest->emplace_back(df_path / "save" / save / "scripts"); } - dest->emplace_back(df_path + "hack/scripts"); + dest->emplace_back(df_path / "hack" / "scripts"); for (auto & path : script_paths[2]) dest->emplace_back(path); for (auto & path : script_paths[1]) dest->emplace_back(path); } -std::string Core::findScript(std::string name) +std::filesystem::path Core::findScript(std::string name) { - std::vector paths; + std::vector paths; getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { - std::string path = *it + "/" + name; + std::filesystem::path path = *it / name; if (Filesystem::isfile(path)) return path; } @@ -546,9 +552,8 @@ std::string Core::findScript(std::string name) bool loadScriptPaths(color_ostream &out, bool silent = false) { - using namespace std; - std::string filename(CONFIG_PATH + "script-paths.txt"); - ifstream file(filename); + std::filesystem::path filename{ CONFIG_PATH / "script-paths.txt" }; + std::ifstream file(filename); if (!file) { if (!silent) @@ -560,12 +565,12 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) while (getline(file, raw)) { ++line; - istringstream ss(raw); + std::istringstream ss(raw); char ch; - ss >> skipws; + ss >> std::skipws; if (!(ss >> ch) || ch == '#') continue; - ss >> ws; // discard whitespace + ss >> std::ws; // discard whitespace std::string path; getline(ss, path); if (ch == '+' || ch == '-') @@ -857,8 +862,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if(!plug) { - std::string lua = findScript(part + ".lua"); - if (lua.size()) + std::filesystem::path lua = findScript(part + ".lua"); + if (!lua.empty()) { res = enableLuaScript(con, part, enable); } @@ -961,7 +966,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } con << parts[0]; bool builtin = is_builtin(con, parts[0]); - std::string lua_path = findScript(parts[0] + ".lua"); + std::filesystem::path lua_path = findScript(parts[0] + ".lua"); Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); if (builtin) { @@ -976,7 +981,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { con << " is a command implemented by the plugin " << plug->getName() << std::endl; } - else if (lua_path.size()) + else if (!lua_path.empty()) { con << " is a Lua script: " << lua_path << std::endl; } @@ -1123,7 +1128,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if(parts.size() == 1) { - loadScriptFile(con, parts[0], false); + loadScriptFile(con, std::filesystem::canonical(std::filesystem::path{parts[0]}), false); } else { @@ -1286,8 +1291,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else if (res == CR_NOT_IMPLEMENTED) { std::string completed; - std::string filename = findScript(first + ".lua"); - bool lua = filename != ""; + std::filesystem::path filename = findScript(first + ".lua"); + bool lua = !filename.empty(); if ( !lua ) { filename = findScript(first + ".rb"); } @@ -1330,17 +1335,17 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s return CR_OK; } -bool Core::loadScriptFile(color_ostream &out, std::string fname, bool silent) +bool Core::loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent) { if(!silent) { INFO(script,out) << "Running script: " << fname << std::endl; std::cerr << "Running script: " << fname << std::endl; } - std::ifstream script(fname.c_str()); + std::ifstream script(fname); if ( !script.good() ) { if(!silent) - out.printerr("Error loading script: %s\n", fname.c_str()); + out.printerr("Error loading script: %s\n", fname.string().c_str()); return false; } std::string command; @@ -1379,11 +1384,11 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // load baseline defaults - core->loadScriptFile(out, CONFIG_PATH + "init/default.dfhack.init", false); + core->loadScriptFile(out, CONFIG_PATH / "init" / "default.dfhack.init", false); // load user overrides std::vector prefixes(1, "dfhack"); - loadScriptFiles(core, out, prefixes, CONFIG_PATH + "init"); + loadScriptFiles(core, out, prefixes, CONFIG_PATH / "init"); // show the terminal if requested auto L = DFHack::Core::getInstance().getLuaState(); @@ -1407,7 +1412,7 @@ void fInitthread(void * iodata) // A thread function... for the interactive console. void fIOthread(void * iodata) { - static const std::string HISTORY_FILE = CONFIG_PATH + "dfhack.history"; + static const std::filesystem::path HISTORY_FILE = CONFIG_PATH / "dfhack.history"; IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; @@ -1455,7 +1460,7 @@ void fIOthread(void * iodata) { // a proper, non-empty command was entered main_history.add(command); - main_history.save(HISTORY_FILE.c_str()); + main_history.save(HISTORY_FILE); } auto rv = core->runCommand(con, command); @@ -1534,13 +1539,9 @@ void Core::fatal (std::string output, const char * title) } } -std::string Core::getHackPath() +std::filesystem::path Core::getHackPath() { -#ifdef LINUX_BUILD - return p->getPath() + "/hack/"; -#else - return p->getPath() + "\\hack\\"; -#endif + return p->getPath() / "hack"; } df::viewscreen * Core::getTopViewscreen() { @@ -1781,8 +1782,8 @@ bool Core::InitSimulationThread() con.printerr("Failed to create config directory: '%s'\n", CONFIG_PATH.c_str()); // copy over default config files if necessary - std::map config_files; - std::map default_config_files; + std::map config_files; + std::map default_config_files; if (Filesystem::listdir_recursive(CONFIG_PATH, config_files, 10, false) != 0) con.printerr("Failed to list directory: '%s'\n", CONFIG_PATH.c_str()); else if (Filesystem::listdir_recursive(CONFIG_DEFAULTS_PATH, default_config_files, 10, false) != 0) @@ -1794,7 +1795,7 @@ bool Core::InitSimulationThread() // skip over files if (!entry.second) continue; - std::string dirname = CONFIG_PATH + entry.first; + std::filesystem::path dirname = CONFIG_PATH / entry.first; if (!Filesystem::mkdir_recursive(dirname)) con.printerr("Failed to create config directory: '%s'\n", dirname.c_str()); } @@ -1804,12 +1805,12 @@ bool Core::InitSimulationThread() // skip over directories if (entry.second) continue; - std::string filename = entry.first; + std::filesystem::path filename = entry.first; if (!config_files.count(filename)) { - std::string src_file = CONFIG_DEFAULTS_PATH + filename; + std::filesystem::path src_file = CONFIG_DEFAULTS_PATH / filename; if (!Filesystem::isfile(src_file)) continue; - std::string dest_file = CONFIG_PATH + filename; + std::filesystem::path dest_file = CONFIG_PATH / filename; std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); if (!src.good() || !dest.good()) { @@ -2185,42 +2186,30 @@ void Core::onUpdate(color_ostream &out) perf_counters.incCounter(perf_counters.update_lua_ms, step_start_ms); } -void getFilesWithPrefixAndSuffix(const std::string& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { - std::vector files; +void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { + std::vector files; DFHack::Filesystem::listdir(folder, files); - for ( size_t a = 0; a < files.size(); a++ ) { - if ( prefix.length() > files[a].length() ) - continue; - if ( suffix.length() > files[a].length() ) - continue; - if ( files[a].compare(0, prefix.length(), prefix) != 0 ) - continue; - if ( files[a].compare(files[a].length()-suffix.length(), suffix.length(), suffix) != 0 ) - continue; - result.push_back(files[a]); + for ( auto f : files) { + if (f.stem().string().starts_with(prefix) && f.extension() == suffix) + result.push_back(f); } return; } -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder) { +size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder) { static const std::string suffix = ".init"; - std::vector scriptFiles; + std::vector scriptFiles; for ( size_t a = 0; a < prefix.size(); a++ ) { getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles); } std::sort(scriptFiles.begin(), scriptFiles.end(), - [&](const std::string &a, const std::string &b) { - std::string a_base = a.substr(0, a.size() - suffix.size()); - std::string b_base = b.substr(0, b.size() - suffix.size()); - return a_base < b_base; + [&](const std::filesystem::path &a, const std::filesystem::path &b) { + return a < b; }); size_t result = 0; for ( size_t a = 0; a < scriptFiles.size(); a++ ) { result++; - std::string path = ""; - if (folder != ".") - path = folder + "/"; - core->loadScriptFile(out, path + scriptFiles[a], false); + core->loadScriptFile(out, folder / scriptFiles[a], false); } return result; } @@ -2272,16 +2261,16 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve if (!df::global::world) return; - std::string rawFolder = !isWorldLoaded() ? "" : "save/" + World::ReadWorldFolder() + "/init"; + std::filesystem::path rawFolder = !isWorldLoaded() ? std::filesystem::path{} : std::filesystem::path{} / "save" / World::ReadWorldFolder() / "init"; auto i = table.find(event); if ( i != table.end() ) { const std::vector& set = i->second; // load baseline defaults - this->loadScriptFile(out, CONFIG_PATH + "init/default." + set[0] + ".init", false); + this->loadScriptFile(out, CONFIG_PATH / "init" / ("default." + set[0] + ".init"), false); - loadScriptFiles(this, out, set, CONFIG_PATH + "init"); + loadScriptFiles(this, out, set, CONFIG_PATH / "init"); loadScriptFiles(this, out, set, rawFolder); } @@ -2295,7 +2284,7 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve } else if (it->save_specific && isWorldLoaded()) { - loadScriptFile(out, rawFolder + it->path, false); + loadScriptFile(out, rawFolder / it->path, false); } } } @@ -2366,7 +2355,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) strftime(timebuf, sizeof(timebuf), "[%Y-%m-%dT%H:%M:%S%z] ", timeinfo); evtlog << timebuf; evtlog << "DFHack " << Version::git_description() << " on " << ostype << "; "; - evtlog << "cwd md5: " << md5w.getHashFromString(getHackPath()).substr(0, 10) << "; "; + evtlog << "cwd md5: " << md5w.getHashFromString(getHackPath().string().c_str()).substr(0, 10) << "; "; evtlog << "save: " << world->cur_savegame.save_dir << "; "; evtlog << sc_event_name(event) << "; "; if (gametype) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 637df32211..d2162a33d1 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -115,6 +115,7 @@ distribution. #include #include #include +#include namespace DFHack { DBG_DECLARE(core, luaapi, DebugCategory::LINFO); @@ -1351,8 +1352,8 @@ static string getArchitectureName() static string getDFVersion() { return Core::getInstance().vinfo->getVersion(); } static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); } -static string getDFPath() { return Core::getInstance().p->getPath(); } -static string getHackPath() { return Core::getInstance().getHackPath(); } +static std::filesystem::path getDFPath() { return Core::getInstance().p->getPath(); } +static std::filesystem::path getHackPath() { return Core::getInstance().getHackPath(); } static bool isWorldLoaded() { return Core::getInstance().isWorldLoaded(); } static bool isMapLoaded() { return Core::getInstance().isMapLoaded(); } @@ -3153,8 +3154,6 @@ static const LuaWrapper::FunctionReg dfhack_filesystem_module[] = { WRAPM(Filesystem, exists), WRAPM(Filesystem, isfile), WRAPM(Filesystem, isdir), - WRAPM(Filesystem, atime), - WRAPM(Filesystem, ctime), WRAPM(Filesystem, mtime), {NULL, NULL} }; @@ -3163,7 +3162,7 @@ static int filesystem_listdir(lua_State *L) { luaL_checktype(L,1,LUA_TSTRING); string dir=lua_tostring(L,1); - vector files; + vector files; int err = DFHack::Filesystem::listdir(dir, files); if (err) { @@ -3176,7 +3175,7 @@ static int filesystem_listdir(lua_State *L) for(size_t i=0;i= 3 && !lua_isnil(L, 3)) include_prefix = lua_toboolean(L, 3); - std::map files; + std::map files; int err = DFHack::Filesystem::listdir_recursive(dir, files, depth, include_prefix); if (err != 0 && err != -1) { lua_pushnil(L); @@ -3207,7 +3206,7 @@ static int filesystem_listdir_recursive(lua_State *L) lua_pushinteger(L, i++); lua_newtable(L); lua_pushstring(L, "path"); - lua_pushstring(L, (it->first).c_str()); + lua_pushstring(L, (it->first).string().c_str()); lua_settable(L, -3); lua_pushstring(L, "isdir"); lua_pushboolean(L, it->second); @@ -3947,12 +3946,12 @@ static int internal_getScriptPaths(lua_State *L) { int i = 1; lua_newtable(L); - vector paths; + vector paths; Core::getInstance().getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { lua_pushinteger(L, i++); - lua_pushstring(L, it->c_str()); + lua_pushstring(L, it->string().c_str()); lua_settable(L, -3); } return 1; @@ -3961,9 +3960,9 @@ static int internal_getScriptPaths(lua_State *L) static int internal_findScript(lua_State *L) { const char *name = luaL_checkstring(L, 1); - string path = Core::getInstance().findScript(name); - if (path.size()) - lua_pushstring(L, path.c_str()); + std::filesystem::path path = Core::getInstance().findScript(name); + if (!path.empty()) + lua_pushstring(L, path.string().c_str()); else lua_pushnil(L); return 1; diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 059e6aef95..1cafc791f5 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -18,7 +18,7 @@ #define global_search_handle() GetModuleHandle(nullptr) #define get_function_address(plugin, function) GetProcAddress((HMODULE)plugin, function) #define clear_error() -#define load_library(fn) LoadLibrary(fn) +#define load_library(fn) LoadLibraryW(fn.c_str()) #define close_library(handle) (!(FreeLibrary((HMODULE)handle))) #else #include @@ -33,7 +33,7 @@ #define global_search_handle() (RTLD_DEFAULT) #define get_function_address(plugin, function) dlsym((void*)plugin, function) #define clear_error() dlerror() -#define load_library(fn) dlopen(fn, RTLD_NOW | RTLD_LOCAL); +#define load_library(fn) dlopen(fn.c_str(), RTLD_NOW | RTLD_LOCAL); #define close_library(handle) dlclose((void*)handle) #endif @@ -71,7 +71,7 @@ namespace DFHack } } - DFLibrary * OpenPlugin (const char * filename) + DFLibrary * OpenPlugin (std::filesystem::path filename) { clear_error(); DFLibrary* ret = (DFLibrary*)load_library(filename); diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 458c0377c2..98a9eaeec4 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -48,7 +48,8 @@ using namespace DFHack; #include #include #include -using namespace std; + +using std::string; #include @@ -60,14 +61,14 @@ using namespace std; static const string plugin_suffix = ".plug.dll"; #endif -static string getPluginPath() +static std::filesystem::path getPluginPath() { - return Core::getInstance().getHackPath() + "plugins/"; + return Core::getInstance().getHackPath() / "plugins"; } -static string getPluginPath (std::string name) +static std::filesystem::path getPluginPath (std::string name) { - return getPluginPath() + name + plugin_suffix; + return getPluginPath() / (name + plugin_suffix); } struct Plugin::RefLock @@ -175,7 +176,7 @@ struct Plugin::LuaEvent : public Lua::Event::Owner { } }; -Plugin::Plugin(Core * core, const std::string & path, +Plugin::Plugin(Core * core, const std::filesystem::path& path, const std::string &name, PluginManager * pm) :path(path), name(name), @@ -246,7 +247,7 @@ bool Plugin::load(color_ostream &con) CoreSuspender suspend; // open the library, etc fprintf(stderr, "loading plugin %s\n", name.c_str()); - DFLibrary * plug = OpenPlugin(path.c_str()); + DFLibrary * plug = OpenPlugin(path); if(!plug) { RefAutolock lock(access); @@ -311,8 +312,8 @@ bool Plugin::load(color_ostream &con) { std::string msg = stl_sprintf("Warning: Plugin %s compiled for DFHack %s, running DFHack %s\n", *plug_name, plug_git_desc, dfhack_git_desc); - con << msg << flush; - cerr << msg << flush; + con << msg << std::flush; + std::cerr << msg << std::flush; } } else @@ -895,7 +896,7 @@ bool PluginManager::addPlugin(string name) Core::printerr("Plugin already exists: %s\n", name.c_str()); return false; } - string path = getPluginPath(name); + std::filesystem::path path = getPluginPath(name); if (!Filesystem::isfile(path)) { Core::printerr("Plugin does not exist: %s\n", name.c_str()); @@ -906,16 +907,17 @@ bool PluginManager::addPlugin(string name) return true; } -vector PluginManager::listPlugins() +std::vector PluginManager::listPlugins() { - vector results; - vector files; + std::vector results; + std::vector files; Filesystem::listdir(getPluginPath(), files); for (auto file = files.begin(); file != files.end(); ++file) { - if (hasEnding(*file, plugin_suffix)) + string fname = file->filename().string(); + if (hasEnding(file->filename().string(), plugin_suffix)) { - string shortname = file->substr(0, file->find(plugin_suffix)); + string shortname = fname.substr(0, fname.find(plugin_suffix)); results.push_back(shortname); } } @@ -924,7 +926,7 @@ vector PluginManager::listPlugins() void PluginManager::refresh() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; auto files = listPlugins(); for (auto f = files.begin(); f != files.end(); ++f) { @@ -935,7 +937,7 @@ void PluginManager::refresh() bool PluginManager::load (const string &name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name] && !addPlugin(name)) return false; Plugin *p = (*this)[name]; @@ -949,7 +951,7 @@ bool PluginManager::load (const string &name) bool PluginManager::loadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; auto files = listPlugins(); bool ok = true; // load all plugins in hack/plugins @@ -963,7 +965,7 @@ bool PluginManager::loadAll() bool PluginManager::unload (const string &name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name]) { Core::printerr("Plugin does not exist: %s\n", name.c_str()); @@ -974,7 +976,7 @@ bool PluginManager::unload (const string &name) bool PluginManager::unloadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; bool ok = true; // only try to unload plugins that are in all_plugins for (auto it = begin(); it != end(); ++it) @@ -989,7 +991,7 @@ bool PluginManager::reload (const string &name) { // equivalent to "unload(name); load(name);" if plugin is recognized, // "load(name);" otherwise - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name]) return load(name); if (!unload(name)) @@ -999,7 +1001,7 @@ bool PluginManager::reload (const string &name) bool PluginManager::reloadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; bool ok = true; if (!unloadAll()) ok = false; @@ -1010,8 +1012,8 @@ bool PluginManager::reloadAll() Plugin *PluginManager::getPluginByCommand(const std::string &command) { - lock_guard lock{*cmdlist_mutex}; - map ::iterator iter = command_map.find(command); + std::lock_guard lock{*cmdlist_mutex}; + std::map ::iterator iter = command_map.find(command); if (iter != command_map.end()) return iter->second; else @@ -1059,8 +1061,8 @@ void PluginManager::OnStateChange(color_ostream &out, state_change_event event) void PluginManager::registerCommands( Plugin * p ) { - lock_guard lock{*cmdlist_mutex}; - vector & cmds = p->commands; + std::lock_guard lock{*cmdlist_mutex}; + std::vector & cmds = p->commands; for (size_t i = 0; i < cmds.size();i++) { std::string name = cmds[i].name; @@ -1076,8 +1078,8 @@ void PluginManager::registerCommands( Plugin * p ) void PluginManager::unregisterCommands( Plugin * p ) { - lock_guard lock{*cmdlist_mutex}; - vector & cmds = p->commands; + std::lock_guard lock{*cmdlist_mutex}; + std::vector & cmds = p->commands; for(size_t i = 0; i < cmds.size();i++) { command_map.erase(cmds[i].name); @@ -1126,7 +1128,7 @@ void PluginManager::doLoadSiteData(color_ostream &out) Plugin *PluginManager::operator[] (std::string name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (all_plugins.find(name) == all_plugins.end()) { if (Filesystem::isfile(getPluginPath(name))) diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index a3fa9f2d67..ce64cc113b 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -195,7 +195,7 @@ uint32_t Process::getTickCount() return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); } -string Process::getPath() +std::filesystem::path Process::getPath() { return Filesystem::get_initial_cwd(); } diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index 79bbea8002..fe8c5d591a 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -29,6 +29,7 @@ distribution. #include #include #include +#include #include "Error.h" #include "Internal.h" @@ -53,7 +54,6 @@ using std::vector; using std::endl; using std::cerr; - namespace DFHack { class PlatformSpecific @@ -382,7 +382,7 @@ uint32_t Process::getTickCount() return GetTickCount(); } -string Process::getPath() +std::filesystem::path Process::getPath() { HMODULE hmod; DWORD junk; diff --git a/library/Types.cpp b/library/Types.cpp index ffed86eb59..1dab657c1c 100644 --- a/library/Types.cpp +++ b/library/Types.cpp @@ -33,24 +33,16 @@ distribution. #include "df/general_ref.h" #include "df/specific_ref.h" -#ifndef LINUX_BUILD - #include - #include "wdirent.h" -#else - #include - #include - #include - #include -#endif - #include #include +#include #include #include +#include -int DFHack::getdir(std::string dir, std::vector &files) +int DFHack::getdir(std::filesystem::path dir, std::vector &files) { return DFHack::Filesystem::listdir(dir, files); } diff --git a/library/include/Console.h b/library/include/Console.h index a4ca8c5452..4397c75954 100644 --- a/library/include/Console.h +++ b/library/include/Console.h @@ -33,6 +33,7 @@ distribution. #include #include #include +#include namespace DFHack { @@ -43,7 +44,7 @@ namespace DFHack { this->capacity = capacity; } - bool load (const char * filename) + bool load (std::filesystem::path filename) { std::string reader; std::ifstream infile(filename); @@ -58,7 +59,7 @@ namespace DFHack } return true; } - bool save (const char * filename) + bool save (std::filesystem::path filename) { if (!history.size()) return true; diff --git a/library/include/Core.h b/library/include/Core.h index 556e5e1b03..6794a6b28f 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -175,13 +175,13 @@ namespace DFHack command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream &out, const std::string &command); - bool loadScriptFile(color_ostream &out, std::string fname, bool silent = false); + bool loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent = false); bool addScriptPath(std::string path, bool search_before = false); bool setModScriptPaths(const std::vector &mod_script_paths); bool removeScriptPath(std::string path); - std::string findScript(std::string name); - void getScriptPaths(std::vector *dest); + std::filesystem::path findScript(std::string name); + void getScriptPaths(std::vector *dest); bool getSuppressDuplicateKeyboardEvents(); void setSuppressDuplicateKeyboardEvents(bool suppress); @@ -201,7 +201,7 @@ namespace DFHack std::map> ListAliases(); std::string GetAliasCommand(const std::string &name, bool ignore_params = false); - std::string getHackPath(); + std::filesystem::path getHackPath(); bool isWorldLoaded() { return (last_world_data_ptr != NULL); } bool isMapLoaded() { return (last_local_map_ptr != NULL && last_world_data_ptr != NULL); } diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index bcbab75bd3..95f7e256d1 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -33,6 +33,7 @@ distribution. #include #include #include +#include #include "VersionInfo.h" @@ -262,7 +263,7 @@ namespace DFHack /// get the DF Process ID int getPID(); /// get the DF Process FilePath - std::string getPath(); + std::filesystem::path getPath(); /// Adjust between in-memory and in-file image offset int adjustOffset(int offset, bool to_file = false); diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index 3749ff9548..50b9b0447d 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -64,7 +64,7 @@ namespace DFHack // DFLibrary* that can be used to resolve global names extern DFLibrary* GLOBAL_NAMES; // Open a plugin library - DFHACK_EXPORT DFLibrary * OpenPlugin (const char * filename); + DFHACK_EXPORT DFLibrary * OpenPlugin (std::filesystem::path filename); // find a symbol inside plugin DFHACK_EXPORT void * LookupPlugin (DFLibrary * plugin ,const char * function); // Close a plugin library. returns true on success, false on failure @@ -143,7 +143,7 @@ namespace DFHack struct RefAutoinc; friend class PluginManager; friend class RPCService; - Plugin(DFHack::Core* core, const std::string& filepath, + Plugin(DFHack::Core* core, const std::filesystem::path& filepath, const std::string &plug_name, PluginManager * pm); ~Plugin(); command_result on_update(color_ostream &out); @@ -202,7 +202,7 @@ namespace DFHack RefLock * access; std::vector commands; std::vector services; - std::string path; + std::filesystem::path path; std::string name; DFLibrary * plugin_lib; PluginManager * parent; diff --git a/library/include/Types.h b/library/include/Types.h index 53b6aee18a..84408ecf2a 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -26,6 +26,8 @@ distribution. #pragma once #include +#include +#include #include "Export.h" @@ -118,7 +120,7 @@ namespace DFHack return rect.second - rect.first + df::coord2d(1,1); } - DFHACK_EXPORT int getdir(std::string dir, std::vector &files); + DFHACK_EXPORT int getdir(std::filesystem::path dir, std::vector &files); DFHACK_EXPORT bool hasEnding (std::string const &fullString, std::string const &ending); DFHACK_EXPORT df::general_ref *findRef(std::vector &vec, df::general_ref_type type); diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 8e7bab9b20..8c376a7a01 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -50,123 +50,29 @@ SOFTWARE. #include #include -#ifndef _WIN32 - #ifndef _AIX - #define _FILE_OFFSET_BITS 64 /* Linux, Solaris and HP-UX */ - #else - #define _LARGE_FILES 1 /* AIX */ - #endif -#endif +#include -#ifndef _LARGEFILE64_SOURCE - #define _LARGEFILE64_SOURCE -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 - #include - #define NOMINMAX - #include - #include - #include - #ifdef __BORLANDC__ - #include - #else - #include - #endif - #include - #include "wdirent.h" -#else - #include - #include - #include - #include - #include -#endif - -#ifdef _WIN32 - #ifdef __BORLANDC__ - #define lfs_setmode(L,file,m) ((void)L, setmode(_fileno(file), m)) - #define STAT_STRUCT struct stati64 - #else - #define lfs_setmode(L,file,m) ((void)L, _setmode(_fileno(file), m)) - #define STAT_STRUCT struct _stati64 - #endif - #define STAT_FUNC _stati64 - #define LSTAT_FUNC STAT_FUNC -#else - #define _O_TEXT 0 - #define _O_BINARY 0 - #define lfs_setmode(L,file,m) ((void)L, (void)file, (void)m, 0) - #define STAT_STRUCT struct stat - #define STAT_FUNC stat - #define LSTAT_FUNC lstat -#endif - -#ifdef _WIN32 - #ifndef S_ISDIR - #define S_ISDIR(mode) (mode&_S_IFDIR) - #endif - #ifndef S_ISREG - #define S_ISREG(mode) (mode&_S_IFREG) - #endif - #ifndef S_ISLNK - #define S_ISLNK(mode) (0) - #endif - #ifndef S_ISSOCK - #define S_ISSOCK(mode) (0) - #endif - #ifndef S_ISCHR - #define S_ISCHR(mode) (mode&_S_IFCHR) - #endif - #ifndef S_ISBLK - #define S_ISBLK(mode) (0) - #endif -#endif - -enum _filetype { - FILETYPE_NONE = -2, - FILETYPE_UNKNOWN = -1, - FILETYPE_FILE = 1, - FILETYPE_DIRECTORY, - FILETYPE_LINK, - FILETYPE_SOCKET, - FILETYPE_NAMEDPIPE, - FILETYPE_CHAR_DEVICE, - FILETYPE_BLOCK_DEVICE -}; namespace DFHack { namespace Filesystem { DFHACK_EXPORT void init (); - DFHACK_EXPORT bool chdir (std::string path); - DFHACK_EXPORT std::string getcwd (); + DFHACK_EXPORT bool chdir (std::filesystem::path path); + DFHACK_EXPORT std::filesystem::path getcwd (); DFHACK_EXPORT bool restore_cwd (); - DFHACK_EXPORT std::string get_initial_cwd (); - DFHACK_EXPORT bool mkdir (std::string path); + DFHACK_EXPORT std::filesystem::path get_initial_cwd (); + DFHACK_EXPORT bool mkdir (std::filesystem::path path); // returns true on success or if directory already exists - DFHACK_EXPORT bool mkdir_recursive (std::string path); - DFHACK_EXPORT bool rmdir (std::string path); - DFHACK_EXPORT bool stat (std::string path, STAT_STRUCT &info); - DFHACK_EXPORT bool exists (std::string path); - DFHACK_EXPORT _filetype filetype (std::string path); - DFHACK_EXPORT bool isfile (std::string path); - DFHACK_EXPORT bool isdir (std::string path); - DFHACK_EXPORT int64_t atime (std::string path); - DFHACK_EXPORT int64_t ctime (std::string path); - DFHACK_EXPORT int64_t mtime (std::string path); - DFHACK_EXPORT int listdir (std::string dir, std::vector &files); + DFHACK_EXPORT bool mkdir_recursive (std::filesystem::path path); + DFHACK_EXPORT bool rmdir (std::filesystem::path path); + DFHACK_EXPORT bool stat (std::filesystem::path path, std::filesystem::file_status &info); + DFHACK_EXPORT bool exists (std::filesystem::path path); + DFHACK_EXPORT bool isfile (std::filesystem::path path); + DFHACK_EXPORT bool isdir (std::filesystem::path path); + DFHACK_EXPORT std::time_t mtime (std::filesystem::path path); + DFHACK_EXPORT int listdir (std::filesystem::path dir, std::vector &files); // set include_prefix to false to prevent dir from being prepended to // paths returned in files - DFHACK_EXPORT int listdir_recursive (std::string dir, std::map &files, + DFHACK_EXPORT int listdir_recursive (std::filesystem::path dir, std::map &files, int depth = 10, bool include_prefix = true); } } diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 2c275ceba8..8b316d34c8 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -47,13 +47,15 @@ SOFTWARE. #include #include +#include +#include #include "modules/Filesystem.h" using namespace DFHack; static bool initialized = false; -static std::string initial_cwd; +static std::filesystem::path initial_cwd; void Filesystem::init () { @@ -64,24 +66,23 @@ void Filesystem::init () } } -bool Filesystem::chdir (std::string path) +bool Filesystem::chdir (std::filesystem::path path) { Filesystem::init(); - return ::chdir(path.c_str()) == 0; + try + { + std::filesystem::current_path(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -std::string Filesystem::getcwd () +std::filesystem::path Filesystem::getcwd () { - char *path; - char buf[FILENAME_MAX]; - std::string result = ""; -#ifdef _WIN32 - if ((path = ::_getcwd(buf, FILENAME_MAX)) != NULL) -#else - if ((path = ::getcwd(buf, FILENAME_MAX)) != NULL) -#endif - result = buf; - return result; + return std::filesystem::current_path(); } bool Filesystem::restore_cwd () @@ -89,196 +90,124 @@ bool Filesystem::restore_cwd () return Filesystem::chdir(initial_cwd); } -std::string Filesystem::get_initial_cwd () +std::filesystem::path Filesystem::get_initial_cwd () { Filesystem::init(); return initial_cwd; } -bool Filesystem::mkdir (std::string path) +bool Filesystem::mkdir (std::filesystem::path path) { - int fail; -#ifdef _WIN32 - fail = ::_mkdir(path.c_str()); -#else - fail = ::mkdir(path.c_str(), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | - S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); -#endif - return fail == 0; -} - -static bool mkdir_recursive_impl (std::string path) -{ - size_t last_slash = path.find_last_of("/"); - if (last_slash != std::string::npos) + try { - std::string parent_path = path.substr(0, last_slash); - bool parent_exists = mkdir_recursive_impl(parent_path); - if (!parent_exists) - { - return false; - } + std::filesystem::create_directory(path); + return true; } - return (Filesystem::mkdir(path) || errno == EEXIST) && Filesystem::isdir(path); -} - -bool Filesystem::mkdir_recursive (std::string path) -{ -#ifdef _WIN32 - // normalize to forward slashes - std::replace(path.begin(), path.end(), '\\', '/'); -#endif - - if (path.size() > FILENAME_MAX) + catch (std::filesystem::filesystem_error&) { - // path too long return false; } - - return mkdir_recursive_impl(path); } -bool Filesystem::rmdir (std::string path) +bool Filesystem::mkdir_recursive (std::filesystem::path path) { - int fail; -#ifdef _WIN32 - fail = ::_rmdir(path.c_str()); -#else - fail = ::rmdir(path.c_str()); -#endif - return fail == 0; -} - -#ifdef _WIN32 -_filetype mode2type (unsigned short mode) { -#else -_filetype mode2type (mode_t mode) { -#endif - if (S_ISREG(mode)) - return FILETYPE_FILE; - else if (S_ISDIR(mode)) - return FILETYPE_DIRECTORY; - else if (S_ISLNK(mode)) - return FILETYPE_LINK; - else if (S_ISSOCK(mode)) - return FILETYPE_SOCKET; - else if (S_ISCHR(mode)) - return FILETYPE_CHAR_DEVICE; - else if (S_ISBLK(mode)) - return FILETYPE_BLOCK_DEVICE; - else - return FILETYPE_UNKNOWN; + try + { + std::filesystem::create_directories(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -bool Filesystem::stat (std::string path, STAT_STRUCT &info) +bool Filesystem::rmdir (std::filesystem::path path) { - return (STAT_FUNC(path.c_str(), &info)) == 0; + try + { + std::filesystem::remove(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -bool Filesystem::exists (std::string path) +bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status &info) { - STAT_STRUCT info; - return Filesystem::stat(path, info); + try + { + info = std::filesystem::status(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -_filetype Filesystem::filetype (std::string path) +bool Filesystem::exists (std::filesystem::path path) { - STAT_STRUCT info; - if (!Filesystem::stat(path, info)) - return FILETYPE_NONE; - return mode2type(info.st_mode); + return std::filesystem::exists(path); } -bool Filesystem::isfile (std::string path) +bool Filesystem::isfile(std::filesystem::path path) { - return Filesystem::exists(path) && Filesystem::filetype(path) == FILETYPE_FILE; + return std::filesystem::exists(path) && std::filesystem::is_regular_file(path); } -bool Filesystem::isdir (std::string path) +bool Filesystem::isdir (std::filesystem::path path) { - return Filesystem::exists(path) && Filesystem::filetype(path) == FILETYPE_DIRECTORY; -} - -#define DEFINE_STAT_TIME_WRAPPER(attr) \ -int64_t Filesystem::attr (std::string path) \ -{ \ - STAT_STRUCT info; \ - if (!Filesystem::stat(path, info)) \ - return -1; \ - return (int64_t)info.st_##attr; \ + return std::filesystem::exists(path) && std::filesystem::is_directory(path); } -DEFINE_STAT_TIME_WRAPPER(atime) -DEFINE_STAT_TIME_WRAPPER(ctime) -DEFINE_STAT_TIME_WRAPPER(mtime) - -#undef DEFINE_STAT_TIME_WRAPPER - -int Filesystem::listdir (std::string dir, std::vector &files) +std::time_t Filesystem::mtime (std::filesystem::path path) { - DIR *dp; - struct dirent *dirp; - if((dp = opendir(dir.c_str())) == NULL) + try { - return errno; + auto ftime = std::filesystem::last_write_time(path); + return ftime.time_since_epoch().count(); } - while ((dirp = readdir(dp)) != NULL) { - files.push_back(std::string(dirp->d_name)); + catch (std::filesystem::filesystem_error&) + { + return 0; } - closedir(dp); - return 0; } -// prefix is the top-level dir where we started recursing -// path is the relative path under the prefix; must be empty or end in a '/' -// files is the output list of files and directories (bool == true for dir) -// depth is the remaining dir depth to recurse into. function returns -1 if -// we haven't finished recursing when we run out of depth. -// include_prefix controls whether the directory where we started recursing is -// included in the filenames returned in files. -static int listdir_recursive_impl (std::string prefix, std::string path, - std::map &files, int depth, bool include_prefix) +int Filesystem::listdir (std::filesystem::path dir, std::vector &files) { - if (depth < 0) - return -1; - std::string prefixed_path = prefix + "/" + path; - std::vector curdir_files; - int err = Filesystem::listdir(prefixed_path, curdir_files); - if (err) - return err; - bool out_of_depth = false; - for (auto file = curdir_files.begin(); file != curdir_files.end(); ++file) - { - if (*file == "." || *file == "..") - continue; - std::string prefixed_file = prefixed_path + *file; - std::string path_file = path + *file; - if (Filesystem::isdir(prefixed_file)) - { - files.insert(std::pair(include_prefix ? prefixed_file : path_file, true)); - - if (depth == 0) - { - out_of_depth = true; - continue; - } - - err = listdir_recursive_impl(prefix, path_file + "/", files, depth - 1, include_prefix); - if (err) - return err; - } - else + try { + for (auto const& dirent : std::filesystem::directory_iterator(dir)) { - files.insert(std::pair(include_prefix ? prefixed_file : path_file, false)); + files.push_back(dirent.path().filename()); } + return 0; + } + catch (std::filesystem::filesystem_error&) + { + return 1; } - return out_of_depth ? -1 : 0; } -int Filesystem::listdir_recursive (std::string dir, std::map &files, +int Filesystem::listdir_recursive (std::filesystem::path dir, std::map &files, int depth /* = 10 */, bool include_prefix /* = true */) { - if (dir.size() && dir[dir.size()-1] == '/') - dir.resize(dir.size()-1); - return listdir_recursive_impl(dir, "", files, depth, include_prefix); + try { + for (auto i = std::filesystem::recursive_directory_iterator(dir); + i != std::filesystem::recursive_directory_iterator(); + ++i) + { + if (i->is_directory() && i.depth() >= depth) + i.disable_recursion_pending(); + auto dirent = include_prefix ? *i : std::filesystem::relative(dir, *i); + files.emplace(dirent, i->is_directory()); + } + return 0; + } + catch (std::filesystem::filesystem_error&) + { + return 1; + } } diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index 0a6f1c3772..9e1dd4d018 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -183,12 +183,12 @@ static std::string filterSaveFileName(std::string s) { return s; } -static std::string getSavePath(const std::string &world) { - return "save/" + world; +static std::filesystem::path getSavePath(const std::string &world) { + return std::filesystem::path{} / "save" / world; } -static std::string getSaveFilePath(const std::string &world, const std::string &name) { - return getSavePath(world) + "/dfhack-" + filterSaveFileName(name) + ".dat"; +static std::filesystem::path getSaveFilePath(const std::string &world, const std::string &name) { + return getSavePath(world) / ("dfhack-" + filterSaveFileName(name) + ".dat"); } struct LastLoadSaveTickCountUpdater { @@ -266,7 +266,7 @@ static void add_entry(int entity_id, std::shared_ptr ent add_entry(store[entity_id], entry); } -static bool load_file(const std::string & path, int entity_id) { +static bool load_file(const std::filesystem::path & path, int entity_id) { Json::Value json; try { std::ifstream file(path); @@ -298,8 +298,8 @@ void Persistence::Internal::load(color_ostream& out) { clear(out); std::string world_name = World::ReadWorldFolder(); - std::string save_path = getSavePath(world_name); - std::vector files; + std::filesystem::path save_path = getSavePath(world_name); + std::vector files; if (0 != Filesystem::listdir(save_path, files)) { DEBUG(persistence,out).print("not loading state; save directory doesn't exist: '%s'\n", save_path.c_str()); return; @@ -308,11 +308,11 @@ void Persistence::Internal::load(color_ostream& out) { bool found = false; for (auto & fname : files) { int entity_id = Persistence::WORLD_ENTITY_ID; - if (fname != "dfhack-world.dat" && !get_entity_id(fname, entity_id)) + if (fname != "dfhack-world.dat" && !get_entity_id(fname.string(), entity_id)) continue; found = true; - std::string path = save_path + "/" + fname; + std::filesystem::path path = save_path / fname; if (!load_file(path, entity_id)) out.printerr("Cannot load data from: '%s'\n", path.c_str()); } @@ -321,7 +321,7 @@ void Persistence::Internal::load(color_ostream& out) { return; // new file formats not found; attempt to load legacy file - const std::string legacy_fname = getSaveFilePath(world_name, "legacy-data"); + const std::filesystem::path legacy_fname = getSaveFilePath(world_name, "legacy-data"); if (Filesystem::exists(legacy_fname)) { int synthesized_entity_id = Persistence::WORLD_ENTITY_ID; if (World::IsSiteLoaded()) diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 49558c1b88..a2da1f0ae9 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -134,7 +134,7 @@ static command_result orders_command(color_ostream & out, std::vector files; + std::map files; if (0 < Filesystem::listdir_recursive(ORDERS_LIBRARY_DIR, files, 0, false)) { // if the library directory doesn't exist, just skip it return; @@ -145,15 +145,15 @@ static void list_library(color_ostream &out) { return; } - for (auto it : files) + for (auto& it : files) { if (it.second) continue; // skip directories - std::string name = it.first; - if (name.length() <= 5 || name.rfind(".json") != name.length() - 5) + std::filesystem::path name = it.first; + if (name.extension() != ".json") continue; // skip non-.json files - name.resize(name.length() - 5); - out << "library/" << name << std::endl; + auto sname = name.stem(); + out << "library/" << sname << std::endl; } } @@ -162,17 +162,17 @@ static command_result orders_list_command(color_ostream & out) // use listdir_recursive instead of listdir even though orders doesn't // support subdirs so we can identify and ignore subdirs with ".json" names. // also listdir_recursive will alphabetize the list for us. - std::map files; + std::map files; Filesystem::listdir_recursive(ORDERS_DIR, files, 0, false); - for (auto it : files) { + for (auto& it : files) { if (it.second) continue; // skip directories - std::string name = it.first; - if (name.length() <= 5 || name.rfind(".json") != name.length() - 5) + std::filesystem::path name = it.first; + if (name.extension() != ".json") continue; // skip non-.json files - name.resize(name.length() - 5); - out << name << std::endl; + auto sname = name.stem(); + out << sname << std::endl; } list_library(out); From e8274319e92c82c45a8d549b751ba8d30cfd2038 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 09:34:24 -0500 Subject: [PATCH 0194/1082] fix docs --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ed65524207..ee2551a94e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,7 +62,7 @@ Template for new versions: ## Documentation ## API -- `Filesystem` module: rewritten to use C++ standard library components, for better portability +- ``Filesystem`` module: rewritten to use C++ standard library components, for better portability ## Lua From a17c0ca56bc4481e8e306aaa29a3d921872763a7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 09:41:31 -0500 Subject: [PATCH 0195/1082] transform argument to printf really wish we could use `std::format` --- library/PlugLoad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 1cafc791f5..ec72e36fb9 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -77,7 +77,7 @@ namespace DFHack DFLibrary* ret = (DFLibrary*)load_library(filename); if (!ret) { auto error = get_error(); - WARN(plugins).print("OpenPlugin on %s failed: %s\n", filename, error.c_str()); + WARN(plugins).print("OpenPlugin on %s failed: %s\n", filename.string().c_str(), error.c_str()); } return ret; } From 1945981b945046182e4a99e65fb3b2111471741d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 10:12:23 -0500 Subject: [PATCH 0196/1082] add `canonicalize` method to `Filesystem` and use in tests instead of hacky regexp --- library/LuaApi.cpp | 1 + library/include/modules/Filesystem.h | 1 + library/modules/Filesystem.cpp | 6 ++++++ test/core.lua | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d2162a33d1..cc8c643b74 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3155,6 +3155,7 @@ static const LuaWrapper::FunctionReg dfhack_filesystem_module[] = { WRAPM(Filesystem, isfile), WRAPM(Filesystem, isdir), WRAPM(Filesystem, mtime), + WRAPM(Filesystem, canonicalize), {NULL, NULL} }; diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 8c376a7a01..d2b6f7a9b8 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -74,5 +74,6 @@ namespace DFHack { // paths returned in files DFHACK_EXPORT int listdir_recursive (std::filesystem::path dir, std::map &files, int depth = 10, bool include_prefix = true); + DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p); } } diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 8b316d34c8..9e81ba8d10 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -211,3 +211,9 @@ int Filesystem::listdir_recursive (std::filesystem::path dir, std::map Date: Mon, 17 Mar 2025 10:29:59 -0500 Subject: [PATCH 0197/1082] eradicate extra blank line in `Filesystem.cpp` --- library/modules/Filesystem.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 9e81ba8d10..b626319928 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -216,4 +216,3 @@ std::filesystem::path Filesystem::canonicalize(std::filesystem::path p) { return std::filesystem::canonical(p); } - From 951621d5cae7fb1346e12c77f1250b1e8259b5a2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 11:24:50 -0500 Subject: [PATCH 0198/1082] don't error on an existing but empty script --- library/Core.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index c4c611ee9d..cb310a4777 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1341,8 +1341,8 @@ 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); - if ( !script.good() ) + std::ifstream script{ fname.c_str() }; + if ( !script ) { if(!silent) out.printerr("Error loading script: %s\n", fname.string().c_str()); @@ -2203,9 +2203,9 @@ size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector Date: Mon, 17 Mar 2025 12:17:31 -0500 Subject: [PATCH 0199/1082] fix stupidly reversed arguments to `relative` --- library/modules/Filesystem.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index b626319928..d95afecf8b 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -49,6 +49,7 @@ SOFTWARE. #include #include #include +#include #include "modules/Filesystem.h" @@ -201,8 +202,9 @@ int Filesystem::listdir_recursive (std::filesystem::path dir, std::mapis_directory() && i.depth() >= depth) i.disable_recursion_pending(); - auto dirent = include_prefix ? *i : std::filesystem::relative(dir, *i); - files.emplace(dirent, i->is_directory()); + auto p = i->path(); + auto pp = include_prefix ? p : std::filesystem::relative(p, dir); + files.emplace(pp, std::filesystem::is_directory(p)); } return 0; } From 0f83cac96f6fd711ec845ac5dc23105ef5962df6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 12:37:06 -0500 Subject: [PATCH 0200/1082] fix mtime to return -1 (not 0) on failure --- library/modules/Filesystem.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index d95afecf8b..14f1ec6341 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -169,11 +169,12 @@ std::time_t Filesystem::mtime (std::filesystem::path path) try { auto ftime = std::filesystem::last_write_time(path); - return ftime.time_since_epoch().count(); + auto t = ftime.time_since_epoch().count(); + return t; } catch (std::filesystem::filesystem_error&) { - return 0; + return -1; } } From c4de1207ec8573884a96a4a6dc511d375125f198 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 13:50:57 -0500 Subject: [PATCH 0201/1082] use filesystem::path for mod script paths also make testing slightly noisier --- ci/test.lua | 1 + library/Core.cpp | 24 ++++++++++++++---------- library/include/Core.h | 8 ++++---- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/ci/test.lua b/ci/test.lua index e57facf14b..32466a4f9a 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -289,6 +289,7 @@ local MODES = { local function load_test_config(config_file) local config = {} + print ("loading test config from " .. config_file) if dfhack.filesystem.isfile(config_file) then config = json.decode_file(config_file) end diff --git a/library/Core.cpp b/library/Core.cpp index cb310a4777..642ff7d7a6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -480,10 +480,10 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: return false; } -bool Core::addScriptPath(std::string path, bool search_before) +bool Core::addScriptPath(std::filesystem::path path, bool search_before) { std::lock_guard lock(script_path_mutex); - std::vector &vec = script_paths[search_before ? 0 : 1]; + auto &vec = script_paths[search_before ? 0 : 1]; if (std::find(vec.begin(), vec.end(), path) != vec.end()) return false; if (!Filesystem::isdir(path)) @@ -492,19 +492,19 @@ bool Core::addScriptPath(std::string path, bool search_before) return true; } -bool Core::setModScriptPaths(const std::vector &mod_script_paths) { +bool Core::setModScriptPaths(const std::vector &mod_script_paths) { std::lock_guard lock(script_path_mutex); script_paths[2] = mod_script_paths; return true; } -bool Core::removeScriptPath(std::string path) +bool Core::removeScriptPath(std::filesystem::path path) { std::lock_guard lock(script_path_mutex); bool found = false; for (int i = 0; i < 2; i++) { - std::vector &vec = script_paths[i]; + auto &vec = script_paths[i]; while (1) { auto it = std::find(vec.begin(), vec.end(), path); @@ -547,7 +547,7 @@ std::filesystem::path Core::findScript(std::string name) if (Filesystem::isfile(path)) return path; } - return ""; + return {}; } bool loadScriptPaths(color_ostream &out, bool silent = false) @@ -585,14 +585,18 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) } static void loadModScriptPaths(color_ostream &out) { - std::vector mod_script_paths; + std::vector mod_script_paths_str; + std::vector mod_script_paths; Lua::CallLuaModuleFunction(out, "script-manager", "get_mod_script_paths", {}, 1, [&](lua_State *L) { - Lua::GetVector(L, mod_script_paths); + Lua::GetVector(L, mod_script_paths_str); }); DEBUG(script,out).print("final mod script paths:\n"); - for (auto & path : mod_script_paths) - DEBUG(script,out).print(" %s\n", path.c_str()); + for (auto& path : mod_script_paths_str) + { + DEBUG(script, out).print(" %s\n", path.c_str()); + mod_script_paths.push_back(std::filesystem::canonical(std::filesystem::path{ path })); + } Core::getInstance().setModScriptPaths(mod_script_paths); } diff --git a/library/include/Core.h b/library/include/Core.h index 6794a6b28f..9ef8fb8f0e 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -177,9 +177,9 @@ namespace DFHack command_result runCommand(color_ostream &out, const std::string &command); bool loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent = false); - bool addScriptPath(std::string path, bool search_before = false); - bool setModScriptPaths(const std::vector &mod_script_paths); - bool removeScriptPath(std::string path); + bool addScriptPath(std::filesystem::path path, bool search_before = false); + bool setModScriptPaths(const std::vector & mod_script_paths); + bool removeScriptPath(std::filesystem::path path); std::filesystem::path findScript(std::string name); void getScriptPaths(std::vector *dest); @@ -273,7 +273,7 @@ namespace DFHack std::vector> allModules; DFHack::PluginManager * plug_mgr; - std::vector script_paths[3]; + std::vector script_paths[3]; std::mutex script_path_mutex; // hotkey-related stuff From 9c8688aea987ce3245365c5a87caec275e40aa46 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 15:12:02 -0500 Subject: [PATCH 0202/1082] add noisy logging to try to debug ci --- library/Core.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Core.cpp b/library/Core.cpp index 642ff7d7a6..0fa04d1d08 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -544,6 +544,7 @@ std::filesystem::path Core::findScript(std::string name) for (auto it = paths.begin(); it != paths.end(); ++it) { std::filesystem::path path = *it / name; + std::cerr << "findScript " << name << " | " << path << " | " << Filesystem::isfile(path) << std::endl; if (Filesystem::isfile(path)) return path; } From be1c8e95290ed5445e92b20b152fc8e028b52d8b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 15:29:12 -0500 Subject: [PATCH 0203/1082] different noisy crickets --- library/Core.cpp | 1 - library/LuaApi.cpp | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index 0fa04d1d08..642ff7d7a6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -544,7 +544,6 @@ std::filesystem::path Core::findScript(std::string name) for (auto it = paths.begin(); it != paths.end(); ++it) { std::filesystem::path path = *it / name; - std::cerr << "findScript " << name << " | " << path << " | " << Filesystem::isfile(path) << std::endl; if (Filesystem::isfile(path)) return path; } diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index cc8c643b74..c37fd946e7 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3962,6 +3962,7 @@ static int internal_findScript(lua_State *L) { const char *name = luaL_checkstring(L, 1); std::filesystem::path path = Core::getInstance().findScript(name); + std::cerr << "findScript " << name << " -> " << path << std::endl; if (!path.empty()) lua_pushstring(L, path.string().c_str()); else From 3ee3edf0b2f9f31b7c53a6469fb4894871d24c95 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 15:36:45 -0500 Subject: [PATCH 0204/1082] switch to weak canonicalization --- library/Core.cpp | 6 +++--- library/modules/Filesystem.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 642ff7d7a6..227c4378f0 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -543,7 +543,7 @@ std::filesystem::path Core::findScript(std::string name) getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { - std::filesystem::path path = *it / name; + std::filesystem::path path = std::filesystem::weakly_canonical(*it / name); if (Filesystem::isfile(path)) return path; } @@ -595,7 +595,7 @@ static void loadModScriptPaths(color_ostream &out) { for (auto& path : mod_script_paths_str) { DEBUG(script, out).print(" %s\n", path.c_str()); - mod_script_paths.push_back(std::filesystem::canonical(std::filesystem::path{ path })); + mod_script_paths.push_back(std::filesystem::weakly_canonical(std::filesystem::path{ path })); } Core::getInstance().setModScriptPaths(mod_script_paths); } @@ -1132,7 +1132,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if(parts.size() == 1) { - loadScriptFile(con, std::filesystem::canonical(std::filesystem::path{parts[0]}), false); + loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); } else { diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 14f1ec6341..670e0640ef 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -217,5 +217,5 @@ int Filesystem::listdir_recursive (std::filesystem::path dir, std::map Date: Mon, 17 Mar 2025 16:02:26 -0500 Subject: [PATCH 0205/1082] make most Filesystem modules noexcept --- library/LuaApi.cpp | 1 - library/include/DataDefs.h | 19 +++++++++++++++ library/include/DataFuncs.h | 35 ++++++++++++++++++++++++++++ library/include/modules/Filesystem.h | 24 +++++++++---------- library/lua/dfhack.lua | 1 + library/modules/Filesystem.cpp | 33 +++++++++++++++----------- 6 files changed, 87 insertions(+), 26 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index c37fd946e7..cc8c643b74 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3962,7 +3962,6 @@ static int internal_findScript(lua_State *L) { const char *name = luaL_checkstring(L, 1); std::filesystem::path path = Core::getInstance().findScript(name); - std::cerr << "findScript " << name << " -> " << path << std::endl; if (!path.empty()) lua_pushstring(L, path.string().c_str()); else diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index c6a7eb8272..6738054423 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -615,6 +615,25 @@ namespace df static const bool is_method = true; }; + template + struct return_type { + using type = RT; + static const bool is_method = false; + }; + + template + struct return_type { + using type = RT; + using class_type = CT; + static const bool is_method = true; + }; + + template + struct return_type { + using type = RT; + using class_type = CT; + static const bool is_method = true; + }; } diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index e8bfc7e284..a8d2caca51 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -133,6 +133,41 @@ namespace df { }; }; + template + struct function_wrapper { + static const int num_args = sizeof...(AT); + static void execute(lua_State* L, int base, RT(fun)(DFHack::color_ostream& out, AT...)) { + cur_lua_ostream_argument out(L); + call_and_push(L, base, fun, out); + } + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT); + static void execute(lua_State* L, int base, RT(fun)(AT...)) { + call_and_push(L, base, fun); + } + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT) + 1; + static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...)) { + CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); + call_and_push(L, base, mem_fun, self); + }; + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT) + 1; + static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...) const) { + CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); + call_and_push(L, base, mem_fun, self); + }; + }; + template class function_identity : public function_identity_base { T ptr; diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index d2b6f7a9b8..c4a1c8ad94 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -56,24 +56,24 @@ SOFTWARE. namespace DFHack { namespace Filesystem { DFHACK_EXPORT void init (); - DFHACK_EXPORT bool chdir (std::filesystem::path path); + DFHACK_EXPORT bool chdir (std::filesystem::path path) noexcept; DFHACK_EXPORT std::filesystem::path getcwd (); DFHACK_EXPORT bool restore_cwd (); DFHACK_EXPORT std::filesystem::path get_initial_cwd (); - DFHACK_EXPORT bool mkdir (std::filesystem::path path); + DFHACK_EXPORT bool mkdir (std::filesystem::path path) noexcept; // returns true on success or if directory already exists - DFHACK_EXPORT bool mkdir_recursive (std::filesystem::path path); - DFHACK_EXPORT bool rmdir (std::filesystem::path path); - DFHACK_EXPORT bool stat (std::filesystem::path path, std::filesystem::file_status &info); - DFHACK_EXPORT bool exists (std::filesystem::path path); - DFHACK_EXPORT bool isfile (std::filesystem::path path); - DFHACK_EXPORT bool isdir (std::filesystem::path path); - DFHACK_EXPORT std::time_t mtime (std::filesystem::path path); - DFHACK_EXPORT int listdir (std::filesystem::path dir, std::vector &files); + DFHACK_EXPORT bool mkdir_recursive (std::filesystem::path path) noexcept; + DFHACK_EXPORT bool rmdir (std::filesystem::path path) noexcept; + DFHACK_EXPORT bool stat (std::filesystem::path path, std::filesystem::file_status &info) noexcept; + DFHACK_EXPORT bool exists (std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isfile (std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isdir (std::filesystem::path path) noexcept; + DFHACK_EXPORT std::time_t mtime (std::filesystem::path path) noexcept; + DFHACK_EXPORT int listdir (std::filesystem::path dir, std::vector &files) noexcept; // set include_prefix to false to prevent dir from being prepended to // paths returned in files DFHACK_EXPORT int listdir_recursive (std::filesystem::path dir, std::map &files, - int depth = 10, bool include_prefix = true); - DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p); + int depth = 10, bool include_prefix = true) noexcept; + DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; } } diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index ef08fb7f4b..88327a419e 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -1074,6 +1074,7 @@ function dfhack.run_script_with_env(envVars, name, flags, ...) end end + print ( "run_script_with_env, name="..name..", file="..file ) local env = scripts[file].env if env == nil then env = {} diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 670e0640ef..55a7fd2194 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -67,7 +67,7 @@ void Filesystem::init () } } -bool Filesystem::chdir (std::filesystem::path path) +bool Filesystem::chdir (std::filesystem::path path) noexcept { Filesystem::init(); try @@ -97,7 +97,7 @@ std::filesystem::path Filesystem::get_initial_cwd () return initial_cwd; } -bool Filesystem::mkdir (std::filesystem::path path) +bool Filesystem::mkdir (std::filesystem::path path) noexcept { try { @@ -110,7 +110,7 @@ bool Filesystem::mkdir (std::filesystem::path path) } } -bool Filesystem::mkdir_recursive (std::filesystem::path path) +bool Filesystem::mkdir_recursive (std::filesystem::path path) noexcept { try { @@ -123,7 +123,7 @@ bool Filesystem::mkdir_recursive (std::filesystem::path path) } } -bool Filesystem::rmdir (std::filesystem::path path) +bool Filesystem::rmdir (std::filesystem::path path) noexcept { try { @@ -136,7 +136,7 @@ bool Filesystem::rmdir (std::filesystem::path path) } } -bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status &info) +bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status &info) noexcept { try { @@ -149,22 +149,22 @@ bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status } } -bool Filesystem::exists (std::filesystem::path path) +bool Filesystem::exists (std::filesystem::path path) noexcept { return std::filesystem::exists(path); } -bool Filesystem::isfile(std::filesystem::path path) +bool Filesystem::isfile(std::filesystem::path path) noexcept { return std::filesystem::exists(path) && std::filesystem::is_regular_file(path); } -bool Filesystem::isdir (std::filesystem::path path) +bool Filesystem::isdir (std::filesystem::path path) noexcept { return std::filesystem::exists(path) && std::filesystem::is_directory(path); } -std::time_t Filesystem::mtime (std::filesystem::path path) +std::time_t Filesystem::mtime (std::filesystem::path path) noexcept { try { @@ -178,7 +178,7 @@ std::time_t Filesystem::mtime (std::filesystem::path path) } } -int Filesystem::listdir (std::filesystem::path dir, std::vector &files) +int Filesystem::listdir (std::filesystem::path dir, std::vector &files) noexcept { try { for (auto const& dirent : std::filesystem::directory_iterator(dir)) @@ -194,7 +194,7 @@ int Filesystem::listdir (std::filesystem::path dir, std::vector &files, - int depth /* = 10 */, bool include_prefix /* = true */) + int depth /* = 10 */, bool include_prefix /* = true */) noexcept { try { for (auto i = std::filesystem::recursive_directory_iterator(dir); @@ -215,7 +215,14 @@ int Filesystem::listdir_recursive (std::filesystem::path dir, std::map Date: Mon, 17 Mar 2025 16:20:45 -0500 Subject: [PATCH 0206/1082] more cricket shuffling --- library/lua/dfhack.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 88327a419e..97351e8825 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -1042,7 +1042,9 @@ end function dfhack.run_script_with_env(envVars, name, flags, ...) if type(flags) ~= 'table' then flags = {} end + dfhack.printerr ( "run_script_with_env, name="..name..", file=" ) local file = dfhack.findScript(name) + dfhack.printerr ( "run_script_with_env, name="..name..", file="..file ) if not file then error('Could not find script ' .. name) end @@ -1074,7 +1076,6 @@ function dfhack.run_script_with_env(envVars, name, flags, ...) end end - print ( "run_script_with_env, name="..name..", file="..file ) local env = scripts[file].env if env == nil then env = {} From 5beb9405bfb570b6c3f51ab27ffcc9b95ba4dd8e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 16:34:31 -0500 Subject: [PATCH 0207/1082] more cricket shuffling --- library/Core.cpp | 1 + library/lua/dfhack.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/library/Core.cpp b/library/Core.cpp index 227c4378f0..4d0a505d06 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -350,6 +350,7 @@ static bool init_run_script(color_ostream &out, lua_State *state, void *info) static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) { + std::cerr << "runLuaScript " << name << std::endl; ScriptArgs data; data.pcmd = &name; data.pargs = &args; diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 97351e8825..888a266d14 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -997,6 +997,7 @@ local valid_script_flags = { local warned_scripts = {} function dfhack.run_script(name,...) + dfhack.printerr ( "run_script, name="..name ) if not warned_scripts[name] then local helpdb = require('helpdb') if helpdb.has_tag(name, 'unavailable') then From 6ab84505d402f6eaf7b0fceda08cd6f783c01ce8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 16:48:31 -0500 Subject: [PATCH 0208/1082] better type jenga (???) --- library/LuaTools.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index c0f8bd1d2e..084ba4be3e 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1240,13 +1240,13 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, if (histfile != histname) { if (!histname.empty()) - hist.save(histname.c_str()); + hist.save(std::filesystem::path{ histname }); hist.clear(); histname = histfile; if (!histname.empty()) - hist.load(histname.c_str()); + hist.load(std::filesystem::path{ histname }); } if (prompt.empty()) @@ -1269,7 +1269,7 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, } if (!histname.empty()) - hist.save(histname.c_str()); + hist.save(std::filesystem::path{ histname }); { CoreSuspender suspend; From 8979fe2535e95b320f05841bf5a14b8c1276941f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 17:04:13 -0500 Subject: [PATCH 0209/1082] fix a couple more type jengas --- library/Core.cpp | 2 ++ library/LuaApi.cpp | 2 +- library/LuaTools.cpp | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 4d0a505d06..a1cdcbac41 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -357,6 +357,8 @@ static command_result runLuaScript(color_ostream &out, std::string name, std::ve bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_run_script, &data); + std::cerr << "runLuaScript " << name << " res=" << ok << std::endl; + return ok ? CR_OK : CR_FAILURE; } diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index cc8c643b74..e082076ae6 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1290,7 +1290,7 @@ static void addCommandToHistory(string id, string src_file, string command) { CommandHistory *history = ensureCommandHistory(id, src_file); history->add(command); - history->save(src_file.c_str()); + history->save(std::filesystem::path{ src_file }); } /************************ diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 084ba4be3e..2d9df61ab4 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -426,7 +426,7 @@ static int dfhack_lineedit_sync(lua_State *S, Console *pstream) DFHack::CommandHistory hist; if (hfile) - hist.load(hfile); + hist.load(std::filesystem::path{ hfile }); std::string ret; int rv = pstream->lineedit(prompt, ret, hist); @@ -446,7 +446,7 @@ static int dfhack_lineedit_sync(lua_State *S, Console *pstream) else { if (hfile) - hist.save(hfile); + hist.save(std::filesystem::path{ hfile }); lua_pushlstring(S, ret.data(), ret.size()); return 1; } From b98913b6ed6c0b0525f12d49ce8a295a2c86ab59 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 17:15:17 -0500 Subject: [PATCH 0210/1082] add missing slash --- ci/test.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test.lua b/ci/test.lua index 32466a4f9a..c0d021d0a6 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -295,7 +295,7 @@ local function load_test_config(config_file) end if not config.test_dir then - config.test_dir = dfhack.getHackPath() .. 'scripts/test' + config.test_dir = dfhack.getHackPath() .. '/scripts/test' end if not config.save_dir then From 5683c7aee513fe3aec1276748083612734a655e4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 17:24:35 -0500 Subject: [PATCH 0211/1082] silence at least some of the crickets --- library/Core.cpp | 3 --- library/lua/dfhack.lua | 3 --- 2 files changed, 6 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index a1cdcbac41..227c4378f0 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -350,15 +350,12 @@ static bool init_run_script(color_ostream &out, lua_State *state, void *info) static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) { - std::cerr << "runLuaScript " << name << std::endl; ScriptArgs data; data.pcmd = &name; data.pargs = &args; bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_run_script, &data); - std::cerr << "runLuaScript " << name << " res=" << ok << std::endl; - return ok ? CR_OK : CR_FAILURE; } diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 888a266d14..ef08fb7f4b 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -997,7 +997,6 @@ local valid_script_flags = { local warned_scripts = {} function dfhack.run_script(name,...) - dfhack.printerr ( "run_script, name="..name ) if not warned_scripts[name] then local helpdb = require('helpdb') if helpdb.has_tag(name, 'unavailable') then @@ -1043,9 +1042,7 @@ end function dfhack.run_script_with_env(envVars, name, flags, ...) if type(flags) ~= 'table' then flags = {} end - dfhack.printerr ( "run_script_with_env, name="..name..", file=" ) local file = dfhack.findScript(name) - dfhack.printerr ( "run_script_with_env, name="..name..", file="..file ) if not file then error('Could not find script ' .. name) end From 4a8b292438dc36e94967639523bcf0980e42259f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 17:35:57 -0500 Subject: [PATCH 0212/1082] use same cwd mechanics in both linux and windows --- library/Process-windows.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index fe8c5d591a..603636176d 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -33,11 +33,13 @@ distribution. #include "Error.h" #include "Internal.h" + #include "MemAccess.h" #include "Memory.h" #include "MiscUtils.h" #include "VersionInfo.h" #include "VersionInfoFactory.h" +#include "modules/Filesystem.h" #define _WIN32_WINNT 0x0600 #define WINVER 0x0600 @@ -384,13 +386,7 @@ uint32_t Process::getTickCount() std::filesystem::path Process::getPath() { - HMODULE hmod; - DWORD junk; - char String[255]; - EnumProcessModules(d->my_handle, &hmod, 1 * sizeof(HMODULE), &junk); //get the module from the handle - GetModuleFileNameEx(d->my_handle,hmod,String,sizeof(String)); //get the filename from the module - string out(String); - return(out.substr(0,out.find_last_of("\\"))); + return Filesystem::get_initial_cwd(); } int Process::getPID() From 4d0b66124f9b9af59d9dd0a82baf52691549f31a Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 11:37:16 -0500 Subject: [PATCH 0213/1082] create toolbar icon for mass remove --- data/art/mass_remove_toolbar.png | Bin 0 -> 1400 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/art/mass_remove_toolbar.png diff --git a/data/art/mass_remove_toolbar.png b/data/art/mass_remove_toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..d1b09ac93bb28ea29db7767139378f90ba2f6207 GIT binary patch literal 1400 zcmV-;1&8{HP)Px)GD$>1RA_P*NzCmO|1)GLw04 zXW!fGd&yGzpLXZXoA3Ysnfd>j|J5wss#)ZJEH!~ZU`-rgnkE3r!C}(50;!=Sfk0p_ zP+A$AybZv_$T;a-fka=Nj_{tMe$Um_)qUzW+%!#+gTwrl`<+ZpBR9YMY~78Y7#Zj5 z56+UQY2?D$qgvwuYW;?rrioz~{Cw$001k#W`HgUtvB}%aPR*3f_uT;inQR__x8jHB z@9kOF*|+l}vG$U)53r$9h}h#g8BNba+nc0w1=qfnI!EkrH-5yWuhs#(JG;4)y5g$D z(GO3vYjZ8lO+ne#e&}7!bnRr|+5=KUNxBZ~=Wtg{K7aCS4uIySAY*qQapR{SD&~Ll z<+nxrEw8;U`H8+b7kiF7@+)2e0J}EV%0W*i1^_S&gG@Hhy{o4!#tX23i}!E%#19`n z2Egpp44G`6OXs^S#+Tw3#em(N-SYQ|j}ndwOwTO zU7KrVf8DH9i?59VViq#lyhWi>ik#`%2_Vy+s+|3IN87fdS@DZ6(%x$L+LPdp?Tv(j z4Wx4gGTA&w_JyTVsuCjlt7#9MgO7;5Iw!1_FQUJi_EyU;ItK`^3(u!>1^Rn?w1XuF zhZ(r`plsvI5gk7gYp1@og^1|X7YCMZdZX$2vW?e?#R0f<-ymjUS1%Gdne-xB$&<>*s;BlXf!H~uWxMu z;K;HPp{cV5&u_ymo{Fkm&X*dfDI_(NBsn-NXF-LFoO>{| ziRPF8l<)lUI1N~%ta|2F4Q_4JDL zWeuvc;0D+A^(xiiTO4pFbwv&=a`0;5ZUN4zZ#DQuMbW~;`@HeaNm4^eUV5d44U5*I zu{~CZ6u5C~NVY$Hw#d_Gi)?wZhUvKl`CmK}?Eu+dW>BT@@IH^{{~^&A=i2pKj(qjx zS1I4IEN+@6ou~S>&q^5&EBV)+mh5eSwjv~CZ1Ogre{o6std#MqX1W Date: Thu, 20 Mar 2025 08:38:37 -0500 Subject: [PATCH 0214/1082] Create sitemap_toolbar.png --- data/art/sitemap_toolbar.png | Bin 0 -> 483 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/art/sitemap_toolbar.png diff --git a/data/art/sitemap_toolbar.png b/data/art/sitemap_toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..07871884a64a746cd5aba7b1d6a1c72a00d6ad7c GIT binary patch literal 483 zcmV<90UZ8`P)Px$ok>JNRA_?kF~10=g6^XaV23FF-R**RtcM{XzVVs({D&$&PO>wdQFCA@JRG zD;)0u@FG-}fCO%54iQb)ir{wc$9^CoT7V62Lrxu!2?u{a_HUoL zX)f&(fPfjGy{YjD2X8`!yb$pcuw?kqI6ns02oE?4SSvi>AYje#z=VLc!viA%N`MCh z0j0nLjDV8h0ZTw>@PHwpM0mguK+vh-f%pA=N_b#l{|+}@n-m^cn1CGlycErS0l8DW ZfFGZ36}hoadiekV002ovPDHLkV1f<2((3>K literal 0 HcmV?d00001 From 6dc491464ceed1acefc2a4a395d806748f0a747a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 16:27:35 -0700 Subject: [PATCH 0215/1082] remove obsolete comment --- test/core.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/test/core.lua b/test/core.lua index 3a0a1d5ee9..7270f57067 100644 --- a/test/core.lua +++ b/test/core.lua @@ -1,7 +1,6 @@ config.target = 'core' local function clean_path(p) - -- todo: replace with dfhack.filesystem call? return dfhack.filesystem.canonicalize(p) end From dc4b40e554d2905694c103c79cbc3696c0cddf43 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 23 Mar 2025 02:19:24 +0000 Subject: [PATCH 0216/1082] 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 93496b510f..c783692f43 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 93496b510fcfe691f8baeb54d885a8ac811cae1e +Subproject commit c783692f43a205cef821e669adc26bc725f5c159 diff --git a/scripts b/scripts index 90431556c4..9fe2a467fe 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 90431556c4c2924c206403232ea1c24491355d07 +Subproject commit 9fe2a467fe5c2c51a74db02c6a317453904e095f From a801e96901af3db8706370d185ea7d7479dce86b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 20:55:49 -0700 Subject: [PATCH 0217/1082] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 9fe2a467fe..ccdc48f2dd 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9fe2a467fe5c2c51a74db02c6a317453904e095f +Subproject commit ccdc48f2dd4d1a777dc9d1002c707202ffdffff3 From 8b8b70b6e6d5bd2dea05ce9e113c6e19acdcbabb Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Mar 2025 00:45:34 -0500 Subject: [PATCH 0218/1082] deal with directory separators cleanly dfhack assumes that scripts are in a namespace in which directories are separated by slashes, and also that pathnames will be slash-separated this enforces that when the transition is made from a `std::filesystem::path` to dfhack's internal script name space, backslashes are replaced with slashes on systems where the preferred separator is not already '/' --- library/LuaApi.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index e082076ae6..e5ce40ac18 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3207,7 +3207,12 @@ static int filesystem_listdir_recursive(lua_State *L) lua_pushinteger(L, i++); lua_newtable(L); lua_pushstring(L, "path"); - lua_pushstring(L, (it->first).string().c_str()); + auto p = (it->first).string(); + if constexpr (std::filesystem::path::preferred_separator != '/') + { + std::ranges::replace(p, std::filesystem::path::preferred_separator, '/'); + } + lua_pushstring(L, p.c_str()); lua_settable(L, -3); lua_pushstring(L, "isdir"); lua_pushboolean(L, it->second); From 1fdc92866e92cabca6f44cce1dc5f363249898fc Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 23 Mar 2025 10:18:56 +0100 Subject: [PATCH 0219/1082] add a changelog entry --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee2551a94e..8f5f60f713 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `spectate`: show dwarves' activities (like prayer) ## Documentation From 19023334bce9046f0f546237ca831f7882660497 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Mar 2025 08:09:20 -0500 Subject: [PATCH 0220/1082] address another path/string confusion added `Filesystem::as_string` to provide namespace recanonicalization and change use in LuaApi to use this exported API use `as_string` in `orders` --- library/LuaApi.cpp | 7 +---- library/include/modules/Filesystem.h | 39 +++++++++++++++++----------- plugins/orders.cpp | 4 +-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index e5ce40ac18..0ce74f9f06 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3207,12 +3207,7 @@ static int filesystem_listdir_recursive(lua_State *L) lua_pushinteger(L, i++); lua_newtable(L); lua_pushstring(L, "path"); - auto p = (it->first).string(); - if constexpr (std::filesystem::path::preferred_separator != '/') - { - std::ranges::replace(p, std::filesystem::path::preferred_separator, '/'); - } - lua_pushstring(L, p.c_str()); + lua_pushstring(L, Filesystem::as_string(it->first).c_str()); lua_settable(L, -3); lua_pushstring(L, "isdir"); lua_pushboolean(L, it->second); diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index c4a1c8ad94..7c68983a3a 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -55,25 +55,34 @@ SOFTWARE. namespace DFHack { namespace Filesystem { - DFHACK_EXPORT void init (); - DFHACK_EXPORT bool chdir (std::filesystem::path path) noexcept; - DFHACK_EXPORT std::filesystem::path getcwd (); - DFHACK_EXPORT bool restore_cwd (); - DFHACK_EXPORT std::filesystem::path get_initial_cwd (); - DFHACK_EXPORT bool mkdir (std::filesystem::path path) noexcept; + DFHACK_EXPORT void init(); + DFHACK_EXPORT bool chdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT std::filesystem::path getcwd(); + DFHACK_EXPORT bool restore_cwd(); + DFHACK_EXPORT std::filesystem::path get_initial_cwd(); + DFHACK_EXPORT bool mkdir(std::filesystem::path path) noexcept; // returns true on success or if directory already exists - DFHACK_EXPORT bool mkdir_recursive (std::filesystem::path path) noexcept; - DFHACK_EXPORT bool rmdir (std::filesystem::path path) noexcept; - DFHACK_EXPORT bool stat (std::filesystem::path path, std::filesystem::file_status &info) noexcept; - DFHACK_EXPORT bool exists (std::filesystem::path path) noexcept; - DFHACK_EXPORT bool isfile (std::filesystem::path path) noexcept; - DFHACK_EXPORT bool isdir (std::filesystem::path path) noexcept; - DFHACK_EXPORT std::time_t mtime (std::filesystem::path path) noexcept; - DFHACK_EXPORT int listdir (std::filesystem::path dir, std::vector &files) noexcept; + DFHACK_EXPORT bool mkdir_recursive(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool rmdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool stat(std::filesystem::path path, std::filesystem::file_status& info) noexcept; + DFHACK_EXPORT bool exists(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isfile(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT std::time_t mtime(std::filesystem::path path) noexcept; + DFHACK_EXPORT int listdir(std::filesystem::path dir, std::vector& files) noexcept; // set include_prefix to false to prevent dir from being prepended to // paths returned in files - DFHACK_EXPORT int listdir_recursive (std::filesystem::path dir, std::map &files, + DFHACK_EXPORT int listdir_recursive(std::filesystem::path dir, std::map& files, int depth = 10, bool include_prefix = true) noexcept; DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; + inline std::string as_string(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; + } } } diff --git a/plugins/orders.cpp b/plugins/orders.cpp index a2da1f0ae9..5891f924a3 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -153,7 +153,7 @@ static void list_library(color_ostream &out) { if (name.extension() != ".json") continue; // skip non-.json files auto sname = name.stem(); - out << "library/" << sname << std::endl; + out << Filesystem::as_string("library" / sname) << std::endl; } } @@ -172,7 +172,7 @@ static command_result orders_list_command(color_ostream & out) if (name.extension() != ".json") continue; // skip non-.json files auto sname = name.stem(); - out << sname << std::endl; + out << sname.string() << std::endl; } list_library(out); From da4b53adc4dae165007ff4f71d769ad3d317ec8e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 26 Mar 2025 17:31:32 -0500 Subject: [PATCH 0221/1082] add `Military::addToSquad` does not handle setting a squad leader yet so will just fail if you try to add to position 0 --- docs/changelog.txt | 1 + library/include/modules/Military.h | 1 + library/modules/Military.cpp | 55 ++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee2551a94e..ebb3905391 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: ## API - ``Filesystem`` module: rewritten to use C++ standard library components, for better portability +- ''Military`` module: added `addToSquad` function ## Lua diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index dc69dac52b..3b51a921e2 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -18,6 +18,7 @@ DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); +DFHACK_EXPORT bool addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos = -1); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 2e86fb958d..83cf240d19 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -408,6 +408,61 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ df::global::world->history.events.push_back(former_pos_event); } +static void add_soldier_entity_link(df::historical_figure* hf, df::squad* squad, int32_t squad_pos) +{ + auto squad_link = df::allocate(); + squad_link->squad_id = squad->id; + squad_link->squad_position = squad_pos; + squad_link->entity_id = squad->entity_id; + squad_link->start_year = *df::global::cur_year; + squad_link->link_strength = 100; + + hf->entity_links.push_back(squad_link); +} + +bool Military::addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos) +{ + df::unit* unit = df::unit::find(unit_id); + if (unit == nullptr || unit->military.squad_id != -1) return false; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) + return false; + + df::squad* squad = df::squad::find(squad_id); + if (squad == nullptr) return false; + + if (squad_pos == -1) + { + for (int p = 0; p < 10; p++) + { + auto pp = vector_get(squad->positions, p); + if (pp == nullptr || pp->occupant == -1) + { + squad_pos = p; + break; + } + } + } + if (squad_pos == -1) return false; + + // this function cannot (currently) change the squad commander + if (squad_pos == 0) return false; + + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos == nullptr) + pos = squad->positions[squad_pos] = df::allocate(); + + pos->occupant = hf->id; + // does anything else need to be set here? + + unit->military.squad_id = squad->id; + unit->military.squad_position = squad_pos; + + add_soldier_entity_link(hf, squad, squad_pos); + return true; +} + bool Military::removeFromSquad(int32_t unit_id) { df::unit *unit = df::unit::find(unit_id); From 7eb83f1322cf64a54c16a55cc08042d96871bdd5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 26 Mar 2025 18:34:33 -0500 Subject: [PATCH 0222/1082] fix markup in changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ebb3905391..187b49a0a7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,7 +63,7 @@ Template for new versions: ## API - ``Filesystem`` module: rewritten to use C++ standard library components, for better portability -- ''Military`` module: added `addToSquad` function +- ``Military`` module: added ``addToSquad`` function ## Lua From 95461eab8fabb6a2a5f6016c11dc5755bdba545a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 27 Mar 2025 20:20:50 -0500 Subject: [PATCH 0223/1082] bump msvc version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11e95c6d8d..c8f8d90d43 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ endif() if(WIN32) set(MSVC_MIN_VER 1930) - set(MSVC_MAX_VER 1942) + set(MSVC_MAX_VER 1943) if(NOT MSVC) message(SEND_ERROR "No MSVC found! MSVC 2022 version ${MSVC_MIN_VER} to ${MSVC_MAX_VER} is required.") elseif((MSVC_VERSION LESS MSVC_MIN_VER) OR (MSVC_VERSION GREATER MSVC_MAX_VER)) From e7fa72ef9d52f8334643945a1fdf9ae457b1c064 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 28 Mar 2025 15:23:41 -0500 Subject: [PATCH 0224/1082] Create design_toolbar.png --- data/art/design_toolbar.png | Bin 0 -> 599 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/art/design_toolbar.png diff --git a/data/art/design_toolbar.png b/data/art/design_toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..935a332f00b512ca61b0fb57f229dc6ffff63285 GIT binary patch literal 599 zcmV-d0;v6oP)Px%5lKWrRA_!aYoU~`q&62o4nQxPw!b$ zgfeVD!v=4RVI0S|HsI^*0M5q8fU^ULLeAOr+;>y}>zv^-rl{9HffIWj0G2I7N^%Ul zG{JSQyB%Qy6?`4)&n_E?CNyP$Bsda9xFpyMmjM>x)BTxS=grsBdT;)!0h-teol~;g zm|&VS+{WbU1z5Bu*2=H-f+qudZ@8RI4JDFu?!Ms`{)YjQc+TzOuyl^!EoFfwzL#Ih zf`Y~q5&YatQ2iCqQpb{BEzR&~es&UE=d9MC^%n4%r36YAMDV*ZfD&`{X1N6# zhzCdTtCL_M!7rku)N$OXdJ)ZE{TON~1Fj< Date: Sun, 30 Mar 2025 20:38:01 +0200 Subject: [PATCH 0225/1082] region map export using neither GDAL nor boost --- library/include/df/custom/coord2d.methods.inc | 2 +- plugins/CMakeLists.txt | 1 + plugins/export-map.cpp | 572 ++++++++++++++++++ 3 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 plugins/export-map.cpp diff --git a/library/include/df/custom/coord2d.methods.inc b/library/include/df/custom/coord2d.methods.inc index 512149ce36..c828b0c47e 100644 --- a/library/include/df/custom/coord2d.methods.inc +++ b/library/include/df/custom/coord2d.methods.inc @@ -1,4 +1,4 @@ -coord2d(uint16_t _x, uint16_t _y) : x(_x), y(_y) {} +coord2d(int16_t _x, int16_t _y) : x(_x), y(_y) {} bool isValid() const { return x >= 0; } void clear() { x = y = -30000; } diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 8f230ff583..f87f9002ce 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -71,6 +71,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) + dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique LINK_LIBRARIES gdal) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp new file mode 100644 index 0000000000..0a06ff2130 --- /dev/null +++ b/plugins/export-map.cpp @@ -0,0 +1,572 @@ +#include "Debug.h" +#include "Error.h" +#include "PluginManager.h" +#include "MiscUtils.h" + +#include "modules/Maps.h" +#include "modules/Translation.h" + +#include "df/entity_raw.h" +#include "df/creature_raw.h" +#include "df/historical_entity.h" +#include "df/world.h" +#include "df/map_block.h" +#include "df/world_data.h" +#include "df/world_site.h" +#include "df/region_map_entry.h" +#include "df/world_region.h" +#include "df/world_landmass.h" +#include "df/world_region_details.h" + +#include "gdal/ogrsf_frmts.h" + +#include +#include +#include +#include +#include +#include +#include + + +using std::string; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("export-map"); + +REQUIRE_GLOBAL(world); + +namespace DFHack { + DBG_DECLARE(exportmap, log); +} + +static command_result do_command(color_ostream &out, vector ¶meters); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(log,out).print("initializing %s\n", plugin_name); + + commands.push_back(PluginCommand( + plugin_name, + "Export the world map.", + do_command)); + + return CR_OK; +} + +auto setGeometry(OGRFeature *feature, double x, double y, double dimx, double dimy = 0) { + if (dimy == 0) { dimy = dimx; } + auto poly = new OGRPolygon(); + auto boundary = new OGRLinearRing(); + y = -y; // in GIS negative y-coordinates mean further south + boundary->addPoint(x,y); + boundary->addPoint(x,y-dimy); + boundary->addPoint(x+dimx,y-dimy); + boundary->addPoint(x+dimx,y); + boundary->closeRings(); + //the "Directly" variants assume ownership of the objects created above + poly->addRingDirectly(boundary); + feature->SetGeometryDirectly( poly ); +} + +df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { + switch (dir) { + case 1: world_x-- ; world_y++; break; + case 2: ; world_y++; break; + case 3: world_x++ ; world_y++; break; + case 4: world_x-- ; ; break; + // case 5 induces no change + case 6: world_x++ ; ; break; + case 7: world_x-- ; world_y--; break; + case 8: ; world_y--; break; + case 9: world_x++ ; world_y--; break; + } + world_x = std::min(std::max((int16_t)0,world_x),(int16_t)(world->world_data->world_width - 1)); + world_y = std::min(std::max((int16_t)0,world_y),(int16_t)(world->world_data->world_height - 1)); + return df::coord2d(world_x, world_y); +} + +auto create_field(OGRLayer *layer, std::string name, OGRFieldType type, int width = 0, OGRFieldSubType subtype = OFSTNone) { + OGRFieldDefn field( name.c_str() , type ); + if (subtype != OFSTNone) { + field.SetSubType(subtype); + } + if (width != 0) { + field.SetWidth(width); + } + // this should create a copy internally + if( layer->CreateField( &field ) != OGRERR_NONE ){ + throw CR_FAILURE; + } +} + +// PROJ.4 description of EPSG:3857 (https://epsg.io/3857) +static const char* EPSG_3857 = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs"; + +const char* describe_surroundings(int savagery, int evilness) { + constexpr std::arraysurroundings{ + "Serene", "Mirthful", "Joyous Wilds", + "Calm", "Wilderness", "Untamed Wilds", + "Sinister", "Haunted", "Terrifying" + }; + auto savagery_index = savagery < 33 ? 0 : (savagery > 65 ? 2 : 1); + auto evilness_index = evilness < 33 ? 0 : (evilness > 65 ? 2 : 1); + return surroundings[3 * evilness_index + savagery_index]; +} + +// static int wdim = 768; // dimension of a world tile +static int rdim = 48; // dimension of a region tile + +static command_result export_region_tiles(color_ostream &out); +static command_result export_sites(color_ostream &out); + +static command_result do_command(color_ostream &out, vector ¶meters) +{ + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()){ + out.printerr("This command requires a world to be loaded\n"); + return CR_WRONG_USAGE; + } + + if (parameters.size() && parameters[0] == "sites") + { + return export_sites(out); + } + else + { + return export_region_tiles(out); + } +} + +static command_result export_sites(color_ostream &out) +{ + out.print("exporting sites... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // set up coordinate system + OGRSpatialReference CRS; + if (CRS.importFromProj4(EPSG_3857) != OGRERR_NONE) { + out.printerr("could not set up coordinate system"); + return CR_FAILURE; + } + + // set up output driver + GDALAllRegister(); + const char *driver_name = "SQLite"; + const char *extension = "sqlite"; + auto driver = GetGDALDriverManager()->GetDriverByName(driver_name); + if (!driver) { + out.printerr("could not find sqlite driver"); + return CR_FAILURE; + } + + // create a dataset and associate it to a file + std::string sites("sites."); + sites.append(extension); + const char* options[] = { "SPATIALITE=YES", nullptr }; + auto dataset = driver->Create( sites.c_str(), 0, 0, 0, GDT_Unknown, options); + if (!dataset) { + out.printerr("could not create dataset"); + return CR_FAILURE; + } + + // create a layer for the biome data + // const char* format[] = { "FORMAT=WKT", nullptr }; + auto layer = dataset->CreateLayer( "world_sites", &CRS, wkbPolygon, nullptr ); + if (!layer) { + out.printerr("could not create layer"); + return CR_FAILURE; + } + + try { + create_field(layer, "site_id", OFTInteger); + create_field(layer, "civ_id", OFTInteger); + create_field(layer, "created_year", OFTInteger); + create_field(layer, "cur_owner_id", OFTInteger); + + create_field(layer, "type", OFTString, 15); + + create_field(layer, "site_name_df", OFTString, 100); + create_field(layer, "site_name_en", OFTString, 100); + + create_field(layer, "civ_name_df", OFTString, 100); + create_field(layer, "civ_name_en", OFTString, 100); + + create_field(layer, "site_government_df", OFTString, 100); + create_field(layer, "site_government_en", OFTString, 100); + + create_field(layer, "owner_race", OFTString, 15); + + // create_field(layer, "local_ruler", OFTString, 100); + + } + catch (const DFHack::command_result& r) { + out.printerr("could not create fields for output layer"); + return r; + } + + if (dataset->StartTransaction() != OGRERR_NONE) { + out.printerr("could not start a transaction\n"); + } + + for (auto const site : world->world_data->sites) + { + + auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); + + setGeometry( + feature, + site->global_min_x * rdim, + site->global_min_y * rdim, + (site->global_max_x - site->global_min_x + 1) * rdim, + (site->global_max_y - site->global_min_y + 1) * rdim + ); + feature->SetField( "site_id", site->id ); + feature->SetField( "type", ENUM_KEY_STR(world_site_type, site->type).c_str() ); + #define SET_FIELD(name) feature->SetField( #name, site->name) + SET_FIELD(civ_id); + SET_FIELD(created_year); + SET_FIELD(cur_owner_id); + #undef SET_FIELD + + #define TRANSLATE_NAME(field_name, name_object)\ + feature->SetField((#field_name"_df"), DF2UTF(Translation::translateName(&name_object, false)).c_str());\ + feature->SetField((#field_name"_en"), DF2UTF(Translation::translateName(&name_object, true)).c_str()); + + TRANSLATE_NAME(site_name, site->name) + + auto civ = df::historical_entity::find(site->civ_id); + if (civ) { TRANSLATE_NAME(civ_name,civ->name) } + + auto owner = df::historical_entity::find(site->cur_owner_id); + if (owner) { + TRANSLATE_NAME(site_government,owner->name) + auto race = df::creature_raw::find(owner->race); + if (!race){ + race = df::creature_raw::find(civ->race); + } + if (race) { + feature->SetField( "owner_race", race->name[2].c_str() ); + } + } + + + // this updates the feature with the id it receives in the layer + if( layer->CreateFeature( feature ) != OGRERR_NONE ) + return CR_FAILURE; + // but we don't care and destroy the feature + OGRFeature::DestroyFeature( feature ); + } + + dataset->CommitTransaction(); + + GDALClose( dataset ); + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in %f ms !\n", elapsed_seconds.count()); + return CR_OK; +} + +/********************************************************************** */ + +template<> +struct std::hash +{ + std::size_t operator()(const df::coord2d& pos) const noexcept + { + // hashing is easy, if values are smaller than the hash + return ((std::size_t)pos.x << 16) | (std::size_t)pos.y; + } +}; + +using coord = df::coord2d; + +enum class direction : int { North = 0, West = 1, South = 2, East = 3 }; + +direction turn_left(direction dir) { + return (direction)(((int)dir+1) % 4); +} + +direction turn_right(direction dir) { + return (direction)(((int)dir+3) % 4); +} + +df::coord2d as_offset(direction dir) { + switch (dir) { + case direction::North: + return { 0, -1 }; + case direction::West: + return { -1, 0 }; + case direction::South: + return { 0, 1 }; + case direction::East: + return { 1, 0 }; + default: + abort(); + } +} + +coord advance(coord pos, direction dir) { + return pos + as_offset(dir); +} + +auto print_path(std::vector path, int scale = rdim) { + assert(path.size()); + // create the path in clockwise direction, so that it becomes counterclockwise with the y-flip + std::ranges::reverse(path); + std::ostringstream out; + out << scale * path[0].x << " " << -scale * path[0].y; + for (size_t i = 1; i < path.size(); ++i) { + out << "," << scale * path[i].x << " " << -scale * path[i].y; + } + return out.str(); +} + +std::pair ahead(const std::vector &component, coord pos, direction dir) { + auto test = [&](int16_t x, int16_t y){ + coord offset{x,y}; + return std::find(component.begin(), component.end(), pos + offset) != component.end(); + }; + + switch (dir) { + case direction::North: + return { test(-1,-1), test(0,-1)}; + case direction::West: + return { test(-1,0), test(-1,-1)}; + case direction::South: + return { test(0,0), test(-1, 0)}; + case direction::East: + return { test(0,-1), test(0, 0)}; + default: + abort(); + } +} + +static command_result export_region_tiles(color_ostream &out) +{ + out.print("%lu / %d region map tiles loaded\n", + world->world_data->midmap_data.region_details.size(), + world->world_data->world_width * world->world_data->world_height + ); + out.print("exporting map... \n"); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // ensure that we have an output file + std::string filename("map.csv"); + std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + if (!out_file) { + return CR_FAILURE; + } + out_file << "world_x; world_y; num_tiles; num_components; biome_type; boundary_wkt" << std::endl; + + /* + try { + create_field(layer, "region_id", OFTInteger); + create_field(layer, "region_name_en", OFTString, 100); + create_field(layer, "region_name_df", OFTString, 100); + create_field(layer, "landmass_id", OFTInteger); + create_field(layer, "landmass_name_en", OFTString, 100); + create_field(layer, "landmass_name_df", OFTString, 100); + + create_field(layer, "biome_type", OFTString, 32); + create_field(layer, "surroundings", OFTString, 16); + create_field(layer, "elevation", OFTInteger); + + create_field(layer, "evilness", OFTInteger); + create_field(layer, "savagery", OFTInteger); + create_field(layer, "volcanism", OFTInteger); + create_field(layer, "drainage", OFTInteger); + create_field(layer, "temperature", OFTInteger); + create_field(layer, "vegetation", OFTInteger); + create_field(layer, "rainfall", OFTInteger); + create_field(layer, "snowfall", OFTInteger); + create_field(layer, "salinity", OFTInteger); + + create_field(layer, "reanimating", OFTInteger, 0, OFSTBoolean); + create_field(layer, "has_bogeymen", OFTInteger, 0, OFSTBoolean); + + } catch (const DFHack::command_result& r) { + out.printerr("could not create fields for output layer"); + return r; + } + */ + + + + // iterating over the region details allows the user to do partial map exports + // by manually scrolling on the embark site selection + + std::unordered_map> world_tile_region; + + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto world_x = region_details->pos.x; + auto world_y = region_details->pos.y; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) { + + // auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); + // setGeometry( + // feature, + // (double)(world_x * wdim + region_x * rdim), + // (double)(world_y * wdim + region_y * rdim), + // rdim + // ); + + // get the world tile coordinates used for the biome information of the local region tile + auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); + world_tile_region[biome_tile].emplace_back(16 * world_x + region_x, 16 * world_y + region_y); + + } + } + } + + out.print("processing %ld world tile regions\n", world_tile_region.size()); + + auto region_order = [](df::coord2d p1, df::coord2d p2) { + return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); + }; + static std::array directions{ + as_offset(direction::North), + as_offset(direction::West), + as_offset(direction::South), + as_offset(direction::East) + }; + + for (auto& [biome_tile, region] : world_tile_region) + { + assert(region.size() > 0); + // compute the connected components of the world tile region + std::ranges::sort(region, region_order); + + std::vector component_assignment; + component_assignment.resize(region.size(),0); + std::deque agenda; + + auto current_component = 0; + for (size_t i = 0; i < region.size(); ++i) { + if (component_assignment[i]) { + continue; + } else { + ++current_component; + component_assignment[i] = current_component; + agenda.push_back(i); + } + while(!agenda.empty()) { + auto pos_idx = agenda.front(); agenda.pop_front(); + auto pos = region[pos_idx]; + + for (auto const offset : directions) { + // FIXME: use something O(log n) instead of the O(n) find + auto n_it = std::find(region.begin(), region.end(), pos + offset); + auto n_idx = std::distance(region.begin(), n_it); + if (n_it != region.end() && component_assignment[n_idx] == 0) { + component_assignment[n_idx] = current_component; + agenda.push_back(n_idx); + } + } + } + } + + // assert that all parts of the region are accounted for + assert(std::ranges::all_of(component_assignment, [](int comp){ return comp > 0;})); + + std::vector> components; + components.resize(current_component); + for (size_t i = 0; i < region.size(); ++i) { + components.at(component_assignment.at(i) - 1).push_back(region.at(i)); + } + + std::vector> paths; + for (auto const &component : components) { + + auto start = component.at(0); + std::vector path; + path.push_back(start); + + auto current_direction = direction::South; + auto current_position = advance(start,current_direction); + + while (current_position != start) + { + auto [left, right] = ahead(component, current_position, current_direction); + if (left && right) { + // in front of a wall: turn right + path.push_back(current_position); + current_direction = turn_right(current_direction); + } + else if (!left && !right) { + // no walls ahead: turn left + path.push_back(current_position); + current_direction = turn_left(current_direction); + } + assert(! (!left && right)); // shape has a hole or is not connected + current_position = advance(current_position, current_direction); + } + // close the path + path.push_back(current_position); + paths.push_back(std::move(path)); + path.clear(); + } + + out_file << biome_tile.x << ";" << biome_tile.y << ";" << region.size() << ";" << components.size() << ";"; + out_file << ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_tile.x, biome_tile.y)) << ";"; + assert(paths.size() > 0); + if (paths.size() == 1) { + out_file << "POLYGON((" << print_path(paths[0]) << "))" << std::endl; + } else { + out_file << "MULTIPOLYGON(" << "((" << print_path(paths[0]) << "))"; + for (size_t i = 1; i < paths.size(); ++i) { + out_file << ",((" << print_path(paths[i]) << "))"; + } + out_file << ")" << std::endl; + } + } + + + // feature->SetField( "biome_type", ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_x,biome_y)).c_str() ); + + // gets supplementary information from the world tile + // auto& region_map_entry = world->world_data->region_map[biome_x][biome_y]; + // #define SET_FIELD(name) feature->SetField( #name, region_map_entry.name) + // SET_FIELD(region_id); + // SET_FIELD(landmass_id); + // SET_FIELD(evilness); + // SET_FIELD(savagery); + // SET_FIELD(volcanism); + // SET_FIELD(drainage); + // SET_FIELD(temperature); + // SET_FIELD(vegetation); + // SET_FIELD(rainfall); + // SET_FIELD(snowfall); + // SET_FIELD(salinity); + // #undef SET_FIELD + + // feature->SetField( "surroundings", describe_surroundings(region_map_entry.savagery, region_map_entry.evilness)); + + // auto region = df::world_region::find(region_map_entry.region_id); + // if (region) { + // auto region_name_en = DF2UTF(Translation::translateName(®ion->name, true)); + // feature->SetField( "region_name_en", region_name_en.c_str()); + // auto region_name_df = DF2UTF(Translation::translateName(®ion->name, false)); + // feature->SetField( "region_name_df", region_name_df.c_str()); + // feature->SetField("reanimating", region->reanimating); + // feature->SetField("has_bogeymen", region->has_bogeymen); + // } + // auto landmass = df::world_landmass::find(region_map_entry.landmass_id); + // if (landmass) { + // auto landmass_name_en = DF2UTF(Translation::translateName(&landmass->name, true)); + // feature->SetField( "landmass_name_en", landmass_name_en.c_str()); + // auto landmass_name_df = DF2UTF(Translation::translateName(&landmass->name, false)); + // feature->SetField( "landmass_name_df", landmass_name_df.c_str()); + // } + + + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in %f ms !\n", elapsed_seconds.count()); + return CR_OK; +} From f0765a4b36b844d68d9ba690e90bbd8d95bc66f8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 18:23:16 -0700 Subject: [PATCH 0226/1082] add hotkey for gui/journal in adv mode --- data/init/dfhack.keybindings.init | 6 +++--- docs/changelog.txt | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index 776a8425b3..018c66ccde 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -47,6 +47,9 @@ keybinding add Ctrl-G@dwarfmode/Default|dungeonmode/Default gui/sitemap # toggle keyboard cursor keybinding add Alt-K@dwarfmode|dungeonmode/Default|dungeonmode/Look toggle-kbd-cursor +# gui/journal +keybinding add Ctrl-J@dwarfmode|dungeonmode gui/journal + ###################### # dwarfmode bindings # @@ -151,9 +154,6 @@ keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort keybinding add Ctrl-D@dwarfmode/Default gui/design keybinding add Ctrl-M@dwarfmode/Default gui/mass-remove -# gui/journal -keybinding add Ctrl-J@dwarfmode gui/journal - ######################## # dungeonmode bindings # diff --git a/docs/changelog.txt b/docs/changelog.txt index ee2551a94e..ce9d7f80d6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,7 @@ Template for new versions: ## New Tools ## New Features +- `gui/journal`: Ctrl-j hotkey to launch `gui/journal` now works in adventure mode! ## Fixes From 9b00c08c6b117fa44a01d82153ed221e4de6d0c7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 18:31:30 -0700 Subject: [PATCH 0227/1082] adapt to structure changes --- docs/changelog.txt | 1 + plugins/lua/sort/diplomacy.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee2551a94e..b6ba2ba2f5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- Fix processing error in the overlay that displays unit preferences in the baron selection list ## Misc Improvements diff --git a/plugins/lua/sort/diplomacy.lua b/plugins/lua/sort/diplomacy.lua index 3e78593862..505e02b743 100644 --- a/plugins/lua/sort/diplomacy.lua +++ b/plugins/lua/sort/diplomacy.lua @@ -64,7 +64,7 @@ local function get_preferences(unit) if not unit then return {} end local preferences = {} for _, pref in ipairs(unit.status.current_soul.preferences) do - if pref.type == df.unitpref_type.LikeItem and pref.active then + if pref.type == df.unitpref_type.LikeItem and pref.flags.visible then table.insert(preferences, make_item_description(pref.item_type, pref.item_subtype)) end end From 739a5eeabc916a05c9a9bdd7bfc33e9724ce5595 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 1 Apr 2025 01:33:45 +0000 Subject: [PATCH 0228/1082] 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 c783692f43..d0c14fa0a6 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit c783692f43a205cef821e669adc26bc725f5c159 +Subproject commit d0c14fa0a6702f396e147bbd3c9b325c0677aa2b diff --git a/scripts b/scripts index ccdc48f2dd..a9f4502faf 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ccdc48f2dd4d1a777dc9d1002c707202ffdffff3 +Subproject commit a9f4502faf2e2e026361aa9e84a138077d1dff7b From e4359781d7d5034b4e6000f5967be750e1610ccd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 18:51:37 -0700 Subject: [PATCH 0229/1082] make toolbar icon fit in with others in the group --- data/art/sitemap_toolbar.png | Bin 483 -> 4833 bytes scripts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/art/sitemap_toolbar.png b/data/art/sitemap_toolbar.png index 07871884a64a746cd5aba7b1d6a1c72a00d6ad7c..a29fa9b9004532e764504ea6e758236029de8e65 100644 GIT binary patch literal 4833 zcmeHLeN+=y7M~y@Mny$zJ)$+F+7)mzlgT7xvI(C-NY@|>6j`;dlbIJXm3)v45TI6M ztJRg&YU?*zJXpU$Xzj68D{zF>-6B$ZdiK=agOsYU)m2b;b&=J5GlZ|RJ!g;S)c;7% zn|J5E`+o1<-@WfnCL2>zk|s%KN)ZH^WJ)%s!9EtYxq$)jN=?*ngIz1gGFeTg2qX~R zKuw81B7ssYY!aV57s`H;F~}I$uZJxd%7Tv2W=h72eHoNPee#D;p6}BU)+i-@$Rya8 z!$v?k1op>ZpYZ&PoVgX@u{A$rIabPI+D_< zNw~kHV#p!^NzzgGGQ7UaB_yPp5)x!Cx6{hy00i-tuik1%zK|NSZt*uSB}N40PszQM zI=fL3(UEmL^$ zSoo<8J?oRATh4ZNx2-N|JwaTS&O^+nFE5G+zjgn(qerM(>aX7is9E!3N5rK|VdUsrxHq7!Xd`dQ1h&Kpr>ArH5M>ThPo zNjGJc`cIYqc%(uan%jH(VAiG9cdlNHxzjt(9e(3j$yP?9w!Qym%;V6${Z((2?|HaQ zZj$wNXy;VMUQx8ZrEPgs`@^Y}l6IBrqPqLd7#Xca@T@|b9;-m8NEcEHmzuRX`%(RmGW5myIg zVHlIkL=}FOL7oNkD8cClGEzY*a5T}&g)Klo-X)o&VyedHSV;F&ladR%7b2?-KCT(%%@dmjZ&dUbu zv%AdZL3oG9#{%R7^U^L%slYM29UH#F!zboL5T8Lmxx$lC=mJ<8@Hq3_3`ooe4*u2Q z5G*se-j(Oh5#3=K4CDYiRQ14Jl_RzkPTb%Mfr4z#?h;o)vPWq0oOOt-5w;04qC3M4 zfy)PRM`#cDE-FJUvsq_!GI@e|CZj1G%SOv zD5XZj0-7Qy?Jy{l!^6`K1_)3PT){ycwN*vpR=}bbrA331ERLfzV^yN06%Y(fYgItY z41-wg=3rLRIm4|IpjZe+5Tu4y0UD)ol^P|rj0)980V@hLswkSEDJ^MHi%=}1Tj+G# zX&6q7hOhc{1mL`xlBs{U<6N z>J*;aA(i(h{m3jPwB@!i8<@=@0beSGz#z_N|;UmlZ9FPwAd z?IcRuxx(6B^T!#VR6|+FesD*}e0Z)YB)Pt5e$x4?KA6 z&5LNiej2m3C9SKes_%JwLfEaFEu{(dedoMs{*mLigkAV|Czw8cZRwhF!}!T{;Zsle z>&FHB;HvsCaKv6WD7FM{65?BRk&^5x=)i!_LOEWo5+lZQYRqWl1`AenIJFe93yQTkK)!NAi z$ZK@^^~1)_>qv=Kt)qdRw>C4>_gL-@SjT{ZAoXk}Zj?rPptShM6Cmr4_TEeHnXlOGwel eH8cJ*ZFh`qQ|O(xqqcQ0a)>E0#aJD`vh=^*5SmW_ delta 457 zcmV;)0XF{OCF28-B!2;OQb$4nuFf3k0004ZusIAwNyR5Wk&43*IAXI#|7K)e8lus_VLF0pGbVKr>C( zvg4=yLHv!XfXDgCj&Clt=4l2Y@ZEJQ9Pa_}B2<=u1a4;z5r0kBir{wc$9^CoT7V62 zLrxu!2?u{a_HUoLHA33?odxcVkQuej5CNuah0PEkiLl2h zYOQd{9o2#lZG^abl@W6zCwZj7=0!n}f1OcVM1B`%@;6edQKxy!RA)rKfzz{&tso{b5 z{e4P!U}66bH(i?)9$1)w9QnKy&3ys6Q@ns5pKcYou}*sV00000NkvXXu0mjf2(Z%V diff --git a/scripts b/scripts index a9f4502faf..20d201a8ff 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit a9f4502faf2e2e026361aa9e84a138077d1dff7b +Subproject commit 20d201a8ffe5df52563a37fdde88986973faca4e From a8bd0f84dcfeafb820c91abdcf716c51a6bd4288 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:12:10 +0000 Subject: [PATCH 0230/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 20d201a8ff..31e31bf187 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 20d201a8ffe5df52563a37fdde88986973faca4e +Subproject commit 31e31bf18728c9913deb234ef47a6f11a7208a1d From 7b6da0043b66d0d1addacf95b5c00fdeec260203 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:19:19 +0000 Subject: [PATCH 0231/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 31e31bf187..3a919bc098 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 31e31bf18728c9913deb234ef47a6f11a7208a1d +Subproject commit 3a919bc098a08e0e1c259f82d03ecf268bd10a41 From d7a04d6c2f2d60c77711b4ef452e085bd6f2e2f8 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:35:23 +0200 Subject: [PATCH 0232/1082] clean up and reinstate export of the remaining biome and region values --- plugins/export-map.cpp | 131 ++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 28 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 0a06ff2130..8d4b4c7039 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -272,6 +272,40 @@ static command_result export_sites(color_ostream &out) /********************************************************************** */ +template Callable> +void print_vector( + std::ostream &out, + const std::vector &elements, + Callable&& print_element, + const char* prefix = "[", + const char* separator = ", ", + const char* suffix = "]" +){ + out << prefix; + if (elements.size()) { + print_element(out, elements[0]); + for (size_t i = 1; i < elements.size(); ++i) { + out << separator; + print_element(out, elements[i]); + } + } + out << suffix; +} + +template +void print_vector( + std::ostream &out, + const std::vector &elements, + const char* prefix = "[", + const char* separator = ", ", + const char* suffix = "]" +){ + auto print_element = [](std::ostream &out, const T& e) { out << e; }; + print_vector(out, elements, print_element, prefix, separator, suffix); +} + + + template<> struct std::hash { @@ -313,22 +347,22 @@ coord advance(coord pos, direction dir) { return pos + as_offset(dir); } -auto print_path(std::vector path, int scale = rdim) { +auto print_path(std::ostream &out, const std::vector &path) { + auto scale = rdim; assert(path.size()); - // create the path in clockwise direction, so that it becomes counterclockwise with the y-flip - std::ranges::reverse(path); - std::ostringstream out; - out << scale * path[0].x << " " << -scale * path[0].y; - for (size_t i = 1; i < path.size(); ++i) { - out << "," << scale * path[i].x << " " << -scale * path[i].y; - } - return out.str(); + auto print_point = [scale](std::ostream &out, const coord &pos){ + out << scale * pos.x << " " << -scale * pos.y;}; + print_vector(out, path, print_point, "(", ",", ")"); } +auto region_order = [](df::coord2d p1, df::coord2d p2) { + return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); +}; + std::pair ahead(const std::vector &component, coord pos, direction dir) { auto test = [&](int16_t x, int16_t y){ coord offset{x,y}; - return std::find(component.begin(), component.end(), pos + offset) != component.end(); + return std::ranges::binary_search(component, pos + offset, region_order); }; switch (dir) { @@ -361,7 +395,17 @@ static command_result export_region_tiles(color_ostream &out) if (!out_file) { return CR_FAILURE; } - out_file << "world_x; world_y; num_tiles; num_components; biome_type; boundary_wkt" << std::endl; + + vector headings = { + "world_x", "world_y", "num_tiles", "num_components", "biome_type", + "region_id", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df", + "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "snowfall", "salinity", + "surroundings", "elevation", "reanimating", "has_bogeymen" + }; + print_vector(out_file, headings,"",";",";boundary_wkt\n" ); + + + /* try { @@ -426,9 +470,7 @@ static command_result export_region_tiles(color_ostream &out) out.print("processing %ld world tile regions\n", world_tile_region.size()); - auto region_order = [](df::coord2d p1, df::coord2d p2) { - return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); - }; + static std::array directions{ as_offset(direction::North), as_offset(direction::West), @@ -461,7 +503,10 @@ static command_result export_region_tiles(color_ostream &out) for (auto const offset : directions) { // FIXME: use something O(log n) instead of the O(n) find - auto n_it = std::find(region.begin(), region.end(), pos + offset); + auto lower = std::ranges::lower_bound(region, pos + offset, region_order); + auto n_it = (*lower == pos + offset) ? lower : region.end(); + + // auto n_it = std::find(region.begin(), region.end(), pos + offset); auto n_idx = std::distance(region.begin(), n_it); if (n_it != region.end() && component_assignment[n_idx] == 0) { component_assignment[n_idx] = current_component; @@ -487,7 +532,7 @@ static command_result export_region_tiles(color_ostream &out) std::vector path; path.push_back(start); - auto current_direction = direction::South; + auto current_direction = direction::East; auto current_position = advance(start,current_direction); while (current_position != start) @@ -496,14 +541,14 @@ static command_result export_region_tiles(color_ostream &out) if (left && right) { // in front of a wall: turn right path.push_back(current_position); - current_direction = turn_right(current_direction); + current_direction = turn_left(current_direction); } else if (!left && !right) { // no walls ahead: turn left path.push_back(current_position); - current_direction = turn_left(current_direction); + current_direction = turn_right(current_direction); } - assert(! (!left && right)); // shape has a hole or is not connected + assert(! (left && !right)); // shape has a hole or is not connected current_position = advance(current_position, current_direction); } // close the path @@ -511,18 +556,48 @@ static command_result export_region_tiles(color_ostream &out) paths.push_back(std::move(path)); path.clear(); } - - out_file << biome_tile.x << ";" << biome_tile.y << ";" << region.size() << ";" << components.size() << ";"; - out_file << ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_tile.x, biome_tile.y)) << ";"; assert(paths.size() > 0); + + auto& region_map_entry = world->world_data->region_map[biome_tile.x][biome_tile.y]; + auto world_region = df::world_region::find(region_map_entry.region_id); + auto landmass = df::world_landmass::find(region_map_entry.landmass_id); + + auto print_csv = [&out_file](auto ...args){ ([&]{ out_file << args << ";" ; }() ,...); }; + print_csv( + // "world_x", "world_y", "num_tiles", "num_components", "biome_type", + biome_tile.x, + biome_tile.y, + region.size(), + components.size(), + ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_tile.x, biome_tile.y)), + // "region_id", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df" + region_map_entry.region_id, + world_region ? DF2UTF(Translation::translateName(&world_region->name, true)) : "NONE", + world_region ? DF2UTF(Translation::translateName(&world_region->name, false)) : "NONE", + region_map_entry.landmass_id, + landmass ? DF2UTF(Translation::translateName(&landmass->name, true)) : "NONE", + landmass ? DF2UTF(Translation::translateName(&landmass->name, false)) : "NONE", + // "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "snowfall", "salinity" + region_map_entry.evilness, + region_map_entry.savagery, + region_map_entry.volcanism, + region_map_entry.drainage, + region_map_entry.temperature, + region_map_entry.vegetation, + region_map_entry.rainfall, + region_map_entry.snowfall, + region_map_entry.salinity, + // "surroundings", "elevation", "reanimating", "has_bogeymen" + describe_surroundings(region_map_entry.savagery, region_map_entry.evilness), + region_map_entry.elevation, + world_region->reanimating, + world_region->has_bogeymen + ); + // output geometry if (paths.size() == 1) { - out_file << "POLYGON((" << print_path(paths[0]) << "))" << std::endl; + print_vector(out_file, paths, print_path , "POLYGON(", ",", ")\n" ); } else { - out_file << "MULTIPOLYGON(" << "((" << print_path(paths[0]) << "))"; - for (size_t i = 1; i < paths.size(); ++i) { - out_file << ",((" << print_path(paths[i]) << "))"; - } - out_file << ")" << std::endl; + print_vector(out_file, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); } } From c5eff407cb4ca054578b0150d520f0a1050197c5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 27 Mar 2025 20:20:50 -0500 Subject: [PATCH 0233/1082] bump msvc version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11e95c6d8d..c8f8d90d43 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ endif() if(WIN32) set(MSVC_MIN_VER 1930) - set(MSVC_MAX_VER 1942) + set(MSVC_MAX_VER 1943) if(NOT MSVC) message(SEND_ERROR "No MSVC found! MSVC 2022 version ${MSVC_MIN_VER} to ${MSVC_MAX_VER} is required.") elseif((MSVC_VERSION LESS MSVC_MIN_VER) OR (MSVC_VERSION GREATER MSVC_MAX_VER)) From f589397542d6f3e9f6c6185f530b58f9a5f2ba39 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 1 Apr 2025 14:37:07 -0500 Subject: [PATCH 0234/1082] add `addToSquad` to Lua API --- library/LuaApi.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 0ce74f9f06..f169650f90 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2344,6 +2344,7 @@ static const LuaWrapper::FunctionReg dfhack_military_module[] = { WRAPM(Military, updateRoomAssignments), WRAPM(Military, getSquadName), WRAPM(Military, removeFromSquad), + WRAPM(Military, addToSquad), { NULL, NULL } }; From 0560b0089a999c06476d3c9c8b251ea007228dac Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 1 Apr 2025 14:43:18 -0500 Subject: [PATCH 0235/1082] add documentation --- docs/dev/Lua API.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index d449c8810b..e2d9eaa786 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1995,6 +1995,23 @@ Military module to indicate former squad membership or command, and creates a corresponding world history event. + * ``dfhack.military.addToSquad(unit_id, squad_id, squad_pos)`` + + Adds a unit to a squad. Sets the unit's + military information (i.e., ``unit.military.squad_id`` and + ``unit.military.squad_pos``), the squad's position information (i.e., + ``squad.positions[squad_pos].occupant``), adds a unit's entity links to + indicate squad membership. Does not currently add world history events. + If ``squad_pos`` is -1, the unit will be added to the first open slot in + the squad. + + This API cannot be used to set or change the leader of a squad and will fail + if ``squad_pos`` is specified as 0 or if ``squad_pos`` is specified as -1 and + the squad leader position is currently vacant. It will also fail if + the requested squad position is already occupied, the squad does not exist, + the unit does not exist, or the requested unit is already a member of another + squad. + Items module ------------ From 2302b256b969e06c9631e94a90cf1062a2930f29 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:43:46 +0000 Subject: [PATCH 0236/1082] 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 860017b193..1732e54395 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 860017b193c5cce1ee6c16321301b0a1d4788569 +Subproject commit 1732e54395247019ff21aa77a09b0fe3869a5c87 From b040c783f46fcc7b6cef2c92fa0ee9cc092205f7 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:52:01 +0000 Subject: [PATCH 0237/1082] Auto-update structures ref for 51.09 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 1732e54395..94f95a3dd5 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1732e54395247019ff21aa77a09b0fe3869a5c87 +Subproject commit 94f95a3dd53a2a4a35ed645e2afea9109079269a From 4d988aa1b600d4ba5632ec1305a64ded9ced251c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 2 Apr 2025 09:12:18 -0700 Subject: [PATCH 0238/1082] update for 51.09 --- CMakeLists.txt | 2 +- docs/changelog.txt | 14 +++++++++++--- scripts | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c8f8d90d43..fcc15168ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.08") +set(DF_VERSION "51.09") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index f384939d40..7c4babf881 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,22 +54,30 @@ Template for new versions: ## New Tools ## New Features -- `gui/journal`: Ctrl-j hotkey to launch `gui/journal` now works in adventure mode! ## Fixes -- Fix processing error in the overlay that displays unit preferences in the baron selection list ## Misc Improvements ## Documentation ## API -- ``Filesystem`` module: rewritten to use C++ standard library components, for better portability ## Lua ## Removed +# 51.09-r1 + +## New Features +- `gui/journal`: Ctrl-j hotkey to launch `gui/journal` now works in adventure mode! + +## Fixes +- Fix processing error in the overlay that displays unit preferences in the baron selection list + +## API +- ``Filesystem`` module: rewritten to use C++ standard library components, for better portability + # 51.08-r1 ## Misc Improvements diff --git a/scripts b/scripts index 3a919bc098..d8912409bb 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3a919bc098a08e0e1c259f82d03ecf268bd10a41 +Subproject commit d8912409bb3c7398a2859aa0c0092d792c9211e7 From d899a2a0fdd821c8323afc0d1b2a6d50f2b9f975 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:16:38 +0200 Subject: [PATCH 0239/1082] comments and cleanup --- library/include/MiscUtils.h | 36 ++++++ plugins/export-map.cpp | 239 ++++++++++++------------------------ 2 files changed, 114 insertions(+), 161 deletions(-) diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 8568a55c0a..62a09c98bf 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -395,6 +395,42 @@ inline bool static_add_to_map(CT *pmap, typename CT::key_type key, typename CT:: /* * MISC */ +template< + std::ranges::input_range Range, + std::invocable> Callable> +void print_range( + std::ostream &out, + const Range &elements, + Callable&& print_element, + const std::string &prefix = "[", + const std::string &separator = ", ", + const std::string &suffix = "]" +){ + out << prefix; + auto it = std::ranges::begin(elements); + auto end = std::ranges::end(elements); + + if (it != end) { + print_element(out, *it); + for (++it; it != end; ++it) { + out << separator; + print_element(out, *it); + } + } + out << suffix; +} + +template +void print_range( + std::ostream &out, + const Range& elements, + const std::string &prefix = "[", + const std::string &separator = ", ", + const std::string &suffix = "]" +){ + auto print_element = [](std::ostream &out, auto& e) { out << e; }; + print_range(out, elements, print_element, prefix, separator, suffix); +} DFHACK_EXPORT bool split_string(std::vector *out, const std::string &str, const std::string &separator, diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 8d4b4c7039..0e0db0e83e 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -39,9 +39,23 @@ DFHACK_PLUGIN("export-map"); REQUIRE_GLOBAL(world); namespace DFHack { - DBG_DECLARE(exportmap, log); + DBG_DECLARE(exportmap, log, DebugCategory::LINFO); + DBG_DECLARE(exportmap, warning, DebugCategory::LWARNING); } +template<> +struct std::hash +{ + std::size_t operator()(const df::coord2d& pos) const noexcept + { + // hashing is easy, if values are smaller than the hash + return ((std::size_t)pos.x << 16) | (std::size_t)pos.y; + } +}; + +// were only dealing with 2D coordinates in this file +using coord = df::coord2d; + static command_result do_command(color_ostream &out, vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { @@ -84,7 +98,7 @@ df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { } world_x = std::min(std::max((int16_t)0,world_x),(int16_t)(world->world_data->world_width - 1)); world_y = std::min(std::max((int16_t)0,world_y),(int16_t)(world->world_data->world_height - 1)); - return df::coord2d(world_x, world_y); + return { world_x, world_y }; } auto create_field(OGRLayer *layer, std::string name, OGRFieldType type, int width = 0, OGRFieldSubType subtype = OFSTNone) { @@ -272,52 +286,12 @@ static command_result export_sites(color_ostream &out) /********************************************************************** */ -template Callable> -void print_vector( - std::ostream &out, - const std::vector &elements, - Callable&& print_element, - const char* prefix = "[", - const char* separator = ", ", - const char* suffix = "]" -){ - out << prefix; - if (elements.size()) { - print_element(out, elements[0]); - for (size_t i = 1; i < elements.size(); ++i) { - out << separator; - print_element(out, elements[i]); - } - } - out << suffix; -} - -template -void print_vector( - std::ostream &out, - const std::vector &elements, - const char* prefix = "[", - const char* separator = ", ", - const char* suffix = "]" -){ - auto print_element = [](std::ostream &out, const T& e) { out << e; }; - print_vector(out, elements, print_element, prefix, separator, suffix); -} - -template<> -struct std::hash -{ - std::size_t operator()(const df::coord2d& pos) const noexcept - { - // hashing is easy, if values are smaller than the hash - return ((std::size_t)pos.x << 16) | (std::size_t)pos.y; - } +bool region_order(coord p1, coord p2) { + return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); }; -using coord = df::coord2d; - enum class direction : int { North = 0, West = 1, South = 2, East = 3 }; direction turn_left(direction dir) { @@ -328,7 +302,7 @@ direction turn_right(direction dir) { return (direction)(((int)dir+3) % 4); } -df::coord2d as_offset(direction dir) { +coord as_offset(direction dir) { switch (dir) { case direction::North: return { 0, -1 }; @@ -352,12 +326,10 @@ auto print_path(std::ostream &out, const std::vector &path) { assert(path.size()); auto print_point = [scale](std::ostream &out, const coord &pos){ out << scale * pos.x << " " << -scale * pos.y;}; - print_vector(out, path, print_point, "(", ",", ")"); + print_range(out, path, print_point, "(", ",", ")"); } -auto region_order = [](df::coord2d p1, df::coord2d p2) { - return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); -}; + std::pair ahead(const std::vector &component, coord pos, direction dir) { auto test = [&](int16_t x, int16_t y){ @@ -396,74 +368,31 @@ static command_result export_region_tiles(color_ostream &out) return CR_FAILURE; } + // If you change anything in this vector, don't forget to change the + // corresponding comments and arguments in the call to print_csv below vector headings = { "world_x", "world_y", "num_tiles", "num_components", "biome_type", "region_id", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df", - "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "snowfall", "salinity", + "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "salinity", "surroundings", "elevation", "reanimating", "has_bogeymen" }; - print_vector(out_file, headings,"",";",";boundary_wkt\n" ); - - - - - /* - try { - create_field(layer, "region_id", OFTInteger); - create_field(layer, "region_name_en", OFTString, 100); - create_field(layer, "region_name_df", OFTString, 100); - create_field(layer, "landmass_id", OFTInteger); - create_field(layer, "landmass_name_en", OFTString, 100); - create_field(layer, "landmass_name_df", OFTString, 100); - - create_field(layer, "biome_type", OFTString, 32); - create_field(layer, "surroundings", OFTString, 16); - create_field(layer, "elevation", OFTInteger); - - create_field(layer, "evilness", OFTInteger); - create_field(layer, "savagery", OFTInteger); - create_field(layer, "volcanism", OFTInteger); - create_field(layer, "drainage", OFTInteger); - create_field(layer, "temperature", OFTInteger); - create_field(layer, "vegetation", OFTInteger); - create_field(layer, "rainfall", OFTInteger); - create_field(layer, "snowfall", OFTInteger); - create_field(layer, "salinity", OFTInteger); - - create_field(layer, "reanimating", OFTInteger, 0, OFSTBoolean); - create_field(layer, "has_bogeymen", OFTInteger, 0, OFSTBoolean); - - } catch (const DFHack::command_result& r) { - out.printerr("could not create fields for output layer"); - return r; - } - */ + print_range(out_file, headings,"",";",";boundary_wkt\n" ); + /* Preprocessing: cluster region tiles by the world tile used for the biome information */ + // map world tile coord -> vector of region tiles referencing of world title + std::unordered_map> world_tile_region; // iterating over the region details allows the user to do partial map exports // by manually scrolling on the embark site selection - - std::unordered_map> world_tile_region; - for (auto const region_details : world->world_data->midmap_data.region_details) { auto world_x = region_details->pos.x; auto world_y = region_details->pos.y; for (int region_x = 0; region_x < 16; ++region_x) { - for (int region_y = 0; region_y < 16; ++region_y) { - - // auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); - // setGeometry( - // feature, - // (double)(world_x * wdim + region_x * rdim), - // (double)(world_y * wdim + region_y * rdim), - // rdim - // ); - - // get the world tile coordinates used for the biome information of the local region tile + for (int region_y = 0; region_y < 16; ++region_y) + { auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); world_tile_region[biome_tile].emplace_back(16 * world_x + region_x, 16 * world_y + region_y); - } } } @@ -481,18 +410,26 @@ static command_result export_region_tiles(color_ostream &out) for (auto& [biome_tile, region] : world_tile_region) { assert(region.size() > 0); - // compute the connected components of the world tile region - std::ranges::sort(region, region_order); + // sorting the region provides O(log n) membership test. + std::ranges::sort(region, region_order); + + /* Phase I : compute the connected components of the world tile region */ + + // component_assignment[i] is the component id of region[i] std::vector component_assignment; component_assignment.resize(region.size(),0); + + // (indices of) blocks in the current component that have been discovered but not yet explored std::deque agenda; + unsigned int current_component = 0; - auto current_component = 0; for (size_t i = 0; i < region.size(); ++i) { if (component_assignment[i]) { + // skip region tiles that have already been assigned a component continue; } else { + // start a new component for tiles that haven't been assigned yet ++current_component; component_assignment[i] = current_component; agenda.push_back(i); @@ -501,37 +438,47 @@ static command_result export_region_tiles(color_ostream &out) auto pos_idx = agenda.front(); agenda.pop_front(); auto pos = region[pos_idx]; - for (auto const offset : directions) { - // FIXME: use something O(log n) instead of the O(n) find - auto lower = std::ranges::lower_bound(region, pos + offset, region_order); - auto n_it = (*lower == pos + offset) ? lower : region.end(); - - // auto n_it = std::find(region.begin(), region.end(), pos + offset); - auto n_idx = std::distance(region.begin(), n_it); - if (n_it != region.end() && component_assignment[n_idx] == 0) { - component_assignment[n_idx] = current_component; - agenda.push_back(n_idx); + for (const auto& offset : directions) { + auto lb = std::ranges::lower_bound(region, pos + offset, region_order); + if (lb != region.end() && *lb == pos + offset) { + auto n_idx = std::distance(region.begin(), lb); + if (component_assignment[n_idx] == 0) { + component_assignment[n_idx] = current_component; + agenda.push_back(n_idx); + } } } } } - // assert that all parts of the region are accounted for + // check that all parts of the region are accounted for assert(std::ranges::all_of(component_assignment, [](int comp){ return comp > 0;})); + // distribute region tiles according to their component assignment (preserves region order) std::vector> components; components.resize(current_component); for (size_t i = 0; i < region.size(); ++i) { components.at(component_assignment.at(i) - 1).push_back(region.at(i)); } + /* Phase II : create paths by clockwise traversal along the outside of every component */ + /** + * Note: DF uses "picture coordinates" (positive y values go "south") + * while in GIS software positve y values go "north". Thus, [print_path] + * negates the y-coordinates, turning the clockwise traversals into + * counterclockwise traversals as specified by WKT. + * https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry + */ + std::vector> paths; for (auto const &component : components) { + // start at the NW corner of the west-most tile of the northmost row... auto start = component.at(0); std::vector path; path.push_back(start); + // ... ensuring that a step to the east is a valid clockwise step along the boundary. auto current_direction = direction::East; auto current_position = advance(start,current_direction); @@ -548,7 +495,14 @@ static command_result export_region_tiles(color_ostream &out) path.push_back(current_position); current_direction = turn_right(current_direction); } - assert(! (left && !right)); // shape has a hole or is not connected + else if (left && !right) { + // diagonal step: turn right (following the outline of the inclusion) + // this does not seem to happen with the maps currently generated by DF + DEBUG(warning,out).print("Region has self-intersecting outline"); + path.push_back(current_position); + current_direction = turn_right(current_direction); + } + // case !left && right requires no turn; advance the position in all cases current_position = advance(current_position, current_direction); } // close the path @@ -558,6 +512,8 @@ static command_result export_region_tiles(color_ostream &out) } assert(paths.size() > 0); + /* Phase III: output the CSV line */ + auto& region_map_entry = world->world_data->region_map[biome_tile.x][biome_tile.y]; auto world_region = df::world_region::find(region_map_entry.region_id); auto landmass = df::world_landmass::find(region_map_entry.landmass_id); @@ -577,7 +533,7 @@ static command_result export_region_tiles(color_ostream &out) region_map_entry.landmass_id, landmass ? DF2UTF(Translation::translateName(&landmass->name, true)) : "NONE", landmass ? DF2UTF(Translation::translateName(&landmass->name, false)) : "NONE", - // "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "snowfall", "salinity" + // "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "salinity" region_map_entry.evilness, region_map_entry.savagery, region_map_entry.volcanism, @@ -585,7 +541,6 @@ static command_result export_region_tiles(color_ostream &out) region_map_entry.temperature, region_map_entry.vegetation, region_map_entry.rainfall, - region_map_entry.snowfall, region_map_entry.salinity, // "surroundings", "elevation", "reanimating", "has_bogeymen" describe_surroundings(region_map_entry.savagery, region_map_entry.evilness), @@ -593,55 +548,17 @@ static command_result export_region_tiles(color_ostream &out) world_region->reanimating, world_region->has_bogeymen ); - // output geometry + + // output geometry as WKT if (paths.size() == 1) { - print_vector(out_file, paths, print_path , "POLYGON(", ",", ")\n" ); + print_range(out_file, paths, print_path , "POLYGON(", ",", ")\n" ); } else { - print_vector(out_file, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); + print_range(out_file, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); } } - - // feature->SetField( "biome_type", ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_x,biome_y)).c_str() ); - - // gets supplementary information from the world tile - // auto& region_map_entry = world->world_data->region_map[biome_x][biome_y]; - // #define SET_FIELD(name) feature->SetField( #name, region_map_entry.name) - // SET_FIELD(region_id); - // SET_FIELD(landmass_id); - // SET_FIELD(evilness); - // SET_FIELD(savagery); - // SET_FIELD(volcanism); - // SET_FIELD(drainage); - // SET_FIELD(temperature); - // SET_FIELD(vegetation); - // SET_FIELD(rainfall); - // SET_FIELD(snowfall); - // SET_FIELD(salinity); - // #undef SET_FIELD - - // feature->SetField( "surroundings", describe_surroundings(region_map_entry.savagery, region_map_entry.evilness)); - - // auto region = df::world_region::find(region_map_entry.region_id); - // if (region) { - // auto region_name_en = DF2UTF(Translation::translateName(®ion->name, true)); - // feature->SetField( "region_name_en", region_name_en.c_str()); - // auto region_name_df = DF2UTF(Translation::translateName(®ion->name, false)); - // feature->SetField( "region_name_df", region_name_df.c_str()); - // feature->SetField("reanimating", region->reanimating); - // feature->SetField("has_bogeymen", region->has_bogeymen); - // } - // auto landmass = df::world_landmass::find(region_map_entry.landmass_id); - // if (landmass) { - // auto landmass_name_en = DF2UTF(Translation::translateName(&landmass->name, true)); - // feature->SetField( "landmass_name_en", landmass_name_en.c_str()); - // auto landmass_name_df = DF2UTF(Translation::translateName(&landmass->name, false)); - // feature->SetField( "landmass_name_df", landmass_name_df.c_str()); - // } - - const auto finish{std::chrono::steady_clock::now()}; const std::chrono::duration elapsed_seconds{finish - start}; - out.print("done in %f ms !\n", elapsed_seconds.count()); + out.print("done in %f s !\n", elapsed_seconds.count()); return CR_OK; } From 7fcfecc9e61091618f6e0f694c95ab24f7ef4e02 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:57:23 +0000 Subject: [PATCH 0240/1082] 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 94f95a3dd5..eccd051abb 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 94f95a3dd53a2a4a35ed645e2afea9109079269a +Subproject commit eccd051abb7dcbecceac06b861347304578e897e From 7043d2c2dcd14a2ecd6bd97fb17e3462bce11504 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 3 Apr 2025 16:14:26 +0200 Subject: [PATCH 0241/1082] convert site export to CSV and remove GDAL dependency --- plugins/CMakeLists.txt | 2 +- plugins/export-map.cpp | 227 +++++++++++++---------------------------- 2 files changed, 73 insertions(+), 156 deletions(-) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f87f9002ce..fbc2c9901d 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -71,7 +71,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) - dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique LINK_LIBRARIES gdal) + dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 0e0db0e83e..f2887f3859 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -56,32 +56,20 @@ struct std::hash // were only dealing with 2D coordinates in this file using coord = df::coord2d; -static command_result do_command(color_ostream &out, vector ¶meters); - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(log,out).print("initializing %s\n", plugin_name); - - commands.push_back(PluginCommand( - plugin_name, - "Export the world map.", - do_command)); - - return CR_OK; -} +// static int wdim = 768; // dimension of a world tile +static int rdim = 48; // dimension of a region tile -auto setGeometry(OGRFeature *feature, double x, double y, double dimx, double dimy = 0) { - if (dimy == 0) { dimy = dimx; } - auto poly = new OGRPolygon(); - auto boundary = new OGRLinearRing(); - y = -y; // in GIS negative y-coordinates mean further south - boundary->addPoint(x,y); - boundary->addPoint(x,y-dimy); - boundary->addPoint(x+dimx,y-dimy); - boundary->addPoint(x+dimx,y); - boundary->closeRings(); - //the "Directly" variants assume ownership of the objects created above - poly->addRingDirectly(boundary); - feature->SetGeometryDirectly( poly ); +/** + * Takes a vector of coordinates interpreted as global region tile coordinates + * (i.e. 16 region tiles per world tile) and emits a WKT path in GIS-compatible + * local tile coordinates (negative y-coordinates, 48 stepts per region tile) + */ +auto print_path(std::ostream &out, const std::vector &path) { + auto scale = rdim; + assert(path.size()); + auto print_point = [scale](std::ostream &out, const coord &pos){ + out << scale * pos.x << " " << -scale * pos.y;}; + print_range(out, path, print_point, "(", ",", ")"); } df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { @@ -101,23 +89,6 @@ df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { return { world_x, world_y }; } -auto create_field(OGRLayer *layer, std::string name, OGRFieldType type, int width = 0, OGRFieldSubType subtype = OFSTNone) { - OGRFieldDefn field( name.c_str() , type ); - if (subtype != OFSTNone) { - field.SetSubType(subtype); - } - if (width != 0) { - field.SetWidth(width); - } - // this should create a copy internally - if( layer->CreateField( &field ) != OGRERR_NONE ){ - throw CR_FAILURE; - } -} - -// PROJ.4 description of EPSG:3857 (https://epsg.io/3857) -static const char* EPSG_3857 = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs"; - const char* describe_surroundings(int savagery, int evilness) { constexpr std::arraysurroundings{ "Serene", "Mirthful", "Joyous Wilds", @@ -129,8 +100,18 @@ const char* describe_surroundings(int savagery, int evilness) { return surroundings[3 * evilness_index + savagery_index]; } -// static int wdim = 768; // dimension of a world tile -static int rdim = 48; // dimension of a region tile + +static command_result do_command(color_ostream &out, vector ¶meters); +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(log,out).print("initializing %s\n", plugin_name); + + commands.push_back(PluginCommand( + plugin_name, + "Export the world map.", + do_command)); + + return CR_OK; +} static command_result export_region_tiles(color_ostream &out); static command_result export_sites(color_ostream &out); @@ -154,133 +135,75 @@ static command_result do_command(color_ostream &out, vector ¶meters) } } +/********************************************************************** */ + static command_result export_sites(color_ostream &out) { out.print("exporting sites... "); out.flush(); const auto start{std::chrono::steady_clock::now()}; - // set up coordinate system - OGRSpatialReference CRS; - if (CRS.importFromProj4(EPSG_3857) != OGRERR_NONE) { - out.printerr("could not set up coordinate system"); - return CR_FAILURE; - } - - // set up output driver - GDALAllRegister(); - const char *driver_name = "SQLite"; - const char *extension = "sqlite"; - auto driver = GetGDALDriverManager()->GetDriverByName(driver_name); - if (!driver) { - out.printerr("could not find sqlite driver"); - return CR_FAILURE; - } - - // create a dataset and associate it to a file - std::string sites("sites."); - sites.append(extension); - const char* options[] = { "SPATIALITE=YES", nullptr }; - auto dataset = driver->Create( sites.c_str(), 0, 0, 0, GDT_Unknown, options); - if (!dataset) { - out.printerr("could not create dataset"); - return CR_FAILURE; - } - - // create a layer for the biome data - // const char* format[] = { "FORMAT=WKT", nullptr }; - auto layer = dataset->CreateLayer( "world_sites", &CRS, wkbPolygon, nullptr ); - if (!layer) { - out.printerr("could not create layer"); + // ensure that we have an output file + std::string filename("sites.csv"); + std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + if (!out_file) { return CR_FAILURE; } - try { - create_field(layer, "site_id", OFTInteger); - create_field(layer, "civ_id", OFTInteger); - create_field(layer, "created_year", OFTInteger); - create_field(layer, "cur_owner_id", OFTInteger); - - create_field(layer, "type", OFTString, 15); - - create_field(layer, "site_name_df", OFTString, 100); - create_field(layer, "site_name_en", OFTString, 100); - - create_field(layer, "civ_name_df", OFTString, 100); - create_field(layer, "civ_name_en", OFTString, 100); - - create_field(layer, "site_government_df", OFTString, 100); - create_field(layer, "site_government_en", OFTString, 100); - - create_field(layer, "owner_race", OFTString, 15); - - // create_field(layer, "local_ruler", OFTString, 100); + // If you change anything in this vector, don't forget to change the + // corresponding comments and arguments in the call to print_csv below + vector headings = { + "site_id", "civ_id", "created_year", "cur_owner_id", "type", + "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" + }; + print_range(out_file, headings,"",";",";boundary_wkt\n" ); - } - catch (const DFHack::command_result& r) { - out.printerr("could not create fields for output layer"); - return r; - } - - if (dataset->StartTransaction() != OGRERR_NONE) { - out.printerr("could not start a transaction\n"); - } + #define TRANSLATE_DF_EN(guard, name_object)\ + guard ? DF2UTF(Translation::translateName(&name_object, false)) : "NONE",\ + guard ? DF2UTF(Translation::translateName(&name_object, true)) : "NONE" for (auto const site : world->world_data->sites) { - - auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); - - setGeometry( - feature, - site->global_min_x * rdim, - site->global_min_y * rdim, - (site->global_max_x - site->global_min_x + 1) * rdim, - (site->global_max_y - site->global_min_y + 1) * rdim - ); - feature->SetField( "site_id", site->id ); - feature->SetField( "type", ENUM_KEY_STR(world_site_type, site->type).c_str() ); - #define SET_FIELD(name) feature->SetField( #name, site->name) - SET_FIELD(civ_id); - SET_FIELD(created_year); - SET_FIELD(cur_owner_id); - #undef SET_FIELD - - #define TRANSLATE_NAME(field_name, name_object)\ - feature->SetField((#field_name"_df"), DF2UTF(Translation::translateName(&name_object, false)).c_str());\ - feature->SetField((#field_name"_en"), DF2UTF(Translation::translateName(&name_object, true)).c_str()); - - TRANSLATE_NAME(site_name, site->name) - auto civ = df::historical_entity::find(site->civ_id); - if (civ) { TRANSLATE_NAME(civ_name,civ->name) } - auto owner = df::historical_entity::find(site->cur_owner_id); - if (owner) { - TRANSLATE_NAME(site_government,owner->name) - auto race = df::creature_raw::find(owner->race); - if (!race){ - race = df::creature_raw::find(civ->race); - } - if (race) { - feature->SetField( "owner_race", race->name[2].c_str() ); + + df::creature_raw *race = nullptr; + if (owner){ + race = df::creature_raw::find(owner->race); + DEBUG(warning, out).print("owner (%d) of site (%d) has undefined race (%d)", owner->id, site->id, owner->race); + if (!race) { + df::creature_raw::find(civ->race); } } - - // this updates the feature with the id it receives in the layer - if( layer->CreateFeature( feature ) != OGRERR_NONE ) - return CR_FAILURE; - // but we don't care and destroy the feature - OGRFeature::DestroyFeature( feature ); + auto print_csv = [&out_file](auto ...args){ ([&]{ out_file << args << ";" ; }() ,...); }; + print_csv( + // "site_id", "civ_id", "created_year", "cur_owner_id", "type", + site->id, + site->civ_id, + site->created_year, + site->cur_owner_id, + ENUM_KEY_STR(world_site_type, site->type), + // "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" + TRANSLATE_DF_EN(true, site->name), + TRANSLATE_DF_EN(civ, civ->name), + TRANSLATE_DF_EN(owner, owner->name), + race ? race->name[2] : "NONE" + ); + const vector path{ + coord(site->global_min_x, site->global_min_y), + coord(site->global_max_x+1, site->global_min_y), + coord(site->global_max_x+1, site->global_max_y+1), + coord(site->global_min_x, site->global_max_y+1), + coord(site->global_min_x, site->global_min_y) + }; + print_range(out_file, std::vector>{path}, print_path , "POLYGON(", ",", ")\n" ); } - dataset->CommitTransaction(); - GDALClose( dataset ); const auto finish{std::chrono::steady_clock::now()}; const std::chrono::duration elapsed_seconds{finish - start}; - out.print("done in %f ms !\n", elapsed_seconds.count()); + out.print("done in %.2fs !\n", elapsed_seconds.count()); return CR_OK; } @@ -321,13 +244,7 @@ coord advance(coord pos, direction dir) { return pos + as_offset(dir); } -auto print_path(std::ostream &out, const std::vector &path) { - auto scale = rdim; - assert(path.size()); - auto print_point = [scale](std::ostream &out, const coord &pos){ - out << scale * pos.x << " " << -scale * pos.y;}; - print_range(out, path, print_point, "(", ",", ")"); -} + From f6850514a17fa9f22fa92e27f1e0cb7ff6593168 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:37:37 +0000 Subject: [PATCH 0242/1082] Auto-update structures ref for 51.10 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index eccd051abb..ec3ab067ae 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit eccd051abb7dcbecceac06b861347304578e897e +Subproject commit ec3ab067aef716817d9c5b84a6fb85bc22516caa From f324abb20546fcf21b4b29ab55921bf02cf5f618 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Apr 2025 07:51:48 -0700 Subject: [PATCH 0243/1082] bump version to 51.10 --- CMakeLists.txt | 2 +- docs/changelog.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fcc15168ba..017edd4460 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.09") +set(DF_VERSION "51.10") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7c4babf881..7c25e650f5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,11 @@ Template for new versions: ## Removed +# 51.10-r1 + +## Misc Improvements +- Compatibility with DF 51.10 + # 51.09-r1 ## New Features From 69e72e4541c66bd89696cf7486db41c1803f84bf Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 3 Apr 2025 13:18:35 -0500 Subject: [PATCH 0244/1082] Update the DFHack Logo stuff Make it one image file and clean up the references to it. --- data/art/logo.png | Bin 997 -> 1213 bytes data/art/logo_hovered.png | Bin 5124 -> 0 bytes docs/dev/Lua API.rst | 12 ++++++++---- plugins/lua/hotkeys.lua | 7 +++++-- 4 files changed, 13 insertions(+), 6 deletions(-) delete mode 100644 data/art/logo_hovered.png diff --git a/data/art/logo.png b/data/art/logo.png index 133dc5aaf8c98921bcc0203ebd986d4915c726cd..c34407b372cdb138314827e163ec38686cc42678 100644 GIT binary patch delta 1193 zcmV;a1Xlay2fYb@iBL{Q4GJ0x0000DNk~Le0000$0000a2m$~A0Jd(c#Q*>R0drDE zLIAGL9O(c61YJo)K~z|Uh#*Sn1`iPr0T%^HKnVT=HsV1L z-9!4ClijDBbIJs=B!bs;t|A5xfskZ#5sac@YpZ*ZF^v#^G2NFvZ0+G`r|859YHHYJ zzeA{c_3C@|x~jUq>bM9Yz!HDn_A%w}1AuSeerLokNxuQW?602~@mK%&yN{04ThPzt z`}eq*ES^Akc6iBu>Qa{Um3An3YI z2vJqFtFz00<*LmOu+cSv5AtIIt+$XM2=v~6uq+DzCMPGO(Wq_IG_6!BwQr2x-m4F-b%Aj@*cfimK6odfsx_ZBRc6D=e69D@A`)MCX^-cI7fA<7p zu^3%lU0sE?1&wmxefS{%r`MdYn$PFy$0N%!E>N6P8-s&`T?fjDzqJQWPEP0sMNx{y zBF)b8{MyN25{0FkV|2J?D2O&}cO1 zHJ8ul3xxszq|<2(e|~-r05dZ)=oX1YCMG7(+2`|Rv)R{%&WOL203H^}WD-w7P17vP z+TGm+09lsD#>N~yIgX1&BD6Rul}f>XU~pq&e*X4a49#4%s=5L;vaN>8Yuy_MtQ7cdAi5$u&&_0L!v)eyi2$&CLz+ z1_A*9C=?2oWjQ{CSeE5>yP@qm!j#{+2S!Fl?2K(ZK0abk0Py?$c(K^q+p`^iQ2jat zOh@#bzm>q<-5mftJUr0N7-?>9E*uWqJ}WCL0FX+huCK4@zIK3)(uEQK^Af65D#cl8@LN6$xX6F}FQ$J0(ergMw65}5 HlfMHdlqf{a delta 975 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 Z2o#j-Nb=lLw87Bo0{&|6lI80+{TEB3t%Cpn diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index d449c8810b..071ab48ecb 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -5945,6 +5945,8 @@ common text token lists that you can then pass as ``text`` to a ``Label``: Example 2: The DFHack logo - a graphical button in graphics mode and a text button in ASCII mode. The ASCII colors use the default for hovering:: + local logo_textures=dfhack.textures.loadTileset( + 'hack/data/art/logo.png', 8, 12, true), widgets.Label{ text=widgets.makeButtonLabelText{ chars={ @@ -5952,10 +5954,12 @@ common text token lists that you can then pass as ``text`` to a ``Label``: {179, 'H', 'a', 179}, {179, 'c', 'k', 179}, }, - tileset=dfhack.textures.loadTileset( - 'hack/data/art/logo.png', 8, 12, true), - tileset_hover=dfhack.textures.loadTileset( - 'hack/data/art/logo_hovered.png', 8, 12, true), + tileset=logo_textures, + tileset_offset=1, + tileset_stride=8, + tileset_hover=logo_textures, + tileset_hover_offset=5, + tileset_hover_stride=8, }, on_click=function() dfhack.run_command{'hotkeys', 'menu', self.name} diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 74d0b96231..d0cecb5ba0 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -6,7 +6,6 @@ local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12, true) -local logo_hovered_textures = dfhack.textures.loadTileset('hack/data/art/logo_hovered.png', 8, 12, true) local function get_command(cmdline) local first_word = cmdline:trim():split(' +')[1] @@ -41,7 +40,11 @@ function HotspotMenuWidget:init() {VERT_BAR, 'c', 'k', VERT_BAR}, }, tileset=logo_textures, - tileset_hover=logo_hovered_textures, + tileset_offset=1, + tileset_stride=8, + tileset_hover=logo_textures, + tileset_hover_offset=5, + tileset_hover_stride=8, }, on_click=function() dfhack.run_command{'hotkeys', 'menu', self.name} end, }, From 3468e6f20265f7f823f928be50cfe14d29fd4ec8 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:15:32 +0200 Subject: [PATCH 0245/1082] use proper site classification and remove gdal include --- plugins/export-map.cpp | 87 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index f2887f3859..f21570d2d7 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -13,13 +13,12 @@ #include "df/map_block.h" #include "df/world_data.h" #include "df/world_site.h" +#include "df/site_map_infost.h" #include "df/region_map_entry.h" #include "df/world_region.h" #include "df/world_landmass.h" #include "df/world_region_details.h" -#include "gdal/ogrsf_frmts.h" - #include #include #include @@ -137,6 +136,87 @@ static command_result do_command(color_ostream &out, vector ¶meters) /********************************************************************** */ +const char* classify_site(df::world_site *site) { + using wst = df::enums::world_site_type::world_site_type; + switch (site->type) { + case wst::PlayerFortress: + case wst::MountainHalls: + if (site->min_depth == 0 && (0 < site->max_depth)){ + return "fortress"; + } + if (site->min_depth > 0) { + return "mountain halls"; + } + return "hillocks"; + + case wst::DarkFortress: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "fortress" : "pits"; + } + + case wst::Cave: + return "cave"; + + case wst::ForestRetreat: + return "forest retreat"; + + case wst::Town: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "town" : "hamlet"; + } + + case wst::ImportantLocation: + return "important location"; + + case wst::LairShrine: + if (site->subtype_info) { + switch (site->subtype_info->lair_type) { + case df::enums::lair_type::LABYRINTH: + return "labyrinth"; + case df::enums::lair_type::SHRINE: + return "shrine"; + default: + break; + } + } + return "lair"; + + case wst::Fortress: + if (site->subtype_info) { + switch (site->subtype_info->fortress_type) { + case df::enums::fortress_type::TOWER: + return "tower"; + case df::enums::fortress_type::MONASTERY: + return "monastery"; + case df::enums::fortress_type::FORT: + return "fort"; + default: + return "castle"; + } + } + return "fortress"; + + case wst::Camp: + return "camp"; + + case wst::Monument: + if (site->subtype_info) { + switch (site->subtype_info->monument_type) { + case df::enums::monument_type::TOMB: + return "tomb"; + case df::enums::monument_type::VAULT: + return "vault"; + default: + break; + } + } + return "monument"; + + default: + return "site"; + } +} + static command_result export_sites(color_ostream &out) { out.print("exporting sites... "); @@ -183,7 +263,8 @@ static command_result export_sites(color_ostream &out) site->civ_id, site->created_year, site->cur_owner_id, - ENUM_KEY_STR(world_site_type, site->type), + //ENUM_KEY_STR(world_site_type, site->type), + classify_site(site), // "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" TRANSLATE_DF_EN(true, site->name), TRANSLATE_DF_EN(civ, civ->name), From aa803c6cf971444a7da391941166ebd1bec36ec8 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:33:46 +0000 Subject: [PATCH 0246/1082] 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 ec3ab067ae..4740f8967f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ec3ab067aef716817d9c5b84a6fb85bc22516caa +Subproject commit 4740f8967f4a77c1228d8c98acb9996412a36f29 diff --git a/scripts b/scripts index d8912409bb..43948f3cb1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d8912409bb3c7398a2859aa0c0092d792c9211e7 +Subproject commit 43948f3cb10848b107ddf0e97cfbcc6c8c2bd0d9 From 12c07638ae77b4ab2d3e63fe0285257fc5a31ab1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Apr 2025 23:14:39 +0000 Subject: [PATCH 0247/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 43948f3cb1..139ee179b9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 43948f3cb10848b107ddf0e97cfbcc6c8c2bd0d9 +Subproject commit 139ee179b93471fa4e3d37fd5333c11a87f461cf From 6d9cfda41f4d85297169c287ea14c036ffe9653c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Apr 2025 18:03:53 -0700 Subject: [PATCH 0248/1082] fix conversion of enum token to value was not correctly handling enums that did not start at 0 --- docs/changelog.txt | 1 + plugins/stockpiles/StockpileSerializer.cpp | 30 +++++++--------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8a037da96a..65e7edcb49 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `stockpiles`: fix one-off error in item type when importing furniture stockpile settings ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index afd0a6dbc8..6abab93baf 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -210,24 +210,15 @@ bool StockpileSettingsSerializer::unserialize_from_file(color_ostream &out, cons /** * Find an enum's value based off the string label. - * @param traits the enum's trait struct * @param token the string value in key_table - * @return the enum's value, -1 if not found + * @return the enum's value, -1 if not found */ template -static typename df::enum_traits::base_type linear_index(df::enum_traits traits, const string& token) { - auto j = traits.first_item_value; - auto limit = traits.last_item_value; - // sometimes enums start at -1, which is bad news for array indexing - if (j < 0) { - j += abs(traits.first_item_value); - limit += abs(traits.first_item_value); - } - for (; j <= limit; ++j) { - if (token.compare(traits.key_table[j]) == 0) - return j; - } - return -1; +static typename df::enum_traits::base_type token_to_enum_val(const string& token) { + E val; + if (!find_enum_item(&val, token)) + return -1; + return val; } static bool matches_filter(color_ostream& out, const vector& filters, const string& name) { @@ -360,10 +351,9 @@ static void unserialize_list_quality(color_ostream& out, const char* subcat, boo } using df::enums::item_quality::item_quality; - df::enum_traits quality_traits; for (int i = 0; i < list_size; ++i) { const string quality = read_value(i); - df::enum_traits::base_type idx = linear_index(quality_traits, quality); + df::enum_traits::base_type idx = token_to_enum_val(quality); if (idx < 0) { WARN(log, out).print("invalid quality token: %s\n", quality.c_str()); continue; @@ -551,11 +541,9 @@ static void unserialize_list_item_type(color_ostream& out, const char* subcat, b } using df::enums::item_type::item_type; - df::enum_traits type_traits; for (int i = 0; i < list_size; ++i) { const string token = read_value(i); - // subtract one because item_type starts at -1 - const df::enum_traits::base_type idx = linear_index(type_traits, token) - 1; + const df::enum_traits::base_type idx = token_to_enum_val(token); if (!is_allowed((item_type)idx)) continue; if (idx < 0 || size_t(idx) >= num_elems) { @@ -1806,7 +1794,7 @@ void StockpileSettingsSerializer::read_furniture(color_ostream& out, Deserialize } else { for (int i = 0; i < bfurniture.type_size(); ++i) { const string token = bfurniture.type(i); - df::enum_traits::base_type idx = linear_index(type_traits, token); + df::enum_traits::base_type idx = token_to_enum_val(token); if (idx < 0 || size_t(idx) >= pfurniture.type.size()) { WARN(log, out).print("furniture type index invalid %s, idx=%d\n", token.c_str(), idx); continue; From 5856c803a1131f1465a39380ba7166644e53acba Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:12:58 +0200 Subject: [PATCH 0249/1082] move sites classification to map module and additional cleanup --- library/include/modules/Maps.h | 9 +++ library/modules/Maps.cpp | 86 ++++++++++++++++++++++++ plugins/export-map.cpp | 117 +++++---------------------------- 3 files changed, 110 insertions(+), 102 deletions(-) diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index f5a3be142c..c57dab4ea0 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -42,6 +42,7 @@ distribution. #include "df/flow_type.h" #include "df/tile_dig_designation.h" #include "df/tiletype.h" +#include "df/world_site.h" namespace df { struct block_square_event; @@ -399,6 +400,14 @@ 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; }); + + +/** + * A single function does not merit a "Sites" module, hence we collect site functions here in the meantime. + */ + +// Get the classification string (e.g. "town", "hillocs", "tower", etc.) for a site +DFHACK_EXPORT const char* getSiteTypeName(df::world_site *site); } } #endif diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 54fb43c810..1b7b854deb 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -1265,3 +1265,89 @@ int Maps::removeAreaAquifer(df::coord pos1, df::coord pos2, std::functiontype) { + case wst::PlayerFortress: + case wst::MountainHalls: + if (site->min_depth == 0 && (0 < site->max_depth)){ + return "fortress"; + } + if (site->min_depth > 0) { + return "mountain halls"; + } + return "hillocks"; + + case wst::DarkFortress: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "fortress" : "pits"; + } + + case wst::Cave: + return "cave"; + + case wst::ForestRetreat: + return "forest retreat"; + + case wst::Town: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "town" : "hamlet"; + } + + case wst::ImportantLocation: + return "important location"; + + case wst::LairShrine: + if (site->subtype_info) { + switch (site->subtype_info->lair_type) { + case df::enums::lair_type::LABYRINTH: + return "labyrinth"; + case df::enums::lair_type::SHRINE: + return "shrine"; + default: + break; + } + } + return "lair"; + + case wst::Fortress: + if (site->subtype_info) { + switch (site->subtype_info->fortress_type) { + case df::enums::fortress_type::TOWER: + return "tower"; + case df::enums::fortress_type::MONASTERY: + return "monastery"; + case df::enums::fortress_type::FORT: + return "fort"; + default: + return "castle"; + } + } + return "fortress"; + + case wst::Camp: + return "camp"; + + case wst::Monument: + if (site->subtype_info) { + switch (site->subtype_info->monument_type) { + case df::enums::monument_type::TOMB: + return "tomb"; + case df::enums::monument_type::VAULT: + return "vault"; + default: + break; + } + } + return "monument"; + + default: + return "site"; + } +} diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index f21570d2d7..973ed169ed 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -1,32 +1,31 @@ #include "Debug.h" #include "Error.h" -#include "PluginManager.h" #include "MiscUtils.h" +#include "PluginManager.h" #include "modules/Maps.h" #include "modules/Translation.h" -#include "df/entity_raw.h" #include "df/creature_raw.h" +#include "df/entity_raw.h" #include "df/historical_entity.h" -#include "df/world.h" #include "df/map_block.h" -#include "df/world_data.h" -#include "df/world_site.h" -#include "df/site_map_infost.h" #include "df/region_map_entry.h" -#include "df/world_region.h" +#include "df/site_map_infost.h" +#include "df/world_data.h" #include "df/world_landmass.h" #include "df/world_region_details.h" +#include "df/world_region.h" +#include "df/world.h" -#include -#include -#include -#include #include -#include +#include +#include #include - +#include +#include +#include +#include using std::string; using std::vector; @@ -83,8 +82,8 @@ df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { case 8: ; world_y--; break; case 9: world_x++ ; world_y--; break; } - world_x = std::min(std::max((int16_t)0,world_x),(int16_t)(world->world_data->world_width - 1)); - world_y = std::min(std::max((int16_t)0,world_y),(int16_t)(world->world_data->world_height - 1)); + world_x = std::clamp(world_x,(int16_t)0,(int16_t)(world->world_data->world_width - 1)); + world_y = std::clamp(world_y,(int16_t)0,(int16_t)(world->world_data->world_height - 1)); return { world_x, world_y }; } @@ -136,87 +135,6 @@ static command_result do_command(color_ostream &out, vector ¶meters) /********************************************************************** */ -const char* classify_site(df::world_site *site) { - using wst = df::enums::world_site_type::world_site_type; - switch (site->type) { - case wst::PlayerFortress: - case wst::MountainHalls: - if (site->min_depth == 0 && (0 < site->max_depth)){ - return "fortress"; - } - if (site->min_depth > 0) { - return "mountain halls"; - } - return "hillocks"; - - case wst::DarkFortress: { - bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); - return has_market ? "fortress" : "pits"; - } - - case wst::Cave: - return "cave"; - - case wst::ForestRetreat: - return "forest retreat"; - - case wst::Town: { - bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); - return has_market ? "town" : "hamlet"; - } - - case wst::ImportantLocation: - return "important location"; - - case wst::LairShrine: - if (site->subtype_info) { - switch (site->subtype_info->lair_type) { - case df::enums::lair_type::LABYRINTH: - return "labyrinth"; - case df::enums::lair_type::SHRINE: - return "shrine"; - default: - break; - } - } - return "lair"; - - case wst::Fortress: - if (site->subtype_info) { - switch (site->subtype_info->fortress_type) { - case df::enums::fortress_type::TOWER: - return "tower"; - case df::enums::fortress_type::MONASTERY: - return "monastery"; - case df::enums::fortress_type::FORT: - return "fort"; - default: - return "castle"; - } - } - return "fortress"; - - case wst::Camp: - return "camp"; - - case wst::Monument: - if (site->subtype_info) { - switch (site->subtype_info->monument_type) { - case df::enums::monument_type::TOMB: - return "tomb"; - case df::enums::monument_type::VAULT: - return "vault"; - default: - break; - } - } - return "monument"; - - default: - return "site"; - } -} - static command_result export_sites(color_ostream &out) { out.print("exporting sites... "); @@ -263,8 +181,7 @@ static command_result export_sites(color_ostream &out) site->civ_id, site->created_year, site->cur_owner_id, - //ENUM_KEY_STR(world_site_type, site->type), - classify_site(site), + DFHack::Maps::getSiteTypeName(site), // "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" TRANSLATE_DF_EN(true, site->name), TRANSLATE_DF_EN(civ, civ->name), @@ -325,10 +242,6 @@ coord advance(coord pos, direction dir) { return pos + as_offset(dir); } - - - - std::pair ahead(const std::vector &component, coord pos, direction dir) { auto test = [&](int16_t x, int16_t y){ coord offset{x,y}; From aecadd7b1f8d45621f5d51f866ec7759c17b025d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Apr 2025 18:09:24 -0700 Subject: [PATCH 0250/1082] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 139ee179b9..1f7663b57b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 139ee179b93471fa4e3d37fd5333c11a87f461cf +Subproject commit 1f7663b57bdda6bb02dc2f950f3465df832b2fe6 From 6b642281f73d77f5f36ac0911304b7675cc3e7d7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Apr 2025 19:26:57 -0700 Subject: [PATCH 0251/1082] fix output boulder/rough gem material calculation --- docs/changelog.txt | 1 + plugins/dig-now.cpp | 58 +++++++++++++++++++++++---------------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 65e7edcb49..d880426aa7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## Fixes - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings +- `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index c3c79122fc..16fe585157 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -20,9 +20,11 @@ #include "modules/World.h" #include "df/building.h" +#include "df/builtin_mats.h" #include "df/historical_entity.h" #include "df/item.h" #include "df/map_block.h" +#include "df/material.h" #include "df/plotinfost.h" #include "df/reaction_product_itemst.h" #include "df/tile_designation.h" @@ -391,7 +393,7 @@ struct dug_tile_info { DFCoord pos; df::tiletype_material tmat; df::item_type itype; - int32_t imat; // mat idx of boulder/gem potentially generated at this pos + t_matpair imat; // matpair of boulder/gem potentially generated at this pos dug_tile_info(MapExtras::MapCache &map, const DFCoord &pos) { this->pos = pos; @@ -403,32 +405,32 @@ struct dug_tile_info { imat = -1; df::tiletype_shape shape = tileShape(tt); - if (shape == df::tiletype_shape::WALL || shape == df::tiletype_shape::FORTIFICATION) { - switch (tmat) { - case df::tiletype_material::STONE: - case df::tiletype_material::MINERAL: - case df::tiletype_material::FEATURE: - imat = map.baseMaterialAt(pos).mat_index; - break; - case df::tiletype_material::LAVA_STONE: - { - MaterialInfo mi; - if (mi.findInorganic("OBSIDIAN")) - imat = mi.index; - return; // itype should always be BOULDER, regardless of vein - } - default: - break; - } - } - - switch (map.BlockAtTile(pos)->veinTypeAt(pos)) { - case df::inclusion_type::CLUSTER_ONE: - case df::inclusion_type::CLUSTER_SMALL: - itype = df::item_type::ROUGH; + if (shape != df::tiletype_shape::WALL && shape != df::tiletype_shape::FORTIFICATION) + return; + + switch (tmat) { + case df::tiletype_material::STONE: + case df::tiletype_material::MINERAL: + case df::tiletype_material::FEATURE: + case df::tiletype_material::LAVA_STONE: + imat = map.baseMaterialAt(pos); break; - default: + case df::tiletype_material::FROZEN_LIQUID: + // assume frozen water + // we can't use baseMaterialAt here because it will return the underlying river bed material + imat = t_matpair(df::builtin_mats::WATER, -1); break; + default: + return; + } + + MaterialInfo mi; + mi.decode(imat); + if (mi.type == -1 || !mi.material) + return; + + if (mi.material->isGem()) { + itype = df::item_type::ROUGH; } } }; @@ -737,7 +739,7 @@ static bool produces_item(const boulder_percent_options &options, return rng.random(100) < probability; } -typedef std::map, std::vector> +typedef std::map, std::vector> item_coords_t; static void do_dig(color_ostream &out, std::vector &dug_coords, @@ -876,8 +878,8 @@ static void create_boulders(color_ostream &out, prod->item_type = entry.first.first; prod->item_subtype = -1; - prod->mat_type = 0; - prod->mat_index = entry.first.second; + prod->mat_type = entry.first.second.mat_type; + prod->mat_index = entry.first.second.mat_index; prod->probability = 100; prod->product_dimension = 1; From 06a038fefe050db9193e93931621fd907d0ce5a0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Apr 2025 19:27:40 -0700 Subject: [PATCH 0252/1082] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index d880426aa7..cd43392b78 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls +- `dig-now`: properly generate ice boulders when digging through ice walls ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) From d9d2c7a60a3c2867ba06a9b68e7b57c89df3d626 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:12:18 -0700 Subject: [PATCH 0253/1082] define -> operator for DF linked lists --- library/include/BitArray.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 3679323eb8..bad77d7935 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -318,6 +318,22 @@ namespace DFHack return cur->item; } + I * operator->() + { + CHECK_NULL_POINTER(root); + CHECK_NULL_POINTER(cur); + + return cur->item; + } + + I * operator->() const + { + CHECK_NULL_POINTER(root); + CHECK_NULL_POINTER(cur); + + return cur->item; + } + operator const_iterator() const { return const_iterator(*this); From 3565121aebe9376abff451cf013d9074ead321f8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:15:56 -0700 Subject: [PATCH 0254/1082] generalize linked_list_remove so it can take a predicate --- library/include/MiscUtils.h | 53 +++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 8568a55c0a..c916a6a9fb 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -309,24 +309,43 @@ Link *linked_list_insert_after(Link *pos, Link *link) } /** - * Returns true if the item with id idToRemove was found, deleted, and removed - * from the list. Otherwise returns false. + * Returns true if an item that matches the given function was found, deleted, + * and removed from the list. Otherwise returns false. Only removes the first + * match. + * + * Example usage: + * + * linked_list_remove(&world->projectiles.all, [&](df::projectile *proj) { + * if (proj->getType() != df::enums::projectile_type::Unit) + * return false; + * if (auto unit_proj = virtual_cast(proj)) + * return unit_proj->unit == unit; + * return false; + * }); */ -template -bool linked_list_remove(Link *head, int32_t idToRemove) -{ - for (Link *link = head; link; link = link->next) - { - if (!link->item || link->item->id != idToRemove) - continue; - - link->prev->next = link->next; - if (link->next) - link->next->prev = link->prev; - delete(link); - return true; - } - return false; +template F> +bool linked_list_remove(L *list, F matches) { + auto matches_wrapper = [&](L::iterator::value_type item) { + return item && matches(item); + }; + auto it = std::find_if(list->begin(), list->end(), matches_wrapper); + if (it == list->end()) + return false; + auto item = *it; + list->erase(it); + delete item; + return true; +} + +/** + * Returns true if the item with id idToRemove was found, deleted, and + * removed from the list. Otherwise returns false. + */ +template +bool linked_list_remove(L *list, int32_t idToRemove) { + return linked_list_remove(list, [&](L::iterator::value_type item) { + return item->id == idToRemove; + }); } template From 0b7c04bb156d835547405a5088d7ae06c01cf9bc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:16:17 -0700 Subject: [PATCH 0255/1082] remove projectile info for teleported units otherwise they will be teleported back to their next projectile targeted tile on the next tick --- docs/changelog.txt | 2 ++ library/modules/Units.cpp | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index cd43392b78..5d64c50a78 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls +- `gui/teleport`: now properly handles teleporting units that are currently falling or being flung ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) @@ -67,6 +68,7 @@ Template for new versions: ## API - ``Military`` module: added ``addToSquad`` function +- ``Units::teleport``: projectile information is now cleared for teleported units ## Lua - ``dfhack.military.addToSquad``: expose Military API function diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 21f39932fe..e604562c77 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -67,6 +67,7 @@ distribution. #include "df/nemesis_record.h" #include "df/personality_goalst.h" #include "df/plotinfost.h" +#include "df/proj_unitst.h" #include "df/reputation_profilest.h" #include "df/syndrome.h" #include "df/tile_occupancy.h" @@ -773,6 +774,18 @@ bool Units::teleport(df::unit *unit, df::coord target_pos) else old_occ->bits.unit = false; + // Clear unit projectile info + if (unit->flags1.bits.projectile) { + unit->flags1.bits.projectile = false; + linked_list_remove(&world->projectiles.all, [&](df::projectile *proj) { + if (proj->getType() != df::enums::projectile_type::Unit) + return false; + if (auto unit_proj = virtual_cast(proj)) + return unit_proj->unit == unit; + return false; + }); + } + // If there's already somebody standing at the destination, then force the unit to lay down if (new_occ->bits.unit) unit->flags1.bits.on_ground = true; From 3b9d731f71bd49123b09177f877fce99ccda6646 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:31:21 -0700 Subject: [PATCH 0256/1082] don't show hover tooltip for hidden creatures --- docs/changelog.txt | 1 + plugins/lua/spectate.lua | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index cd43392b78..e311e6441d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `spectate`: don't show a hover tooltip for hidden units (e.g. invisible snatchers) - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index fd2f736d0e..8c7e653a7b 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -345,11 +345,15 @@ local function GetUnitInfoText(unit, settings_group_name) return txt end +local function unit_filter(unit) + return not dfhack.units.isHidden(unit) +end + local function GetHoverText(pos) if not pos then return end local txt = {} - local units = dfhack.units.getUnitsInBox(pos, pos) or {} -- todo: maybe (optionally) use filter parameter here? + local units = dfhack.units.getUnitsInBox(pos, pos, unit_filter) or {} for _,unit in ipairs(units) do local info = GetUnitInfoText(unit, 'hover') From 4f75aa4a6e59f37daff9b94f51f6a185b2dbc8c0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:36:09 -0700 Subject: [PATCH 0257/1082] try to make msvc happy --- library/include/MiscUtils.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index c916a6a9fb..2b9426202d 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -328,8 +328,8 @@ bool linked_list_remove(L *list, F matches) { auto matches_wrapper = [&](L::iterator::value_type item) { return item && matches(item); }; - auto it = std::find_if(list->begin(), list->end(), matches_wrapper); - if (it == list->end()) + typename L::const_iterator it = std::find_if(list->cbegin(), list->cend(), matches_wrapper); + if (it == list->cend()) return false; auto item = *it; list->erase(it); From 118861d67943b8f2f32b10970f3608b65f88cb60 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:32:50 +0000 Subject: [PATCH 0258/1082] [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.31.2 → 0.32.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.2...0.32.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 6904eb760c..8348cb4d85 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.31.2 + rev: 0.32.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 4eb3ec703ebe2acda71457a68ccc09d444bd9b5d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 7 Apr 2025 14:58:46 -0500 Subject: [PATCH 0259/1082] add `army-controller-sanity` plugin --- plugins/CMakeLists.txt | 1 + plugins/army-controller-sanity.cpp | 133 +++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 plugins/army-controller-sanity.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 8f230ff583..3ef551a49e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -35,6 +35,7 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) option(BUILD_SUPPORTED "Build the supported plugins (reveal, probe, etc.)." ON) if(BUILD_SUPPORTED) dfhack_plugin(3dveins 3dveins.cpp) + dfhack_plugin(army-controller-sanity army-controller-sanity.cpp) dfhack_plugin(add-spatter add-spatter.cpp) dfhack_plugin(aquifer aquifer.cpp LINK_LIBRARIES lua) dfhack_plugin(autobutcher autobutcher.cpp LINK_LIBRARIES lua) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp new file mode 100644 index 0000000000..6e5efceb39 --- /dev/null +++ b/plugins/army-controller-sanity.cpp @@ -0,0 +1,133 @@ +#include "Debug.h" +#include "PluginManager.h" + +#include "df/world.h" +#include "df/army_controller.h" +#include "df/army.h" +#include "df/historical_entity.h" +#include "df/unit.h" +#include "df/global_objects.h" + +#include + +DFHACK_PLUGIN("army-controller-sanity"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(army_controller_next_id); +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(pause_state); + +namespace DFHack { + DBG_DECLARE(army_controller_sanity, log, DebugCategory::LWARNING); +} + +namespace { + bool checkArmyControllerSanity() + { + // army controllers are found: + // in viewscreen_worldst (army_controller (list), last_hover_ac) (not checked) + // in entitst (army_controller (list)) + // in armyst (controller) + // in unitst (army_controller) + // + // master list is in army_controller_handlerst + + static size_t last_ac_vec_size = 0; + static int last_army_controller_next_id = 0; + + if (last_army_controller_next_id == *df::global::army_controller_next_id && + last_ac_vec_size == df::global::world->army_controllers.all.size()) + return true; + + std::unordered_set ac_set{}; + + for (auto ac : df::global::world->army_controllers.all) + { + ac_set.insert(ac); + } + + bool ok = true; + + for (auto ent : df::global::world->entities.all) + { + for (auto ac : ent->army_controllers) + { + if (ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller %08lx found in entity id %d\n", ac, ent->id); + ok = false; + } + } + } + + for (auto ar : df::global::world->armies.all) + { + auto ac = ar->controller; + if (ac && ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller %08lx found in army id %d\n", ac, ar->id); + ok = false; + } + else if (ac && ac->id != ar->controller_id) + { + WARN(log).print("acValidationError: controller %08lx id mismatch (%d != %d) in army %d\n", ac, ar->controller_id, ac->id, ar->id); + ok = false; + } + else if (!ac && ar->controller_id != -1) + { + WARN(log).print("acValidationError: army %d has nonzero controller %d but controller pointer is null\n", ar->id, ar->controller_id); + ok = false; + } + } + + for (auto un : df::global::world->units.all) + { + auto ac = un->enemy.army_controller; + if (ac && ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller %08lx found in unit id %d\n", ac, un->id); + ok = false; + } + else if (ac && ac->id != un->enemy.army_controller_id) + { + WARN(log).print("acValidationError: controller %08lx id mismatch (%d != %d) in unit %d\n", ac, un->enemy.army_controller_id, ac->id, un->id); + ok = false; + } + else if (!ac && un->enemy.army_controller_id != -1) + { + WARN(log).print("acValidationError: unit %d has has nonzero controller %d but controller pointer is null\n", un->id, un->enemy.army_controller_id); + ok = false; + } + } + + last_army_controller_next_id = *df::global::army_controller_next_id; + last_ac_vec_size = df::global::world->army_controllers.all.size(); + + INFO(log).print("acValidation: controller count = %d, next id = %d, season tick count = %d\n", + last_ac_vec_size, last_army_controller_next_id, *df::global::cur_year_tick); + + return ok; + } +} + +DFhackCExport DFHack::command_result plugin_init(DFHack::color_ostream& out, std::vector & commands) +{ + return DFHack::CR_OK; +} + +DFhackCExport DFHack::command_result plugin_enable(DFHack::color_ostream& out, bool enable) +{ + is_enabled = enable; + return DFHack::CR_OK; +} + +DFhackCExport DFHack::command_result plugin_onupdate(DFHack::color_ostream& out) +{ + if (is_enabled) + { + bool ok = checkArmyControllerSanity(); + if (!ok) { + ERR(log).print("Army controller sanity check failed! Game pause forced.\n"); + *df::global::pause_state = true; + is_enabled = false; + } + } + return DFHack::CR_OK; +} \ No newline at end of file From ab1922baf67f82e65bd44bc80441e6d9901f336b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 7 Apr 2025 15:23:56 -0500 Subject: [PATCH 0260/1082] make gcc happy --- plugins/army-controller-sanity.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 6e5efceb39..7dd3abf92d 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -53,7 +53,7 @@ namespace { for (auto ac : ent->army_controllers) { if (ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %08lx found in entity id %d\n", ac, ent->id); + WARN(log).print("acValidationError: Bad controller %p found in entity id %d\n", ac, ent->id); ok = false; } } @@ -63,12 +63,12 @@ namespace { { auto ac = ar->controller; if (ac && ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %08lx found in army id %d\n", ac, ar->id); + WARN(log).print("acValidationError: Bad controller %p found in army id %d\n", ac, ar->id); ok = false; } else if (ac && ac->id != ar->controller_id) { - WARN(log).print("acValidationError: controller %08lx id mismatch (%d != %d) in army %d\n", ac, ar->controller_id, ac->id, ar->id); + WARN(log).print("acValidationError: controller %p id mismatch (%d != %d) in army %d\n", ac, ar->controller_id, ac->id, ar->id); ok = false; } else if (!ac && ar->controller_id != -1) @@ -82,12 +82,12 @@ namespace { { auto ac = un->enemy.army_controller; if (ac && ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %08lx found in unit id %d\n", ac, un->id); + WARN(log).print("acValidationError: Bad controller %p found in unit id %d\n", ac, un->id); ok = false; } else if (ac && ac->id != un->enemy.army_controller_id) { - WARN(log).print("acValidationError: controller %08lx id mismatch (%d != %d) in unit %d\n", ac, un->enemy.army_controller_id, ac->id, un->id); + WARN(log).print("acValidationError: controller %p id mismatch (%d != %d) in unit %d\n", ac, un->enemy.army_controller_id, ac->id, un->id); ok = false; } else if (!ac && un->enemy.army_controller_id != -1) @@ -100,7 +100,7 @@ namespace { last_army_controller_next_id = *df::global::army_controller_next_id; last_ac_vec_size = df::global::world->army_controllers.all.size(); - INFO(log).print("acValidation: controller count = %d, next id = %d, season tick count = %d\n", + INFO(log).print("acValidation: controller count = %ld, next id = %d, season tick count = %d\n", last_ac_vec_size, last_army_controller_next_id, *df::global::cur_year_tick); return ok; From 8946176318f7479754d184f0257a07d7acc9c1a0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 7 Apr 2025 15:33:03 -0500 Subject: [PATCH 0261/1082] eof --- plugins/army-controller-sanity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 7dd3abf92d..19f8d74a5b 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -130,4 +130,4 @@ DFhackCExport DFHack::command_result plugin_onupdate(DFHack::color_ostream& out) } } return DFHack::CR_OK; -} \ No newline at end of file +} From 174018f1ccb149b917afd649e910b5619636aae8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 7 Apr 2025 21:13:54 -0500 Subject: [PATCH 0262/1082] yay globalism --- plugins/army-controller-sanity.cpp | 32 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 19f8d74a5b..3f4ac0b1c3 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -16,6 +16,7 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(army_controller_next_id); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(pause_state); +REQUIRE_GLOBAL(cur_year_tick); namespace DFHack { DBG_DECLARE(army_controller_sanity, log, DebugCategory::LWARNING); @@ -35,20 +36,20 @@ namespace { static size_t last_ac_vec_size = 0; static int last_army_controller_next_id = 0; - if (last_army_controller_next_id == *df::global::army_controller_next_id && - last_ac_vec_size == df::global::world->army_controllers.all.size()) + if (last_army_controller_next_id == *army_controller_next_id && + last_ac_vec_size == world->army_controllers.all.size()) return true; std::unordered_set ac_set{}; - for (auto ac : df::global::world->army_controllers.all) + for (auto ac : world->army_controllers.all) { ac_set.insert(ac); } bool ok = true; - for (auto ent : df::global::world->entities.all) + for (auto ent : world->entities.all) { for (auto ac : ent->army_controllers) { @@ -59,7 +60,7 @@ namespace { } } - for (auto ar : df::global::world->armies.all) + for (auto ar : world->armies.all) { auto ac = ar->controller; if (ac && ac_set.count(ac) == 0) { @@ -78,7 +79,7 @@ namespace { } } - for (auto un : df::global::world->units.all) + for (auto un : world->units.all) { auto ac = un->enemy.army_controller; if (ac && ac_set.count(ac) == 0) { @@ -97,11 +98,11 @@ namespace { } } - last_army_controller_next_id = *df::global::army_controller_next_id; - last_ac_vec_size = df::global::world->army_controllers.all.size(); + last_army_controller_next_id = *army_controller_next_id; + last_ac_vec_size = world->army_controllers.all.size(); INFO(log).print("acValidation: controller count = %ld, next id = %d, season tick count = %d\n", - last_ac_vec_size, last_army_controller_next_id, *df::global::cur_year_tick); + last_ac_vec_size, last_army_controller_next_id, *cur_year_tick); return ok; } @@ -120,14 +121,11 @@ DFhackCExport DFHack::command_result plugin_enable(DFHack::color_ostream& out, b DFhackCExport DFHack::command_result plugin_onupdate(DFHack::color_ostream& out) { - if (is_enabled) - { - bool ok = checkArmyControllerSanity(); - if (!ok) { - ERR(log).print("Army controller sanity check failed! Game pause forced.\n"); - *df::global::pause_state = true; - is_enabled = false; - } + bool ok = checkArmyControllerSanity(); + if (!ok) { + ERR(log).print("Army controller sanity check failed! Game pause forced.\n"); + *pause_state = true; + is_enabled = false; } return DFHack::CR_OK; } From e66973008ca3bcce8eef1e6179c2e86843ab6af2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 8 Apr 2025 07:19:23 +0000 Subject: [PATCH 0263/1082] Auto-update submodules library/xml: master scripts: master depends/dfhooks: main --- depends/dfhooks | 2 +- library/xml | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 364f71f745..ca30a90f8a 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 364f71f74544c37f47a4ddfebcb9d6b9e4cdfcce +Subproject commit ca30a90f8a3f9cc61531ac24430fd2434fff40ec diff --git a/library/xml b/library/xml index 4740f8967f..8f4b816a52 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 4740f8967f4a77c1228d8c98acb9996412a36f29 +Subproject commit 8f4b816a52f94c95772100cd4652a5a43697059e diff --git a/scripts b/scripts index 1f7663b57b..bbba0a3b3c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1f7663b57bdda6bb02dc2f950f3465df832b2fe6 +Subproject commit bbba0a3b3c0697b6760f308e22fb5b300bf84826 From 2ded1754efd2c7cb196abb4e58b1866cfb3a21eb Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 8 Apr 2025 19:02:02 -0700 Subject: [PATCH 0264/1082] Also set filter for follow tooltip --- 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 8c7e653a7b..0cb2d3bd33 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -465,7 +465,7 @@ function TooltipOverlay:render_unit_banners(dc) local height = vp.max_y local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} - local units = dfhack.units.getUnitsInBox(topleft, bottomright) + local units = dfhack.units.getUnitsInBox(topleft, bottomright, unit_filter) if not units or #units == 0 then return end local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) From 77ea136b09ccb516b44bdd2ff5dcd13aac0332b4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 10 Apr 2025 18:26:19 -0700 Subject: [PATCH 0265/1082] fix docs for `dfhack.gui.getCurFocus()` --- docs/dev/Lua API.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 795df5b377..b11c8862b2 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1073,7 +1073,8 @@ Screens * ``dfhack.gui.getCurFocus([skip_dismissed])`` - Returns the focus string of the current viewscreen. + Returns a list of focus strings for the current viewscreen. Equivalent to + ``dfhack.gui.getFocusStrings(dfhack.gui.getCurViewscreen(skip_dismissed))``. * ``dfhack.gui.getViewscreenByType(type[, depth])`` From 3745107f98390cfaab0df0db4006f58f5ab29fbf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 11 Apr 2025 15:38:01 -0500 Subject: [PATCH 0266/1082] code modernization this commit changes several templates to have more precisely constrained match patterns to yield more clear compilation errors when a mistake is made certain `Lua::Push` overloads have been made either more or less general; a side effect is that more "string-like" objects (notably, `std::string_view`) will be converted to lua strings on push a chunk of ancient dead code bracketed by `#f 0` was removed a declaration for the undefined and unused `Lua::Core::Init` was removed it also replaces several uses of `enable_if` with `if constexpr` or templates constrained by appropriate types, to make that code more readable some other minor cleanups, mostly around namespaces blocks,, are also included --- library/include/DataDefs.h | 151 +++++++++++++++++-------------------- library/include/LuaTools.h | 63 +++++++--------- plugins/debug.cpp | 34 ++++----- 3 files changed, 113 insertions(+), 135 deletions(-) diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 6738054423..59f47abcb6 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -494,22 +494,18 @@ namespace df using DFHack::DfLinkedList; using DFHack::DfOtherVectors; - template - typename std::enable_if< - std::is_copy_assignable::value, - void* - >::type allocator_try_assign(void *out, const void *in) { - *(T*)out = *(const T*)in; - return out; - } + template concept copy_assignable = std::assignable_from && std::assignable_from; - template - typename std::enable_if< - !std::is_copy_assignable::value, - void* - >::type allocator_try_assign(void *out, const void *in) { - // assignment is not possible; do nothing - return NULL; + template + void* allocator_try_assign(void *out, const void *in) { + if constexpr (copy_assignable) { + *(T*)out = *(const T*)in; + return out; + } + else { + // assignment is not possible; do nothing + return nullptr; + } } #pragma GCC diagnostic push @@ -651,98 +647,87 @@ namespace DFHack { /** * Return the next item in the enum, wrapping to the first one at the end if 'wrap' is true (otherwise an invalid item). */ - template - inline typename std::enable_if< - !df::enum_traits::is_complex, - typename df::enum_traits::enum_type - >::type next_enum_item(T v, bool wrap = true) + template concept complex_enum = (df::enum_traits::is_complex); + + template + inline auto next_enum_item(T v, bool wrap = true) -> typename df::enum_traits::enum_type { - typedef df::enum_traits traits; - typedef typename traits::base_type base_type; - base_type iv = base_type(v); - if (iv < traits::last_item_value) - { - return T(iv + 1); - } - else + using traits = df::enum_traits; + + if constexpr (complex_enum) { - if (wrap) - return traits::first_item; + const auto& complex = traits::complex; + const auto it = complex.value_index_map.find(v); + if (it != complex.value_index_map.end()) + { + if (!wrap && it->second + 1 == complex.size()) + { + return T(traits::last_item_value + 1); + } + size_t next_index = (it->second + 1) % complex.size(); + return T(complex.index_value_map[next_index]); + } else return T(traits::last_item_value + 1); } - } - - template - inline typename std::enable_if< - df::enum_traits::is_complex, - typename df::enum_traits::enum_type - >::type next_enum_item(T v, bool wrap = true) - { - typedef df::enum_traits traits; - const auto &complex = traits::complex; - const auto it = complex.value_index_map.find(v); - if (it != complex.value_index_map.end()) + else { - if (!wrap && it->second + 1 == complex.size()) + using base_type = typename traits::base_type; + base_type iv = base_type(v); + if (iv < traits::last_item_value) { - return T(traits::last_item_value + 1); + return T(iv + 1); + } + else + { + if (wrap) + return traits::first_item; + else + return T(traits::last_item_value + 1); } - size_t next_index = (it->second + 1) % complex.size(); - return T(complex.index_value_map[next_index]); } - else - return T(traits::last_item_value + 1); } /** * Check if the value is valid for its enum type. */ - template - inline typename std::enable_if< - !df::enum_traits::is_complex, - bool - >::type is_valid_enum_item(T v) + template + inline bool is_valid_enum_item(T v) { - return df::enum_traits::is_valid(v); + if constexpr (complex_enum) + { + const auto& complex = df::enum_traits::complex; + return complex.value_index_map.find(v) != complex.value_index_map.end(); + } + else + { + return df::enum_traits::is_valid(v); + } } - template - inline typename std::enable_if< - df::enum_traits::is_complex, - bool - >::type is_valid_enum_item(T v) - { - const auto &complex = df::enum_traits::complex; - return complex.value_index_map.find(v) != complex.value_index_map.end(); - } /** * Return the enum item key string pointer, or NULL if none. */ template - inline typename std::enable_if< - !df::enum_traits::is_complex, - const char * - >::type enum_item_raw_key(T val) { - typedef df::enum_traits traits; - return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : NULL; - } - - template - inline typename std::enable_if< - df::enum_traits::is_complex, - const char * - >::type enum_item_raw_key(T val) { - typedef df::enum_traits traits; - const auto &value_index_map = traits::complex.value_index_map; - auto it = value_index_map.find(val); - if (it != value_index_map.end()) - return traits::key_table[it->second]; + const char* enum_item_raw_key(T val) { + using traits = df::enum_traits; + if constexpr (complex_enum) + { + const auto& value_index_map = traits::complex.value_index_map; + auto it = value_index_map.find(val); + if (it != value_index_map.end()) + return traits::key_table[it->second]; + else + return nullptr; + } else - return NULL; + { + return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : nullptr; + } } + /** * Return the enum item key string pointer, or "?" if none. */ @@ -773,7 +758,7 @@ namespace DFHack { */ template inline bool find_enum_item(T *var, const std::string &name) { - typedef df::enum_traits traits; + using traits = df::enum_traits; int size = traits::last_item_value-traits::first_item_value+1; int idx = findEnumItem(name, size, traits::key_table); if (idx < 0) return false; diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 51ae098f49..8b71862ec3 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -32,6 +32,7 @@ distribution. #include #include #include +#include #include "Core.h" #include "ColorText.h" @@ -60,7 +61,7 @@ namespace DFHack { }; } -namespace DFHack {namespace Lua { +namespace DFHack::Lua { /** * Create or initialize a lua interpreter with access to DFHack tools. */ @@ -140,10 +141,15 @@ namespace DFHack {namespace Lua { DFHACK_EXPORT void CheckDFAssign(lua_State *state, type_identity *type, void *target, int val_index, bool exact_type = false); + template concept df_object = requires(T x) + { + { df::identity_traits::get() } -> std::convertible_to; + }; + /** * Push the pointer onto the stack as a wrapped DF object of a specific type. */ - template + template void PushDFObject(lua_State *state, T *ptr) { PushDFObject(state, df::identity_traits::get(), ptr); } @@ -151,7 +157,7 @@ namespace DFHack {namespace Lua { /** * Check that the value is a wrapped DF object of the correct type, and if so return the pointer. */ - template + template T *GetDFObject(lua_State *state, int val_index, bool exact_type = false) { return (T*)GetDFObject(state, df::identity_traits::get(), val_index, exact_type); } @@ -159,7 +165,7 @@ namespace DFHack {namespace Lua { /** * Check that the value is a wrapped DF object of the correct type, and if so return the pointer. Otherwise throw an argument type error. */ - template + template T *CheckDFObject(lua_State *state, int val_index, bool exact_type = false) { return (T*)CheckDFObject(state, df::identity_traits::get(), val_index, exact_type); } @@ -167,7 +173,7 @@ namespace DFHack {namespace Lua { /** * Assign the value at val_index to the target using df.assign(). */ - template + template bool AssignDFObject(color_ostream &out, lua_State *state, T *target, int val_index, bool exact_type = false, bool perr = true) { return AssignDFObject(out, state, df::identity_traits::get(), @@ -178,7 +184,7 @@ namespace DFHack {namespace Lua { * Assign the value at val_index to the target using df.assign(). * Throws in case of an error. */ - template + template void CheckDFAssign(lua_State *state, T *target, int val_index, bool exact_type = false) { CheckDFAssign(state, df::identity_traits::get(), target, val_index, exact_type); } @@ -298,42 +304,33 @@ namespace DFHack {namespace Lua { /** * Push utility functions */ -#if 0 -#define NUMBER_PUSH(type) inline void Push(lua_State *state, type value) { lua_pushnumber(state, value); } - NUMBER_PUSH(char) - NUMBER_PUSH(int8_t) NUMBER_PUSH(uint8_t) - NUMBER_PUSH(int16_t) NUMBER_PUSH(uint16_t) - NUMBER_PUSH(int32_t) NUMBER_PUSH(uint32_t) - NUMBER_PUSH(int64_t) NUMBER_PUSH(uint64_t) - NUMBER_PUSH(float) NUMBER_PUSH(double) -#undef NUMBER_PUSH -#else - template - inline typename std::enable_if::value || std::is_enum::value>::type - Push(lua_State *state, T value) { + template concept lua_integral = (std::is_integral_v || std::is_enum_v); + + inline void Push(lua_State *state, lua_integral auto value) { lua_pushinteger(state, value); } - template - inline typename std::enable_if::value>::type - Push(lua_State *state, T value) { + inline void Push(lua_State* state, std::floating_point auto value) { lua_pushnumber(state, lua_Number(value)); } -#endif inline void Push(lua_State *state, bool value) { lua_pushboolean(state, value); } - inline void Push(lua_State *state, const char *str) { - lua_pushstring(state, str); - } - inline void Push(lua_State *state, const std::string &str) { - lua_pushlstring(state, str.data(), str.size()); + + template concept lua_string = (std::convertible_to); + + inline void Push(lua_State *state, lua_string auto str) { + std::string_view sv{ str }; + lua_pushlstring(state, sv.data(), sv.size()); } + DFHACK_EXPORT void Push(lua_State *state, const df::coord &obj); DFHACK_EXPORT void Push(lua_State *state, const df::coord2d &obj); void Push(lua_State *state, const Units::NoblePosition &pos); DFHACK_EXPORT void Push(lua_State *state, const MaterialInfo &info); DFHACK_EXPORT void Push(lua_State *state, const Screen::Pen &info); - template inline void Push(lua_State *state, T *ptr) { + + template inline void Push(lua_State *state, T *ptr) + { PushDFObject(state, ptr); } @@ -472,10 +469,6 @@ namespace DFHack {namespace Lua { * All accesses must be done under CoreSuspender. */ namespace Core { -// DFHACK_EXPORT extern lua_State *State; - - // Not exported; for use by the Core class - lua_State* Init(color_ostream &out); DFHACK_EXPORT void Reset(color_ostream &out, const char *where); // Events signalled by the core @@ -559,7 +552,7 @@ namespace DFHack {namespace Lua { Notification(function_identity_base *handler = NULL) : state(NULL), key(NULL), handler(handler), count(0) {} - int get_listener_count() { return count; } + int get_listener_count() const { return count; } lua_State *get_state() { return state; } function_identity_base *get_handler() { return handler; } @@ -572,7 +565,7 @@ namespace DFHack {namespace Lua { void bind(lua_State *state, const char *name); void bind(lua_State *state, void *key); }; -}} +} #define DEFINE_LUA_EVENT_0(name, handler) \ static DFHack::Lua::Notification name##_event(df::wrap_function(handler, true)); \ diff --git a/plugins/debug.cpp b/plugins/debug.cpp index c3f336afc0..eb2fc4c1c7 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -48,30 +48,30 @@ DBG_DECLARE(debug,example,DebugCategory::LINFO); namespace serialization { -template -struct nvp : public std::pair { - using parent_t = std::pair; - nvp(const char* name, T& value) : - parent_t{name, &value} - {} -}; + template + struct nvp : public std::pair { + using parent_t = std::pair; + nvp(const char* name, T& value) : + parent_t{name, &value} + {} + }; -template -nvp make_nvp(const char* name, T& value) { - return {name, value}; -} + template + nvp make_nvp(const char* name, T& value) { + return {name, value}; + } } #define NVP(variable) serialization::make_nvp(#variable, variable) namespace Json { -template -typename std::enable_if::value, ET>::type -get(Json::Value& ar, const std::string &key, const ET& default_) -{ - return static_cast(as(ar.get(key, static_cast(default_)))); -} + template + requires (std::is_enum_v) + ET get(Value& ar, const std::string &key, const ET& default_) + { + return static_cast(as(ar.get(key, static_cast(default_)))); + } } namespace DFHack { namespace debugPlugin { From f6e4ed493562df0eabead5fdd6b96bde98b0dd44 Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Sat, 12 Apr 2025 02:04:30 -0400 Subject: [PATCH 0267/1082] L803: Added reload bool to catch whether the reload command was given in Core::runCommand ---- L831: Added reload to else if statement to short circuit the PluginManager::reload statement so that it only runs if reload was given as an argument to Core::runCommand --- library/Core.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index 227c4378f0..2b02c46fe4 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -800,6 +800,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s bool all = false; bool load = (first == "load"); bool unload = (first == "unload"); + bool reload = (first == "reload"); if (parts.size()) { for (auto p = parts.begin(); p != parts.end(); p++) @@ -828,7 +829,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s ret = CR_FAILURE; else if (unload && !plug_mgr->unload(*p)) ret = CR_FAILURE; - else if (!plug_mgr->reload(*p)) + else if (reload && !plug_mgr->reload(*p)) ret = CR_FAILURE; } if (ret != CR_OK) From 426733615ff8cf8b707cabcc449da1a4f577213c Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Sat, 12 Apr 2025 02:10:09 -0400 Subject: [PATCH 0268/1082] Updated Changelog to Core.cpp module reload fixes --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index d65aeace62..01a59a8381 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung +- `Core.cpp`: now properly handles "load", "unload", and "reload" plugin commands given to Core::runCommand() ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) From 8ebf0362eb78b66f07eda6012bbccad18de77150 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 12 Apr 2025 05:10:40 -0700 Subject: [PATCH 0269/1082] Fix format in changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 01a59a8381..23839e179a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,7 +61,7 @@ Template for new versions: - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung -- `Core.cpp`: now properly handles "load", "unload", and "reload" plugin commands given to Core::runCommand() +- ``Core.cpp``: now properly handles "load", "unload", and "reload" plugin commands given to Core::runCommand() ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) From 8cde8f7f36703d391510bf9f9bb8810d0922f42e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 12 Apr 2025 14:16:26 -0700 Subject: [PATCH 0270/1082] also unbreak unload -a --- docs/changelog.txt | 2 +- library/Core.cpp | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 23839e179a..310c538652 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,7 +61,7 @@ Template for new versions: - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung -- ``Core.cpp``: now properly handles "load", "unload", and "reload" plugin commands given to Core::runCommand() +- `unload`: fix recent regression where `unload` would immediately `reload` the target ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/library/Core.cpp b/library/Core.cpp index 2b02c46fe4..6e875648fb 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -818,19 +818,22 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s ret = CR_FAILURE; else if (unload && !plug_mgr->unloadAll()) ret = CR_FAILURE; - else if (!plug_mgr->reloadAll()) + else if (reload && !plug_mgr->reloadAll()) ret = CR_FAILURE; } - for (auto p = parts.begin(); p != parts.end(); p++) + else { - if (!p->size() || (*p)[0] == '-') - continue; - if (load && !plug_mgr->load(*p)) - ret = CR_FAILURE; - else if (unload && !plug_mgr->unload(*p)) - ret = CR_FAILURE; - else if (reload && !plug_mgr->reload(*p)) - ret = CR_FAILURE; + for (auto p = parts.begin(); p != parts.end(); p++) + { + if (!p->size() || (*p)[0] == '-') + continue; + if (load && !plug_mgr->load(*p)) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unload(*p)) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reload(*p)) + ret = CR_FAILURE; + } } if (ret != CR_OK) con.printerr("%s failed\n", first.c_str()); From e7860b4c83ffb404c8928bb76807ab07e302c9ea Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 12 Apr 2025 14:32:37 -0700 Subject: [PATCH 0271/1082] restore overlays after running unit tests --- ci/test.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/test.lua b/ci/test.lua index c0d021d0a6..a4846520ec 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -6,6 +6,7 @@ local gui = require('gui') local helpdb = require('helpdb') local json = require('json') local mock = require('test_util.mock') +local overlay = require('plugins.overlay') local script = require('gui.script') local utils = require('utils') @@ -427,6 +428,7 @@ end local function finish_tests(done_command) dfhack.internal.IN_TEST = false + overlay.rescan() if done_command and #done_command > 0 then dfhack.run_command(done_command) end From 436b6408134c014efbfe2ae668f030d59dc3de25 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 12 Apr 2025 21:59:37 +0000 Subject: [PATCH 0272/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index bbba0a3b3c..c0ea8aa176 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit bbba0a3b3c0697b6760f308e22fb5b300bf84826 +Subproject commit c0ea8aa176ba93aafe52eb8592d5aafa1c323ba8 From 090aef032531196a37eadb9e44feb35c6ec81d5a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 13 Apr 2025 15:54:40 +0000 Subject: [PATCH 0273/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index c0ea8aa176..bb096b2733 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit c0ea8aa176ba93aafe52eb8592d5aafa1c323ba8 +Subproject commit bb096b27333207c65974ed90bf6a9884d6d287c8 From 02d117c7d50dd8d23c8270bdd6d2e7fabd81745e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 13 Apr 2025 12:11:36 -0500 Subject: [PATCH 0274/1082] add safety check to stockpile iterator the stockpile iterator will crash if a `map_block` contains an item pointer to an item that is not on the master item list. this should never happen if DF was working correctly, but DF often does not work correctly related discord discussion: https://discord.com/channels/329272032778780672/1049402643342168114/1360977656820273322 --- docs/changelog.txt | 1 + library/modules/Buildings.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 310c538652..ee422916ef 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: - `dig-now`: properly generate ice boulders when digging through ice walls - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung - `unload`: fix recent regression where `unload` would immediately `reload` the target +- ``Buildings`` module: do not crash if a ``map_block`` unexpectedly contains an item that is not on the master item vector ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 1135fb5307..df31b1de7b 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -1654,7 +1654,7 @@ StockpileIterator& StockpileIterator::operator++() { // If the current item isn't properly stored, move on to the next. item = df::item::find(block->items[current]); - if (!item->flags.bits.on_ground) { + if (!item || !item->flags.bits.on_ground) { continue; } From 72260531287669d446c3d66477d895d23b9d1714 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:41:51 +0000 Subject: [PATCH 0275/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index bb096b2733..3220332217 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit bb096b27333207c65974ed90bf6a9884d6d287c8 +Subproject commit 322033221745e9cbf537d59152138df385c505b7 From f21aa92a76039b6eb218bb68bdc03e9e8d91bfef Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 14 Apr 2025 15:58:54 -0500 Subject: [PATCH 0276/1082] add another test to army controller sanity --- plugins/army-controller-sanity.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 3f4ac0b1c3..089b626b5f 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -57,6 +57,10 @@ namespace { WARN(log).print("acValidationError: Bad controller %p found in entity id %d\n", ac, ent->id); ok = false; } + if (ac_set.count(ac) != 0 && ac->entity_id != ent->id) + { + WARN(log).print("acValidationError: Army controller %d has entity id %d but is linked from entity with id %d\n", ac->id, ac->entity_id, ent->id); + } } } From eb1ae67b2c7ef706b9ba008d1be48cb402dd4fae Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 15 Apr 2025 07:21:20 +0000 Subject: [PATCH 0277/1082] 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 d0c14fa0a6..d65c4ac560 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit d0c14fa0a6702f396e147bbd3c9b325c0677aa2b +Subproject commit d65c4ac560264c4a0d69a802bc31b636c3da55fc From 9e99ca4dc560c3fda29ee0b318dfbd79bbf1f3a6 Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Fri, 18 Apr 2025 12:34:57 +0100 Subject: [PATCH 0278/1082] Add star icon to gui design --- data/art/design.png | Bin 1491 -> 1732 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/art/design.png b/data/art/design.png index 787b9962a1bc68d6d5d46bfa9c9d402a4f8cc4a1..38fed7e0885f20a9ee9e1b23cd6c90dde2c7cad0 100644 GIT binary patch literal 1732 zcmV;#20QtQP)11hLtOkivgpOG#z3|AH36vXymOkrWREQ}_e4FolgED6GZC5EL|tMAw9W z-`;uuqticsRsp?zbn@*FUqyw$2R;LT;nAIQ%U51gL3hr*`NrFk_rM1}1HXy>eEG{y z(h4><6MqFh@U!r*6torRKfe1+J_7!IaqgUZ_WMy-2z=l>=T--+T>K`gL&YNRDfnI2 zcOL90;;Z}LI^1=VQm*d5k6s~s#rUrdR*9bN;y2aPw4qTo)YG}hAp)gD?lVt0c+F=Z z7m9ZAEMDGht!|!p;3BzVms6@p=uH$o`A9a48%0FxE9hu^!w0gO)cx#ee=vqYId;lJ z$41MIb2SBJk9bAhZOd&2x6omAcJ4gbNv~zKNFRD=5NbxB)=^M;&-uV*4el@yCDxz@ z+F6>KLmn0q(TJ4vS{es4T#u@Ba3OZ8h$tOl=*g&ci|KUgsFqA^x}Os}OE1`>k&s^3 zK|!nK@(g-evQtGwLl6pI(+@ZEW>29kp1sqgo1B$HBhJ5+%pvh9%GkqoJ0tBB%BR`x zk5)zDJl;$9uxW`(=c%9uS^RZpXCjZLpWkLlX*cW?Oem;G8w|1~b#|6G@XM`~?!%%= z2WPe1ewv#?E^n}9kyS;l;6p*D$j;KVusI`))&{GCmH%<%Q>8j)qN8=(A2OBr*`|7!D2L9o$>k;YQ3`2<4FSToWSz)Y%i^N|rQKy*ws0o*K}+m| z8jsA;>g4l4F9LBEZ7QyaDI;luiIL4ds6oRGZZM@=K_Ni>eb6B<usjnM<;ia-zX*8Uw>cpmIEL74E)BUx4+!o*zEnz zp3r~~e363sH;rha|90|g_)p*ipND@7?+1llk2NR^27<( z#8>yfb-3#!rCi;AAH72Oit)ey@HEl0UHtkNmVIzH@7xQ^w|6hkpI?kNW)wL@pp?*& zBZnE~SWJJpS5wd-(hQj= z6Qd7?YH7=P$WaID>gdq=Qjwj9d&{=r-4BL*l9MU)kR9sWIW}v4)>(s|KfhosuqYw+ zfy)})Z6LaROevA{u?jnfA`=Ryiq1tIww+i=L?cqtXZdPL5}nYw>`#1J$HCDgeou#>%y zW5!M`5k1^nM*Fg9E~X!z=NpzwXYuTvCdqhKTpA|S-LFKW5RDS9m!s)n8FtgqGqe3) zKFVeB2K%7w<&a9E-HNDi$_f+q-js-v21zKT-2*&| zB+vQ6a(9xVlu&|hz)syACB`%zwt^9*bft73DiAa`E4QEKrjX0?PMfoaEhIep>A|2B@zPVmQN)3RoF>A-mn~g55NCAd-?Y1k84ce1D}U~rJ(*zBPF~CKJeN2ct<_#nyf)#Fc1_513_Uk avi|_7^f7$z*D;v@0000D!@9mqN zKf8O&OmSQ$cfXyT{q4M&+4pW?Wo2cgqJotLC6I~|NJR;xGVcVsa^vpyj}I)Mi_@)_ zZ=X3z#V->7kU$o_`kOw6MX%m8=b-qd;v47R2Or;42UuG6{8#Y{!yh+^`Tf;HdVKq{ zziVH=e493vieDw8 z|4M`+HxVEjy3KHXknvo|yxum9W^pY8FOB3F?^qS8hYVrg^nJ_m9(||b#AFh9`eOc| z9|ytP-s#f>4I#%Ky6ezrp?)495MswL#;r=v6`~o%!*wq9*w6cODK1hrEyTInqzylV zE~qCKv^kpOlgNQ0#2#tt^jR1hgvKM4i1`r9g+_=yIM{K{;m7${ZkMx?*n>_;Gy=t| z#c&%SCVSY*_+nO9s0u?@^1R8r99o~j?QdxW%CQe5+6CaHX_*lyLKQae?D14v@bejs zKuC7R>>{$Whxfo>oGLuPGlTeHJcn8=Ldh94MBgkl0%cvv3+)-kGln!d5WK&PTZsr~ zVWRAb*6{-Nojsp1$)^E1^eLtmqny_Y?O`GTl2u{O1pKxl4kJfV?gx_S14Z6BgTwiq zW8!nabD5Lu!kqg+5hjj4fzPWC)JG$_2O+I`^eFHqO3o!D-6~VeOp=?*MV&cv#gQ_8 zak_QF`$Py(e*W6))GL0G_@+j$-q4Pk#CMsiyw&&4b;)dmb7A zk(&q*4IJ4q7@-d`KFE7?UjfAc;{il)%ix*3)e~vdMuupB?0J0e7)1{m!cM>MTaNeO z;PCx`ClH;{p+>joZ++LKQILb08=#_KJGN1xohW+-{m)1pXCXrUJa7qvW7Ug|@KN6= zl^%!eEC6pD>dS8!*<**OCf@#HbG?bwaV*5SnkNUc+ZSSfVwTPiXDA}{|aKbs4n;=>vG&!)xGmsE_a2a$O0>Q(NGcok#OgfE= z#vYc-mdCRak^R5c+aYm%0E1I;2bjNmpSp*~6GiIYHOoKxwyWIzM0XFO&&YVV0c{*uz_~ zQxY9J3y+t^slo$1+m9c{bEw5|8F$duf>&FfS&TD@4ZKxb8I`=y#VaTX4e~fm4pe7q zgMCk2_>r!{q}d}Hew=D+&`AG8j6ctkYrxc}AAO5Qyiq}}3Zv9wl=E7lJxt`WNp Date: Sat, 19 Apr 2025 17:24:36 +0200 Subject: [PATCH 0279/1082] fix walls being treated as potential suitable access if another wall is built beneath them --- docs/changelog.txt | 1 + plugins/suspendmanager.cpp | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee422916ef..fb0c1175a9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung - `unload`: fix recent regression where `unload` would immediately `reload` the target - ``Buildings`` module: do not crash if a ``map_block`` unexpectedly contains an item that is not on the master item vector +- `suspendmanager`: fix walls being treated as potential suitable access if another wall is built underneath ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/plugins/suspendmanager.cpp b/plugins/suspendmanager.cpp index 10c8d241f0..d63d6a99c9 100644 --- a/plugins/suspendmanager.cpp +++ b/plugins/suspendmanager.cpp @@ -387,7 +387,12 @@ class SuspendManager { if (isSuitableAccess(pos)) return true; - // if a wall is being constructed below, the tile will be suitable + // wall-type tiles can never become suitable + auto tile_type = Maps::getTileType(pos); + if (!tile_type || isWallTerrain(*tile_type)) + return false; + + // other tiles can become suitable if a wall is being constructed below auto below = Buildings::findAtTile(coord(pos.x,pos.y,pos.z-1)); if (below && below->getType() == df::building_type::Construction && below->getSubtype() == construction_type::Wall) From e4389a28ec464d5ecb88844cbcfb6e55acc46215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 21 Apr 2025 11:45:57 +0200 Subject: [PATCH 0280/1082] Fix selection tests for TextArea They are now closer to real behaviour. In real TextArea cursor dissapears when there is a selection. On tests id did not - now it do. --- .../lua/gui/widgets/text_area/text_area_content.lua | 10 ++++------ test/library/gui/widgets.TextArea.lua | 10 +++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index cfa790a355..928cf7dc66 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -240,12 +240,10 @@ function TextAreaContent:onRenderBody(dc) dc:newline() end - local show_focus = not self.enable_cursor_blink - or ( - not self:hasSelection() - and self.parent_view:hasFocus() - and gui.blink_visible(530) - ) + local show_focus = not self:hasSelection() and ( + not self.enable_cursor_blink + or (self.parent_view:hasFocus() and gui.blink_visible(530)) + ) if show_focus then local x, y = self.wrapped_text:indexToCoords(self.cursor) diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index a5748afd5f..c9cd805782 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -1726,7 +1726,7 @@ function test.arrows_reset_selection() simulate_input_keys('CUSTOM_CTRL_A') - expect.eq(read_rendered_text(text_area), text .. '_'); + expect.eq(read_rendered_text(text_area), text); expect.eq(read_selected_text(text_area), text); @@ -1768,7 +1768,7 @@ function test.click_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1803,7 +1803,7 @@ function test.line_navigation_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1836,7 +1836,7 @@ function test.jump_begin_or_end_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -3057,7 +3057,7 @@ function test.fast_rewind_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ From eb0723f0b67406010d5a6805d95f2a53b22646f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 21 Apr 2025 11:47:17 +0200 Subject: [PATCH 0281/1082] Fix TextArea cursor dissapearing on Select All when there is no text --- .../lua/gui/widgets/text_area/text_area_content.lua | 5 +++++ test/library/gui/widgets.TextArea.lua | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index 928cf7dc66..83099fac76 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -98,7 +98,12 @@ function TextAreaContent:setCursor(cursor_offset) end function TextAreaContent:setSelection(from_offset, to_offset) + if #self.text == 0 then + return + end + -- text selection is always start on self.cursor and end on self.sel_end + self:setCursor(from_offset) self.sel_end = to_offset diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index c9cd805782..5a7d006d59 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -1655,6 +1655,18 @@ function test.select_all() screen:dismiss() end +function test.ignore_select_all_for_empty_text() + local text_area, screen, window = arrange_textarea({w=65}) + + expect.eq(read_rendered_text(text_area), '_'); + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), '_'); + + screen:dismiss() +end + function test.text_key_replace_selection() local text_area, screen, window = arrange_textarea({w=65}) From 7c75baa0e7d92210a14b30a4d0c054c1156a1fa4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:09:22 -0500 Subject: [PATCH 0282/1082] update `Buildings` for 51.11 Also added `Units::get_cached_unit_by_global_id` as a utility function to support compatible updating of `buildingst::owner_unit_cached_index` --- library/include/modules/Units.h | 2 ++ library/modules/Buildings.cpp | 29 ++++++++++++++++------------- library/modules/Units.cpp | 21 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 2e0f188b5c..86238308d7 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -350,5 +350,7 @@ DFHACK_EXPORT void multiplyActionTimers(color_ostream &out, df::unit *unit, floa DFHACK_EXPORT void multiplyGroupActionTimers(color_ostream &out, df::unit *unit, float amount, df::unit_action_type_group affectedActionTypeGroup); DFHACK_EXPORT void setActionTimers(color_ostream &out, df::unit *unit, int32_t amount, df::unit_action_type affectedActionType); DFHACK_EXPORT void setGroupActionTimers(color_ostream &out, df::unit *unit, int32_t amount, df::unit_action_type_group affectedActionTypeGroup); + +DFHACK_EXPORT df::unit* get_cached_unit_by_global_id(int32_t id, int32_t& index); } } diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index df31b1de7b..5fe8bdc5e3 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -38,6 +38,7 @@ distribution. #include "modules/Buildings.h" #include "modules/Maps.h" #include "modules/Job.h" +#include "modules/Units.h" #include "df/building_axle_horizontalst.h" #include "df/building_bars_floorst.h" @@ -321,26 +322,30 @@ bool Buildings::setOwner(df::building_civzonest *bld, df::unit *unit) { CHECK_NULL_POINTER(bld); - if (bld->assigned_unit == unit) + auto unit_id = unit ? unit->id : -1; + + if (bld->assigned_unit_id == unit_id) return true; - if (bld->assigned_unit) + if (bld->assigned_unit_id != -1) { - auto &blist = bld->assigned_unit->owned_buildings; - vector_erase_at(blist, linear_index(blist, bld)); - - if (auto spouse = df::unit::find(bld->assigned_unit->relationship_ids[df::unit_relationship_type::Spouse])) + if (auto old_unit = df::unit::find(bld->assigned_unit_id)) { - auto &blist = spouse->owned_buildings; + auto& blist = old_unit->owned_buildings; vector_erase_at(blist, linear_index(blist, bld)); + + if (auto spouse = df::unit::find(old_unit->relationship_ids[df::unit_relationship_type::Spouse])) + { + auto& blist = spouse->owned_buildings; + vector_erase_at(blist, linear_index(blist, bld)); + } } } - bld->assigned_unit = unit; + bld->assigned_unit_id = unit_id; if (unit) { - bld->assigned_unit_id = unit->id; unit->owned_buildings.push_back(bld); if (auto spouse = df::unit::find(unit->relationship_ids[df::unit_relationship_type::Spouse])) @@ -350,10 +355,8 @@ bool Buildings::setOwner(df::building_civzonest *bld, df::unit *unit) blist.push_back(bld); } } - else - { - bld->assigned_unit_id = -1; - } + + Units::get_cached_unit_by_global_id(unit_id, bld->owner_unit_cached_index); return true; } diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index e604562c77..726e2343c0 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2188,3 +2188,24 @@ void Units::setGroupActionTimers(color_ostream &out, df::unit *unit, } } } + +// this is a (loose) reimplementation of df's `unit_handlerst::get_cached_unit_by_global_id` +df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) +{ + auto& vector = df::unit::get_vector(); + auto len = vector.size(); + + if (len == 0 || id == -1) + return nullptr; + + if (index > -1 && index < len) + { + auto unit = vector[index]; + if (index == unit->id) + return unit; + index = -1; + } + index = binsearch_index(vector, &df::unit::id, id); + return index != -1 ? vector[index] : nullptr; +} + From 29b9965a146834aca193354efe6f7349395ab806 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:16:51 -0500 Subject: [PATCH 0283/1082] add `Buildings::getOwner` this correctly leverages the `get_cached_unit_by_global_id` mechanic for civzones --- library/include/modules/Buildings.h | 5 +++++ library/modules/Buildings.cpp | 5 +++++ plugins/preserve-tombs.cpp | 5 +++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index c323958e17..1a37cb8304 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -73,6 +73,11 @@ DFHACK_EXPORT bool ReadCustomWorkshopTypes(std::map & bt DFHACK_EXPORT df::general_ref *getGeneralRef(df::building *building, df::general_ref_type type); DFHACK_EXPORT df::specific_ref *getSpecificRef(df::building *building, df::specific_ref_type type); +/** + * Gets the owner unit for the zone. Uses the cached index in the civzone if valid, updates if not + */ +DFHACK_EXPORT df::unit* getOwner(df::building_civzonest* building); + /** * Sets the owner unit for the zone. */ diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 5fe8bdc5e3..b7aeb55996 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -318,6 +318,11 @@ std::string Buildings::getName(df::building* building) return tmp; } +df::unit* Buildings::getOwner(df::building_civzonest* bld) +{ + return Units::get_cached_unit_by_global_id(bld->assigned_unit_id, bld->owner_unit_cached_index); +} + bool Buildings::setOwner(df::building_civzonest *bld, df::unit *unit) { CHECK_NULL_POINTER(bld); diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 17f06c6c92..5a20d65371 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -198,8 +198,9 @@ static void update_tomb_assignments(color_ostream &out) { // check tomb civzones for assigned units for (auto* tomb : world->buildings.other.ZONE_TOMB) { if (!tomb || !tomb->flags.bits.exists) continue; - if (!tomb->assigned_unit) continue; - if (Units::isDead(tomb->assigned_unit)) continue; // we only care about living units + if (tomb->assigned_unit_id == -1) continue; + auto unit = Buildings::getOwner(tomb); + if (Units::isDead(unit)) continue; // we only care about living units auto it = tomb_assignments.find(tomb->assigned_unit_id); From 11e5369bdd44a520614c21e98201edd987090ea5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:22:25 -0500 Subject: [PATCH 0284/1082] fix typo --- library/modules/Units.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 726e2343c0..ad44214501 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2201,7 +2201,7 @@ df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) if (index > -1 && index < len) { auto unit = vector[index]; - if (index == unit->id) + if (id == unit->id) return unit; index = -1; } From 97ae50ab9de6cea3b939664a046e0e9fefe9a727 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:24:55 -0500 Subject: [PATCH 0285/1082] updates to `preserve-rooms` and `preserve-tombs` for 51.11 --- plugins/preserve-rooms.cpp | 12 +++++++----- plugins/preserve-tombs.cpp | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index 52f6151637..a34273f7f2 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -315,13 +315,14 @@ static void assign_nobles(color_ostream &out) { auto zone = virtual_cast(df::building::find(zone_id)); if (!zone) continue; + auto owner = Buildings::getOwner(zone); bool assigned = false; for (auto it = group_codes.rbegin(); it != group_codes.rend(); it++) { auto code = *it; vector units; Units::getUnitsByNobleRole(units, code); // if zone is already assigned to a proper unit (or their spouse), skip - if (linear_index(units, zone->assigned_unit) >= 0 || + if (linear_index(units, owner) >= 0 || linear_index(get_spouse_ids(out, units), zone->assigned_unit_id) >= 0) { assigned = true; @@ -352,7 +353,7 @@ static void assign_nobles(color_ostream &out) { if (assigned) break; } - if (!assigned && (zone->spec_sub_flag.bits.active || zone->assigned_unit)) { + if (!assigned && (zone->spec_sub_flag.bits.active || zone->assigned_unit_id != -1)) { DEBUG(cycle,out).print("noble zone now reserved for eventual office holder: %d\n", zone_id); zone->spec_sub_flag.bits.active = false; Buildings::setOwner(zone, NULL); @@ -506,11 +507,12 @@ static void process_rooms(color_ostream &out, WARN(cycle, out).print("invalid building pointer %p in building vector\n", zone); continue; } - if (!zone->assigned_unit) { + if (zone->assigned_unit_id == -1) { handle_missing_assignments(out, active_unit_ids, &it, it_end, share_with_spouse, zone->id); continue; } - auto hf = df::historical_figure::find(zone->assigned_unit->hist_figure_id); + auto owner = Buildings::getOwner(zone); + auto hf = df::historical_figure::find(owner->hist_figure_id); if (!hf) continue; int32_t spouse_hfid = share_with_spouse ? get_spouse_hfid(out, hf) : -1; @@ -593,7 +595,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { if (!zone) continue; zone->spec_sub_flag.bits.active = true; - if (zone->assigned_unit || spouse_has_sharable_room(out, hfid, zone->type)) + if (zone->assigned_unit_id != -1 || spouse_has_sharable_room(out, hfid, zone->type)) continue; DEBUG(event,out).print("reassigning zone %d\n", zone->id); Buildings::setOwner(zone, unit); diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 5a20d65371..b7f7be28b6 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -249,7 +249,7 @@ static bool assign_to_tomb(df::unit * unit, int32_t building_id) { if (!bld) return false; auto tomb = virtual_cast(bld); - if (!tomb || tomb->assigned_unit) return false; + if (!tomb || tomb->assigned_unit_id != -1) return false; Buildings::setOwner(tomb, unit); return true; From ea882fe39204a9a12c5b1f776eea8c028acf36d8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:29:33 -0500 Subject: [PATCH 0286/1082] add changelog --- docs/changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index fb0c1175a9..2f4e1a15ee 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -72,7 +72,10 @@ Template for new versions: ## API - ``Military`` module: added ``addToSquad`` function +- ``Units`` module: added ``get_cached_unit_by_global_id`` to emulate how DF handles unit vector index caching (used in civzones and in general references) +- ``Buildings`` module: add `getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 - ``Units::teleport``: projectile information is now cleared for teleported units +- ``Buildings::setOwner``: updated for changes in 51.11 ## Lua - ``dfhack.military.addToSquad``: expose Military API function From c285e49c2d536a38ee0efa63ed224d4624c598d5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:32:17 -0500 Subject: [PATCH 0287/1082] trim eof --- library/modules/Units.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index ad44214501..06018f7577 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2208,4 +2208,3 @@ df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) index = binsearch_index(vector, &df::unit::id, id); return index != -1 ? vector[index] : nullptr; } - From 5d4b014379300edaa1ef66bc527b9ec980dda14c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:34:06 -0500 Subject: [PATCH 0288/1082] fix markup in changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2f4e1a15ee..fe6184aa7b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -73,7 +73,7 @@ Template for new versions: ## API - ``Military`` module: added ``addToSquad`` function - ``Units`` module: added ``get_cached_unit_by_global_id`` to emulate how DF handles unit vector index caching (used in civzones and in general references) -- ``Buildings`` module: add `getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 +- ``Buildings`` module: add ``getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 - ``Units::teleport``: projectile information is now cleared for teleported units - ``Buildings::setOwner``: updated for changes in 51.11 From c8011f47ab4c612e3947bb927aa4c3cc9b612e00 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:40:59 -0500 Subject: [PATCH 0289/1082] minor tweek --- library/modules/Units.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 06018f7577..e049bf53ac 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2203,7 +2203,6 @@ df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) auto unit = vector[index]; if (id == unit->id) return unit; - index = -1; } index = binsearch_index(vector, &df::unit::id, id); return index != -1 ? vector[index] : nullptr; From d6d21519a40a05989b94692a75246ff1a4744682 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:24:02 -0700 Subject: [PATCH 0290/1082] fix signed/unsigned comparison --- library/modules/Units.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index e049bf53ac..e68318fa1b 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2198,7 +2198,7 @@ df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) if (len == 0 || id == -1) return nullptr; - if (index > -1 && index < len) + if (index > -1 && (size_t)index < len) { auto unit = vector[index]; if (id == unit->id) From 980b95c40135bc181c96be97bfca33e92461e0ce Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:24:12 -0700 Subject: [PATCH 0291/1082] update xml ref --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 8f4b816a52..e86f7763ef 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 8f4b816a52f94c95772100cd4652a5a43697059e +Subproject commit e86f7763ef81c3265b4842a620ef9345ee75c2c7 From 315252026f885387b78c7202e30b229c1f58b423 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:29:14 -0700 Subject: [PATCH 0292/1082] expose getOwner to Lua --- docs/dev/Lua API.rst | 4 ++++ library/LuaApi.cpp | 1 + 2 files changed, 5 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index b11c8862b2..c1c2f78e76 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2465,6 +2465,10 @@ General Searches for a specific_ref with the given type. +* ``dfhack.buildings.getOwner(civzone)`` + + Returns the owner of the zone or *nil* if there isn't one. + * ``dfhack.buildings.setOwner(civzone,unit)`` Replaces the owner of the civzone. If unit is *nil*, removes ownership. diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index f169650f90..c80fecffc2 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2736,6 +2736,7 @@ static bool buildings_containsTile(df::building *bld, int x, int y) { static const LuaWrapper::FunctionReg dfhack_buildings_module[] = { WRAPM(Buildings, getGeneralRef), WRAPM(Buildings, getSpecificRef), + WRAPM(Buildings, getOwner), WRAPM(Buildings, setOwner), WRAPM(Buildings, allocInstance), WRAPM(Buildings, checkFreeTiles), From 8253a9c25d82238d2753f5726cb9ab2cb7dc5297 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:39:39 -0700 Subject: [PATCH 0293/1082] use new assigned_unit API for 51.11 --- plugins/lua/sort/places.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/lua/sort/places.lua b/plugins/lua/sort/places.lua index 35a5569493..d58f306584 100644 --- a/plugins/lua/sort/places.lua +++ b/plugins/lua/sort/places.lua @@ -79,8 +79,9 @@ local function get_zone_search_key(zone) end -- allow zones w/ assignments to be searchable by their assigned unit - if zone.assigned_unit ~= nil then - table.insert(result, sortoverlay.get_unit_search_key(zone.assigned_unit)) + local owner = dfhack.buildings.getOwner(zone) + if owner then + table.insert(result, sortoverlay.get_unit_search_key(owner)) end -- allow zones to be searchable by type From b4f37ccd6f5328e243df013e3efd6410c5570c23 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:41:55 -0700 Subject: [PATCH 0294/1082] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index fb0c1175a9..e54cb327b9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ Template for new versions: - `unload`: fix recent regression where `unload` would immediately `reload` the target - ``Buildings`` module: do not crash if a ``map_block`` unexpectedly contains an item that is not on the master item vector - `suspendmanager`: fix walls being treated as potential suitable access if another wall is built underneath +- text widgets no longer lose their cursor when the Ctrl-a (select all) hotkey is pressed when there is no text to select ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) From 279f54222df2d22569eb9dfa81199c088aa8157d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:45:17 +0000 Subject: [PATCH 0295/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 3220332217..6cf7d0adb4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 322033221745e9cbf537d59152138df385c505b7 +Subproject commit 6cf7d0adb49f508811edf940898c53b56d5d9bb3 From e2ac17f408dd715a581c9ba008111cf197028726 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:48:48 -0700 Subject: [PATCH 0296/1082] bump to 51.11 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++---- scripts | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 017edd4460..df9e4ea767 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.10") +set(DF_VERSION "51.11") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index 52c2f194a8..23fc4e3028 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,20 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 51.11-r1 + ## Fixes - `spectate`: don't show a hover tooltip for hidden units (e.g. invisible snatchers) - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings @@ -69,8 +83,6 @@ Template for new versions: ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) -## Documentation - ## API - ``Military`` module: added ``addToSquad`` function - ``Units`` module: added ``get_cached_unit_by_global_id`` to emulate how DF handles unit vector index caching (used in civzones and in general references) @@ -81,8 +93,6 @@ Template for new versions: ## Lua - ``dfhack.military.addToSquad``: expose Military API function -## Removed - # 51.10-r1 ## Misc Improvements diff --git a/scripts b/scripts index 6cf7d0adb4..b5178d671f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6cf7d0adb49f508811edf940898c53b56d5d9bb3 +Subproject commit b5178d671f9e6142c7c3fccadf025e7eb07d9b04 From c806420fcb4be0449c3cba214019b3c8726b0346 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:06:51 +0000 Subject: [PATCH 0297/1082] Auto-update structures ref for 51.11 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index e86f7763ef..6502bdefd1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e86f7763ef81c3265b4842a620ef9345ee75c2c7 +Subproject commit 6502bdefd1796315365a7bb58e30e0ce34359bea From adf6217bbce04774840ad2cc4a15c6521d0d4845 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 10:26:04 -0700 Subject: [PATCH 0298/1082] add missing changelog entry --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 23fc4e3028..76e9510890 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -92,6 +92,7 @@ Template for new versions: ## Lua - ``dfhack.military.addToSquad``: expose Military API function +- ``dfhack.buildings.getOwner``: make new Buildings API available to Lua # 51.10-r1 From 503d2f2595b032675dac44780e72f5ea8e4514b3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 14:17:14 -0500 Subject: [PATCH 0299/1082] `preserve-rooms`: validate assigned unit id before using --- docs/changelog.txt | 1 + plugins/preserve-rooms.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 76e9510890..510050aee6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist ## Misc Improvements diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index a34273f7f2..805e10c837 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -512,6 +512,10 @@ static void process_rooms(color_ostream &out, continue; } auto owner = Buildings::getOwner(zone); + if (!owner) { + WARN(cycle, out).print("building %d has owner id %d but no such unit exists\n", zone->id, zone->assigned_unit_id); + continue; + } auto hf = df::historical_figure::find(owner->hist_figure_id); if (!hf) continue; From 453d0088c685ce955c5d76b1f82f6135fe9ac915 Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 22 Apr 2025 18:27:42 -0700 Subject: [PATCH 0300/1082] Revert "Add star icon to gui/design" --- data/art/design.png | Bin 1732 -> 1491 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/art/design.png b/data/art/design.png index 38fed7e0885f20a9ee9e1b23cd6c90dde2c7cad0..787b9962a1bc68d6d5d46bfa9c9d402a4f8cc4a1 100644 GIT binary patch literal 1491 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 avi|_7^f7$z*D;v@0000 Date: Tue, 22 Apr 2025 18:28:25 -0700 Subject: [PATCH 0301/1082] Revert "Revert "Add star icon to gui/design"" --- data/art/design.png | Bin 1491 -> 1732 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/art/design.png b/data/art/design.png index 787b9962a1bc68d6d5d46bfa9c9d402a4f8cc4a1..38fed7e0885f20a9ee9e1b23cd6c90dde2c7cad0 100644 GIT binary patch literal 1732 zcmV;#20QtQP)11hLtOkivgpOG#z3|AH36vXymOkrWREQ}_e4FolgED6GZC5EL|tMAw9W z-`;uuqticsRsp?zbn@*FUqyw$2R;LT;nAIQ%U51gL3hr*`NrFk_rM1}1HXy>eEG{y z(h4><6MqFh@U!r*6torRKfe1+J_7!IaqgUZ_WMy-2z=l>=T--+T>K`gL&YNRDfnI2 zcOL90;;Z}LI^1=VQm*d5k6s~s#rUrdR*9bN;y2aPw4qTo)YG}hAp)gD?lVt0c+F=Z z7m9ZAEMDGht!|!p;3BzVms6@p=uH$o`A9a48%0FxE9hu^!w0gO)cx#ee=vqYId;lJ z$41MIb2SBJk9bAhZOd&2x6omAcJ4gbNv~zKNFRD=5NbxB)=^M;&-uV*4el@yCDxz@ z+F6>KLmn0q(TJ4vS{es4T#u@Ba3OZ8h$tOl=*g&ci|KUgsFqA^x}Os}OE1`>k&s^3 zK|!nK@(g-evQtGwLl6pI(+@ZEW>29kp1sqgo1B$HBhJ5+%pvh9%GkqoJ0tBB%BR`x zk5)zDJl;$9uxW`(=c%9uS^RZpXCjZLpWkLlX*cW?Oem;G8w|1~b#|6G@XM`~?!%%= z2WPe1ewv#?E^n}9kyS;l;6p*D$j;KVusI`))&{GCmH%<%Q>8j)qN8=(A2OBr*`|7!D2L9o$>k;YQ3`2<4FSToWSz)Y%i^N|rQKy*ws0o*K}+m| z8jsA;>g4l4F9LBEZ7QyaDI;luiIL4ds6oRGZZM@=K_Ni>eb6B<usjnM<;ia-zX*8Uw>cpmIEL74E)BUx4+!o*zEnz zp3r~~e363sH;rha|90|g_)p*ipND@7?+1llk2NR^27<( z#8>yfb-3#!rCi;AAH72Oit)ey@HEl0UHtkNmVIzH@7xQ^w|6hkpI?kNW)wL@pp?*& zBZnE~SWJJpS5wd-(hQj= z6Qd7?YH7=P$WaID>gdq=Qjwj9d&{=r-4BL*l9MU)kR9sWIW}v4)>(s|KfhosuqYw+ zfy)})Z6LaROevA{u?jnfA`=Ryiq1tIww+i=L?cqtXZdPL5}nYw>`#1J$HCDgeou#>%y zW5!M`5k1^nM*Fg9E~X!z=NpzwXYuTvCdqhKTpA|S-LFKW5RDS9m!s)n8FtgqGqe3) zKFVeB2K%7w<&a9E-HNDi$_f+q-js-v21zKT-2*&| zB+vQ6a(9xVlu&|hz)syACB`%zwt^9*bft73DiAa`E4QEKrjX0?PMfoaEhIep>A|2B@zPVmQN)3RoF>A-mn~g55NCAd-?Y1k84ce1D}U~rJ(*zBPF~CKJeN2ct<_#nyf)#Fc1_513_Uk avi|_7^f7$z*D;v@0000D!@9mqN zKf8O&OmSQ$cfXyT{q4M&+4pW?Wo2cgqJotLC6I~|NJR;xGVcVsa^vpyj}I)Mi_@)_ zZ=X3z#V->7kU$o_`kOw6MX%m8=b-qd;v47R2Or;42UuG6{8#Y{!yh+^`Tf;HdVKq{ zziVH=e493vieDw8 z|4M`+HxVEjy3KHXknvo|yxum9W^pY8FOB3F?^qS8hYVrg^nJ_m9(||b#AFh9`eOc| z9|ytP-s#f>4I#%Ky6ezrp?)495MswL#;r=v6`~o%!*wq9*w6cODK1hrEyTInqzylV zE~qCKv^kpOlgNQ0#2#tt^jR1hgvKM4i1`r9g+_=yIM{K{;m7${ZkMx?*n>_;Gy=t| z#c&%SCVSY*_+nO9s0u?@^1R8r99o~j?QdxW%CQe5+6CaHX_*lyLKQae?D14v@bejs zKuC7R>>{$Whxfo>oGLuPGlTeHJcn8=Ldh94MBgkl0%cvv3+)-kGln!d5WK&PTZsr~ zVWRAb*6{-Nojsp1$)^E1^eLtmqny_Y?O`GTl2u{O1pKxl4kJfV?gx_S14Z6BgTwiq zW8!nabD5Lu!kqg+5hjj4fzPWC)JG$_2O+I`^eFHqO3o!D-6~VeOp=?*MV&cv#gQ_8 zak_QF`$Py(e*W6))GL0G_@+j$-q4Pk#CMsiyw&&4b;)dmb7A zk(&q*4IJ4q7@-d`KFE7?UjfAc;{il)%ix*3)e~vdMuupB?0J0e7)1{m!cM>MTaNeO z;PCx`ClH;{p+>joZ++LKQILb08=#_KJGN1xohW+-{m)1pXCXrUJa7qvW7Ug|@KN6= zl^%!eEC6pD>dS8!*<**OCf@#HbG?bwaV*5SnkNUc+ZSSfVwTPiXDA}{|aKbs4n;=>vG&!)xGmsE_a2a$O0>Q(NGcok#OgfE= z#vYc-mdCRak^R5c+aYm%0E1I;2bjNmpSp*~6GiIYHOoKxwyWIzM0XFO&&YVV0c{*uz_~ zQxY9J3y+t^slo$1+m9c{bEw5|8F$duf>&FfS&TD@4ZKxb8I`=y#VaTX4e~fm4pe7q zgMCk2_>r!{q}d}Hew=D+&`AG8j6ctkYrxc}AAO5Qyiq}}3Zv9wl=E7lJxt`WNp Date: Tue, 22 Apr 2025 18:31:28 -0700 Subject: [PATCH 0302/1082] bump version to 51.11-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index df9e4ea767..fdf638bb29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.11") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 510050aee6..94223c2326 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,7 +56,6 @@ Template for new versions: ## New Features ## Fixes -- `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist ## Misc Improvements @@ -68,6 +67,12 @@ Template for new versions: ## Removed +# 51.11-r1.1 + +## Fixes +- `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist +- `gui/design`: fix misaligned shape icons + # 51.11-r1 ## Fixes From 2310f0d05718dd88a5b83715b29cc46c7280b41d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 22 Apr 2025 19:28:16 -0700 Subject: [PATCH 0303/1082] remove out of date reference to "k" mode --- docs/plugins/changeitem.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/plugins/changeitem.rst b/docs/plugins/changeitem.rst index f452f6a742..cf3fb83d94 100644 --- a/docs/plugins/changeitem.rst +++ b/docs/plugins/changeitem.rst @@ -18,9 +18,10 @@ Usage Show details about the selected item. Does not change the item. You can use this command to discover RAW ids for existing items. ``changeitem []`` - Change the item selected in the ``k`` list or inside a container/inventory. + Change the item that is selected in the UI. ``changeitem here []`` - Change all items at the cursor position. Requires in-game cursor. + Change all items at the keyboard cursor position. Requires the vanilla + keyboard cursor to be enabled. Examples -------- From 9cc6851cc157c5931b9e449673869ee61c8105f2 Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Wed, 23 Apr 2025 02:52:34 -0400 Subject: [PATCH 0304/1082] Fix for mods not being properly reloaded on Windows --- docs/about/Authors.rst | 1 + docs/changelog.txt | 1 + library/lua/script-manager.lua | 3 +++ 3 files changed, 5 insertions(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index d806d92aaa..e8030e881a 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -63,6 +63,7 @@ DoctorVanGogh DoctorVanGogh Donald Ruegsegger hashaash doomchild doomchild Droseran Droseran +dvantwisk dvantwisk DwarvenM DwarvenM Eamon Bode eamondo2 Baron Von Munchhausen EarthPulseAcademy EarthPulseAcademy diff --git a/docs/changelog.txt b/docs/changelog.txt index 94223c2326..30e64c2b99 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -72,6 +72,7 @@ Template for new versions: ## Fixes - `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist - `gui/design`: fix misaligned shape icons +- `scripts/script-manager`: fix lua scripts in mods not being reloaded properly on Windows. # 51.11-r1 diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index c2647fbe2d..6e9da549c5 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -52,6 +52,9 @@ function reload(refresh_active_mod_scripts) local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name) if script_path:find('scripts_modactive') then local full_path = script_path..'/'..script_name + if dfhack.getOSType() == 'windows' then + full_path = script_path..'\\'..script_name + end internal_script = dfhack.internal.scripts[full_path] if internal_script then dfhack.internal.scripts[full_path] = nil From 8b0a4825bc22753af3b296abca27741dd4477bec Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 23 Apr 2025 08:55:26 -0500 Subject: [PATCH 0305/1082] don't crash on a tomb owned by a unit that doesn't exist --- docs/changelog.txt | 1 + plugins/preserve-tombs.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 94223c2326..df96baaa1e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `preserve-tombs`: will no longer crash when a tomb is assigned to a unit that does not exist ## Misc Improvements diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index b7f7be28b6..40b9eb56fd 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -200,7 +200,7 @@ static void update_tomb_assignments(color_ostream &out) { if (!tomb || !tomb->flags.bits.exists) continue; if (tomb->assigned_unit_id == -1) continue; auto unit = Buildings::getOwner(tomb); - if (Units::isDead(unit)) continue; // we only care about living units + if (!unit || Units::isDead(unit)) continue; // we only care about living units auto it = tomb_assignments.find(tomb->assigned_unit_id); From 3d0f2969e3cee738646e3a2038558fee679367c1 Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Wed, 23 Apr 2025 13:28:48 -0400 Subject: [PATCH 0306/1082] Changelog entry modifications --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index dac194aa2f..6f0cd82c8b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -73,7 +73,7 @@ Template for new versions: ## Fixes - `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist - `gui/design`: fix misaligned shape icons -- `scripts/script-manager`: fix lua scripts in mods not being reloaded properly on Windows. +- `script-manager`: fix lua scripts in mods not being reloaded properly on Windows. # 51.11-r1 From 6816b676927ab300e7fdf99aa02157b0b235442e Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Wed, 23 Apr 2025 13:33:55 -0400 Subject: [PATCH 0307/1082] Modified changelog log --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6f0cd82c8b..12a8fcbe61 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -73,7 +73,7 @@ Template for new versions: ## Fixes - `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist - `gui/design`: fix misaligned shape icons -- `script-manager`: fix lua scripts in mods not being reloaded properly on Windows. +- `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows. # 51.11-r1 From bbb5ec4d356a0b918df0b8fa6afdeeaffa1c6d84 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 23 Apr 2025 17:44:40 -0700 Subject: [PATCH 0308/1082] bump version to 51.11-r1.2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fdf638bb29..d2d1c8ec9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.11") -set(DFHACK_RELEASE "r1.1") +set(DFHACK_RELEASE "r1.2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index df96baaa1e..35018ebce5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,7 +56,6 @@ Template for new versions: ## New Features ## Fixes -- `preserve-tombs`: will no longer crash when a tomb is assigned to a unit that does not exist ## Misc Improvements @@ -68,6 +67,11 @@ Template for new versions: ## Removed +# 51.11-r1.2 + +## Fixes +- `preserve-tombs`: will no longer crash when a tomb is assigned to a unit that does not exist + # 51.11-r1.1 ## Fixes From a2cda9bb3b468718ae88fdc55074864987f5a73c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Apr 2025 20:17:52 -0500 Subject: [PATCH 0309/1082] unset "fail randomly' flag on `getplants` specifically when handling plants that have growths that do not drop seeds when processed --- docs/changelog.txt | 1 + plugins/getplants.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 35018ebce5..9a37f81c96 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed ## Misc Improvements diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 9bd3f6f0d4..ab8333e51a 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -154,9 +154,10 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo bool seedSource = plant->growths[i]->item_type == df::item_type::SEEDS; if (plant->growths[i]->item_type == df::item_type::PLANT_GROWTH) { - for (size_t k = 0; growth_mat.material->reaction_product.material.mat_type.size(); k++) { - if (growth_mat.material->reaction_product.material.mat_type[k] == plant->material_defs.type[plant_material_def::seed] && - growth_mat.material->reaction_product.material.mat_index[k] == plant->material_defs.idx[plant_material_def::seed]) { + auto& mat = growth_mat.material->reaction_product.material; + for (size_t k = 0; k < mat.mat_type.size(); k++) { + if (mat.mat_type[k] == plant->material_defs.type[plant_material_def::seed] && + mat.mat_index[k] == plant->material_defs.idx[plant_material_def::seed]) { seedSource = true; break; } From c0b1bf886b7f62040ffe8a4acca902375d82b746 Mon Sep 17 00:00:00 2001 From: Quietust Date: Fri, 25 Apr 2025 17:09:52 -0600 Subject: [PATCH 0310/1082] Fix getplants "ripe" and "picked" logic Added Splitmix64 RNG to Random module; not yet exposed from Lua, but that can be added later if deemed necessary. --- docs/changelog.txt | 3 +++ library/include/modules/Random.h | 31 ++++++++++++++++++++++ plugins/getplants.cpp | 44 ++++++++++++++++---------------- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9a37f81c96..9d035c598c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,12 +57,15 @@ Template for new versions: ## Fixes - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed +- `getplants`: use updated formula for calculating whether plant growths are ripe +- `getplants`: fix logic for determining whether plant growths have been picked ## Misc Improvements ## Documentation ## API +- ``Random`` module: added SplitmixRNG class, implements the Splitmix64 RNG used by Dwarf Fortress for "simple" randomness ## Lua diff --git a/library/include/modules/Random.h b/library/include/modules/Random.h index 38fbdd0f89..8ed703be79 100644 --- a/library/include/modules/Random.h +++ b/library/include/modules/Random.h @@ -106,6 +106,37 @@ namespace Random extern template void DFHACK_IMPORT MersenneRNG::unitvector(double *p, int size); #endif + // Standard Splitmix64 RNG, as used by Dwarf Fortress's "hash_rngst" class + class SplitmixRNG + { + uint64_t state; + + public: + SplitmixRNG(uint64_t seed) { + init(seed); + } + + void init(uint64_t seed) { + state = seed; + } + + uint64_t next() { + state += 0x9e3779b97f4a7c15; + uint64_t z = state; + z ^= z >> 30; + z *= 0xbf58476d1ce4e5b9; + z ^= z >> 27; + z *= 0x94d049bb133111eb; + z ^= z >> 31; + return z; + } + + int32_t df_trandom(uint32_t max) { + uint32_t val = next() >> 32; + return (int32_t)(val % max); + } + }; + /* * Classical Perlin noise function in template form. * http://mrl.nyu.edu/~perlin/doc/oscar.html#noise diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index ab8333e51a..29c6655fc9 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -5,6 +5,7 @@ #include "modules/Designations.h" #include "modules/Maps.h" #include "modules/Materials.h" +#include "modules/Random.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -211,32 +212,31 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo } // Formula for determination of the variance in plant growth maturation time, determined via disassembly. -// The x and y parameters are in tiles relative to the embark. -bool ripe(int32_t x, int32_t y, int32_t start, int32_t end) { - int32_t time = (((435522653 - (((y + 3) * x + 5) * ((y + 7) * y * 400181475 + 289700012))) & 0x3FFFFFFF) % 2000 + *cur_year_tick) % 403200; +// The coordinates are relative to the embark region. +bool ripe(int32_t x, int32_t y, int32_t z, int32_t start, int32_t end) { + DFHack::Random::SplitmixRNG rng((world->map.region_x * 48 + x) + (world->map.region_y * 48 + y) * 10000 + (world->map.region_z + z) * 100000000); + int32_t time = (rng.df_trandom(2000) + *cur_year_tick) % 403200; return time >= start && (end == -1 || time <= end); } -// Looks in the picked growths vector to see if a matching growth has been marked as picked. -bool picked(const df::plant* plant, int32_t growth_subtype) { - df::world_data* world_data = world->world_data; - df::world_site* site = df::world_site::find(plotinfo->site_id); - int32_t pos_x = site->global_min_x + plant->pos.x / 48; - int32_t pos_y = site->global_min_y + plant->pos.y / 48; - size_t id = pos_x + pos_y * 16 * world_data->world_width; - df::world_object_data* object_data = df::world_object_data::find(id); - if (!object_data) { +// Looks in the local creation zone's picked growths vector to see if a matching growth has been marked as picked. +bool picked(const df::plant* plant, int32_t growth_subtype, int32_t growth_density) { + int32_t pos_x = plant->pos.x / 48 + world->map.region_x; + int32_t pos_y = plant->pos.y / 48 + world->map.region_y; + size_t cz_id = pos_x + pos_y * 16 * world->world_data->world_width; + auto cz = df::world_object_data::find(cz_id); + if (!cz) { return false; } - df::map_block_column* column = world->map.map_block_columns[(plant->pos.x / 16) * world->map.x_count_block + (plant->pos.y / 16)]; - - for (size_t i = 0; i < object_data->picked_growths.x.size(); i++) { - if (object_data->picked_growths.x[i] == plant->pos.x && - object_data->picked_growths.y[i] == plant->pos.y && - object_data->picked_growths.z[i] - column->z_base == plant->pos.z && - object_data->picked_growths.subtype[i] == growth_subtype && - object_data->picked_growths.year[i] == *cur_year) { + + for (size_t i = 0; i < cz->picked_growths.x.size(); i++) { + if (cz->picked_growths.x[i] == (plant->pos.x % 48) && + cz->picked_growths.y[i] == (plant->pos.y % 48) && + cz->picked_growths.z[i] == (plant->pos.z + world->map.region_z) && + cz->picked_growths.density[i] >= growth_density && + cz->picked_growths.subtype[i] == growth_subtype && + cz->picked_growths.year[i] == *cur_year) { return true; } } @@ -310,8 +310,8 @@ bool designate(color_ostream& out, const df::plant* plant, bool farming) { } if ((!farming || seedSource) && - ripe(plant->pos.x, plant->pos.y, plant_raw->growths[i]->timing_1, plant_raw->growths[i]->timing_2) && - !picked(plant, i)) + ripe(plant->pos.x, plant->pos.y, plant->pos.z, plant_raw->growths[i]->timing_1, plant_raw->growths[i]->timing_2) && + !picked(plant, i, plant_raw->growths[i]->density)) return Designations::markPlant(plant); } From 170b5443d656a55a849115505502748640b5f9c2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 25 Apr 2025 18:41:39 -0700 Subject: [PATCH 0311/1082] add more mod utility functions --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 23 ++++++++++ library/lua/script-manager.lua | 82 ++++++++++++++++++++++++++++------ 3 files changed, 93 insertions(+), 13 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 35018ebce5..545ca09f5a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ Template for new versions: ## API ## Lua +- ``script-manager``: new ``get_active_mods()`` function for getting information on active mods ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index c1c2f78e76..aaf5731b54 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3783,6 +3783,29 @@ paths will be relative to the top level game directory and will end in a slash Which would open ``dfhack-config/mods/my_awesome_mod/settings.json``. After calling ``getModStatePath``, the returned directory is guaranteed to exist. +* ``get_active_mods()`` + + Returns a list of all active mods in the current world. The list elements are + tables containing the following fields: + + - id: mod id + - name: mod display name + - version: mod display version + - numeric_version: numeric mod version + - path: path to the mod directory + - vanilla: true if this is a vanilla mod + +* ``get_mod_info_metadata(mod_path, tags)`` + + Returns a table with the values of the given tags from the ``info.txt`` file + in the given mod directory. The ``mod_path`` argument must be a path to a mod + directory (retrieved, say, from ``get_active_mods()``). The ``tags`` argument + is a string or a list of strings representing the tags to retrieve. The + function will return a table with the tag names as keys and their values as + values. If a requested tag includes the string ``NUMERIC_``, it will return + the numeric value for that tag (e.g., ``NUMERIC_VERSION`` will return the + numeric version of the mod as a number instead of a string). + utils ===== diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index c2647fbe2d..9a1ecb1df0 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -88,25 +88,52 @@ local INSTALLED_MODS_PATH = 'data/installed_mods/' -- changes to the files) local MOD_PATH_ROOTS = {WORKSHOP_MODS_PATH, MODS_PATH, INSTALLED_MODS_PATH} -local function get_mod_id_and_version(path) - local idfile = path .. '/info.txt' +-- returns the values of the given list of tags from the info.txt file in the given mod directory +-- if a requested tag includes the string 'NUMERIC_', it will return the numeric value for that tag +-- (e.g. NUMERIC_VERSION will return the numeric version of the mod as a number instead of a string). +function get_mod_info_metadata(mod_path, tags) + local idfile = mod_path .. '/info.txt' local ok, lines = pcall(io.lines, idfile) if not ok then return end - local id, version + + if type(tags) ~= 'table' then + tags = {tags} + end + + local tag_regexes = {} + for _,tag in ipairs(tags) do + local tag_regex = ('^%%[%s:'):format(tag) + if tag:find('NUMERIC_') then + -- note this doesn't go all the way to the closing brace since some people put + -- non-number characters in here, and DF only reads the leading digits for + -- numeric fields + tag_regex = tag_regex .. '(%d+)' + else + tag_regex = tag_regex .. '([^%]]+)' + end + tag_regexes[tag] = tag_regex + end + + local values = {} for line in lines do - if not id then - _,_,id = line:find('^%[ID:([^%]]+)%]') + local _,_,info_tag = line:find('^%[([^:]+):') + if not info_tag or not tag_regexes[info_tag] then + goto continue end - if not version then - -- note this doesn't include the closing brace since some people put - -- non-number characters in here, and DF only reads the leading digits - -- as the numeric version - _,_,version = line:find('^%[NUMERIC_VERSION:(%d+)') + local _,_,value = line:find(tag_regexes[info_tag]) + if value then + values[info_tag] = value end - -- note that we do *not* want to break out of this loop early since - -- lines has to hit EOF to close the file + ::continue:: end - return id, version + + return values +end + +local function get_mod_id_and_version(path) + local values = get_mod_info_metadata(path, {'ID', 'NUMERIC_VERSION'}) + if not values then return end + return values.ID, values.NUMERIC_VERSION end local function add_mod_paths(mod_paths, id, base_path, subdir) @@ -172,6 +199,35 @@ function get_mod_paths(installed_subdir, active_subdir) return mod_paths end +-- returns a list of tables in load order with the following fields: +-- id: mod id +-- name: mod display name +-- version: mod display version +-- numeric_version: numeric mod version +-- path: path to the mod directory +-- vanilla: true if this is a vanilla mod +function get_active_mods() + if not dfhack.isWorldLoaded() then return {} end + + local mods = {} + + local ol = df.global.world.object_loader + + for idx=0,#ol.object_load_order_id-1 do + local path = ol.object_load_order_src_dir[idx].value + table.insert(mods, { + id=ol.object_load_order_id[idx].value, + name=ol.object_load_order_name[idx].value, + version=ol.object_load_order_displayed_version[idx].value, + numeric_version=ol.object_load_order_numeric_version[idx], + path=path, + vanilla=path:startswith('data/vanilla/'), -- windows also uses forward slashes + }) + end + + return mods +end + function get_mod_script_paths() local paths = {} for _,v in ipairs(get_mod_paths('scripts_modinstalled', 'scripts_modactive')) do From 02e2e4387007ae6dba98314c982035783ec87488 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 25 Apr 2025 18:51:05 -0700 Subject: [PATCH 0312/1082] add missing changelog for new script-manager function --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 508f3ccfad..bf4f38e5db 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## Lua - ``script-manager``: new ``get_active_mods()`` function for getting information on active mods +- ``script-manager``: new ``get_mod_info_metadata()`` function for getting information out of mod ``info.txt`` files ## Removed From 6a90b6976be41ded4e16cbcae5dd594cf8bfe285 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 25 Apr 2025 18:52:34 -0700 Subject: [PATCH 0313/1082] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index b5178d671f..3ea6b8811c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b5178d671f9e6142c7c3fccadf025e7eb07d9b04 +Subproject commit 3ea6b8811c13e6084ca92230bd5b789d428dfb88 From c0ed5302e89497ea76c897746e211c8a04defce0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 26 Apr 2025 11:27:04 -0700 Subject: [PATCH 0314/1082] add translated english name to getReadableName output --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 10 +++++----- library/modules/Units.cpp | 13 +++++++++++-- plugins/lua/sort/sortoverlay.lua | 5 +---- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf4f38e5db..187467ac26 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed ## Misc Improvements +- All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. ## Documentation diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index aaf5731b54..2c7f9cacbc 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1775,11 +1775,11 @@ Units module * ``dfhack.units.getReadableName(unit or historical_figure)`` - Returns a string that includes the language name of the unit (if any), the - race of the unit (if different from fort), whether it is trained for war or - hunting, any syndrome-given descriptions (such as "necromancer"), the - training level (if tame), and profession or noble role. If a - ``historical_figure`` is passed instead of a unit, some information + Returns a string that includes the native and english language name of the + unit (if any), the race of the unit (if different from fort), whether it is + trained for war or hunting, any syndrome-given descriptions (such as + "necromancer"), the training level (if tame), and profession or noble role. + If a ``historical_figure`` is passed instead of a unit, some information (e.g., agitation status) is not available, and the profession may be different (e.g., "Monk") from what is displayed in fort mode. diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index e68318fa1b..4ccce42277 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1190,8 +1190,17 @@ string Units::getReadableName(df::unit *unit) { if (isTame(unit)) prof_name += " (" + getTameTag(unit) + ")"; - string name = Translation::translateName(getVisibleName(unit)); - return name.empty() ? prof_name : name + ", " + prof_name; + auto visible_name = getVisibleName(unit); + string native_name = Translation::translateName(visible_name); + + if (native_name.empty()) + return prof_name; + + string english_name = Translation::translateName(visible_name, true, true); + if (english_name.empty()) + return native_name + ", " + prof_name; + + return native_name + " \"" + english_name + "\", " + prof_name; } double Units::getAge(df::unit *unit, bool true_age) { diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua index c823c4c34e..53a591251d 100644 --- a/plugins/lua/sort/sortoverlay.lua +++ b/plugins/lua/sort/sortoverlay.lua @@ -4,10 +4,7 @@ local overlay = require('plugins.overlay') local utils = require('utils') function get_unit_search_key(unit) - return ('%s %s %s'):format( - dfhack.units.getReadableName(unit), - dfhack.units.getProfessionName(unit), - dfhack.translation.translateName(unit.name, true, true)) -- get English last name + return dfhack.units.getReadableName(unit) end local function copy_to_lua_table(vec) From 58e29a5e6bf5a242fd9cd56aee6e76e2e3d97329 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 26 Apr 2025 18:37:34 +0000 Subject: [PATCH 0315/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 3ea6b8811c..ed4a014400 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3ea6b8811c13e6084ca92230bd5b789d428dfb88 +Subproject commit ed4a0144008979492f92175de66a322ed8fb0629 From 38699891ed0cee43686a8c8f49b1ab78c5f6bf67 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 26 Apr 2025 12:21:28 -0700 Subject: [PATCH 0316/1082] change order of operations to avoid a use after free --- docs/changelog.txt | 1 + library/modules/Items.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 187467ac26..e1ddf8d47c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## Fixes - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed +- `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index ff92b3cece..36d1748c5b 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -816,9 +816,12 @@ static bool detachItem(df::item *item) } } - if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) - return linked_list_remove(&world->projectiles.all, ref->projectile_id) - && removeRef(item->general_refs, general_ref_type::PROJECTILE, ref->getID()); + if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) { + auto proj_id = ref->projectile_id; + bool isRefRemoved = removeRef(item->general_refs, general_ref_type::PROJECTILE, proj_id); + bool isLinkRemoved = linked_list_remove(&world->projectiles.all, proj_id); + return isRefRemoved && isLinkRemoved; + } if (item->flags.bits.on_ground) { if (!removeItemOnGround(item)) From 2d0649f398dcec516504d2cb6090c149e29f316a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 26 Apr 2025 19:33:50 +0000 Subject: [PATCH 0317/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ed4a014400..6c24dc15f3 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ed4a0144008979492f92175de66a322ed8fb0629 +Subproject commit 6c24dc15f3268ee6216af3b8da565348888d2393 From 6762009487ad04762e9fc7872ea0220a3ac34d32 Mon Sep 17 00:00:00 2001 From: Quietust Date: Thu, 17 Apr 2025 19:28:40 -0600 Subject: [PATCH 0318/1082] Fix Items::getDescription * Display correct quality level for items with "non-displayed" improvements * Add ALL item designations here (and remove "XXwearXX" from other spot) * Obey SHOW_IMP_QUALITY setting --- docs/changelog.txt | 1 + library/modules/Items.cpp | 58 +++++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e1ddf8d47c..e9713f8b9f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -105,6 +105,7 @@ Template for new versions: - ``Buildings`` module: add ``getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 - ``Units::teleport``: projectile information is now cleared for teleported units - ``Buildings::setOwner``: updated for changes in 51.11 +- ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys SHOW_IMP_QUALITY ## Lua - ``dfhack.military.addToSquad``: expose Military API function diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 36d1748c5b..a161bea8a2 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -49,6 +49,7 @@ distribution. #include "df/caravan_state.h" #include "df/creature_raw.h" #include "df/dfhack_material_category.h" +#include "df/d_init.h" #include "df/entity_buy_prices.h" #include "df/entity_buy_requests.h" #include "df/entity_raw.h" @@ -678,14 +679,54 @@ string Items::getDescription(df::item *item, int type, bool decorate) { item->getItemDescription(&tmp, type); if (decorate) { - addQuality(tmp, item->getQuality()); + // Special indicators get added in a specific order + // Innermost is at the top, and outermost is at the bottom - if (item->isImproved()) { + // First, figure out the quality levels we're going to display + int craftquality = item->getQuality(); + int craftquality_only_imps = item->getImprovementQuality(); + bool has_displayed_item_improvements = item->isImproved(); + if (!has_displayed_item_improvements && (craftquality < craftquality_only_imps)) + craftquality = craftquality_only_imps; + + // First, actual item quality + addQuality(tmp, craftquality); + + // Next, magic enchants + if (item->getMagic() != NULL) + tmp = '\x11' + tmp + '\x10'; // <| |> + + // Next, improvements + if (has_displayed_item_improvements) { tmp = '\xAE' + tmp + '\xAF'; // («) + tmp + (») - addQuality(tmp, item->getImprovementQuality()); + if (df::global::d_init->display.flags.is_set(d_init_flags1::SHOW_IMP_QUALITY)) + addQuality(tmp, craftquality_only_imps); + } + + // Dwarf mode only, forbid/foreign + if (*df::global::gamemode == game_mode::DWARF) { + if (item->flags.bits.forbid) + tmp = '{' + tmp + '}'; + if (item->flags.bits.foreign) + tmp = '(' + tmp + ')'; + } + + // Wear + switch (item->getWear()) + { + case 1: tmp = 'x' + tmp + 'x'; break; + case 2: tmp = 'X' + tmp + 'X'; break; + case 3: tmp = "XX" + tmp + "XX"; break; } - if (item->flags.bits.foreign) - tmp = "(" + tmp + ")"; + + // Fire + if (item->flags.bits.on_fire) + tmp = '\x13' + tmp + '\x13'; // !! !! + + // Finally, Adventure civzone + if ((*df::global::gamemode == game_mode::ADVENTURE) && + Items::getGeneralRef(item, general_ref_type::BUILDING_CIVZONE_ASSIGNED) != NULL) + tmp = '$' + tmp + '$'; } return tmp; } @@ -721,13 +762,6 @@ string Items::getReadableDescription(df::item *item) { CHECK_NULL_POINTER(item); auto desc = get_base_desc(item); - switch (item->getWear()) - { - case 1: desc = "x" + desc + "x"; break; // Worn - case 2: desc = "X" + desc + "X"; break; // Threadbare - case 3: desc = "XX" + desc + "XX"; break; // Tattered - } - if (auto gref = Items::getGeneralRef(item, general_ref_type::CONTAINS_UNIT)) { if (auto unit = gref->getUnit()) { From 2392d41c3f46e3bf4773bc2463872c6d280ba4a0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:01:56 -0700 Subject: [PATCH 0319/1082] only check scripts for the unavailable tag once --- library/lua/dfhack.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index ef08fb7f4b..15910b46f1 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -994,13 +994,13 @@ local valid_script_flags = { scripts = {required = false}, } -local warned_scripts = {} +local checked_scripts = {} function dfhack.run_script(name,...) - if not warned_scripts[name] then + if not checked_scripts[name] then + checked_scripts[name] = true local helpdb = require('helpdb') if helpdb.has_tag(name, 'unavailable') then - warned_scripts[name] = true dfhack.printerr(('UNTESTED WARNING: the "%s" script has not been validated to work well with this version of DF.'):format(name)) dfhack.printerr('It may not work as expected, or it may corrupt your game.') qerror('Please run the command again to ignore this warning and proceed.') From 74004612d88d8419af237226bb1329df6ac12589 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:02:45 -0700 Subject: [PATCH 0320/1082] revert windows-specific change to script-manager --- library/lua/script-manager.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 71bbd3449e..9a1ecb1df0 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -52,9 +52,6 @@ function reload(refresh_active_mod_scripts) local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name) if script_path:find('scripts_modactive') then local full_path = script_path..'/'..script_name - if dfhack.getOSType() == 'windows' then - full_path = script_path..'\\'..script_name - end internal_script = dfhack.internal.scripts[full_path] if internal_script then dfhack.internal.scripts[full_path] = nil From b260041a03f93a931dfefe070ec0f12a57e72f1b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:02:54 -0700 Subject: [PATCH 0321/1082] canonicalize slashes in path returned from findScript --- docs/dev/Lua API.rst | 10 ++++++---- library/LuaApi.cpp | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 2c7f9cacbc..bf7ee35eff 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3338,12 +3338,14 @@ and are only documented here for completeness: * ``dfhack.internal.findScript(name)`` - Searches `script paths ` for the script ``name`` and returns the - path of the first file found, or ``nil`` on failure. + Searches `script paths ` for the script ``name`` (which + includes the ``.lua`` extension) and returns the absolute path of the first + file found, or ``nil`` on failure. Slashes in the path are canonicalized to + forward slashes. .. note:: - This requires an extension to be specified (``.lua`` or ``.rb``) - use - ``dfhack.findScript()`` to include the ``.lua`` extension automatically. + You can use the ``dfhack.findScript()`` wrapper if you want to specify the + script name without the ``.lua`` extension. * ``dfhack.internal.runCommand(command[, use_console])`` diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index c80fecffc2..04b7741828 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3965,7 +3965,7 @@ static int internal_findScript(lua_State *L) const char *name = luaL_checkstring(L, 1); std::filesystem::path path = Core::getInstance().findScript(name); if (!path.empty()) - lua_pushstring(L, path.string().c_str()); + lua_pushstring(L, Filesystem::as_string(path).c_str()); else lua_pushnil(L); return 1; From 833da7d329af22540af1649b598291d6520f6b7d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:37:58 -0700 Subject: [PATCH 0322/1082] make english name in readable names optional --- docs/dev/Lua API.rst | 23 ++++++++++---------- library/LuaApi.cpp | 5 +++-- library/include/modules/Units.h | 4 ++-- library/modules/Units.cpp | 37 ++++++++++++++++++++------------- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index bf7ee35eff..e810a18440 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1773,17 +1773,18 @@ Units module Get human-readable baby or child name (e.g., "dwarven baby" or "dwarven child"). -* ``dfhack.units.getReadableName(unit or historical_figure)`` - - Returns a string that includes the native and english language name of the - unit (if any), the race of the unit (if different from fort), whether it is - trained for war or hunting, any syndrome-given descriptions (such as - "necromancer"), the training level (if tame), and profession or noble role. - If a ``historical_figure`` is passed instead of a unit, some information - (e.g., agitation status) is not available, and the profession may be - different (e.g., "Monk") from what is displayed in fort mode. - -* ``dfhack.units.getAge(unit[,true_age])`` +* ``dfhack.units.getReadableName(unit or historical_figure[, skip_english])`` + + Returns a string that includes the native and english language name (if + ``skip_english`` is not ``true``) of the unit (if any), the race of the unit + (if different from fort), whether it is trained for war or hunting, any + syndrome-given descriptions (such as "necromancer"), the training level (if + tame), and profession or noble role. If a ``historical_figure`` is passed + instead of a unit, some information (e.g., agitation status) is not + available, and the profession may be different (e.g., "Monk") from what is + displayed in fort mode. + +* ``dfhack.units.getAge(unit[, true_age])`` Returns the age of the unit in years as a floating-point value. If ``true_age`` is true, ignores false identities. diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 04b7741828..228399c84f 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2291,10 +2291,11 @@ static int units_assignTrainer(lua_State *L) { } static int units_getReadablename(lua_State *L) { + bool skip_english = lua_toboolean(L, 2); // defaults to false if (auto unit = Lua::GetDFObject(L, 1)) - Lua::Push(L, Units::getReadableName(unit)); + Lua::Push(L, Units::getReadableName(unit, skip_english)); else if (auto hf = Lua::GetDFObject(L, 1)) - Lua::Push(L, Units::getReadableName(hf)); + Lua::Push(L, Units::getReadableName(hf, skip_english)); else luaL_argerror(L, 1, "Expected a unit or a historical figure"); return 1; diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 86238308d7..0cba251644 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -281,9 +281,9 @@ DFHACK_EXPORT std::string getRaceBabyName(df::unit *unit, bool plural = false); DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id, bool plural = false); DFHACK_EXPORT std::string getRaceChildName(df::unit *unit, bool plural = false); // Full readable name including profession. HF profession might be different from unit profession. -DFHACK_EXPORT std::string getReadableName(df::historical_figure *hf); +DFHACK_EXPORT std::string getReadableName(df::historical_figure *hf, bool skip_english = false); // Full readable name including profession, curse name, and tame description. -DFHACK_EXPORT std::string getReadableName(df::unit *unit); +DFHACK_EXPORT std::string getReadableName(df::unit *unit, bool skip_english = false); // Unit's age (in non-integer years). Ignore false identities if true_age. DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 4ccce42277..ce135b477d 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1153,7 +1153,25 @@ static string getTameTag(df::unit *unit) { } } -string Units::getReadableName(df::historical_figure *hf) { +template +static string formatReadableName(T *unit_or_hf, const string &prof_name, bool skip_english) { + auto visible_name = Units::getVisibleName(unit_or_hf); + string native_name = Translation::translateName(visible_name); + + if (native_name.empty()) + return prof_name; + + if (skip_english) + return native_name + ", " + prof_name; + + string english_name = Translation::translateName(visible_name, true, true); + if (english_name.empty()) + return native_name + ", " + prof_name; + + return native_name + " \"" + english_name + "\", " + prof_name; +} + +string Units::getReadableName(df::historical_figure *hf, bool skip_english) { CHECK_NULL_POINTER(hf); string prof_name = getProfessionName(hf, false, false, true); @@ -1167,11 +1185,10 @@ string Units::getReadableName(df::historical_figure *hf) { prof_name = "Ghostly " + prof_name; } - string name = Translation::translateName(getVisibleName(hf)); - return name.empty() ? prof_name : name + ", " + prof_name; + return formatReadableName(hf, prof_name, skip_english); } -string Units::getReadableName(df::unit *unit) { +string Units::getReadableName(df::unit *unit, bool skip_english) { CHECK_NULL_POINTER(unit); string prof_name = getProfessionName(unit, false, false, true); @@ -1190,17 +1207,7 @@ string Units::getReadableName(df::unit *unit) { if (isTame(unit)) prof_name += " (" + getTameTag(unit) + ")"; - auto visible_name = getVisibleName(unit); - string native_name = Translation::translateName(visible_name); - - if (native_name.empty()) - return prof_name; - - string english_name = Translation::translateName(visible_name, true, true); - if (english_name.empty()) - return native_name + ", " + prof_name; - - return native_name + " \"" + english_name + "\", " + prof_name; + return formatReadableName(unit, prof_name, skip_english); } double Units::getAge(df::unit *unit, bool true_age) { From ee319b17d1d980cb7e6144dd83b06b85726e50a7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:21:40 -0700 Subject: [PATCH 0323/1082] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6c24dc15f3..fdd597e759 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6c24dc15f3268ee6216af3b8da565348888d2393 +Subproject commit fdd597e75904f54c089bec114b3c8f5fa34ce520 From bfc584c6b57507d80c7a20fc336f20c396a8304d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:41:59 -0700 Subject: [PATCH 0324/1082] fix changelog entry --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index bb2f01a8e3..7111999ecb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed - `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air +- `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. @@ -82,7 +83,6 @@ Template for new versions: ## Fixes - `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist - `gui/design`: fix misaligned shape icons -- `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows. # 51.11-r1 From b1dbe0139917d031cc663de02cb847cba669422e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:43:23 -0700 Subject: [PATCH 0325/1082] fix changelog entry --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7111999ecb..0bd39e0cbd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## Documentation ## API +- ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys vanilla SHOW_IMP_QUALITY setting ## Lua - ``script-manager``: new ``get_active_mods()`` function for getting information on active mods @@ -106,7 +107,6 @@ Template for new versions: - ``Buildings`` module: add ``getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 - ``Units::teleport``: projectile information is now cleared for teleported units - ``Buildings::setOwner``: updated for changes in 51.11 -- ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys SHOW_IMP_QUALITY ## Lua - ``dfhack.military.addToSquad``: expose Military API function From 5eb5f83ad114e617ede10794317201896555b052 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 28 Apr 2025 10:38:02 -0500 Subject: [PATCH 0326/1082] replace some `#define`s with `constexpr` --- library/PluginManager.cpp | 1 + library/include/DataFuncs.h | 7 +++++++ library/include/LuaWrapper.h | 34 +++++++++++++++++----------------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 98a9eaeec4..575ebdc7f5 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -772,6 +772,7 @@ int Plugin::lua_cmd_wrapper(lua_State *state) int Plugin::lua_fun_wrapper(lua_State *state) { + using DFHack::LuaWrapper::UPVAL_CONTAINER_ID; auto cmd = (LuaFunction*)lua_touserdata(state, UPVAL_CONTAINER_ID); RefAutoinc lock(cmd->owner->access); diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index a8d2caca51..0b297a68e4 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -54,6 +54,7 @@ namespace df { T get_from_lua_state(lua_State* L, int idx) { using DFHack::LuaWrapper::field_error; + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; using Ptr = std::add_pointer_t>; Ptr ptr{}; df::identity_traits::get()->lua_write(L, UPVAL_METHOD_NAME, &ptr, idx); @@ -69,6 +70,7 @@ namespace df { template T get_from_lua_state(lua_State* L, int idx) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; T val{}; df::identity_traits::get()->lua_write(L, UPVAL_METHOD_NAME, &val, idx); return val; @@ -78,6 +80,7 @@ namespace df { requires std::is_invocable_r_v void call_and_push_impl(lua_State* L, int base, std::index_sequence, FT fun, ET... extra) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; if constexpr (std::is_same_v) { std::invoke(fun, extra..., (get_from_lua_state(L, base+I))...); lua_pushnil(L); @@ -119,6 +122,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT)+1; static void execute(lua_State *L, int base, RT(CT::*mem_fun)(AT...)) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT *self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; @@ -128,6 +132,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT)+1; static void execute(lua_State *L, int base, RT(CT::*mem_fun)(AT...) const) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT *self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; @@ -154,6 +159,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT) + 1; static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...)) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; @@ -163,6 +169,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT) + 1; static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...) const) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 44bc19938c..b95713e572 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -70,51 +70,51 @@ namespace LuaWrapper { extern LuaToken DFHACK_PTR_IDTABLE_TOKEN; // Function registry names -#define DFHACK_CHANGEERROR_NAME "DFHack::ChangeError" -#define DFHACK_COMPARE_NAME "DFHack::ComparePtrs" -#define DFHACK_TYPE_TOSTRING_NAME "DFHack::TypeToString" -#define DFHACK_SIZEOF_NAME "DFHack::Sizeof" -#define DFHACK_DISPLACE_NAME "DFHack::Displace" -#define DFHACK_NEW_NAME "DFHack::New" -#define DFHACK_ASSIGN_NAME "DFHack::Assign" -#define DFHACK_IS_INSTANCE_NAME "DFHack::IsInstance" -#define DFHACK_DELETE_NAME "DFHack::Delete" -#define DFHACK_CAST_NAME "DFHack::Cast" + constexpr auto DFHACK_CHANGEERROR_NAME = "DFHack::ChangeError"; + constexpr auto DFHACK_COMPARE_NAME = "DFHack::ComparePtrs"; + constexpr auto DFHACK_TYPE_TOSTRING_NAME = "DFHack::TypeToString"; + constexpr auto DFHACK_SIZEOF_NAME = "DFHack::Sizeof"; + constexpr auto DFHACK_DISPLACE_NAME = "DFHack::Displace"; + constexpr auto DFHACK_NEW_NAME = "DFHack::New"; + constexpr auto DFHACK_ASSIGN_NAME = "DFHack::Assign"; + constexpr auto DFHACK_IS_INSTANCE_NAME = "DFHack::IsInstance"; + constexpr auto DFHACK_DELETE_NAME = "DFHack::Delete"; + constexpr auto DFHACK_CAST_NAME = "DFHack::Cast"; extern LuaToken DFHACK_EMPTY_TABLE_TOKEN; /* * Upvalue: contents of DFHACK_TYPETABLE_NAME */ -#define UPVAL_TYPETABLE lua_upvalueindex(1) + constexpr auto UPVAL_TYPETABLE = lua_upvalueindex(1); /* * Expected metatable of the current object. */ -#define UPVAL_METATABLE lua_upvalueindex(2) + constexpr auto UPVAL_METATABLE = lua_upvalueindex(2); /* * Table mapping field names to indices or data structure pointers. * Enum index table is linked into here via getmetatable($).__index. * Fields that are actually in UPVAL_METATABLE are marked with NULL light udata. */ -#define UPVAL_FIELDTABLE lua_upvalueindex(3) -#define UPVAL_METHOD_NAME lua_upvalueindex(3) + constexpr auto UPVAL_FIELDTABLE = lua_upvalueindex(3); + constexpr auto UPVAL_METHOD_NAME = lua_upvalueindex(3); /* * Only for containers: light udata with container identity. */ -#define UPVAL_CONTAINER_ID lua_upvalueindex(4) + constexpr auto UPVAL_CONTAINER_ID = lua_upvalueindex(4); /* * Only for containers: light udata with item identity. */ -#define UPVAL_ITEM_ID lua_upvalueindex(5) + constexpr auto UPVAL_ITEM_ID = lua_upvalueindex(5); /* * Only for containers: if not nil, overrides the item count. */ -#define UPVAL_ITEM_COUNT lua_upvalueindex(6) + constexpr auto UPVAL_ITEM_COUNT = lua_upvalueindex(6); inline void lua_dup(lua_State *state) { lua_pushvalue(state, -1); } inline void lua_swap(lua_State *state) { lua_insert(state, -2); } From c21478c256df11735aad4fed61e3faaf9d2ed99f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 28 Apr 2025 10:56:52 -0500 Subject: [PATCH 0327/1082] minor quality issues `using` instead of `typedef` `[[noreturn]]` on a nonreturning function explicit `[[fallthrough]]` --- library/LuaWrapper.cpp | 6 +++--- library/include/LuaWrapper.h | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index f9fd0e7832..8c4a17f8db 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -64,10 +64,10 @@ using namespace DFHack::LuaWrapper; /* */ -static int change_error(lua_State *state) +[[noreturn]] static int change_error(lua_State *state) { luaL_error(state, "Attempt to change a read-only table.\n"); - return 0; + std::abort(); // should never be reached but makes gcc happy } /** @@ -1642,7 +1642,7 @@ static void RenderType(lua_State *state, const compound_identity *node) case IDTYPE_UNION: // TODO: change this to union-type? what relies on this? lua_pushboolean(state, true); lua_setfield(state, ftable, "_union"); - // fall through + [[fallthrough]]; case IDTYPE_STRUCT: lua_pushstring(state, "struct-type"); lua_setfield(state, ftable, "_kind"); diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index b95713e572..6d77af87db 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -231,7 +231,8 @@ namespace LuaWrapper { /** * Wrap functions and add them to the table on the top of the stack. */ - typedef DFHack::FunctionReg FunctionReg; + using DFHack::FunctionReg; + void SetFunctionWrappers(lua_State *state, const FunctionReg *reg); int method_wrapper_core(lua_State *state, function_identity_base *id); From 00597e778e3c50976dab7b695565d9e2842de17c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 28 Apr 2025 11:06:05 -0500 Subject: [PATCH 0328/1082] ColorText: `typedef` -> `using` --- library/include/ColorText.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/ColorText.h b/library/include/ColorText.h index f484e61d4b..f5768c0ffa 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -65,7 +65,7 @@ namespace DFHack class DFHACK_EXPORT color_ostream : public std::ostream { public: - typedef DFHack::color_value color_value; + using color_value = DFHack::color_value; private: color_value cur_color; From a012347210769b58ada362fd7fd77dc6bc734f2e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 28 Apr 2025 11:39:08 -0500 Subject: [PATCH 0329/1082] Core.h: `typedef` -> `using` also this is C++, not C, so `typedef struct T T` doesn't belong here --- library/include/Core.h | 8 ++++---- library/include/DataDefs.h | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/library/include/Core.h b/library/include/Core.h index 9ef8fb8f0e..205b130b91 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -44,10 +44,6 @@ distribution. #include #include -#define DFH_MOD_SHIFT 1 -#define DFH_MOD_CTRL 2 -#define DFH_MOD_ALT 4 - struct WINDOW; struct lua_State; @@ -58,6 +54,10 @@ namespace df namespace DFHack { + constexpr auto DFH_MOD_SHIFT = 1; + constexpr auto DFH_MOD_CTRL = 2; + constexpr auto DFH_MOD_ALT = 4; + class Process; class Module; class Materials; diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 59f47abcb6..e8e54fac41 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -35,7 +35,7 @@ distribution. #include "BitArray.h" #include "Export.h" -typedef struct lua_State lua_State; +struct lua_State; /* * Definitions of DFHack namespace structs used by generated headers. @@ -352,9 +352,9 @@ namespace DFHack }; #ifdef _MSC_VER - typedef void *virtual_ptr; + using virtual_ptr = void*; #else - typedef virtual_class *virtual_ptr; + using virtual_ptr = virtual_class*; #endif class DFHACK_EXPORT VMethodInterposeLinkBase; From 8067b6ce4598a413203c123328d814779a1addc1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 29 Apr 2025 07:21:05 +0000 Subject: [PATCH 0330/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index fdd597e759..15b3be7fa9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit fdd597e75904f54c089bec114b3c8f5fa34ce520 +Subproject commit 15b3be7fa9353b01ccc1b3e1fd4ff7913e428f08 From aae16bd6578d1c9c2e24affd7e24ccfd5ba50a12 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 29 Apr 2025 10:59:06 -0500 Subject: [PATCH 0331/1082] DataIdentity: `typedef` -> `using` --- library/include/DataIdentity.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 7e1f8eaaea..5aa256809a 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -319,7 +319,7 @@ namespace df class DFHACK_EXPORT stl_ptr_vector_identity : public ptr_container_identity { public: - typedef std::vector container; + using container = std::vector; /* * This class assumes that std::vector is equivalent @@ -486,7 +486,7 @@ namespace df * in layout and behavior to BitArray for any T. */ - typedef BitArray container; + using container = BitArray; bit_array_identity(const enum_identity *ienum = NULL) : bit_container_identity(sizeof(container), &allocator_fn, ienum) @@ -516,7 +516,7 @@ namespace df class DFHACK_EXPORT stl_bit_vector_identity : public bit_container_identity { public: - typedef std::vector container; + using container = std::vector; stl_bit_vector_identity(const enum_identity *ienum = NULL) : bit_container_identity(sizeof(container), &df::allocator_fn, ienum) @@ -547,7 +547,7 @@ namespace df template class enum_list_attr_identity : public container_identity { public: - typedef enum_list_attr container; + using container = enum_list_attr; enum_list_attr_identity(const type_identity *item) : container_identity(sizeof(container), NULL, item, NULL) @@ -615,7 +615,7 @@ namespace df template struct DFHACK_EXPORT identity_traits> { static opaque_identity *get() { - typedef std::shared_ptr type; + using type = std::shared_ptr; static std::string name = std::string("shared_ptr<") + typeid(T).name() + ">"; static opaque_identity identity(sizeof(type), allocator_noassign_fn, name); return &identity; @@ -772,7 +772,7 @@ namespace df template inline const container_identity *identity_traits >::get() { - typedef std::vector container; + using container = std::vector; static const stl_container_identity identity("vector", identity_traits::get()); return &identity; } @@ -800,28 +800,28 @@ namespace df #ifdef BUILD_DFHACK_LIB template inline const container_identity *identity_traits >::get() { - typedef std::deque container; + using container = std::deque; static const stl_container_identity identity("deque", identity_traits::get()); return &identity; } template inline const container_identity *identity_traits >::get() { - typedef std::set container; + using container = std::set; static const ro_stl_container_identity identity("set", identity_traits::get()); return &identity; } template inline const container_identity *identity_traits>::get() { - typedef std::map container; + using container = std::map; static const ro_stl_assoc_container_identity identity("map", identity_traits::get(), identity_traits::get()); return &identity; } template inline const container_identity *identity_traits>::get() { - typedef std::unordered_map container; + using container = std::unordered_map; static const ro_stl_assoc_container_identity identity("unordered_map", identity_traits::get(), identity_traits::get()); return &identity; } @@ -834,7 +834,7 @@ namespace df template inline const container_identity *identity_traits >::get() { - typedef DfArray container; + using container = DfArray; static const stl_container_identity identity("DfArray", identity_traits::get()); return &identity; } From 5b5c5c6449cf983b92d9321ded86b09d3cc191a2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 29 Apr 2025 11:00:33 -0500 Subject: [PATCH 0332/1082] LuaApi: template isntead of ad hoc macro can't get rid of the second one until c++26 --- library/LuaApi.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 228399c84f..077d8cd9c5 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2667,20 +2667,17 @@ static const LuaWrapper::FunctionReg dfhack_world_module[] = { { NULL, NULL } }; -#define WORLD_GAMEMODE_WRAPPER(func) \ - static int world_gamemode_##func(lua_State *L) \ - { \ - int gametype = luaL_optint(L, 1, -1); \ - lua_pushboolean(L, World::func((df::game_type)gametype)); \ - return 1;\ - } -#define WORLD_GAMEMODE_FUNC(func) \ - {#func, world_gamemode_##func} +using gamemode_func = auto (df::game_type t) -> bool; +template +static int world_gamemode(lua_State* L) +{ + int gametype = luaL_optint(L, 1, -1); + lua_pushboolean(L, gmf((df::game_type)gametype)); + return 1; +} -WORLD_GAMEMODE_WRAPPER(isFortressMode); -WORLD_GAMEMODE_WRAPPER(isAdventureMode); -WORLD_GAMEMODE_WRAPPER(isArena); -WORLD_GAMEMODE_WRAPPER(isLegends); +#define WORLD_GAMEMODE_FUNC(func) \ + {#func, world_gamemode} static const luaL_Reg dfhack_world_funcs[] = { WORLD_GAMEMODE_FUNC(isFortressMode), From e803731cd14dbc8be19a473b09a8185c5b9dfabf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 1 May 2025 20:33:55 -0500 Subject: [PATCH 0333/1082] LuaApi: templated lambda instead of macro --- library/LuaApi.cpp | 53 ++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 077d8cd9c5..927e2f6d72 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3786,32 +3786,39 @@ static int internal_diffscan(lua_State *L) bool has_newv = !lua_isnil(L, 7); bool has_diffv = !lua_isnil(L, 8); -#define LOOP(esz, etype) \ - case esz: { \ - etype *pold = (etype*)old_data; \ - etype *pnew = (etype*)new_data; \ - etype oldv = (etype)luaL_optint(L, 6, 0); \ - etype newv = (etype)luaL_optint(L, 7, 0); \ - etype diffv = (etype)luaL_optint(L, 8, 0); \ - for (int i = start_idx; i < end_idx; i++) { \ - if (pold[i] == pnew[i]) continue; \ - if (has_oldv && pold[i] != oldv) continue; \ - if (has_newv && pnew[i] != newv) continue; \ - if (has_diffv && etype(pnew[i]-pold[i]) != diffv) continue; \ - lua_pushinteger(L, i); return 1; \ - } \ - break; \ - } + auto loop = [&]() -> std::optional + { + etype* pold = (etype*)old_data; + etype* pnew = (etype*)new_data; + etype oldv = (etype)luaL_optint(L, 6, 0); + etype newv = (etype)luaL_optint(L, 7, 0); + etype diffv = (etype)luaL_optint(L, 8, 0); + for (int i = start_idx; i < end_idx; i++) { + if (pold[i] == pnew[i]) continue; + if (has_oldv && pold[i] != oldv) continue; + if (has_newv && pnew[i] != newv) continue; + if (has_diffv && etype(pnew[i] - pold[i]) != diffv) continue; + return i; + } + return std::nullopt; + }; + + std::optional res; switch (eltsize) { - LOOP(1, uint8_t); - LOOP(2, uint16_t); - LOOP(4, uint32_t); - default: - luaL_argerror(L, 5, "invalid element size"); + case 1: + res = loop.operator()(); break; + case 2: + res = loop.operator()(); break; + case 4: + res = loop.operator()(); break; + default: + luaL_argerror(L, 5, "invalid element size"); } -#undef LOOP + if (res) + lua_pushinteger(L,*res); + else + lua_pushnil(L); - lua_pushnil(L); return 1; } From 2fb9a275c95b3c80e4395dd3581baaabea79ed81 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 2 May 2025 15:37:18 -0700 Subject: [PATCH 0334/1082] downgrade warning to debug DF now routinely has zones assigned to non-existent units --- docs/changelog.txt | 1 + plugins/preserve-rooms.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b94cdce955..816f75469a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: - `getplants`: fix logic for determining whether plant growths have been picked - `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air - `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows +- `preserve-rooms`: don't warn when a room is assigned to a non-existent unit. this is now common behavior for DF when it keeps a room for an unloaded unit ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index 805e10c837..d5b4b5b7a5 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -513,7 +513,7 @@ static void process_rooms(color_ostream &out, } auto owner = Buildings::getOwner(zone); if (!owner) { - WARN(cycle, out).print("building %d has owner id %d but no such unit exists\n", zone->id, zone->assigned_unit_id); + DEBUG(cycle, out).print("building %d has owner id %d but no such unit exists\n", zone->id, zone->assigned_unit_id); continue; } auto hf = df::historical_figure::find(owner->hist_figure_id); From 94cdaf713329fbf25f9fcf7e85bc2ff6cbc26ac6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 10:57:12 -0500 Subject: [PATCH 0335/1082] Remove unused `RegisterData` API from Core as discussed in #5438 and on discord --- docs/changelog.txt | 1 + library/Core.cpp | 23 ----------------------- library/include/Core.h | 8 -------- 3 files changed, 1 insertion(+), 31 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 816f75469a..923e53b482 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -77,6 +77,7 @@ Template for new versions: - ``script-manager``: new ``get_mod_info_metadata()`` function for getting information out of mod ``info.txt`` files ## Removed +- removed historically unused ``Core::RegisterData``/``Core::GetData`` API and associated internal data structures # 51.11-r1.2 diff --git a/library/Core.cpp b/library/Core.cpp index 6e875648fb..d949db688c 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1496,7 +1496,6 @@ Core::Core() : HotkeyCond{}, alias_mutex{}, started{false}, - misc_data_mutex{}, CoreSuspendMutex{}, CoreWakeup{}, ownerThread{}, @@ -1995,28 +1994,6 @@ void Core::printerr(const char *format, ...) va_end(args); } -void Core::RegisterData( void *p, std::string key ) -{ - std::lock_guard lock(misc_data_mutex); - misc_data_map[key] = p; -} - -void *Core::GetData( std::string key ) -{ - std::lock_guard lock(misc_data_mutex); - std::map::iterator it=misc_data_map.find(key); - - if ( it != misc_data_map.end() ) - { - void *p=it->second; - return p; - } - else - { - return 0;// or throw an error. - } -} - Core& Core::getInstance() { static Core instance; return instance; diff --git a/library/include/Core.h b/library/include/Core.h index 205b130b91..dd84d49df0 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -168,11 +168,6 @@ namespace DFHack /// removes the hotkey command and gives it to the caller thread std::string getHotkeyCmd( bool &keep_going ); - /// adds a named pointer (for later or between plugins) - void RegisterData(void *p,std::string key); - /// returns a named pointer. - void *GetData(std::string key); - command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream &out, const std::string &command); bool loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent = false); @@ -316,9 +311,6 @@ namespace DFHack // Additional state change scripts std::vector state_change_scripts; - std::mutex misc_data_mutex; - std::map misc_data_map; - /*! * \defgroup core_suspend CoreSuspender state handling serialization to * DF memory. From 6ad91b66ed2504f81d2f5699218761687b41744f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 3 May 2025 18:27:20 +0000 Subject: [PATCH 0336/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 15b3be7fa9..def13b7c6f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 15b3be7fa9353b01ccc1b3e1fd4ff7913e428f08 +Subproject commit def13b7c6f1bb8b036968490cae696e18cb25bbc From 90cac3d574cda6e8fd1b7c1a8dfb3b6f93a0c5b4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 13:28:19 -0500 Subject: [PATCH 0337/1082] merge `Process.cpp` into a single source file --- library/CMakeLists.txt | 4 +- library/Process-darwin.cpp | 341 --------------- library/Process-linux.cpp | 250 ----------- library/Process-windows.cpp | 455 -------------------- library/Process.cpp | 801 ++++++++++++++++++++++++++++++++++++ 5 files changed, 802 insertions(+), 1049 deletions(-) delete mode 100644 library/Process-darwin.cpp delete mode 100644 library/Process-linux.cpp delete mode 100644 library/Process-windows.cpp create mode 100644 library/Process.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 37f5b12d5e..895ae6eb86 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -105,6 +105,7 @@ set(MAIN_SOURCES PluginManager.cpp PluginStatics.cpp PlugLoad.cpp + Process.cpp TileTypes.cpp VersionInfoFactory.cpp RemoteClient.cpp @@ -126,7 +127,6 @@ endif() set(MAIN_SOURCES_WINDOWS ${CONSOLE_SOURCES} Hooks.cpp - Process-windows.cpp ) if(WIN32) @@ -136,13 +136,11 @@ endif() set(MAIN_SOURCES_LINUX ${CONSOLE_SOURCES} - Process-linux.cpp ) set(MAIN_SOURCES_DARWIN ${CONSOLE_SOURCES} PlugLoad-posix.cpp - Process-darwin.cpp ) set(MODULE_HEADERS diff --git a/library/Process-darwin.cpp b/library/Process-darwin.cpp deleted file mode 100644 index f8c0833a78..0000000000 --- a/library/Process-darwin.cpp +++ /dev/null @@ -1,341 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - - -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "md5wrapper.h" -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - - -Process::Process(const VersionInfoFactory& known_versions) : identified(false), my_pe(0) -{ - char path[1024]; - char *real_path; - uint32_t size = sizeof(path); - if (_NSGetExecutablePath(path, &size) == 0) { - real_path = realpath(path, NULL); - } - - md5wrapper md5; - uint32_t length; - uint8_t first_kb [1024]; - memset(first_kb, 0, sizeof(first_kb)); - // get hash of the running DF process - my_md5 = md5.getHashFromFile(real_path, length, (char *) first_kb); - // create linux process, add it to the vector - auto vinfo = known_versions.getVersionInfoByMD5(my_md5); - if(vinfo) - { - identified = true; - my_descriptor = std::make_shared(*vinfo); - } - else - { - char * wd = getcwd(NULL, 0); - cerr << "Unable to retrieve version information.\n"; - cerr << "File: " << real_path << endl; - cerr << "MD5: " << my_md5 << endl; - cerr << "working dir: " << wd << endl; - cerr << "length:" << length << endl; - cerr << "1KB hexdump follows:" << endl; - for(int i = 0; i < 64; i++) - { - fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", - first_kb[i*16], - first_kb[i*16+1], - first_kb[i*16+2], - first_kb[i*16+3], - first_kb[i*16+4], - first_kb[i*16+5], - first_kb[i*16+6], - first_kb[i*16+7], - first_kb[i*16+8], - first_kb[i*16+9], - first_kb[i*16+10], - first_kb[i*16+11], - first_kb[i*16+12], - first_kb[i*16+13], - first_kb[i*16+14], - first_kb[i*16+15] - ); - } - free(wd); - } -} - -Process::~Process() -{ - // Nothing to do here -} - -string Process::doReadClassName (void * vptr) -{ - char * typeinfo = Process::readPtr(((char *)vptr - sizeof(void*))); - char * typestring = Process::readPtr(typeinfo + sizeof(void*)); - string raw = readCString(typestring); - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -const char * -inheritance_strings[] = { - "SHARE", "COPY", "NONE", "DONATE_COPY", -}; - -const char * -behavior_strings[] = { - "DEFAULT", "RANDOM", "SEQUENTIAL", "RESQNTL", "WILLNEED", "DONTNEED", -}; - -void Process::getMemRanges( vector & ranges ) -{ - static bool log_ranges = (getenv("DFHACK_LOG_MEM_RANGES") != NULL); - - kern_return_t kr; - task_t the_task; - - the_task = mach_task_self(); - -#ifdef DFHACK64 - mach_vm_size_t vmsize; - mach_vm_address_t address; - vm_region_basic_info_data_64_t info; -#else - vm_size_t vmsize; - vm_address_t address; - vm_region_basic_info_data_t info; -#endif - mach_msg_type_number_t info_count; - vm_region_flavor_t flavor; - memory_object_name_t object; - - kr = KERN_SUCCESS; - address = 0; - - string cur_name; - void * cur_base = nullptr; - - do { -#ifdef DFHACK64 - flavor = VM_REGION_BASIC_INFO_64; - info_count = VM_REGION_BASIC_INFO_COUNT_64; - kr = mach_vm_region(the_task, &address, &vmsize, flavor, - (vm_region_info_64_t)&info, &info_count, &object); -#else - flavor = VM_REGION_BASIC_INFO; - info_count = VM_REGION_BASIC_INFO_COUNT; - kr = vm_region(the_task, &address, &vmsize, flavor, - (vm_region_info_t)&info, &info_count, &object); -#endif - - if (kr == KERN_SUCCESS) { - if (info.reserved==1) { - address += vmsize; - continue; - } - Dl_info dlinfo; - int dlcheck; - dlcheck = dladdr((const void*)address, &dlinfo); - if (dlcheck==0) { - dlinfo.dli_fname = ""; - } - - t_memrange temp; - strncpy( temp.name, dlinfo.dli_fname, 1023 ); - temp.name[1023] = 0; - if (cur_name != temp.name) { - cur_name = temp.name; - cur_base = (void *) address; - } - temp.base = cur_base; - temp.start = (void *) address; - temp.end = (void *) (address+vmsize); - temp.read = (info.protection & VM_PROT_READ); - temp.write = (info.protection & VM_PROT_WRITE); - temp.execute = (info.protection & VM_PROT_EXECUTE); - temp.shared = info.shared; - temp.valid = true; - ranges.push_back(temp); - - if (log_ranges) - { - fprintf(stderr, - "%p-%p %8zuK %c%c%c/%c%c%c %11s %6s %10s uwir=%hu sub=%u dlname: %s\n", - (void*)address, - (void*)(address + vmsize), - size_t(vmsize >> 10), - (info.protection & VM_PROT_READ) ? 'r' : '-', - (info.protection & VM_PROT_WRITE) ? 'w' : '-', - (info.protection & VM_PROT_EXECUTE) ? 'x' : '-', - (info.max_protection & VM_PROT_READ) ? 'r' : '-', - (info.max_protection & VM_PROT_WRITE) ? 'w' : '-', - (info.max_protection & VM_PROT_EXECUTE) ? 'x' : '-', - inheritance_strings[info.inheritance], - (info.shared) ? "shared" : "-", - behavior_strings[info.behavior], - info.user_wired_count, - info.reserved, - dlinfo.dli_fname); - } - - address += vmsize; - } else if (kr != KERN_INVALID_ADDRESS) { - - /*if (the_task != MACH_PORT_NULL) { - mach_port_deallocate(mach_task_self(), the_task); - }*/ - return; - } - } while (kr != KERN_INVALID_ADDRESS); - - -/* if (the_task != MACH_PORT_NULL) { - mach_port_deallocate(mach_task_self(), the_task); - }*/ -} - -uintptr_t Process::getBase() -{ - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool /*to_file*/) -{ - return offset; -} - -uint32_t Process::getTickCount() -{ - struct timeval tp; - gettimeofday(&tp, NULL); - return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); -} - -string Process::getPath() -{ - static string cached_path = ""; - if (cached_path.size()) - return cached_path; - char path[1024]; - char *real_path; - uint32_t size = sizeof(path); - if (getcwd(path, size)) - { - cached_path = string(path); - return cached_path; - } - if (_NSGetExecutablePath(path, &size) == 0) { - real_path = realpath(path, NULL); - } - else { - fprintf(stderr, "_NSGetExecutablePath failed!\n"); - cached_path = "."; - return cached_path; - } - std::string path_string(real_path); - int last_slash = path_string.find_last_of("/"); - cached_path = path_string.substr(0,last_slash); - return cached_path; -} - -int Process::getPID() -{ - return getpid(); -} - -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) -{ - int result; - int protect=0; - if(trgrange.read)protect|=PROT_READ; - if(trgrange.write)protect|=PROT_WRITE; - if(trgrange.execute)protect|=PROT_EXEC; - result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); - - return result==0; -} - -// returns -1 on error -void* Process::memAlloc(const int length) -{ - return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); -} - -int Process::memDealloc(void *ptr, const int length) -{ - return munmap(ptr, length); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - - if (prot & Process::MemProt::READ) - prot_native |= PROT_READ; - if (prot & Process::MemProt::WRITE) - prot_native |= PROT_WRITE; - if (prot & Process::MemProt::EXEC) - prot_native |= PROT_EXEC; - - return mprotect(ptr, length, prot_native); -} diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp deleted file mode 100644 index ce64cc113b..0000000000 --- a/library/Process-linux.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "modules/Filesystem.h" -#include "md5wrapper.h" - -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - -Process::Process(const VersionInfoFactory& known_versions) : identified(false), my_pe(0) -{ - const char * exe_link_name = "/proc/self/exe"; - - // valgrind replaces readlink for /proc/self/exe, but not open. - char self_exe[1024]; - memset(self_exe, 0, sizeof(self_exe)); - std::string self_exe_name; - if (readlink(exe_link_name, self_exe, sizeof(self_exe) - 1) < 0) - self_exe_name = exe_link_name; - else - self_exe_name = self_exe; - - md5wrapper md5; - uint32_t length; - uint8_t first_kb [1024]; - memset(first_kb, 0, sizeof(first_kb)); - // get hash of the running DF process - my_md5 = md5.getHashFromFile(self_exe_name, length, (char *) first_kb); - // create linux process, add it to the vector - auto vinfo = known_versions.getVersionInfoByMD5(my_md5); - if(vinfo) - { - identified = true; - my_descriptor = std::make_shared(*vinfo); - } - else - { - char * wd = getcwd(NULL, 0); - cerr << "Unable to retrieve version information.\n"; - cerr << "File: " << exe_link_name << endl; - cerr << "MD5: " << my_md5 << endl; - cerr << "working dir: " << wd << endl; - cerr << "length:" << length << endl; - cerr << "1KB hexdump follows:" << endl; - for(int i = 0; i < 64; i++) - { - fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", - first_kb[i*16], - first_kb[i*16+1], - first_kb[i*16+2], - first_kb[i*16+3], - first_kb[i*16+4], - first_kb[i*16+5], - first_kb[i*16+6], - first_kb[i*16+7], - first_kb[i*16+8], - first_kb[i*16+9], - first_kb[i*16+10], - first_kb[i*16+11], - first_kb[i*16+12], - first_kb[i*16+13], - first_kb[i*16+14], - first_kb[i*16+15] - ); - } - free(wd); - } -} - -Process::~Process() -{ - // Nothing to do here -} - -string Process::doReadClassName (void * vptr) -{ - char* typeinfo = Process::readPtr(((char *)vptr - sizeof(void*))); - char* typestring = Process::readPtr(typeinfo + sizeof(void*)); - string raw = readCString(typestring); - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -//FIXME: cross-reference with ELF segment entries? -void Process::getMemRanges( vector & ranges ) -{ - char buffer[1024]; - char permissions[5]; // r/-, w/-, x/-, p/s, 0 - - FILE *mapFile = ::fopen("/proc/self/maps", "r"); - if (!mapFile) - return; - - size_t start, end, offset, device1, device2, node; - - string cur_name; - void * cur_base = nullptr; - - while (fgets(buffer, 1024, mapFile)) - { - t_memrange temp; - temp.name[0] = 0; - sscanf(buffer, "%zx-%zx %s %zx %zx:%zx %zu %[^\n]", - &start, - &end, - (char*)&permissions, - &offset, &device1, &device2, &node, - (char*)temp.name); - if (cur_name != temp.name) { - cur_name = temp.name; - cur_base = (void *) start; - } - temp.base = cur_base; - temp.start = (void *) start; - temp.end = (void *) end; - temp.read = permissions[0] == 'r'; - temp.write = permissions[1] == 'w'; - temp.execute = permissions[2] == 'x'; - temp.shared = permissions[3] == 's'; - temp.valid = true; - ranges.push_back(temp); - } - - fclose(mapFile); -} - -uintptr_t Process::getBase() -{ - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool /*to_file*/) -{ - return offset; -} - -uint32_t Process::getTickCount() -{ - struct timeval tp; - gettimeofday(&tp, NULL); - return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); -} - -std::filesystem::path Process::getPath() -{ - return Filesystem::get_initial_cwd(); -} - -int Process::getPID() -{ - return getpid(); -} - -bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) -{ - int result; - int protect=0; - if(trgrange.read)protect|=PROT_READ; - if(trgrange.write)protect|=PROT_WRITE; - if(trgrange.execute)protect|=PROT_EXEC; - result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); - - return result==0; -} - -bool Process::flushCache(const void* target, size_t count) -{ - __builtin___clear_cache((char*)target, (char*)target + count - 1); - return true; /* assume always succeeds, as the builtin has no return type */ -} - - -// returns -1 on error -void* Process::memAlloc(const int length) -{ - return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); -} - -int Process::memDealloc(void *ptr, const int length) -{ - return munmap(ptr, length); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - - if (prot & Process::MemProt::READ) - prot_native |= PROT_READ; - if (prot & Process::MemProt::WRITE) - prot_native |= PROT_WRITE; - if (prot & Process::MemProt::EXEC) - prot_native |= PROT_EXEC; - - return mprotect(ptr, length, prot_native); -} diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp deleted file mode 100644 index 603636176d..0000000000 --- a/library/Process-windows.cpp +++ /dev/null @@ -1,455 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Error.h" -#include "Internal.h" - -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "modules/Filesystem.h" - -#define _WIN32_WINNT 0x0600 -#define WINVER 0x0600 - -#define WIN32_LEAN_AND_MEAN -#include -#include - -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - -namespace DFHack -{ - class PlatformSpecific - { - public: - PlatformSpecific() - { - base = 0; - sections = 0; - }; - HANDLE my_handle; - uint32_t my_pid; - IMAGE_NT_HEADERS pe_header; - IMAGE_SECTION_HEADER * sections; - char * base; - }; -} - -Process::Process(const VersionInfoFactory& factory) : identified(false) -{ - HMODULE hmod = NULL; - DWORD needed; - bool found = false; - - d = new PlatformSpecific(); - // open process - d->my_pid = GetCurrentProcessId(); - d->my_handle = GetCurrentProcess(); - // try getting the first module of the process - if(EnumProcessModules(d->my_handle, &hmod, sizeof(hmod), &needed) == 0) - { - return; //if enumprocessModules fails, give up - } - - // got base ;) - d->base = (char *)hmod; - - // read from this process - try - { - uint32_t pe_offset = readDWord(d->base+0x3C); - read(d->base + pe_offset, sizeof(d->pe_header), (uint8_t *)&(d->pe_header)); - const size_t sectionsSize = sizeof(IMAGE_SECTION_HEADER) * d->pe_header.FileHeader.NumberOfSections; - d->sections = (IMAGE_SECTION_HEADER *) malloc(sectionsSize); - read(d->base + pe_offset + sizeof(d->pe_header), sectionsSize, (uint8_t *)(d->sections)); - } - catch (std::exception &) - { - return; - } - my_pe = d->pe_header.FileHeader.TimeDateStamp; - auto vinfo = factory.getVersionInfoByPETimestamp(my_pe); - if(vinfo) - { - identified = true; - // give the process a data model and memory layout fixed for the base of first module - my_descriptor = std::make_shared(*vinfo); - my_descriptor->rebaseTo(getBase()); - } - else - { - fprintf(stderr, "Unable to retrieve version information.\nPE timestamp: 0x%x\n", my_pe); - fflush(stderr); - } -} - -Process::~Process() -{ - // destroy our rebased copy of the memory descriptor - if(d->sections != NULL) - free(d->sections); -} - -string Process::doReadClassName (void * vptr) -{ - char* rtti = readPtr((char *)vptr - sizeof(void*)); -#ifdef DFHACK64 - void* base; - if (!RtlPcToFileHeader(rtti, &base)) - return "dummy"; - char* typeinfo = (char *)base + readDWord(rtti + 0xC); - std::string raw = readCString(typeinfo + 0x10); -#else - char* typeinfo = readPtr(rtti + 0xC); - std::string raw = readCString(typeinfo + 0x8); -#endif - if (raw.length() == 0) - return "dummy"; - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -/* -typedef struct _MEMORY_BASIC_INFORMATION -{ - void * BaseAddress; - void * AllocationBase; - uint32_t AllocationProtect; - size_t RegionSize; - uint32_t State; - uint32_t Protect; - uint32_t Type; -} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; -*/ -/* -//Internal structure used to store heap block information. -struct HeapBlock -{ - PVOID dwAddress; - DWORD dwSize; - DWORD dwFlags; - ULONG reserved; -}; -*/ - -static void GetDosNames(std::map &table) -{ - // Partially based on example from msdn: - // Translate path with device name to drive letters. - TCHAR szTemp[512]; - szTemp[0] = '\0'; - - if (GetLogicalDriveStrings(sizeof(szTemp)-1, szTemp)) - { - TCHAR szName[MAX_PATH]; - TCHAR szDrive[3] = " :"; - BOOL bFound = FALSE; - TCHAR* p = szTemp; - - do - { - // Copy the drive letter to the template string - *szDrive = *p; - - // Look up each device name - if (QueryDosDevice(szDrive, szName, MAX_PATH)) - table[szName] = szDrive; - - // Go to the next NULL character. - while (*p++); - } while (*p); // end of string - } -} - -void Process::getMemRanges( vector & ranges ) -{ - MEMORY_BASIC_INFORMATION MBI; - //map heaps; - uint64_t movingStart = 0; - PVOID LastAllocationBase = 0; - map nameMap; - map dosDrives; - - // get page size - SYSTEM_INFO si; - GetSystemInfo(&si); - uint64_t PageSize = si.dwPageSize; - - // get dos drive names - GetDosNames(dosDrives); - - ranges.clear(); - - HANDLE my_handle = GetCurrentProcess(); - - // enumerate heaps - // HeapNodes(d->my_pid, heaps); - // go through all the VM regions, convert them to our internal format - while (VirtualQueryEx(my_handle, (const void*) (movingStart), &MBI, sizeof(MBI)) == sizeof(MBI)) - { - movingStart = ((uint64_t)MBI.BaseAddress + MBI.RegionSize); - if(movingStart % PageSize != 0) - movingStart = (movingStart / PageSize + 1) * PageSize; - - // Skip unallocated address space - if (MBI.State & MEM_FREE) - continue; - - // Find range and permissions - t_memrange temp; - memset(&temp, 0, sizeof(temp)); - - temp.start = (char *) MBI.BaseAddress; - temp.end = ((char *)MBI.BaseAddress + (uint64_t)MBI.RegionSize); - temp.valid = true; - - if (!(MBI.State & MEM_COMMIT)) - temp.valid = false; // reserved address space - else if (MBI.Protect & PAGE_EXECUTE) - temp.execute = true; - else if (MBI.Protect & PAGE_EXECUTE_READ) - temp.execute = temp.read = true; - else if (MBI.Protect & PAGE_EXECUTE_READWRITE) - temp.execute = temp.read = temp.write = true; - else if (MBI.Protect & PAGE_EXECUTE_WRITECOPY) - temp.execute = temp.read = temp.write = true; - else if (MBI.Protect & PAGE_READONLY) - temp.read = true; - else if (MBI.Protect & PAGE_READWRITE) - temp.read = temp.write = true; - else if (MBI.Protect & PAGE_WRITECOPY) - temp.read = temp.write = true; - - // Merge areas with the same properties - if (!ranges.empty() && LastAllocationBase == MBI.AllocationBase) - { - auto &last = ranges.back(); - - if (last.end == temp.start && - last.valid == temp.valid && last.execute == temp.execute && - last.read == temp.read && last.write == temp.write) - { - last.end = temp.end; - continue; - } - } - -#if 1 - // Find the mapped file name - if (GetMappedFileName(my_handle, temp.start, temp.name, 1024)) - { - int vsize = strlen(temp.name); - - // Translate NT name to DOS name - for (auto it = dosDrives.begin(); it != dosDrives.end(); ++it) - { - int ksize = it->first.size(); - if (strncmp(temp.name, it->first.data(), ksize) != 0) - continue; - - memcpy(temp.name, it->second.data(), it->second.size()); - memmove(temp.name + it->second.size(), temp.name + ksize, vsize + 1 - ksize); - break; - } - } - else - temp.name[0] = 0; -#else - // Find the executable name - char *base = (char*)MBI.AllocationBase; - - if(nameMap.count(base)) - { - strncpy(temp.name, nameMap[base].c_str(), 1023); - } - else if(GetModuleBaseName(d->my_handle, (HMODULE)base, temp.name, 1024)) - { - std::string nm(temp.name); - - nameMap[base] = nm; - - // this is our executable! (could be generalized to pull segments from libs, but whatever) - if(d->base == base) - { - for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) - { - /*char sectionName[9]; - memcpy(sectionName,d->sections[i].Name,8); - sectionName[8] = 0; - string nm; - nm.append(temp.name); - nm.append(" : "); - nm.append(sectionName);*/ - nameMap[base + d->sections[i].VirtualAddress] = nm; - } - } - } - else - temp.name[0] = 0; -#endif - - // Push the entry - LastAllocationBase = MBI.AllocationBase; - ranges.push_back(temp); - } -} - -uintptr_t Process::getBase() -{ - if(d) - return (uintptr_t) d->base; - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool to_file) -{ - if (!d) - return -1; - - for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) - { - auto §ion = d->sections[i]; - - if (to_file) - { - unsigned delta = offset - section.VirtualAddress; - if (delta >= section.Misc.VirtualSize) - continue; - if (!section.PointerToRawData || delta >= section.SizeOfRawData) - return -1; - return (int)(section.PointerToRawData + delta); - } - else - { - unsigned delta = offset - section.PointerToRawData; - if (!section.PointerToRawData || delta >= section.SizeOfRawData) - continue; - if (delta >= section.Misc.VirtualSize) - return -1; - return (int)(section.VirtualAddress + delta); - } - } - - return -1; -} - -uint32_t Process::getTickCount() -{ - return GetTickCount(); -} - -std::filesystem::path Process::getPath() -{ - return Filesystem::get_initial_cwd(); -} - -int Process::getPID() -{ - return (int) GetCurrentProcessId(); -} - - -bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) -{ - DWORD newprotect=0; - if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; - if(trgrange.read && trgrange.write && !trgrange.execute)newprotect=PAGE_READWRITE; - if(!trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE; - if(trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READ; - if(trgrange.read && trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READWRITE; - DWORD oldprotect=0; - bool result; - result=VirtualProtect((LPVOID)range.start,(char *)range.end-(char *)range.start,newprotect,&oldprotect); - - return result; -} - -bool Process::flushCache(const void* target, size_t count) -{ - return 0 != FlushInstructionCache(d->my_handle, (LPCVOID)target, count); -} - -void* Process::memAlloc(const int length) -{ - void *ret; - // returns 0 on error - ret = VirtualAlloc(0, length, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); - if (!ret) - ret = (void*)-1; - return ret; -} - -int Process::memDealloc(void *ptr, const int length) -{ - // can only free the whole region at once - // vfree returns 0 on error - return !VirtualFree(ptr, 0, MEM_RELEASE); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - DWORD old_prot = 0; - - // only support a few constant combinations - if (prot == 0) - prot_native = PAGE_NOACCESS; - else if (prot == Process::MemProt::READ) - prot_native = PAGE_READONLY; - else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE)) - prot_native = PAGE_READWRITE; - else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE | Process::MemProt::EXEC)) - prot_native = PAGE_EXECUTE_READWRITE; - else if (prot == (Process::MemProt::READ | Process::MemProt::EXEC)) - prot_native = PAGE_EXECUTE_READ; - else - return -1; - - return !VirtualProtect(ptr, length, prot_native, &old_prot); -} diff --git a/library/Process.cpp b/library/Process.cpp new file mode 100644 index 0000000000..cc31b0a056 --- /dev/null +++ b/library/Process.cpp @@ -0,0 +1,801 @@ +/* +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. +*/ + +#ifndef WIN32 +#ifndef _DARWIN +#endif /* ! WIN32 */ +#include +#ifndef WIN32 +#endif /* ! _DARWIN */ +#endif /* ! WIN32 */ +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#include +#include +#include +#include +#ifdef _DARWIN +#include + +#include +#include +#include +#include +#include +#include + +#endif /* _DARWIN */ + +#endif /* ! WIN32 */ +#include "Error.h" +#include "Internal.h" +#include "MemAccess.h" +#include "Memory.h" +#include "MiscUtils.h" +#include "VersionInfo.h" +#include "VersionInfoFactory.h" +#include "modules/Filesystem.h" +#ifndef WIN32 +#include "md5wrapper.h" +#else /* WIN32 */ + +#include + +#define _WIN32_WINNT 0x0600 +#define WINVER 0x0600 +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif /* WIN32 */ + +using namespace DFHack; + +using std::string; +using std::map; +using std::vector; +using std::endl; +using std::cerr; + +#ifdef WIN32 +namespace DFHack +{ + class PlatformSpecific + { + public: + PlatformSpecific() + { + base = 0; + sections = 0; + }; + HANDLE my_handle; + uint32_t my_pid; + IMAGE_NT_HEADERS pe_header; + IMAGE_SECTION_HEADER * sections; + char * base; + }; +} + +#endif /* WIN32 */ +Process::Process(const VersionInfoFactory& known_versions) : identified(false) +{ +#ifndef WIN32 + std::string self_exe_name; +#ifndef _DARWIN + const char * exe_link_name = "/proc/self/exe"; + + // valgrind replaces readlink for /proc/self/exe, but not open. + char self_exe[1024]; + memset(self_exe, 0, sizeof(self_exe)); + if (readlink(exe_link_name, self_exe, sizeof(self_exe) - 1) < 0) + self_exe_name = exe_link_name; + else + self_exe_name = self_exe; +#else /* _DARWIN */ + char path[1024]; + char *exec_link_name; + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0) { + exec_link_name = realpath(path, NULL); + self_exe_name = std::string(exec_link_name); +#endif /* _DARWIN */ + } +#else /* WIN32 */ + HMODULE hmod = NULL; + DWORD needed; + bool found = false; + + d = new PlatformSpecific(); + // open process + d->my_pid = GetCurrentProcessId(); + d->my_handle = GetCurrentProcess(); + // try getting the first module of the process + if (EnumProcessModules(d->my_handle, &hmod, sizeof(hmod), &needed) == 0) + { + return; //if enumprocessModules fails, give up + } + // got base ;) + d->base = (char*)hmod; + + // read from this process + try + { + uint32_t pe_offset = readDWord(d->base + 0x3C); + read(d->base + pe_offset, sizeof(d->pe_header), (uint8_t*)&(d->pe_header)); + const size_t sectionsSize = sizeof(IMAGE_SECTION_HEADER) * d->pe_header.FileHeader.NumberOfSections; + d->sections = (IMAGE_SECTION_HEADER*)malloc(sectionsSize); + read(d->base + pe_offset + sizeof(d->pe_header), sectionsSize, (uint8_t*)(d->sections)); + } + catch (std::exception&) + { + return; + } +#endif /* WIN32 */ + +#ifndef WIN32 + my_pe = 0; + md5wrapper md5; + uint32_t length; + uint8_t first_kb [1024]; + memset(first_kb, 0, sizeof(first_kb)); + // get hash of the running DF process + my_md5 = md5.getHashFromFile(self_exe_name, length, (char *) first_kb); + // create linux process, add it to the vector + auto vinfo = known_versions.getVersionInfoByMD5(my_md5); +#else /* WIN32 */ + my_pe = d->pe_header.FileHeader.TimeDateStamp; + auto vinfo = known_versions.getVersionInfoByPETimestamp(my_pe); +#endif /* WIN32 */ + if(vinfo) + { + identified = true; + my_descriptor = std::make_shared(*vinfo); +#ifdef WIN32 + // give the process a data model and memory layout fixed for the base of first module + my_descriptor->rebaseTo(getBase()); +#endif /* WIN32 */ + } + else + { +#ifndef WIN32 + char * wd = getcwd(NULL, 0); +#endif /* ! WIN32 */ + cerr << "Unable to retrieve version information.\n"; +#ifndef WIN32 + cerr << "File: " << exe_link_name << endl; + cerr << "MD5: " << my_md5 << endl; + cerr << "working dir: " << wd << endl; + cerr << "length:" << length << endl; + cerr << "1KB hexdump follows:" << endl; + for(int i = 0; i < 64; i++) + { + fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + first_kb[i*16], + first_kb[i*16+1], + first_kb[i*16+2], + first_kb[i*16+3], + first_kb[i*16+4], + first_kb[i*16+5], + first_kb[i*16+6], + first_kb[i*16+7], + first_kb[i*16+8], + first_kb[i*16+9], + first_kb[i*16+10], + first_kb[i*16+11], + first_kb[i*16+12], + first_kb[i*16+13], + first_kb[i*16+14], + first_kb[i*16+15] + ); + } + free(wd); +#else /* WIN32 */ + cerr << "PE timestamp: " << std::format("{:#0x}", my_pe) << endl; +#endif /* WIN32 */ + } +} + +Process::~Process() +{ +#ifndef WIN32 + // Nothing to do here +#else /* WIN32 */ + // destroy our rebased copy of the memory descriptor + if(d->sections != NULL) + free(d->sections); +#endif /* WIN32 */ +} + +string Process::doReadClassName (void * vptr) +{ + char* rtti = Process::readPtr(((char*)vptr - sizeof(void*))); +#ifndef WIN32 + char* typestring = Process::readPtr(rtti + sizeof(void*)); +#else /* WIN32 */ +#ifdef DFHACK64 + void* base; + if (!RtlPcToFileHeader(rtti, &base)) + return "dummy"; + char* typeinfo = (char *)base + readDWord(rtti + 0xC); + char* typestring = typeinfo + 0x10; +#else + char* typeinfo = readPtr(rtti + 0xC); + char* typestring = typeinfo + 0x8; +#endif +#endif /* WIN32 */ + std::string raw = readCString(typestring); + if (raw.length() == 0) + return "dummy"; + + string status; + string demangled = cxx_demangle(raw, &status); + + if (demangled.length() == 0) { + return "dummy"; + } + + return demangled; +} + +#ifndef WIN32 +#ifndef _DARWIN +//FIXME: cross-reference with ELF segment entries? +#else /* _DARWIN */ +const char* +inheritance_strings[] = { + "SHARE", "COPY", "NONE", "DONATE_COPY", +}; + +const char* +behavior_strings[] = { + "DEFAULT", "RANDOM", "SEQUENTIAL", "RESQNTL", "WILLNEED", "DONTNEED", +}; + +#endif /* _DARWIN */ +void Process::getMemRanges(vector& ranges) +{ +#ifndef _DARWIN + char buffer[1024]; + char permissions[5]; // r/-, w/-, x/-, p/s, 0 +#else /* _DARWIN */ + static bool log_ranges = (getenv("DFHACK_LOG_MEM_RANGES") != NULL); + + kern_return_t kr; + task_t the_task; +#endif /* _DARWIN */ + +#ifndef _DARWIN + FILE* mapFile = ::fopen("/proc/self/maps", "r"); + if (!mapFile) + return; +#else /* _DARWIN */ + the_task = mach_task_self(); +#endif /* _DARWIN */ + +#ifndef _DARWIN + size_t start, end, offset, device1, device2, node; +#else /* _DARWIN */ +#ifdef DFHACK64 + mach_vm_size_t vmsize; + mach_vm_address_t address; + vm_region_basic_info_data_64_t info; +#else + vm_size_t vmsize; + vm_address_t address; + vm_region_basic_info_data_t info; +#endif + mach_msg_type_number_t info_count; + vm_region_flavor_t flavor; + memory_object_name_t object; + + kr = KERN_SUCCESS; + address = 0; +#endif /* _DARWIN */ + + string cur_name; + void* cur_base = nullptr; + +#ifndef _DARWIN + while (fgets(buffer, 1024, mapFile)) + { +#else /* _DARWIN */ + do { +#ifdef DFHACK64 + flavor = VM_REGION_BASIC_INFO_64; + info_count = VM_REGION_BASIC_INFO_COUNT_64; + kr = mach_vm_region(the_task, &address, &vmsize, flavor, + (vm_region_info_64_t)&info, &info_count, &object); +#else + flavor = VM_REGION_BASIC_INFO; + info_count = VM_REGION_BASIC_INFO_COUNT; + kr = vm_region(the_task, &address, &vmsize, flavor, + (vm_region_info_t)&info, &info_count, &object); +#endif + + if (kr == KERN_INVALID_ADDRESS) + break; + if (kr != KERN_SUCCESS) + { + + /*if (the_task != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), the_task); + }*/ + return; + } + if (info.reserved == 1) { + address += vmsize; + continue; + } + Dl_info dlinfo; + int dlcheck; + dlcheck = dladdr((const void*)address, &dlinfo); + if (dlcheck == 0) { + dlinfo.dli_fname = ""; + } + +#endif /* _DARWIN */ + t_memrange temp; +#ifndef _DARWIN + temp.name[0] = 0; + sscanf(buffer, "%zx-%zx %s %zx %zx:%zx %zu %[^\n]", + &start, + &end, + (char*)&permissions, + &offset, &device1, &device2, &node, + (char*)temp.name); +#else /* _DARWIN */ + strncpy(temp.name, dlinfo.dli_fname, 1023); + temp.name[1023] = 0; +#endif /* _DARWIN */ + if (cur_name != temp.name) { + cur_name = temp.name; +#ifndef _DARWIN + cur_base = (void*)start; +#else /* _DARWIN */ + cur_base = (void*)address; +#endif /* _DARWIN */ + } + temp.base = cur_base; +#ifndef _DARWIN + temp.start = (void*)start; + temp.end = (void*)end; + temp.read = permissions[0] == 'r'; + temp.write = permissions[1] == 'w'; + temp.execute = permissions[2] == 'x'; + temp.shared = permissions[3] == 's'; +#else /* _DARWIN */ + temp.start = (void*)address; + temp.end = (void*)(address + vmsize); + temp.read = (info.protection & VM_PROT_READ); + temp.write = (info.protection & VM_PROT_WRITE); + temp.execute = (info.protection & VM_PROT_EXECUTE); + temp.shared = info.shared; +#endif /* _DARWIN */ + temp.valid = true; + ranges.push_back(temp); +#ifndef _DARWIN + } +#endif /* ! _DARWIN */ + +#ifndef _DARWIN + fclose(mapFile); +#else /* _DARWIN */ + if (log_ranges) + { + fprintf(stderr, + "%p-%p %8zuK %c%c%c/%c%c%c %11s %6s %10s uwir=%hu sub=%u dlname: %s\n", + (void*)address, + (void*)(address + vmsize), + size_t(vmsize >> 10), + (info.protection & VM_PROT_READ) ? 'r' : '-', + (info.protection & VM_PROT_WRITE) ? 'w' : '-', + (info.protection & VM_PROT_EXECUTE) ? 'x' : '-', + (info.max_protection & VM_PROT_READ) ? 'r' : '-', + (info.max_protection & VM_PROT_WRITE) ? 'w' : '-', + (info.max_protection & VM_PROT_EXECUTE) ? 'x' : '-', + inheritance_strings[info.inheritance], + (info.shared) ? "shared" : "-", + behavior_strings[info.behavior], + info.user_wired_count, + info.reserved, + dlinfo.dli_fname); + } + + address += vmsize; + } while (kr != KERN_INVALID_ADDRESS); + + +/* if (the_task != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), the_task); + }*/ +#endif /* _DARWIN */ +} +#else /* WIN32 */ +/* +typedef struct _MEMORY_BASIC_INFORMATION +{ + void * BaseAddress; + void * AllocationBase; + uint32_t AllocationProtect; + size_t RegionSize; + uint32_t State; + uint32_t Protect; + uint32_t Type; +} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; +*/ +/* +//Internal structure used to store heap block information. +struct HeapBlock +{ + PVOID dwAddress; + DWORD dwSize; + DWORD dwFlags; + ULONG reserved; +}; +*/ + +static void GetDosNames(std::map& table) +{ + // Partially based on example from msdn: + // Translate path with device name to drive letters. + TCHAR szTemp[512]; + szTemp[0] = '\0'; + + if (GetLogicalDriveStrings(sizeof(szTemp) - 1, szTemp)) + { + TCHAR szName[MAX_PATH]; + TCHAR szDrive[3] = " :"; + BOOL bFound = FALSE; + TCHAR* p = szTemp; + + do + { + // Copy the drive letter to the template string + *szDrive = *p; + + // Look up each device name + if (QueryDosDevice(szDrive, szName, MAX_PATH)) + table[szName] = szDrive; + + // Go to the next NULL character. + while (*p++); + } while (*p); // end of string + } +} + +void Process::getMemRanges(vector& ranges) +{ + MEMORY_BASIC_INFORMATION MBI; + //map heaps; + uint64_t movingStart = 0; + PVOID LastAllocationBase = 0; + map nameMap; + map dosDrives; + + // get page size + SYSTEM_INFO si; + GetSystemInfo(&si); + uint64_t PageSize = si.dwPageSize; + + // get dos drive names + GetDosNames(dosDrives); + + ranges.clear(); + + HANDLE my_handle = GetCurrentProcess(); + + // enumerate heaps + // HeapNodes(d->my_pid, heaps); + // go through all the VM regions, convert them to our internal format + while (VirtualQueryEx(my_handle, (const void*)(movingStart), &MBI, sizeof(MBI)) == sizeof(MBI)) + { + t_memrange temp; + movingStart = ((uint64_t)MBI.BaseAddress + MBI.RegionSize); + if (movingStart % PageSize != 0) + movingStart = (movingStart / PageSize + 1) * PageSize; + + // Skip unallocated address space + if (MBI.State & MEM_FREE) + continue; + + // Find range and permissions + memset(&temp, 0, sizeof(temp)); + + temp.start = (char*)MBI.BaseAddress; + temp.end = ((char*)MBI.BaseAddress + (uint64_t)MBI.RegionSize); + temp.valid = true; + + if (!(MBI.State & MEM_COMMIT)) + temp.valid = false; // reserved address space + else if (MBI.Protect & PAGE_EXECUTE) + temp.execute = true; + else if (MBI.Protect & PAGE_EXECUTE_READ) + temp.execute = temp.read = true; + else if (MBI.Protect & PAGE_EXECUTE_READWRITE) + temp.execute = temp.read = temp.write = true; + else if (MBI.Protect & PAGE_EXECUTE_WRITECOPY) + temp.execute = temp.read = temp.write = true; + else if (MBI.Protect & PAGE_READONLY) + temp.read = true; + else if (MBI.Protect & PAGE_READWRITE) + temp.read = temp.write = true; + else if (MBI.Protect & PAGE_WRITECOPY) + temp.read = temp.write = true; + + // Merge areas with the same properties + if (!ranges.empty() && LastAllocationBase == MBI.AllocationBase) + { + auto& last = ranges.back(); + + if (last.end == temp.start && + last.valid == temp.valid && last.execute == temp.execute && + last.read == temp.read && last.write == temp.write) + { + last.end = temp.end; + continue; + } + } + + // Find the mapped file name + if (GetMappedFileName(my_handle, temp.start, temp.name, 1024)) + { + int vsize = strlen(temp.name); + + // Translate NT name to DOS name + for (auto it = dosDrives.begin(); it != dosDrives.end(); ++it) + { + int ksize = it->first.size(); + if (strncmp(temp.name, it->first.data(), ksize) != 0) + continue; + + memcpy(temp.name, it->second.data(), it->second.size()); + memmove(temp.name + it->second.size(), temp.name + ksize, vsize + 1 - ksize); + break; + } + } + else + temp.name[0] = 0; + + // Push the entry + LastAllocationBase = MBI.AllocationBase; + ranges.push_back(temp); + } +} +#endif + +uintptr_t Process::getBase() +{ +#if WIN32 + if(d) + return (uintptr_t) d->base; +#endif /* WIN32 */ + return DEFAULT_BASE_ADDR; // Memory.h +} + +int Process::adjustOffset(int offset, [[maybe_unused]] bool to_file) +{ +#ifndef WIN32 + return offset; +#else /* WIN32 */ + if (!d) + return -1; + + for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) + { + auto §ion = d->sections[i]; + + if (to_file) + { + unsigned delta = offset - section.VirtualAddress; + if (delta >= section.Misc.VirtualSize) + continue; + if (!section.PointerToRawData || delta >= section.SizeOfRawData) + return -1; + return (int)(section.PointerToRawData + delta); + } + else + { + unsigned delta = offset - section.PointerToRawData; + if (!section.PointerToRawData || delta >= section.SizeOfRawData) + continue; + if (delta >= section.Misc.VirtualSize) + return -1; + return (int)(section.VirtualAddress + delta); + } + } + + return -1; +#endif /* WIN32 */ +} + +uint32_t Process::getTickCount() +{ +#ifndef WIN32 + struct timeval tp; + gettimeofday(&tp, NULL); + return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); +#else /* WIN32 */ + return GetTickCount(); +#endif /* WIN32 */ +} + +std::filesystem::path Process::getPath() +{ +#if defined(WIN32) || !defined(_DARWIN) + return Filesystem::get_initial_cwd(); +#else /* _DARWIN */ + static string cached_path = ""; + if (cached_path.size()) + return cached_path; + char path[1024]; + char *real_path; + uint32_t size = sizeof(path); + if (getcwd(path, size)) + { + cached_path = string(path); + return cached_path; + } + if (_NSGetExecutablePath(path, &size) == 0) { + real_path = realpath(path, NULL); + } + else { + fprintf(stderr, "_NSGetExecutablePath failed!\n"); + cached_path = "."; + return cached_path; + } + std::string path_string(real_path); + int last_slash = path_string.find_last_of("/"); + cached_path = path_string.substr(0,last_slash); + return cached_path; +#endif /* _DARWIN */ +} + +int Process::getPID() +{ +#ifndef WIN32 + return getpid(); +#else /* WIN32 */ + return (int) GetCurrentProcessId(); +#endif /* WIN32 */ +} + +#ifdef WIN32 + +#endif /* WIN32 */ +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) +{ +#ifndef WIN32 + int result; + int protect=0; + if(trgrange.read)protect|=PROT_READ; + if(trgrange.write)protect|=PROT_WRITE; + if(trgrange.execute)protect|=PROT_EXEC; + result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); +#else /* WIN32 */ + DWORD newprotect=0; + if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; + if(trgrange.read && trgrange.write && !trgrange.execute)newprotect=PAGE_READWRITE; + if(!trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE; + if(trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READ; + if(trgrange.read && trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READWRITE; + DWORD oldprotect=0; + bool result; + result=VirtualProtect((LPVOID)range.start,(char *)range.end-(char *)range.start,newprotect,&oldprotect); +#endif /* WIN32 */ + +#ifndef WIN32 + return result==0; +#else /* WIN32 */ + return result; +#endif /* WIN32 */ +} + +bool Process::flushCache(const void* target, size_t count) +{ +#ifndef WIN32 +#ifndef _DARWIN + __builtin___clear_cache((char*)target, (char*)target + count - 1); + return true; /* assume always succeeds, as the builtin has no return type */ +#else /* _DARWIN */ + // FIXME: implement cache flush for MacOS (???) + return false; +#endif /* _DARWIN */ +#else /* WIN32 */ + return 0 != FlushInstructionCache(d->my_handle, (LPCVOID)target, count); +#endif /* WIN32 */ +} + +#ifndef WIN32 +// returns -1 on error +#endif /* ! WIN32 */ +void* Process::memAlloc(const int length) +{ +#ifndef WIN32 +#ifndef _DARWIN + return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); +#else /* _DARWIN */ + return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); +#endif /* _DARWIN */ +#else /* WIN32 */ + void *ret; + // returns 0 on error + ret = VirtualAlloc(0, length, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); + if (!ret) + ret = (void*)-1; + return ret; +#endif /* WIN32 */ +} + +int Process::memDealloc(void *ptr, const int length) +{ +#ifndef WIN32 + return munmap(ptr, length); +#else /* WIN32 */ + // can only free the whole region at once + // vfree returns 0 on error + return !VirtualFree(ptr, 0, MEM_RELEASE); +#endif /* WIN32 */ +} + +int Process::memProtect(void *ptr, const int length, const int prot) +{ + int prot_native = 0; + +#ifndef WIN32 + if (prot & Process::MemProt::READ) + prot_native |= PROT_READ; + if (prot & Process::MemProt::WRITE) + prot_native |= PROT_WRITE; + if (prot & Process::MemProt::EXEC) + prot_native |= PROT_EXEC; +#else /* WIN32 */ + // only support a few constant combinations + if (prot == 0) + prot_native = PAGE_NOACCESS; + else if (prot == Process::MemProt::READ) + prot_native = PAGE_READONLY; + else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE)) + prot_native = PAGE_READWRITE; + else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE | Process::MemProt::EXEC)) + prot_native = PAGE_EXECUTE_READWRITE; + else if (prot == (Process::MemProt::READ | Process::MemProt::EXEC)) + prot_native = PAGE_EXECUTE_READ; + else + return -1; +#endif /* WIN32 */ + +#ifndef WIN32 + return mprotect(ptr, length, prot_native); +#else /* WIN32 */ + DWORD old_prot = 0; + return !VirtualProtect(ptr, length, prot_native, &old_prot); +#endif /* WIN32 */ +} From 21ccb6acd71164f3e0b2529c82560cb8aca7a4cd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 14:40:20 -0500 Subject: [PATCH 0338/1082] off by just a smidgen! --- library/Process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Process.cpp b/library/Process.cpp index cc31b0a056..7a6413c5dd 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -126,8 +126,8 @@ Process::Process(const VersionInfoFactory& known_versions) : identified(false) if (_NSGetExecutablePath(path, &size) == 0) { exec_link_name = realpath(path, NULL); self_exe_name = std::string(exec_link_name); -#endif /* _DARWIN */ } +#endif /* _DARWIN */ #else /* WIN32 */ HMODULE hmod = NULL; DWORD needed; From 09fa4a64288ff0323c6657fae11c0481cb3026ee Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 15:38:09 -0500 Subject: [PATCH 0339/1082] rejuggle `#ifdef` --- library/Process.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/library/Process.cpp b/library/Process.cpp index 7a6413c5dd..101eecbc71 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -24,9 +24,7 @@ distribution. #ifndef WIN32 #ifndef _DARWIN -#endif /* ! WIN32 */ #include -#ifndef WIN32 #endif /* ! _DARWIN */ #endif /* ! WIN32 */ #include @@ -43,19 +41,18 @@ distribution. #include #include #include + #ifdef _DARWIN #include - #include #include #include #include #include #include - #endif /* _DARWIN */ - #endif /* ! WIN32 */ + #include "Error.h" #include "Internal.h" #include "MemAccess.h" @@ -64,17 +61,17 @@ distribution. #include "VersionInfo.h" #include "VersionInfoFactory.h" #include "modules/Filesystem.h" + #ifndef WIN32 #include "md5wrapper.h" #else /* WIN32 */ - -#include - #define _WIN32_WINNT 0x0600 #define WINVER 0x0600 #define WIN32_LEAN_AND_MEAN #include #include + +#include #endif /* WIN32 */ using namespace DFHack; From 269eef80e31b897376846171da5f845a602ce639 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 3 May 2025 17:29:37 -0500 Subject: [PATCH 0340/1082] Bring back zones in `gui/blueprint` (WIP) (#5222) * Restore zones to blueprints * Begin refactor of get_zone_keys * more refactor * Make zones almost completely work * Update the zone keys * Update blueprint.cpp --- docs/plugins/blueprint.rst | 12 -- plugins/blueprint.cpp | 219 +++++++++++-------------------------- plugins/lua/blueprint.lua | 7 +- 3 files changed, 66 insertions(+), 172 deletions(-) diff --git a/docs/plugins/blueprint.rst b/docs/plugins/blueprint.rst index 1a5071b6be..2d84a7ca69 100644 --- a/docs/plugins/blueprint.rst +++ b/docs/plugins/blueprint.rst @@ -15,13 +15,6 @@ selected interactively with the `gui/blueprint` command or, if the GUI is not used, starts at the active cursor location and extends right and down for the requested width and height. -.. admonition:: Note - - blueprint is still in the process of being updated for the new version of - DF. Stockpiles (the "place" phase), zones (the "zone" phase), building - configuration (the "query" phase), and game configuration (the "config" - phase) are not yet supported. - Usage ----- @@ -83,11 +76,6 @@ phases; just separate them with a space. Generate quickfort ``#place`` blueprints for placing stockpiles. ``zone`` Generate quickfort ``#zone`` blueprints for designating zones. -``query`` - Generate quickfort ``#query`` blueprints for configuring stockpiles and - naming buildings. -``rooms`` - Generate quickfort ``#query`` blueprints for defining rooms. If no phases are specified, phases are autodetected. For example, a ``#place`` blueprint will be created only if there are stockpiles in the blueprint area. diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index b82442a66d..d9bff5f329 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -106,9 +106,8 @@ struct blueprint_options { bool construct = false; bool build = false; bool place = false; - // bool zone = false; - // bool query = false; - // bool rooms = false; + bool zone = false; + static struct_identity _identity; }; @@ -132,9 +131,7 @@ static const struct_field_info blueprint_options_fields[] = { { struct_field_info::PRIMITIVE, "construct", offsetof(blueprint_options, construct), &df::identity_traits::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "build", offsetof(blueprint_options, build), &df::identity_traits::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "place", offsetof(blueprint_options, place), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "zone", offsetof(blueprint_options, zone), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "query", offsetof(blueprint_options, query), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "rooms", offsetof(blueprint_options, rooms), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "zone", offsetof(blueprint_options, zone), &df::identity_traits::identity, 0, 0 }, { struct_field_info::END } }; struct_identity blueprint_options::_identity(sizeof(blueprint_options), &df::allocator_fn, NULL, "blueprint_options", NULL, blueprint_options_fields); @@ -1107,78 +1104,70 @@ static const char * get_tile_place(const df::coord &pos, return add_expansion_syntax(ctx, get_place_keys(ctx)); } -/* TODO: understand how this changes for v50 -static bool hospital_maximums_eq(const df::hospital_supplies &a, - const df::hospital_supplies &b) { - return a.max_thread == b.max_thread && - a.max_cloth == b.max_cloth && - a.max_splints == b.max_splints && - a.max_crutches == b.max_crutches && - a.max_plaster == b.max_plaster && - a.max_buckets == b.max_buckets && - a.max_soap == b.max_soap; -} - static const char * get_zone_keys(const df::building_civzonest *zone) { static const uint32_t DEFAULT_GATHER_FLAGS = - df::building_civzonest::T_gather_flags::mask_pick_trees | - df::building_civzonest::T_gather_flags::mask_pick_shrubs | - df::building_civzonest::T_gather_flags::mask_gather_fallen; - static const df::hospital_supplies DEFAULT_HOSPITAL; + df::civzone_gather_flag::mask_pick_trees | + df::civzone_gather_flag::mask_pick_shrubs | + df::civzone_gather_flag::mask_gather_fallen; ostringstream keys; - const df::building_civzonest::T_zone_flags &flags = zone->zone_flags; - - // inverted logic for Active since it's on by default - if (!flags.bits.active) keys << 'a'; - - // in UI order - if (flags.bits.water_source) keys << 'w'; - if (flags.bits.fishing) keys << 'f'; - if (flags.bits.gather) { - keys << 'g'; - if (zone->gather_flags.whole != DEFAULT_GATHER_FLAGS) { - keys << 'G'; - // logic is inverted since they're all on by default - if (!zone->gather_flags.bits.pick_trees) keys << 't'; - if (!zone->gather_flags.bits.pick_shrubs) keys << 's'; - if (!zone->gather_flags.bits.gather_fallen) keys << 'f'; - keys << '^'; - } + const df::civzone_type type = zone->type; + + // in DFHack docs order + if (type == df::civzone_type::MeetingHall) keys << 'm'; + if (type == df::civzone_type::Bedroom) keys << 'b'; + if (type == df::civzone_type::DiningHall) keys << 'h'; + if (type == df::civzone_type::Pen) keys << 'n'; + if (type == df::civzone_type::Pond) keys << 'p'; + if (type == df::civzone_type::WaterSource) keys << 'w'; + if (type == df::civzone_type::Dungeon) keys << 'j'; + if (type == df::civzone_type::FishingArea) keys << 'f'; + if (type == df::civzone_type::SandCollection) keys << 's'; + if (type == df::civzone_type::Office) keys << 'o'; + if (type == df::civzone_type::Dormitory) keys << 'D'; + if (type == df::civzone_type::Barracks) keys << 'B'; + if (type == df::civzone_type::ArcheryRange) keys << 'a'; + if (type == df::civzone_type::Dump) keys << 'd'; + if (type == df::civzone_type::AnimalTraining) keys << 't'; + if (type == df::civzone_type::Tomb) keys << 'T'; + if (type == df::civzone_type::PlantGathering) keys << 'g'; + if (type == df::civzone_type::ClayCollection) keys << 'c'; + + keys << '{'; + if (!zone->name.empty()) { + keys << "name=" << zone->name << ' '; } - if (flags.bits.garbage_dump) keys << 'd'; - if (flags.bits.pen_pasture) keys << 'n'; - if (flags.bits.pit_pond) { - keys << 'p'; - if (zone->pit_flags.bits.is_pond) - keys << "Pf^"; + if (!zone->spec_sub_flag.bits.active) { + keys << "active=false "; } - if (flags.bits.sand) keys << 's'; - if (flags.bits.clay) keys << 'c'; - if (flags.bits.meeting_area) keys << 'm'; - if (flags.bits.hospital) { - keys << 'h'; - const df::hospital_supplies &hospital = zone->hospital; - if (!hospital_maximums_eq(hospital, DEFAULT_HOSPITAL)) { - keys << "H{hospital"; - if (hospital.max_thread != DEFAULT_HOSPITAL.max_thread) - keys << " thread=" << hospital.max_thread; - if (hospital.max_cloth != DEFAULT_HOSPITAL.max_cloth) - keys << " cloth=" << hospital.max_cloth; - if (hospital.max_splints != DEFAULT_HOSPITAL.max_splints) - keys << " splints=" << hospital.max_splints; - if (hospital.max_crutches != DEFAULT_HOSPITAL.max_crutches) - keys << " crutches=" << hospital.max_crutches; - if (hospital.max_plaster != DEFAULT_HOSPITAL.max_plaster) - keys << " plaster=" << hospital.max_plaster; - if (hospital.max_buckets != DEFAULT_HOSPITAL.max_buckets) - keys << " buckets=" << hospital.max_buckets; - if (hospital.max_soap != DEFAULT_HOSPITAL.max_soap) - keys << " soap=" << hospital.max_soap; - keys << "}^"; - } + if (zone->assigned_unit) { + keys << "assigned_unit=" << zone->assigned_unit << ' '; + } + if (!zone->zone_settings.pond.flag.bits.keep_filled) { + keys << "pond=false "; + } + auto archery_settings = zone->zone_settings.archery; // need this to get the `shoot_from` direction + if (archery_settings.dir_y == 0) { + if (archery_settings.dir_x == 1) keys << "shoot_from=west "; + if (archery_settings.dir_x == -1) keys << "shoot_from=east "; + } + if (archery_settings.dir_x == 0) { + if (archery_settings.dir_y == 1) keys << "shoot_from=north "; + if (archery_settings.dir_y == -1) keys << "shoot_from=south "; + } + //FIXEME (Squid): Need to know how to get the location data here + if (!zone->zone_settings.tomb.flags.bits.no_pets) { + keys << "pets=true "; + } + if (zone->zone_settings.tomb.flags.bits.no_citizens) { + keys << "citizens=false "; + } + if (zone->zone_settings.gather.flags.whole != DEFAULT_GATHER_FLAGS) { + // logic is inverted since they're all on by default + if (!zone->zone_settings.gather.flags.bits.pick_trees) keys << "pick_trees=false "; + if (!zone->zone_settings.gather.flags.bits.pick_shrubs) keys << "pick_shrubs=false "; + if (!zone->zone_settings.gather.flags.bits.gather_fallen) keys << "gather_fallen=false "; } - if (flags.bits.animal_training) keys << 't'; string keys_str = keys.str(); @@ -1187,8 +1176,11 @@ static const char * get_zone_keys(const df::building_civzonest *zone) { return NULL; // remove final '^' character if there is one - if (keys_str.back() == '^') + if (keys_str.back() == '{') { keys_str.pop_back(); + } else { + keys << '}'; + } return cache(keys_str); } @@ -1215,84 +1207,7 @@ static const char * get_tile_zone(const df::coord &pos, return add_expansion_syntax(zone, get_zone_keys(zone)); } -// surrounds the given string in quotes and replaces internal double quotes (") -// with double double quotes ("") (as per the csv spec) -static string csv_quote(const string &str) { - ostringstream outstr; - outstr << "\""; - - size_t start = 0; - auto end = str.find('"'); - while (end != string::npos) { - outstr << str.substr(start, end - start); - outstr << "\"\""; - start = end + 1; - end = str.find('"', start); - } - outstr << str.substr(start, end) << "\""; - - return outstr.str(); -} - -static const char * get_tile_query(const df::coord &pos, - const tile_context &ctx) { - string bld_name, zone_name; - auto & seen = ctx.processor->seen; - - if (ctx.b && !seen.count(ctx.b)) { - bld_name = ctx.b->name; - seen.emplace(ctx.b); - } - - vector civzones; - if (Buildings::findCivzonesAt(&civzones, pos)) { - auto civzone = civzones.back(); - if (!seen.count(civzone)) { - zone_name = civzone->name; - seen.emplace(civzone); - } - } - - if (!bld_name.size() && !zone_name.size()) - return NULL; - - ostringstream str; - if (bld_name.size()) - str << "{givename name=" + csv_quote(bld_name) + "}"; - if (zone_name.size()) - str << "{namezone name=" + csv_quote(zone_name) + "}"; - - return cache(csv_quote(str.str())); -} - -static const char * get_tile_rooms(const df::coord &, const tile_context &ctx) { - if (!ctx.b || !ctx.b->is_room) - return NULL; - // get the maximum distance from the center of the building - df::building_extents &room = ctx.b->room; - int32_t x1 = room.x; - int32_t x2 = room.x + room.width - 1; - int32_t y1 = room.y; - int32_t y2 = room.y + room.height - 1; - - int32_t dimx = std::max(ctx.b->centerx - x1, x2 - ctx.b->centerx); - int32_t dimy = std::max(ctx.b->centery - y1, y2 - ctx.b->centery); - int32_t max_dim = std::max(dimx, dimy); - - switch (max_dim) { - case 0: return "r---&"; - case 1: return "r--&"; - case 2: return "r-&"; - case 3: return "r&"; - case 4: return "r+&"; - } - - ostringstream str; - str << "r{+ " << (max_dim - 3) << "}&"; - return cache(str); -} -*/ static bool create_output_dir(color_ostream &out, const blueprint_options &opts) { @@ -1503,13 +1418,7 @@ static bool do_transform(color_ostream &out, get_tile_build, ensure_building); add_processor(processors, opts, "place", "place", opts.place, get_tile_place, ensure_building); -/* TODO: understand how this changes for v50 add_processor(processors, opts, "zone", "zone", opts.zone, get_tile_zone); - add_processor(processors, opts, "query", "query", opts.query, - get_tile_query, ensure_building); - add_processor(processors, opts, "query", "rooms", opts.rooms, - get_tile_rooms, ensure_building); -*/ if (processors.empty()) { out.printerr("no phases requested! nothing to do!\n"); return false; diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index dfe8f4eae6..e43ce5f5f6 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -9,17 +9,14 @@ local valid_phase_list = { 'construct', 'build', 'place', - -- 'zone', - -- 'query', - -- 'rooms', + 'zone', } valid_phases = utils.invert(valid_phase_list) local meta_phase_list = { 'build', 'place', - -- 'zone', - -- 'query', + 'zone', } meta_phases = utils.invert(meta_phase_list) From f8af00b7870c0fb1cb7d9d13da9f78aa4a681713 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 15:30:24 -0700 Subject: [PATCH 0341/1082] record zone information in blueprints can't handle disjoint zones or overlapping non-rectangular zones yet disjoint zone support coming in a follow-up PR need to think about overlapping non-rectangular zones more, though --- plugins/blueprint.cpp | 256 +++++++++++++++++++++---------------- plugins/preserve-rooms.cpp | 15 ++- 2 files changed, 161 insertions(+), 110 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index d9bff5f329..6ec9b9c3c4 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -21,6 +21,7 @@ #include "modules/Buildings.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Maps.h" #include "modules/World.h" #include "df/building_axle_horizontalst.h" @@ -36,6 +37,7 @@ #include "df/building_water_wheelst.h" #include "df/building_workshopst.h" #include "df/engraving.h" +#include "df/entity_position.h" #include "df/tile_bitmask.h" #include "df/tile_designation.h" #include "df/tile_occupancy.h" @@ -161,8 +163,7 @@ typedef vector bp_row; // index is x coordinate typedef map bp_area; // key is y coordinate typedef map bp_volume; // key is z coordinate -typedef const char * (get_tile_fn)(const df::coord &pos, - const tile_context &ctx); +typedef const char * (get_tile_fn)(color_ostream &out, const df::coord &pos, const tile_context &ctx); typedef void (init_ctx_fn)(const df::coord &pos, tile_context &ctx); struct blueprint_processor { @@ -334,7 +335,7 @@ static const char * get_tile_dig_job(df::tile_designation *td, df::job *job) { } } -static const char * get_tile_dig(const df::coord &pos, const tile_context &) { +static const char * get_tile_dig(color_ostream &out, const df::coord &pos, const tile_context &) { df::tile_designation *td = Maps::getTileDesignation(pos); if (td && td->bits.dig != df::tile_dig_designation::No) return add_markers(pos, get_tile_dig_designation(pos, td->bits.dig)); @@ -373,8 +374,7 @@ static const char * get_tile_dig(const df::coord &pos, const tile_context &) { } } -static const char * get_tile_smooth_minimal(const df::coord &pos, - const tile_context &) { +static const char * get_tile_smooth_minimal(color_ostream &out, const df::coord &pos, const tile_context &) { if (dig_job_cache.contains(pos) && dig_job_cache[pos]->job_type == df::job_type::CarveFortification) return "s"; @@ -398,9 +398,8 @@ static const char * get_tile_smooth_minimal(const df::coord &pos, return NULL; } -static const char * get_tile_smooth_with_engravings(const df::coord &pos, - const tile_context &tc) { - const char * smooth_minimal = get_tile_smooth_minimal(pos, tc); +static const char * get_tile_smooth_with_engravings(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * smooth_minimal = get_tile_smooth_minimal(out, pos, tc); if (smooth_minimal) return smooth_minimal; @@ -431,9 +430,8 @@ static const char * get_tile_smooth_with_engravings(const df::coord &pos, return NULL; } -static const char * get_tile_smooth_all(const df::coord &pos, - const tile_context &tc) { - const char * smooth_minimal = get_tile_smooth_minimal(pos, tc); +static const char * get_tile_smooth_all(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * smooth_minimal = get_tile_smooth_minimal(out, pos, tc); if (smooth_minimal) return smooth_minimal; @@ -481,8 +479,7 @@ static const char * get_track_str(const char *prefix, df::tiletype tt, df::tile_ return cache(prefix + dir); } -static const char * get_tile_carve_minimal(const df::coord &pos, - const tile_context &) { +static const char * get_tile_carve_minimal(color_ostream &out, const df::coord &pos, const tile_context &) { df::tiletype *tt = Maps::getTileType(pos); if (!tt) return NULL; @@ -533,8 +530,8 @@ static const char * get_tile_carve_minimal(const df::coord &pos, return NULL; } -static const char * get_tile_carve(const df::coord &pos, const tile_context &tc) { - const char * tile_carve_minimal = get_tile_carve_minimal(pos, tc); +static const char * get_tile_carve(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * tile_carve_minimal = get_tile_carve_minimal(out, pos, tc); if (tile_carve_minimal) return tile_carve_minimal; @@ -643,8 +640,7 @@ static const char * get_constructed_ramp_str(df::tiletype *tt) { return get_constructed_track_str(tt, "trackramp"); } -static const char * get_tile_construct(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_construct(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (ctx.b && ctx.b->getType() == building_type::Construction) return get_construction_str(ctx.b); @@ -1042,8 +1038,7 @@ static const char * add_label(const tile_context &ctx, const char *keys) { return cache(s); } -static const char * get_tile_build(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_build(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (!ctx.b || ctx.b->getType() == building_type::Stockpile) { return NULL; } @@ -1088,8 +1083,7 @@ static const char * get_place_keys(const tile_context &ctx) { return cache(keys); } -static const char * get_tile_place(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_place(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (!ctx.b || ctx.b->getType() != building_type::Stockpile) return NULL; @@ -1104,111 +1098,159 @@ static const char * get_tile_place(const df::coord &pos, return add_expansion_syntax(ctx, get_place_keys(ctx)); } -static const char * get_zone_keys(const df::building_civzonest *zone) { - static const uint32_t DEFAULT_GATHER_FLAGS = - df::civzone_gather_flag::mask_pick_trees | - df::civzone_gather_flag::mask_pick_shrubs | - df::civzone_gather_flag::mask_gather_fallen; +static string get_reservation(color_ostream &out, df::building_civzonest *zone) { + string res; + Lua::CallLuaModuleFunction(out, "plugins.preserve-rooms", "preserve_rooms_getRoleAssignmentForZone", + std::make_tuple(zone), 1, [&](lua_State *L) { + if (lua_isstring(L, -1)) + res = lua_tostring(L, -1); + }); + return res; +} - ostringstream keys; - const df::civzone_type type = zone->type; +// TODO: handle locations +static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_properties) { + const char * symbol = NULL; + vector properties; + + if (!zone->name.empty()) + properties.push_back("name=" + zone->name); + if (!zone->spec_sub_flag.bits.active) + properties.push_back("active=false"); + if (auto reserved_for = get_reservation(out, zone); !reserved_for.empty()) { + properties.push_back("assigned_unit=" + reserved_for); + } // in DFHack docs order - if (type == df::civzone_type::MeetingHall) keys << 'm'; - if (type == df::civzone_type::Bedroom) keys << 'b'; - if (type == df::civzone_type::DiningHall) keys << 'h'; - if (type == df::civzone_type::Pen) keys << 'n'; - if (type == df::civzone_type::Pond) keys << 'p'; - if (type == df::civzone_type::WaterSource) keys << 'w'; - if (type == df::civzone_type::Dungeon) keys << 'j'; - if (type == df::civzone_type::FishingArea) keys << 'f'; - if (type == df::civzone_type::SandCollection) keys << 's'; - if (type == df::civzone_type::Office) keys << 'o'; - if (type == df::civzone_type::Dormitory) keys << 'D'; - if (type == df::civzone_type::Barracks) keys << 'B'; - if (type == df::civzone_type::ArcheryRange) keys << 'a'; - if (type == df::civzone_type::Dump) keys << 'd'; - if (type == df::civzone_type::AnimalTraining) keys << 't'; - if (type == df::civzone_type::Tomb) keys << 'T'; - if (type == df::civzone_type::PlantGathering) keys << 'g'; - if (type == df::civzone_type::ClayCollection) keys << 'c'; - - keys << '{'; - if (!zone->name.empty()) { - keys << "name=" << zone->name << ' '; - } - if (!zone->spec_sub_flag.bits.active) { - keys << "active=false "; - } - if (zone->assigned_unit) { - keys << "assigned_unit=" << zone->assigned_unit << ' '; - } - if (!zone->zone_settings.pond.flag.bits.keep_filled) { - keys << "pond=false "; - } - auto archery_settings = zone->zone_settings.archery; // need this to get the `shoot_from` direction - if (archery_settings.dir_y == 0) { - if (archery_settings.dir_x == 1) keys << "shoot_from=west "; - if (archery_settings.dir_x == -1) keys << "shoot_from=east "; - } - if (archery_settings.dir_x == 0) { - if (archery_settings.dir_y == 1) keys << "shoot_from=north "; - if (archery_settings.dir_y == -1) keys << "shoot_from=south "; - } - //FIXEME (Squid): Need to know how to get the location data here - if (!zone->zone_settings.tomb.flags.bits.no_pets) { - keys << "pets=true "; - } - if (zone->zone_settings.tomb.flags.bits.no_citizens) { - keys << "citizens=false "; - } - if (zone->zone_settings.gather.flags.whole != DEFAULT_GATHER_FLAGS) { - // logic is inverted since they're all on by default - if (!zone->zone_settings.gather.flags.bits.pick_trees) keys << "pick_trees=false "; - if (!zone->zone_settings.gather.flags.bits.pick_shrubs) keys << "pick_shrubs=false "; - if (!zone->zone_settings.gather.flags.bits.gather_fallen) keys << "gather_fallen=false "; + switch (zone->type) { + using namespace df::enums::civzone_type; + case MeetingHall: symbol = "m"; break; + case Bedroom: symbol = "b"; break; + case DiningHall: symbol = "h"; break; + case Pen: symbol = "n"; break; + case Pond: + symbol = "p"; + { + if (zone->zone_settings.pond.flag.bits.keep_filled) + properties.push_back("pond=true"); + } + break; + case WaterSource: symbol = "w"; break; + case Dungeon: symbol = "j"; break; + case FishingArea: symbol = "f"; break; + case SandCollection: symbol = "s"; break; + case Office: symbol = "o"; break; + case Dormitory: symbol = "D"; break; + case Barracks: symbol = "B"; break; + case ArcheryRange: + symbol = "a"; + { + auto & archery = zone->zone_settings.archery; + if (archery.dir_x == 1 && archery.dir_y == 0) + properties.push_back("shoot_from=west"); + else if (archery.dir_x == -1 && archery.dir_y == 0) + properties.push_back("shoot_from=east"); + else if (archery.dir_x == 0 && archery.dir_y == 1) + properties.push_back("shoot_from=north"); + else if (archery.dir_x == 0 && archery.dir_y == -1) + properties.push_back("shoot_from=south"); + else + return NULL; // invalid direction + } + break; + case Dump: symbol = "d"; break; + case AnimalTraining: symbol = "t"; break; + case Tomb: + symbol = "T"; + { + auto & tomb = zone->zone_settings.tomb; + if (!tomb.flags.bits.no_pets) + properties.push_back("pets=true"); + if (tomb.flags.bits.no_citizens) + properties.push_back("citizens=false"); + } + break; + case PlantGathering: + symbol = "g"; + { + auto & gather = zone->zone_settings.gather; + if (!gather.flags.bits.pick_trees) + properties.push_back("pick_trees=false"); + if (!gather.flags.bits.pick_shrubs) + properties.push_back("pick_shrubs=false"); + if (!gather.flags.bits.gather_fallen) + properties.push_back("gather_fallen=false"); + } + break; + case ClayCollection: symbol = "c"; break; + default: + return NULL; } - string keys_str = keys.str(); + if (!add_properties || properties.empty()) + return symbol; - // there is no way to represent an active, but empty zone in quickfort - if (keys_str.empty()) - return NULL; + ostringstream keys; + keys << symbol << '{' << join_strings(" ", properties) << '}'; + return cache(keys.str()); +} - // remove final '^' character if there is one - if (keys_str.back() == '{') { - keys_str.pop_back(); - } else { - keys << '}'; - } +static df::coord get_first_tile(df::building_civzonest *zone) { + df::coord first_pos; + cuboid zone_area(zone->x1, zone->y1, zone->z, zone->x2, zone->y2, zone->z); + zone_area.forCoord([&](const df::coord &pos) { + if (Buildings::containsTile(zone, pos)) { + first_pos = pos; + return false; + } + return true; + }); - return cache(keys_str); + return first_pos; } -static const char * get_tile_zone(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_zone(color_ostream &out, const df::coord &pos, const tile_context &ctx) { vector civzones; if (!Buildings::findCivzonesAt(&civzones, pos)) return NULL; - // we only have one "zone" blueprint, so use the "topmost" zone (that is, - // the one that is highlighted when the cursor is over this tile). - // overlapping zones are outside the scope of this plugin, I think. - df::building_civzonest *zone = civzones.back(); + // we can handle overlapping zones in a single blueprint, but only if one of + // the following is true: + // -- they exactly overlap (even if they aren't rectangular) + // -- no two non-rectangular zones overlap + + // for a first implementation, we will only handle overlapping zones if they + // are rectangular. if this is the upper left corner of a rectangular zone, + // we will output for that zone. otherwise, if this pos is interior to + // all zones, then it doesn't matter which we choose. + + df::building_civzonest * primary_zone = civzones[0]; + df::coord upper_left_corner; + + if (civzones.size() == 1) { + upper_left_corner = get_first_tile(primary_zone); + } else if (civzones.size() > 1) { + for (auto zone : civzones) { + if (!is_rectangular(zone)) + continue; + primary_zone = zone; + upper_left_corner = get_first_tile(zone); + if (pos == upper_left_corner) + break; + } + } + + bool is_first_tile = pos == upper_left_corner; - if (!is_rectangular(zone)) - return get_zone_keys(zone); + if (!is_rectangular(primary_zone)) + return get_zone_keys(out, primary_zone, is_first_tile); - if (zone->x1 != static_cast(pos.x) - || zone->y1 != static_cast(pos.y)) { + if (!is_first_tile) return if_pretty(ctx, "`"); - } - return add_expansion_syntax(zone, get_zone_keys(zone)); + return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, true)); } - - static bool create_output_dir(color_ostream &out, const blueprint_options &opts) { string basename = BLUEPRINT_USER_DIR + opts.name; @@ -1439,7 +1481,7 @@ static bool do_transform(color_ostream &out, ctx.processor = &processor; if (processor.init_ctx) processor.init_ctx(pos, ctx); - const char *tile_str = processor.get_tile(pos, ctx); + const char *tile_str = processor.get_tile(out, pos, ctx); if (tile_str) { // ensure our z-index is in the order we want to write auto area = processor.mapdata.emplace(abs(z - start.z), diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index d5b4b5b7a5..ae8337859d 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -686,17 +686,25 @@ static int preserve_rooms_assignToRole(lua_State *L) { return 0; } -static string preserve_rooms_getRoleAssignment(color_ostream &out) { - auto zone = Gui::getSelectedCivZone(out, true); +static string get_role_assignment(color_ostream &out, df::building_civzonest * zone) { if (!zone) return ""; - TRACE(control,out).print("preserve_rooms_getRoleAssignment: zone_id=%d\n", zone->id); + TRACE(control,out).print("get_role_assignment: zone_id=%d\n", zone->id); auto it = noble_zones.find(zone->id); if (it == noble_zones.end() || it->second.empty()) return ""; return it->second[0]; } + +static string preserve_rooms_getRoleAssignmentForZone(color_ostream &out, df::building_civzonest * zone) { + return get_role_assignment(out, zone); +} + +static string preserve_rooms_getRoleAssignment(color_ostream &out) { + return get_role_assignment(out, Gui::getSelectedCivZone(out, true)); +} + static bool preserve_rooms_isReserved(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) @@ -774,6 +782,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_FUNCTION(preserve_rooms_getFeature), DFHACK_LUA_FUNCTION(preserve_rooms_resetFeatureState), DFHACK_LUA_FUNCTION(preserve_rooms_getRoleAssignment), + DFHACK_LUA_FUNCTION(preserve_rooms_getRoleAssignmentForZone), DFHACK_LUA_FUNCTION(preserve_rooms_isReserved), DFHACK_LUA_FUNCTION(preserve_rooms_getReservationName), DFHACK_LUA_FUNCTION(preserve_rooms_clearReservation), From af5dccf7890af59899d90a880a25f713ffaccdbe Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 3 May 2025 17:39:41 -0500 Subject: [PATCH 0342/1082] Add prefer nicknamed to spectate (#5386) * Add prefer nicknamed * Actually add to the nicknamed bucket * Use other not job weight * Update changelog.txt * weight change * add prefer nicknamed default * fix my misunderstanding of the code in my testing turning off the prefer nicknamed used the jobs to categorize them . (i went from 16 nicknamed to 2 job units and 14 other units) so I'm pretty sure this works as i meant it to now * Update spectate.rst --- docs/changelog.txt | 1 + docs/plugins/spectate.rst | 3 +++ plugins/lua/spectate.lua | 1 + plugins/spectate.cpp | 15 +++++++++++++-- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 923e53b482..9625cf119d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -105,6 +105,7 @@ Template for new versions: ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) +- `spectate`: added prefer nicknamed units ## API - ``Military`` module: added ``addToSquad`` function diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 68d1013f64..dd0174f320 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -105,6 +105,9 @@ Settings Toggle whether to prefer following (non-siege) units that have newly arrived on the map. +``prefer-nicknamed`` (default: enabled) + Toggle whether to prefer following nicknamed units. + ``tooltip-follow`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the tooltips that follow onscreen dwarves around the map. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 0cb2d3bd33..953895eab0 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -25,6 +25,7 @@ local function get_default_state() ['include-wildlife']=false, ['prefer-conflict']=true, ['prefer-new-arrivals']=true, + ['prefer-nicknamed']=true, ['tooltip-follow']=true, ['tooltip-follow-blink-milliseconds']=3000, ['tooltip-follow-hold-to-show']='none', -- one of none, ctrl, alt, or shift diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 54bd4944a0..ae287f6000 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -43,6 +43,7 @@ static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; static const float CITIZEN_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float NICKNAMED_CITIZEN_PREFERRED_WEIGHT = 15.0f; static const float OTHER_COMBAT_PREFERRED_WEIGHT = 10.0f; static const float JOB_WEIGHT = 5.0f; static const float OTHER_WEIGHT = 1.0f; @@ -71,6 +72,7 @@ static struct Configuration { bool include_wildlife; bool prefer_conflict; bool prefer_new_arrivals; + bool prefer_nicknamed; int32_t follow_ms; void reset() { @@ -82,6 +84,7 @@ static struct Configuration { include_wildlife = false; prefer_conflict = true; prefer_new_arrivals = true; + prefer_nicknamed = true; follow_ms = 10000; } } config; @@ -436,6 +439,7 @@ static bool is_in_combat(df::unit *unit) { static void get_dwarf_buckets(color_ostream &out, vector &citizen_combat_units, vector &other_combat_units, + vector &nicknamed_units, vector &job_units, vector &other_units) { @@ -452,11 +456,13 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - TRACE(cycle,out).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + TRACE(cycle, out).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) citizen_combat_units.push_back(unit); else other_combat_units.push_back(unit); + } else if (config.prefer_nicknamed && !unit->name.nickname.empty()) { + nicknamed_units.push_back(unit); } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { job_units.push_back(unit); } else { @@ -510,9 +516,10 @@ static void follow_a_dwarf(color_ostream &out) { vector citizen_combat_units; vector other_combat_units; + vector nicknamed_units; vector job_units; vector other_units; - get_dwarf_buckets(out, citizen_combat_units, other_combat_units, job_units, other_units); + get_dwarf_buckets(out, citizen_combat_units, other_combat_units, nicknamed_units, job_units, other_units); set_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); @@ -523,6 +530,7 @@ static void follow_a_dwarf(color_ostream &out) { intervals.push_back(0); add_bucket(citizen_combat_units, units, intervals, weights, config.prefer_conflict ? CITIZEN_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); add_bucket(other_combat_units, units, intervals, weights, config.prefer_conflict ? OTHER_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(nicknamed_units, units, intervals, weights, NICKNAMED_CITIZEN_PREFERRED_WEIGHT); add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); @@ -538,6 +546,7 @@ static void follow_a_dwarf(color_ostream &out) { if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { DUMP_BUCKET(citizen_combat_units); DUMP_BUCKET(other_combat_units); + DUMP_BUCKET(nicknamed_units); DUMP_BUCKET(job_units); DUMP_BUCKET(other_units); DUMP_FLOAT_VECTOR(intervals); @@ -575,6 +584,8 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { config.prefer_conflict = val; } else if (name == "prefer-new-arrivals") { config.prefer_new_arrivals = val; + } else if (name == "prefer-nicknamed") { + config.prefer_nicknamed = val; } else if (name == "follow-seconds") { if (val <= 0) { WARN(control,out).print("follow-seconds must be a positive integer\n"); From b83426c0a24ebc8c6be39d3d9a2bec6930d8d405 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 3 May 2025 22:41:02 +0000 Subject: [PATCH 0343/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index def13b7c6f..f1e74867f2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit def13b7c6f1bb8b036968490cae696e18cb25bbc +Subproject commit f1e74867f22c5f51eb4bfe380744b43f062d0b02 From 4a8d6b51f45fb0bbd25b48b66414b19ebe20105e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 18:50:10 -0700 Subject: [PATCH 0344/1082] add option for forCoord to iterate in row-major order --- docs/changelog.txt | 1 + library/include/modules/Maps.h | 13 ++++++++----- library/modules/Maps.cpp | 27 ++++++++++++++++++--------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9625cf119d..b8d6752342 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -71,6 +71,7 @@ Template for new versions: ## API - ``Random`` module: added ``SplitmixRNG`` class, implements the Splitmix64 RNG used by Dwarf Fortress for "simple" randomness - ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys vanilla SHOW_IMP_QUALITY setting +- ``cuboid::forCoord``, ``Maps::forCoord``: take additional parameter to control whether iteration goes in column major or row major order ## Lua - ``script-manager``: new ``get_active_mods()`` function for getting information on active mods diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index f5a3be142c..839989237e 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -229,9 +229,11 @@ class cuboid { DFHACK_EXPORT bool containsPos(int16_t x, int16_t y, int16_t z) const; DFHACK_EXPORT bool containsPos(const df::coord &pos) const { return containsPos(pos.x, pos.y, pos.z); } - /// Iterate over every point in the cuboid from top-down, N-S, then W-E. Doesn't guarantee valid map tile! + /// Iterate over every point in the cuboid. Doesn't guarantee valid map tile! /// "fn" should return true to keep iterating. Won't iterate if cuboid invalid. - DFHACK_EXPORT void forCoord(std::function fn) const; + /// If row_major is false, iterates from top-down (z), N-S (y), then W-E (x). + /// If row_major is true, iterates from top-down (z), W-E (x), then N-S (y). + DFHACK_EXPORT void forCoord(std::function fn, bool row_major = false) const; /// Iterate over every non-NULL map block intersecting the tile cuboid from top-down, N-S, then W-E. /// Will also supply the intersection of this cuboid and block to your "fn" for use with cuboid::forCoord. @@ -250,12 +252,13 @@ namespace Maps extern DFHACK_EXPORT bool IsValid(); /// Iterate over points in a cuboid from z1:z2, y1:y2, then x1:x2. +/// If row_major is true, iterates from z1:z2, x1:x2, then y1:y2. /// Doesn't guarantee valid map tile! Can be used to iterate over blocks, etc. /// "fn" should return true to keep iterating. DFHACK_EXPORT void forCoord(std::function fn, - int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2); -inline void forCoord(std::function fn, const df::coord &p1, const df::coord &p2) { - forCoord(fn, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z); + int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2, bool row_major = false); +inline void forCoord(std::function fn, const df::coord &p1, const df::coord &p2, bool row_major = false) { + forCoord(fn, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, row_major); } /** diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 54fb43c810..7e5707fccb 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -197,10 +197,10 @@ bool cuboid::containsPos(int16_t x, int16_t y, int16_t z) const { x <= x_max && y <= y_max && z <= z_max; } -void cuboid::forCoord(std::function fn) const +void cuboid::forCoord(std::function fn, bool row_major) const { if (isValid()) // Only iterate if valid cuboid. - Maps::forCoord(fn, x_min, y_min, z_max, x_max, y_max, z_min); + Maps::forCoord(fn, x_min, y_min, z_max, x_max, y_max, z_min, row_major); } void cuboid::forBlock(std::function fn, bool ensure_block) const @@ -230,18 +230,27 @@ bool Maps::IsValid() { } void Maps::forCoord(std::function fn, int16_t x1, int16_t y1, int16_t z1, - int16_t x2, int16_t y2, int16_t z2) + int16_t x2, int16_t y2, int16_t z2, bool row_major) { int16_t dx = x1 > x2 ? -1 : 1; int16_t dy = y1 > y2 ? -1 : 1; int16_t dz = z1 > z2 ? -1 : 1; - // Process z, y, then x. - for (int16_t x = x1; x != x2 + dx; x += dx) - for (int16_t y = y1; y != y2 + dy; y += dy) - for (int16_t z = z1; z != z2 + dz; z += dz) - if (!fn(df::coord(x, y, z))) - return; // Break iterator. + if (row_major) { + // Process z, y, then x. + for (int16_t z = z1; z != z2 + dz; z += dz) + for (int16_t y = y1; y != y2 + dy; y += dy) + for (int16_t x = x1; x != x2 + dx; x += dx) + if (!fn(df::coord(x, y, z))) + return; // Break iterator. + } else { + // Process z, x, then y. + for (int16_t z = z1; z != z2 + dz; z += dz) + for (int16_t x = x1; x != x2 + dx; x += dx) + for (int16_t y = y1; y != y2 + dy; y += dy) + if (!fn(df::coord(x, y, z))) + return; // Break iterator. + } } // getter for map size in blocks From 4d49a5217a545ac159003ef5f042a064a66921b1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 18:52:06 -0700 Subject: [PATCH 0345/1082] handle disjoint zones --- plugins/blueprint.cpp | 62 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 6ec9b9c3c4..7d2b656886 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -5,9 +5,6 @@ * Written by cdombroski. */ -#include -#include - #include "Console.h" #include "DataDefs.h" #include "DataFuncs.h" @@ -43,6 +40,10 @@ #include "df/tile_occupancy.h" #include "df/world.h" +#include +#include +#include + using std::endl; using std::map; using std::ofstream; @@ -1109,7 +1110,7 @@ static string get_reservation(color_ostream &out, df::building_civzonest *zone) } // TODO: handle locations -static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_properties) { +static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_label, bool add_properties) { const char * symbol = NULL; vector properties; @@ -1187,11 +1188,15 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo return NULL; } - if (!add_properties || properties.empty()) + if (!add_label && (!add_properties || properties.empty())) return symbol; ostringstream keys; - keys << symbol << '{' << join_strings(" ", properties) << '}'; + keys << symbol; + if (add_label) + keys << "/" << "zone_" << zone->id; + if (add_properties) + keys << '{' << join_strings(" ", properties) << '}'; return cache(keys.str()); } @@ -1204,11 +1209,49 @@ static df::coord get_first_tile(df::building_civzonest *zone) { return false; } return true; - }); + }, true); return first_pos; } +static int32_t get_flood_size(const df::building::T_room &room, int32_t start_x, int32_t start_y) { + if (!room.extents) + return 0; + std::unordered_set visited; + std::queue to_visit; + to_visit.push(df::coord(start_x, start_y, 0)); + while (!to_visit.empty()) { + df::coord pos = to_visit.front(); + to_visit.pop(); + if (visited.count(pos)) + continue; + visited.insert(pos); + for (int32_t y = -1; y <= 1; ++y) { + for (int32_t x = -1; x <= 1; ++x) { + if (x == 0 && y == 0) + continue; + int32_t nx = pos.x + x; + int32_t ny = pos.y + y; + if (nx < 0 || ny < 0 || nx >= room.width || ny >= room.height) + continue; + if (room.extents[ny * room.width + nx]) + to_visit.push(df::coord(nx, ny, 0)); + } + } + } + // flood size is the number of tiles we visited + return (int32_t)visited.size(); +} + +static bool is_disjoint(df::building *bld, const df::coord &first_tile) { + const df::building::T_room &room = bld->room; + if (!room.extents) + return false; + + int32_t flood_size = get_flood_size(room, first_tile.x - bld->x1, first_tile.y - bld->y1); + return flood_size != Buildings::countExtentTiles(bld); +} + static const char * get_tile_zone(color_ostream &out, const df::coord &pos, const tile_context &ctx) { vector civzones; if (!Buildings::findCivzonesAt(&civzones, pos)) @@ -1240,15 +1283,16 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons } } + bool disjoint = is_disjoint(primary_zone, upper_left_corner); bool is_first_tile = pos == upper_left_corner; if (!is_rectangular(primary_zone)) - return get_zone_keys(out, primary_zone, is_first_tile); + return get_zone_keys(out, primary_zone, disjoint, is_first_tile); if (!is_first_tile) return if_pretty(ctx, "`"); - return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, true)); + return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, disjoint, true)); } static bool create_output_dir(color_ostream &out, From 7a057fba91ab06424c616605a3ea2a9e8a235c12 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 2 May 2025 18:10:59 -0500 Subject: [PATCH 0346/1082] `Core.cpp`: remove unnecessary casts `std::thread`'s constructor is a template, no reason to type-erase here --- library/Core.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index d949db688c..bfaa30a034 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -269,10 +269,10 @@ struct IODATA // A thread function... for handling hotkeys. This is needed because // all the plugin commands are expected to be run from foreign threads. // Running them from one of the main DF threads will result in deadlock! -void fHKthread(void * iodata) +static void fHKthread(IODATA * iodata) { - Core * core = ((IODATA*) iodata)->core; - PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; + Core * core = iodata->core; + PluginManager * plug_mgr = iodata->plug_mgr; if(plug_mgr == 0 || core == 0) { std::cerr << "Hotkey thread has croaked." << std::endl; @@ -1874,7 +1874,7 @@ bool Core::InitSimulationThread() std::cerr << "Starting DF input capture thread.\n"; // set up hotkey capture - d->hotkeythread = std::thread(fHKthread, (void *) temp); + d->hotkeythread = std::thread(fHKthread, temp); started = true; modstate = 0; From 5040c012f3f90a78183708c508dc8fb8bdd5f30a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 01:59:06 -0500 Subject: [PATCH 0347/1082] remove type erasure from `RunCoreQueryLoop` --- library/Core.cpp | 45 ++++++++++++-------------------------- library/LuaTools.cpp | 27 +++++++---------------- library/include/LuaTools.h | 6 ++--- 3 files changed, 25 insertions(+), 53 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index bfaa30a034..f2f965a7de 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -323,62 +323,45 @@ static std::string dfhack_version_desc() return s.str(); } -namespace { - struct ScriptArgs { - const std::string *pcmd; - std::vector *pargs; - }; - struct ScriptEnableState { - const std::string *pcmd; - bool pstate; - }; -} - -static bool init_run_script(color_ostream &out, lua_State *state, void *info) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, std::vector& pargs) { - auto args = (ScriptArgs*)info; - if (!lua_checkstack(state, args->pargs->size()+10)) + if (!lua_checkstack(state, pargs.size()+10)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "run_script"); lua_remove(state, -2); - lua_pushstring(state, args->pcmd->c_str()); - for (size_t i = 0; i < args->pargs->size(); i++) - lua_pushstring(state, (*args->pargs)[i].c_str()); + lua_pushstring(state, pcmd.c_str()); + for (auto& arg : pargs) + lua_pushstring(state, arg.c_str()); return true; } static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) { - ScriptArgs data; - data.pcmd = &name; - data.pargs = &args; - - bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_run_script, &data); + using namespace std::placeholders; + auto init_fn = std::bind(init_run_script, _1, _2, name, args); + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_fn); return ok ? CR_OK : CR_FAILURE; } -static bool init_enable_script(color_ostream &out, lua_State *state, void *info) +static bool init_enable_script(color_ostream &out, lua_State *state, std::string& name, bool enable) { - auto args = (ScriptEnableState*)info; if (!lua_checkstack(state, 4)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "enable_script"); lua_remove(state, -2); - lua_pushstring(state, args->pcmd->c_str()); - lua_pushboolean(state, args->pstate); + lua_pushstring(state, name.c_str()); + lua_pushboolean(state, enable); return true; } static command_result enableLuaScript(color_ostream &out, std::string name, bool state) { - ScriptEnableState data; - data.pcmd = &name; - data.pstate = state; - - bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_enable_script, &data); + using namespace std::placeholders; + auto init_fn = std::bind(init_enable_script, _1, _2, name, state); + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_fn); return ok ? CR_OK : CR_FAILURE; } diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 2d9df61ab4..95f8017dd8 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1193,9 +1193,7 @@ static int resume_query_loop(color_ostream &out, return rv; } -bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, - bool (*init)(color_ostream&, lua_State*, void*), - void *arg) +bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, DFHack::Lua::init_fn init) { if (!lua_checkstack(state, 20)) return false; @@ -1213,7 +1211,7 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, int base = lua_gettop(state); - if (!init(out, state, arg)) + if (!init(out, state)) { lua_settop(state, base); return false; @@ -1283,21 +1281,13 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, return (rv == LUA_OK); } -namespace { - struct InterpreterArgs { - const char *prompt; - const char *hfile; - }; -} - -static bool init_interpreter(color_ostream &out, lua_State *state, void *info) +static bool init_interpreter(color_ostream &out, lua_State *state, const char* prompt, const char* hfile) { - auto args = (InterpreterArgs*)info; lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_DFHACK_TOKEN); lua_getfield(state, -1, "interpreter"); lua_remove(state, -2); - lua_pushstring(state, args->prompt); - lua_pushstring(state, args->hfile); + lua_pushstring(state, prompt); + lua_pushstring(state, hfile); return true; } @@ -1312,11 +1302,10 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, if (!prompt) prompt = "lua"; - InterpreterArgs args; - args.prompt = prompt; - args.hfile = hfile; + using namespace std::placeholders; + auto init_fn = std::bind(init_interpreter, _1, _2, prompt, hfile); - return RunCoreQueryLoop(out, state, init_interpreter, &args); + return RunCoreQueryLoop(out, state, init_fn); } static bool do_invoke_cleanup(lua_State *L, int nargs, int errorfun, bool success) diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 8b71862ec3..e5a83721cc 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -286,9 +286,9 @@ namespace DFHack::Lua { * is done inside CoreSuspender, while waiting for input happens * without the suspend lock. */ - DFHACK_EXPORT bool RunCoreQueryLoop(color_ostream &out, lua_State *state, - bool (*init)(color_ostream&, lua_State*, void*), - void *arg); + using init_fn = std::function; + + DFHACK_EXPORT bool RunCoreQueryLoop(color_ostream &out, lua_State *state, init_fn init); /** * Attempt to interrupt the currently-executing lua function by raising a lua error From 53f7af165230661fe39baf7f98f432019e412406 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 02:03:04 -0500 Subject: [PATCH 0348/1082] `Core`: remove more unneeded type erasures --- library/Core.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index f2f965a7de..48da47ef13 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1391,9 +1391,8 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // Load dfhack.init in a dedicated thread (non-interactive console mode) -void fInitthread(void * iodata) +static void fInitthread(IODATA * iod) { - IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; color_ostream_proxy out(core->getConsole()); @@ -1401,13 +1400,12 @@ void fInitthread(void * iodata) } // A thread function... for the interactive console. -void fIOthread(void * iodata) +static void fIOthread(IODATA * iod) { static const std::filesystem::path HISTORY_FILE = CONFIG_PATH / "dfhack.history"; - IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; - PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; + PluginManager * plug_mgr = iod->plug_mgr; CommandHistory main_history; main_history.load(HISTORY_FILE.c_str()); @@ -1847,12 +1845,12 @@ bool Core::InitSimulationThread() { std::cerr << "Starting IO thread.\n"; // create IO thread - d->iothread = std::thread{fIOthread, (void*)temp}; + d->iothread = std::thread{fIOthread, temp}; } else { std::cerr << "Starting dfhack.init thread.\n"; - d->iothread = std::thread{fInitthread, (void*)temp}; + d->iothread = std::thread{fInitthread, temp}; } std::cerr << "Starting DF input capture thread.\n"; From 608ae95a36bde75b868cf44fc7588e2595ff1722 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 11:30:03 -0500 Subject: [PATCH 0349/1082] eliminate another type erasure in `Core` --- library/Core.cpp | 4 ++-- library/include/Core.h | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 48da47ef13..b7a4bc5227 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2022,8 +2022,8 @@ void Core::doUpdate(color_ostream &out) } // detect if the game was loaded or unloaded in the meantime - void *new_wdata = NULL; - void *new_mapdata = NULL; + df::world_data* new_wdata = nullptr; + df::map_block**** new_mapdata = nullptr; if (df::global::world && !is_load_save) { df::world_data *wdata = df::global::world->world_data; diff --git a/library/include/Core.h b/library/include/Core.h index dd84d49df0..383a0a9c95 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -50,6 +50,8 @@ struct lua_State; namespace df { struct viewscreen; + struct world_data; + struct map_block; } namespace DFHack @@ -300,9 +302,9 @@ namespace DFHack bool SelectHotkey(int key, int modifiers); // for state change tracking - void *last_world_data_ptr; + df::world_data *last_world_data_ptr; // for state change tracking - void *last_local_map_ptr; + df::map_block**** last_local_map_ptr; friend struct Screen::Hide; df::viewscreen *top_viewscreen; bool last_pause_state; From 5e50664b8f0477656c96d49946105652cbf2359a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 13:28:19 -0500 Subject: [PATCH 0350/1082] merge `Process.cpp` into a single source file --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index f1e74867f2..def13b7c6f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f1e74867f22c5f51eb4bfe380744b43f062d0b02 +Subproject commit def13b7c6f1bb8b036968490cae696e18cb25bbc From bb332c91677afc118cc4d33e40ea551526392944 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 4 May 2025 12:06:56 -0500 Subject: [PATCH 0351/1082] Revert "merge `Process.cpp` into a single source file" This reverts commit 5e50664b8f0477656c96d49946105652cbf2359a. --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index def13b7c6f..f1e74867f2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit def13b7c6f1bb8b036968490cae696e18cb25bbc +Subproject commit f1e74867f22c5f51eb4bfe380744b43f062d0b02 From b53b44f584deb2ee262b1ade9b6b28c1bc9d73fd Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 4 May 2025 20:23:10 +0200 Subject: [PATCH 0352/1082] first version of river export in C++ --- plugins/export-map.cpp | 161 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 973ed169ed..c817ecf989 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -16,6 +16,7 @@ #include "df/world_landmass.h" #include "df/world_region_details.h" #include "df/world_region.h" +#include "df/world_river.h" #include "df/world.h" #include @@ -23,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -54,7 +57,7 @@ struct std::hash // were only dealing with 2D coordinates in this file using coord = df::coord2d; -// static int wdim = 768; // dimension of a world tile +static int wdim = 768; // dimension of a world tile static int rdim = 48; // dimension of a region tile /** @@ -113,6 +116,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { @@ -127,6 +131,9 @@ static command_result do_command(color_ostream &out, vector ¶meters) { return export_sites(out); } + else if (parameters.size() && parameters[0] == "rivers") { + return export_rivers(out); + } else { return export_region_tiles(out); @@ -473,3 +480,155 @@ static command_result export_region_tiles(color_ostream &out) out.print("done in %f s !\n", elapsed_seconds.count()); return CR_OK; } + +/********************************************************************** */ + +// used for global coordinates at local tile granularity (129*768 = 99072 doesn't fit into df::coord2d) +struct gcoord { + int x,y; +}; + +struct river_tile { + using polygon_t = std::list; + polygon_t polygon; + //std::optional north, south, west, east; +}; + +struct gate { + int active,min,max; + + static gate get(const df::world_region_details *const region_details, int region_x, int region_y, direction dir) { + auto& vertical = region_details->rivers_vertical; + auto& horizontal = region_details->rivers_horizontal; + switch (dir) { + case direction::North: + return { vertical.active[region_x][region_y], vertical.x_min[region_x][region_y], vertical.x_max[region_x][region_y] }; + case direction::West: + return { horizontal.active[region_x][region_y], horizontal.y_min[region_x][region_y], horizontal.y_max[region_x][region_y] }; + case direction::South: + return { vertical.active[region_x][region_y+1], vertical.x_min[region_x][region_y+1], vertical.x_max[region_x][region_y+1] }; + case direction::East: + return { horizontal.active[region_x+1][region_y], horizontal.y_min[region_x+1][region_y], horizontal.y_max[region_x+1][region_y] }; + default: + assert(false); + return {}; + } + } + + bool is_valid() const { + return active != 0 && min != -30000 && max != -30000; + } +}; + + +bool is_land(const df::world_region_details *const region_details, int16_t region_x, int16_t region_y) { + CHECK_NULL_POINTER(region_details); + auto [world_x, world_y] = region_details->pos; + auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); + auto region_map_entry = Maps::getRegionBiome(biome_tile); + CHECK_NULL_POINTER(region_map_entry); + return region_map_entry->elevation >= 100 && !region_map_entry->flags.is_set(df::enums::region_map_entry_flags::is_lake); +} + +static command_result export_rivers(color_ostream &out) +{ + // ensure that we have an output file + std::string filename("rivers.csv"); + std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + if (!out_file) { + return CR_FAILURE; + } + + // create lookup table for rivers based on world tile coordinates + std::unordered_map world_river; + + // assign river end first, so that it can be overridden by proper path elements + for (size_t r_idx = 0; r_idx < df::global::world->world_data->rivers.size(); ++r_idx) { + auto river = df::global::world->world_data->rivers[r_idx]; + world_river[river->end_pos] = r_idx; + } + + for (size_t r_idx = 0; r_idx < df::global::world->world_data->rivers.size(); ++r_idx) { + auto river = df::global::world->world_data->rivers[r_idx]; + for (size_t i = 0; i < river->path.size(); ++i) { + auto pos = river->path[i]; + world_river[pos] = r_idx; + } + } + + // river idx -> region tile coord -> tile + std::unordered_map> tile_index; + + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto [world_x, world_y] = region_details->pos; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) + { + gcoord base = { world_x * wdim + region_x * rdim, world_y * wdim + region_y * rdim }; + + auto north = gate::get(region_details, region_x, region_y, direction::North); + auto west = gate::get(region_details, region_x, region_y, direction::West); + auto south = gate::get(region_details, region_x, region_y, direction::South); + auto east = gate::get(region_details, region_x, region_y, direction::East); + + // skip tiles without any gates + if (!(north.is_valid() || west.is_valid() || south.is_valid() || east.is_valid())) + continue; + + // skip any river tiles that are on oceans or lakes + if (!is_land(region_details, region_x, region_y)) + continue; + + river_tile tile; + + if (north.is_valid()) { + tile.polygon.emplace_back(base.x + north.max, base.y); + tile.polygon.emplace_back(base.x + north.min, base.y); + } + if (west.is_valid()) { + tile.polygon.emplace_back(base.x, base.y + west.min); + tile.polygon.emplace_back(base.x, base.y + west.max); + } + if (south.is_valid()) { + tile.polygon.emplace_back(base.x + south.min, base.y + rdim); + tile.polygon.emplace_back(base.x + south.max, base.y + rdim); + } + if (east.is_valid()) { + tile.polygon.emplace_back(base.x + rdim, base.y + east.max); + tile.polygon.emplace_back(base.x + rdim, base.y + east.min); + } + + auto r_idx = world_river.at({world_x, world_y}); + tile_index[r_idx][ coord(world_x * 16 + region_x, world_y * 16 + region_y) ] = std::move(tile); + } + } + } + + + // generate output + out_file << "name_df;name_en;geometry_wkt\n"; + + for (auto& [r_idx, river_index] : tile_index) { + auto river = world->world_data->rivers.at(r_idx); + out_file << DF2UTF(Translation::translateName(&river->name, false)) << ";"; + out_file << DF2UTF(Translation::translateName(&river->name, true)) << ";"; + out_file << "MULTIPOLYGON("; + bool first = true; + for (auto &[tile_pos, tile] : river_index) { + tile.polygon.emplace_back(*tile.polygon.begin()); + auto print_position = [](std::ostream &out, gcoord pos) { + out << pos.x << " " << -pos.y; + }; + if (first) { + first = false; + } else { + out_file << ","; + } + print_range(out_file, tile.polygon, print_position, "((", ",", "))"); + } + out_file << ")\n"; + } + + + return CR_OK; +} From 95eab7ffdab2927187b11e2350d9ab44a5ec9785 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 4 May 2025 20:57:48 +0200 Subject: [PATCH 0353/1082] default to exporting all layers --- plugins/export-map.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index c817ecf989..4c72af1b90 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -134,9 +134,14 @@ static command_result do_command(color_ostream &out, vector ¶meters) else if (parameters.size() && parameters[0] == "rivers") { return export_rivers(out); } + else if (parameters.size() && parameters[0] == "regions") { + return export_region_tiles(out); + } else { - return export_region_tiles(out); + auto ok_region = export_region_tiles(out); + auto ok_sites = ok_region == CR_OK ? export_sites(out) : ok_region; + return ok_sites == CR_OK ? export_rivers(out) : ok_sites; } } From bfb62c4530a4071b36e08529dad947ff85fc36b5 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 5 May 2025 07:20:53 +0000 Subject: [PATCH 0354/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index f1e74867f2..9f76bc9d2a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f1e74867f22c5f51eb4bfe380744b43f062d0b02 +Subproject commit 9f76bc9d2a1c1e1cb237543f320acda352f40881 From 5b063b59f2b08c2ef9cf3c7da63bfafa6c5bef6d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 19:15:53 +0000 Subject: [PATCH 0355/1082] [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.32.1 → 0.33.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.32.1...0.33.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 8348cb4d85..937a9de80d 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.32.1 + rev: 0.33.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 724ba25bc9461372b9d9b2a54f0bfbd179a34541 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 6 May 2025 02:02:41 +0000 Subject: [PATCH 0356/1082] Auto-update submodules library/xml: master scripts: master depends/dfhooks: main --- depends/dfhooks | 2 +- library/xml | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index ca30a90f8a..481dc1a12b 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit ca30a90f8a3f9cc61531ac24430fd2434fff40ec +Subproject commit 481dc1a12b1264ef06ce95e331ef35cbfa0e6ace diff --git a/library/xml b/library/xml index 6502bdefd1..f48fd9ef43 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 6502bdefd1796315365a7bb58e30e0ce34359bea +Subproject commit f48fd9ef4369027499da28441adaf9c572431891 diff --git a/scripts b/scripts index 9f76bc9d2a..e50474efbf 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9f76bc9d2a1c1e1cb237543f320acda352f40881 +Subproject commit e50474efbfa514fd0e4b63cd9a8c114ccf0bb392 From 2ba3c0d61000a1dea8d1d3ada0f15f6e36982e34 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 May 2025 11:05:01 -0500 Subject: [PATCH 0357/1082] adjust `df_object` constraint some (most, actually) `get` methods return a pointer to a `const` instance of a subclass of `type_identity` which isn't convertible to a pointer of non-`const` `type_identity`. adding `const` fixes this fixes #5450 --- docs/changelog.txt | 2 ++ library/include/LuaTools.h | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d6752342..d3484c86a6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,8 @@ Template for new versions: - `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air - `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows - `preserve-rooms`: don't warn when a room is assigned to a non-existent unit. this is now common behavior for DF when it keeps a room for an unloaded unit +- fixed an overly restrictive type constraint that resulted in some object types being glossed as a boolean when passed as an argument from C++ to Lua +- `plants`: will no longer generate a traceback when a filter is used ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index e5a83721cc..429a8bfc6b 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -141,9 +141,9 @@ namespace DFHack::Lua { DFHACK_EXPORT void CheckDFAssign(lua_State *state, type_identity *type, void *target, int val_index, bool exact_type = false); - template concept df_object = requires(T x) + template concept df_object = requires() { - { df::identity_traits::get() } -> std::convertible_to; + { df::identity_traits::get() } -> std::convertible_to; }; /** From d915fe192d84f63eb61c1abe751e12a857b56264 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 13:07:27 -0700 Subject: [PATCH 0358/1082] show priorities for digging designations in ascii overlay --- docs/changelog.txt | 1 + docs/plugins/dig.rst | 12 ++++++------ plugins/dig.cpp | 23 ++++++++++++++--------- plugins/lua/dig.lua | 17 +++++++++++------ 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index d3484c86a6..ff76e3d6b7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. +- `dig`: ASCII overlay now displays priority of digging designations ## Documentation diff --git a/docs/plugins/dig.rst b/docs/plugins/dig.rst index 0a6007f598..d8d9c4ff40 100644 --- a/docs/plugins/dig.rst +++ b/docs/plugins/dig.rst @@ -197,13 +197,13 @@ Overlay This tool also provides three overlays that are managed by the `overlay` framework. -asciicarve -~~~~~~~~~~ +asciidesignated +~~~~~~~~~~~~~~~ -The ``dig.asciicarve`` overlay makes carving designations visible in ASCII -mode. It highlights tiles that are designated for smoothing, engraving, track -carving, or fortification carving. The designations blink (slowly) so you can -still see what is underneath them. +The ``dig.asciidesignated`` overlay makes designations visible in ASCII mode. +It highlights tiles that are designated for digging, smoothing, engraving, +track carving, or fortification carving. The designations blink (slowly) so you +can still see what is underneath them. Due to the limitations of the ASCII mode screen buffer, the designation highlights may show through other interface elements that overlap the diff --git a/plugins/dig.cpp b/plugins/dig.cpp index bc9089a654..8bdcca59d6 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -2388,6 +2388,10 @@ static bool is_designated_for_track_carving(const designation &designation) { return occ.bits.carve_track_east || occ.bits.carve_track_north || occ.bits.carve_track_south || occ.bits.carve_track_west; } +static bool is_designated_for_digging(const designation &designation) { + return designation.td.bits.dig != df::tile_dig_designation::No; +} + static char get_track_char(const designation &designation) { const df::tile_occupancy &occ = designation.to; if (occ.bits.carve_track_east && occ.bits.carve_track_north && occ.bits.carve_track_south && occ.bits.carve_track_west) @@ -2444,8 +2448,8 @@ static char get_tile_char(const df::coord &pos, char desig_char, bool draw_prior } } -static void paintScreenCarve() { - TRACE(log).print("entering paintScreenCarve\n"); +static void paintScreenDesignated() { + TRACE(log).print("entering paintScreenDesignated\n"); if (Screen::inGraphicsMode() || blink(500)) return; @@ -2461,11 +2465,6 @@ static void paintScreenCarve() { if (!Maps::isValidTilePos(map_pos)) continue; - if (!Maps::isTileVisible(map_pos)) { - TRACE(log).print("skipping hidden tile\n"); - continue; - } - TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", map_pos.x, map_pos.y, map_pos.z, x, y); @@ -2486,8 +2485,14 @@ static void paintScreenCarve() { else if (is_designated_for_track_carving(des)) { cur_tile.ch = get_tile_char(map_pos, get_track_char(des), draw_priority); // directional track } + else if (is_designated_for_digging(des)) { + static char empty_char = (char)0x00; + cur_tile.ch = get_tile_char(map_pos, empty_char, draw_priority); + if (cur_tile.ch == empty_char) + continue; + } else { - TRACE(log).print("skipping tile with no carving designation\n"); + TRACE(log).print("skipping tile with no designation\n"); continue; } @@ -2515,6 +2520,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_FUNCTION(toggleCurLevelWarmDig), DFHACK_LUA_FUNCTION(toggleCurLevelDampDig), DFHACK_LUA_FUNCTION(paintScreenWarmDamp), - DFHACK_LUA_FUNCTION(paintScreenCarve), + DFHACK_LUA_FUNCTION(paintScreenDesignated), DFHACK_LUA_END }; diff --git a/plugins/lua/dig.lua b/plugins/lua/dig.lua index b988e9d42e..babdb3b7ab 100644 --- a/plugins/lua/dig.lua +++ b/plugins/lua/dig.lua @@ -348,13 +348,18 @@ function WarmDampOverlay:onRenderFrame(dc, rect) end -- -------------------------------- --- CarveOverlay +-- DesignatedOverlay -- -CarveOverlay = defclass(CarveOverlay, overlay.OverlayWidget) -CarveOverlay.ATTRS{ +DesignatedOverlay = defclass(DesignatedOverlay, overlay.OverlayWidget) +DesignatedOverlay.ATTRS{ desc='Makes existing carving designations visible when in ASCII mode.', viewscreens={ + 'dwarfmode/Designate/DIG_DIG', + 'dwarfmode/Designate/DIG_REMOVE_STAIRS_RAMPS', + 'dwarfmode/Designate/DIG_STAIR_UPDOWN', + 'dwarfmode/Designate/DIG_RAMP', + 'dwarfmode/Designate/DIG_CHANNEL', 'dwarfmode/Designate/SMOOTH', 'dwarfmode/Designate/ENGRAVE', 'dwarfmode/Designate/TRACK', @@ -365,8 +370,8 @@ CarveOverlay.ATTRS{ frame={w=0, h=0}, } -function CarveOverlay:onRenderFrame() - paintScreenCarve() +function DesignatedOverlay:onRenderFrame() + paintScreenDesignated() end -- -------------------------------- @@ -374,7 +379,7 @@ end -- OVERLAY_WIDGETS = { - asciicarve=CarveOverlay, + asciidesignated=DesignatedOverlay, warmdamp=WarmDampOverlay, warmdamptoolbar=WarmDampToolbarOverlay, } From 9933825d9d27041624770c66cf0ee9942beb0180 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 13:30:37 -0700 Subject: [PATCH 0359/1082] canonicalize slashes in output of dfhack.filesystem.listdir --- library/LuaApi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 927e2f6d72..1d77ebf15a 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3176,7 +3176,7 @@ static int filesystem_listdir(lua_State *L) for(size_t i=0;i Date: Fri, 9 May 2025 13:31:59 -0700 Subject: [PATCH 0360/1082] fix misplaced line in changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ff76e3d6b7..0b824ae90d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,7 @@ Template for new versions: ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. - `dig`: ASCII overlay now displays priority of digging designations +- `spectate`: added prefer nicknamed units ## Documentation @@ -109,7 +110,6 @@ Template for new versions: ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) -- `spectate`: added prefer nicknamed units ## API - ``Military`` module: added ``addToSquad`` function From e7f17ca5f81b4c894795b69b07fd3c3658cd6936 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 13:42:01 -0700 Subject: [PATCH 0361/1082] remove obsolete reference to ruby scripts --- library/Core.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b7a4bc5227..1208650579 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1284,9 +1284,6 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s std::string completed; std::filesystem::path filename = findScript(first + ".lua"); bool lua = !filename.empty(); - if ( !lua ) { - filename = findScript(first + ".rb"); - } if ( lua ) res = runLuaScript(con, first, parts); else if (!no_autocomplete && try_autocomplete(con, first, completed)) From 382191cce80e4bb2a0735bc50e8385b6622127df Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 15:43:39 -0700 Subject: [PATCH 0362/1082] record stockpile properties in blueprint --- docs/changelog.txt | 2 + plugins/blueprint.cpp | 347 ++++++++++++++++++++++---------------- plugins/logistics.cpp | 22 ++- plugins/lua/blueprint.lua | 16 ++ 4 files changed, 231 insertions(+), 156 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0b824ae90d..abd1b845fc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -69,6 +69,8 @@ Template for new versions: - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. - `dig`: ASCII overlay now displays priority of digging designations - `spectate`: added prefer nicknamed units +- `blueprint`: support for recording zones +- `blueprint`: support for recording stockpile properties like names and stockpile links; does not yet support recording detailed contents configuration ## Documentation diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 7d2b656886..26caf25816 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -182,7 +182,12 @@ struct blueprint_processor { get_tile(get_tile), init_ctx(init_ctx) { } }; -// global caches, cleared when the string cache is cleared +// global caches, lazily initialized and cleared at the end of each blueprint +// this assumes that no two blueprints are being generated at the same time, +// which is currently ensured by the higher-level DFHack command handling code. +// if this assumption ever becomes untrue, we'll need to protect the caches +// with thread synchronization primitives or make the caches per-blueprint. +static std::set string_cache; static std::unordered_map engravings_cache; static std::unordered_map dig_job_cache; static PersistentDataItem warm_config, damp_config; @@ -214,6 +219,14 @@ static void init_caches(DFHack::color_ostream &out, bool cache_engravings) { }); } +static void clear_caches() { + string_cache.clear(); + engravings_cache.clear(); + dig_job_cache.clear(); + warm_config = PersistentDataItem(); + damp_config = PersistentDataItem(); +} + // We use const char * throughout this code instead of std::string to avoid // having to allocate memory for all the small string literals. This // significantly speeds up processing and allows us to handle very large maps @@ -222,19 +235,9 @@ static void init_caches(DFHack::color_ostream &out, bool cache_engravings) { // allocated until we write out the blueprints at the end. // If NULL is passed as the str, the cache is cleared. static const char * cache(const char *str) { - // this local static assumes that no two blueprints are being generated at - // the same time, which is currently ensured by the higher-level DFHack - // command handling code. if this assumption ever becomes untrue, we'll - // need to protect the cache with thread synchronization primitives or make - // the cache per-blueprint. - static std::set _cache; - if (!str) { - _cache.clear(); - engravings_cache.clear(); - dig_job_cache.clear(); + if (!str) return NULL; - } - return _cache.emplace(str).first->c_str(); + return string_cache.emplace(str).first->c_str(); } // Convenience wrapper for std::string. @@ -1024,21 +1027,18 @@ static const char * add_expansion_syntax(const df::building *bld, return cache(s); } +static void add_expansion_syntax(const df::building *bld, ostringstream &keys) { + if (keys.str().empty()) + return; + pair size = get_building_size(bld); + keys << "(" << size.first << "x" << size.second << ")"; +} + static const char * add_expansion_syntax(const tile_context &ctx, const char *keys) { return add_expansion_syntax(ctx.b, keys); } -static const char * add_label(const tile_context &ctx, const char *keys) { - if (!keys) - return "~"; - auto bld = ctx.b; - ostringstream s; - // use building's id as the unique label - s << keys << "/" << "bld_" << bld->id; - return cache(s); -} - static const char * get_tile_build(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (!ctx.b || ctx.b->getType() == building_type::Stockpile) { return NULL; @@ -1052,51 +1052,152 @@ static const char * get_tile_build(color_ostream &out, const df::coord &pos, con return add_expansion_syntax(ctx, keys); } -static const char * get_place_keys(const tile_context &ctx) { - df::building_stockpilest* sp = - virtual_cast(ctx.b); - if (!sp) { - return NULL; - } +static string quotify_inner(const string &s) { + if (s.find_first_of(" ,") == string::npos) + return s; + ostringstream buf; + buf << "\"\"" << s << "\"\""; + return buf.str(); +} + +static string quotify_outer(const string &s) { + if (s.find_first_of("\",") == string::npos) + return s; + ostringstream buf; + buf << "\"" << s << "\""; + return buf.str(); +} - string keys; +static string quotify_outer(const ostringstream &s) { + return quotify_outer(s.str()); +} + +static void get_place_keys(color_ostream &out, ostringstream &keys, df::building_stockpilest* sp, bool add_label, bool add_properties) { df::stockpile_group_set &flags = sp->settings.flags; - if (flags.bits.animals) keys += 'a'; - if (flags.bits.food) keys += 'f'; - if (flags.bits.furniture) keys += 'u'; - if (flags.bits.coins) keys += 'n'; - if (flags.bits.corpses) keys += 'y'; - if (flags.bits.refuse) keys += 'r'; - if (flags.bits.stone) keys += 's'; - if (flags.bits.wood) keys += 'w'; - if (flags.bits.gems) keys += 'e'; - if (flags.bits.bars_blocks) keys += 'b'; - if (flags.bits.cloth) keys += 'h'; - if (flags.bits.leather) keys += 'l'; - if (flags.bits.ammo) keys += 'z'; - if (flags.bits.sheet) keys += 'S'; - if (flags.bits.finished_goods) keys += 'g'; - if (flags.bits.weapons) keys += 'p'; - if (flags.bits.armor) keys += 'd'; - - if (keys.empty()) - return "c"; - return cache(keys); + if (flags.bits.animals) keys << 'a'; + if (flags.bits.food) keys << 'f'; + if (flags.bits.furniture) keys << 'u'; + if (flags.bits.coins) keys << 'n'; + if (flags.bits.corpses) keys << 'y'; + if (flags.bits.refuse) keys << 'r'; + if (flags.bits.stone) keys << 's'; + if (flags.bits.wood) keys << 'w'; + if (flags.bits.gems) keys << 'e'; + if (flags.bits.bars_blocks) keys << 'b'; + if (flags.bits.cloth) keys << 'h'; + if (flags.bits.leather) keys << 'l'; + if (flags.bits.ammo) keys << 'z'; + if (flags.bits.sheet) keys << 'S'; + if (flags.bits.finished_goods) keys << 'g'; + if (flags.bits.weapons) keys << 'p'; + if (flags.bits.armor) keys << 'd'; + + if (keys.str().empty()) + keys << 'c'; + + if (!add_label && !add_properties) + return; + + if (add_label) + keys << "/" << "sp_" << sp->id; + + if (!add_properties) + return; + + vector properties; + + if (!sp->name.empty()) + properties.push_back("name=" + quotify_inner(sp->name)); + + // only include take_from and give_to targets if they are named + vector take_from, give_to; + for (auto & target : sp->links.take_from_pile) { + if (target->name.empty()) + continue; + take_from.push_back(target->name); + } + for (auto & target : sp->links.take_from_workshop) { + if (target->name.empty()) + continue; + take_from.push_back(target->name); + } + for (auto & target : sp->links.give_to_pile) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); + } + for (auto & target : sp->links.give_to_workshop) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); + } + if (!take_from.empty()) + properties.push_back("take_from=" + quotify_inner(join_strings(",", take_from))); + if (!give_to.empty()) + properties.push_back("give_to=" + quotify_inner(join_strings(",", give_to))); + + if (sp->stockpile_flag.bits.use_links_only) + properties.push_back("links_only=true"); + + // simplify implementation; always record container counts, even if they are set to default values + if (!sp->storage.max_barrels && !sp->storage.max_bins && !sp->storage.max_wheelbarrows) + properties.push_back("containers=0"); + else { + properties.push_back("barrels=" + int_to_string(sp->storage.max_barrels)); + properties.push_back("bins=" + int_to_string(sp->storage.max_bins)); + properties.push_back("wheelbarrows=" + int_to_string(sp->storage.max_wheelbarrows)); + } + + // logistics features + Lua::CallLuaModuleFunction(out, "plugins.blueprint", "get_logistics_settings", + std::make_tuple(sp->stockpile_number), 6, [&](lua_State *L) { + if (lua_toboolean(L, -6)) properties.push_back("automelt=true"); + if (lua_toboolean(L, -5)) properties.push_back("autotrade=true"); + if (lua_toboolean(L, -4)) properties.push_back("autodump=true"); + if (lua_toboolean(L, -3)) properties.push_back("autotrain=true"); + if (lua_toboolean(L, -2)) properties.push_back("autoforbid=true"); + if (lua_toboolean(L, -1)) properties.push_back("autoclaim=true"); + }); + + if (!properties.empty()) + keys << '{' << join_strings(" ", properties) << '}'; +} + +static df::coord get_first_tile(df::building *bld) { + df::coord first_pos; + cuboid bld_area(bld->x1, bld->y1, bld->z, bld->x2, bld->y2, bld->z); + bld_area.forCoord([&](const df::coord &pos) { + if (Buildings::containsTile(bld, pos)) { + first_pos = pos; + return false; + } + return true; + }, true); + + return first_pos; } static const char * get_tile_place(color_ostream &out, const df::coord &pos, const tile_context &ctx) { - if (!ctx.b || ctx.b->getType() != building_type::Stockpile) + df::building_stockpilest* sp = virtual_cast(ctx.b); + + if (!sp || sp->getType() != building_type::Stockpile) return NULL; - if (!is_rectangular(ctx)) - return add_label(ctx, get_place_keys(ctx)); + bool rectangular = is_rectangular(sp); + bool is_first_tile = pos == get_first_tile(sp); + ostringstream keys; - if (ctx.b->x1 != static_cast(pos.x) - || ctx.b->y1 != static_cast(pos.y)) { - return if_pretty(ctx, "`"); + if (!rectangular){ + get_place_keys(out, keys, sp, true, is_first_tile); + return cache(quotify_outer(keys)); } - return add_expansion_syntax(ctx, get_place_keys(ctx)); + if (!is_first_tile) + return if_pretty(ctx, "`"); + + get_place_keys(out, keys, sp, false, true); + add_expansion_syntax(sp, keys); + return cache(quotify_outer(keys)); } static string get_reservation(color_ostream &out, df::building_civzonest *zone) { @@ -1110,12 +1211,11 @@ static string get_reservation(color_ostream &out, df::building_civzonest *zone) } // TODO: handle locations -static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_label, bool add_properties) { - const char * symbol = NULL; +static void get_zone_keys(color_ostream &out, ostringstream &keys, df::building_civzonest *zone, bool add_label, bool add_properties) { vector properties; if (!zone->name.empty()) - properties.push_back("name=" + zone->name); + properties.push_back(quotify_inner("name=" + zone->name)); if (!zone->spec_sub_flag.bits.active) properties.push_back("active=false"); if (auto reserved_for = get_reservation(out, zone); !reserved_for.empty()) { @@ -1125,26 +1225,26 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo // in DFHack docs order switch (zone->type) { using namespace df::enums::civzone_type; - case MeetingHall: symbol = "m"; break; - case Bedroom: symbol = "b"; break; - case DiningHall: symbol = "h"; break; - case Pen: symbol = "n"; break; + case MeetingHall: keys << "m"; break; + case Bedroom: keys << "b"; break; + case DiningHall: keys << "h"; break; + case Pen: keys << "n"; break; case Pond: - symbol = "p"; + keys << "p"; { if (zone->zone_settings.pond.flag.bits.keep_filled) properties.push_back("pond=true"); } break; - case WaterSource: symbol = "w"; break; - case Dungeon: symbol = "j"; break; - case FishingArea: symbol = "f"; break; - case SandCollection: symbol = "s"; break; - case Office: symbol = "o"; break; - case Dormitory: symbol = "D"; break; - case Barracks: symbol = "B"; break; + case WaterSource: keys << "w"; break; + case Dungeon: keys << "j"; break; + case FishingArea: keys << "f"; break; + case SandCollection: keys << "s"; break; + case Office: keys << "o"; break; + case Dormitory: keys << "D"; break; + case Barracks: keys << "B"; break; case ArcheryRange: - symbol = "a"; + keys << "a"; { auto & archery = zone->zone_settings.archery; if (archery.dir_x == 1 && archery.dir_y == 0) @@ -1155,14 +1255,16 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo properties.push_back("shoot_from=north"); else if (archery.dir_x == 0 && archery.dir_y == -1) properties.push_back("shoot_from=south"); - else - return NULL; // invalid direction + else { + keys.clear(); + return; // invalid direction + } } break; - case Dump: symbol = "d"; break; - case AnimalTraining: symbol = "t"; break; + case Dump: keys << "d"; break; + case AnimalTraining: keys << "t"; break; case Tomb: - symbol = "T"; + keys << "T"; { auto & tomb = zone->zone_settings.tomb; if (!tomb.flags.bits.no_pets) @@ -1172,7 +1274,7 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo } break; case PlantGathering: - symbol = "g"; + keys << "g"; { auto & gather = zone->zone_settings.gather; if (!gather.flags.bits.pick_trees) @@ -1183,73 +1285,18 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo properties.push_back("gather_fallen=false"); } break; - case ClayCollection: symbol = "c"; break; + case ClayCollection: keys << "c"; break; default: - return NULL; + return; } if (!add_label && (!add_properties || properties.empty())) - return symbol; + return; - ostringstream keys; - keys << symbol; if (add_label) keys << "/" << "zone_" << zone->id; - if (add_properties) + if (add_properties && !properties.empty()) keys << '{' << join_strings(" ", properties) << '}'; - return cache(keys.str()); -} - -static df::coord get_first_tile(df::building_civzonest *zone) { - df::coord first_pos; - cuboid zone_area(zone->x1, zone->y1, zone->z, zone->x2, zone->y2, zone->z); - zone_area.forCoord([&](const df::coord &pos) { - if (Buildings::containsTile(zone, pos)) { - first_pos = pos; - return false; - } - return true; - }, true); - - return first_pos; -} - -static int32_t get_flood_size(const df::building::T_room &room, int32_t start_x, int32_t start_y) { - if (!room.extents) - return 0; - std::unordered_set visited; - std::queue to_visit; - to_visit.push(df::coord(start_x, start_y, 0)); - while (!to_visit.empty()) { - df::coord pos = to_visit.front(); - to_visit.pop(); - if (visited.count(pos)) - continue; - visited.insert(pos); - for (int32_t y = -1; y <= 1; ++y) { - for (int32_t x = -1; x <= 1; ++x) { - if (x == 0 && y == 0) - continue; - int32_t nx = pos.x + x; - int32_t ny = pos.y + y; - if (nx < 0 || ny < 0 || nx >= room.width || ny >= room.height) - continue; - if (room.extents[ny * room.width + nx]) - to_visit.push(df::coord(nx, ny, 0)); - } - } - } - // flood size is the number of tiles we visited - return (int32_t)visited.size(); -} - -static bool is_disjoint(df::building *bld, const df::coord &first_tile) { - const df::building::T_room &room = bld->room; - if (!room.extents) - return false; - - int32_t flood_size = get_flood_size(room, first_tile.x - bld->x1, first_tile.y - bld->y1); - return flood_size != Buildings::countExtentTiles(bld); } static const char * get_tile_zone(color_ostream &out, const df::coord &pos, const tile_context &ctx) { @@ -1263,9 +1310,9 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons // -- no two non-rectangular zones overlap // for a first implementation, we will only handle overlapping zones if they - // are rectangular. if this is the upper left corner of a rectangular zone, - // we will output for that zone. otherwise, if this pos is interior to - // all zones, then it doesn't matter which we choose. + // are rectangular and have different upper-left corners. if this is the upper + // left corner of a rectangular zone, we will output for that zone. otherwise, + // if this pos is interior to all zones, then it doesn't matter which we choose. df::building_civzonest * primary_zone = civzones[0]; df::coord upper_left_corner; @@ -1283,16 +1330,21 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons } } - bool disjoint = is_disjoint(primary_zone, upper_left_corner); + bool rectangular = is_rectangular(primary_zone); bool is_first_tile = pos == upper_left_corner; + ostringstream keys; - if (!is_rectangular(primary_zone)) - return get_zone_keys(out, primary_zone, disjoint, is_first_tile); + if (!rectangular) { + get_zone_keys(out, keys, primary_zone, true, is_first_tile); + return cache(quotify_outer(keys)); + } if (!is_first_tile) return if_pretty(ctx, "`"); - return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, disjoint, true)); + get_zone_keys(out, keys, primary_zone, false, true); + add_expansion_syntax(primary_zone, keys); + return cache(quotify_outer(keys)); } static bool create_output_dir(color_ostream &out, @@ -1647,8 +1699,7 @@ static command_result do_blueprint(color_ostream &out, bool ok = do_transform(out, start, end, options, files); - // clear caches - cache(NULL); + clear_caches(); return ok ? CR_OK : CR_FAILURE; } diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index bcb59b7ad5..90b31b3509 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -59,6 +59,12 @@ enum StockpileConfigValues { STOCKPILE_CONFIG_FORBID = 6, }; +enum StockpileConfigForbidValues { + STOCKPILE_CONFIG_FORBID_OFF = 0, + STOCKPILE_CONFIG_FORBID_FORBID = 1, + STOCKPILE_CONFIG_FORBID_CLAIM = 2, +}; + static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stockpile_number) { TRACE(control, out).print("ensuring stockpile config stockpile_number=%d\n", stockpile_number); if (watched_stockpiles.count(stockpile_number)) { @@ -76,7 +82,7 @@ static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stock c.set_bool(STOCKPILE_CONFIG_DUMP, false); c.set_bool(STOCKPILE_CONFIG_TRAIN, false); c.set_bool(STOCKPILE_CONFIG_MELT_MASTERWORKS, false); - c.set_int(STOCKPILE_CONFIG_FORBID, 0); + c.set_int(STOCKPILE_CONFIG_FORBID, STOCKPILE_CONFIG_FORBID_OFF); return c; } @@ -132,7 +138,7 @@ static void validate_stockpile_configs(color_ostream& out, !c.get_bool(STOCKPILE_CONFIG_TRADE) && !c.get_bool(STOCKPILE_CONFIG_DUMP) && !c.get_bool(STOCKPILE_CONFIG_TRAIN) && - !(c.get_int(STOCKPILE_CONFIG_FORBID) > 0))) { + !(c.get_int(STOCKPILE_CONFIG_FORBID) > STOCKPILE_CONFIG_FORBID_OFF))) { to_remove.push_back(stockpile_number); continue; } @@ -180,7 +186,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { if (c.key() == CONFIG_KEY) continue; if (c.get_int(STOCKPILE_CONFIG_FORBID) == -1) // remove this once saves from 51.01 are no longer compatible - c.set_int(STOCKPILE_CONFIG_FORBID, 0); + c.set_int(STOCKPILE_CONFIG_FORBID, STOCKPILE_CONFIG_FORBID_OFF); watched_stockpiles.emplace(c.get_int(STOCKPILE_CONFIG_STOCKPILE_NUMBER), c); } migrate_old_keys(out); @@ -560,8 +566,8 @@ static void do_cycle(color_ostream& out, bool trade = c.get_bool(STOCKPILE_CONFIG_TRADE); bool dump = c.get_bool(STOCKPILE_CONFIG_DUMP); bool train = c.get_bool(STOCKPILE_CONFIG_TRAIN); - bool forbid = 1 == c.get_int(STOCKPILE_CONFIG_FORBID); - bool claim = 2 == c.get_int(STOCKPILE_CONFIG_FORBID); + bool forbid = STOCKPILE_CONFIG_FORBID_FORBID == c.get_int(STOCKPILE_CONFIG_FORBID); + bool claim = STOCKPILE_CONFIG_FORBID_CLAIM == c.get_int(STOCKPILE_CONFIG_FORBID); MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats, melt_masterworks); TradeStockProcessor trade_stock_processor(stockpile_number, trade, trade_stats); @@ -645,8 +651,8 @@ static int logistics_getStockpileData(lua_State *L) { bool trade = c.get_bool(STOCKPILE_CONFIG_TRADE); bool dump = c.get_bool(STOCKPILE_CONFIG_DUMP); bool train = c.get_bool(STOCKPILE_CONFIG_TRAIN); - bool forbid = 1 == c.get_int(STOCKPILE_CONFIG_FORBID); - bool claim = 2 == c.get_int(STOCKPILE_CONFIG_FORBID); + bool forbid = STOCKPILE_CONFIG_FORBID_FORBID == c.get_int(STOCKPILE_CONFIG_FORBID); + bool claim = STOCKPILE_CONFIG_FORBID_CLAIM == c.get_int(STOCKPILE_CONFIG_FORBID); unordered_map sconfig; sconfig.emplace("melt", melt ? "true" : "false"); @@ -719,7 +725,7 @@ static unordered_map get_stockpile_config(int32_t stockpile_number) stockpile_config.emplace("trade", false); stockpile_config.emplace("dump", false); stockpile_config.emplace("train", false); - stockpile_config.emplace("forbid", 0); + stockpile_config.emplace("forbid", STOCKPILE_CONFIG_FORBID_OFF); } return stockpile_config; } diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index e43ce5f5f6..8c765d563f 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -1,6 +1,7 @@ local _ENV = mkmodule('plugins.blueprint') local argparse = require('argparse') +local logistics = require('plugins.logistics') local utils = require('utils') local valid_phase_list = { @@ -221,6 +222,21 @@ function get_filename(opts, phase, ordinal) return ('%s-%d-%s.csv'):format(fullname, ordinal, phase) end +function get_logistics_settings(stockpile_number) + local automelt, autotrade, autodump, autotrain, autoforbid, autoclaim = false, false, false, false, false, false + local configs = logistics.logistics_getStockpileConfigs(stockpile_number) + if configs and #configs == 1 then + local config = configs[1] + automelt = config.melt ~= 0 + autotrade = config.trade ~= 0 + autodump = config.dump ~= 0 + autotrain = config.train ~= 0 + autoforbid = config.forbid == 1 + autoclaim = config.forbid == 2 + end + return automelt, autotrade, autodump, autotrain, autoforbid, autoclaim +end + -- compatibility with old exported API. local function do_phase(start_pos, end_pos, name, phase) local width = math.abs(start_pos.x - end_pos.x) + 1 From 1400be4c00f8fd43d7501e8731af9aa2f3d7aa1f Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 21 May 2025 13:42:37 -0500 Subject: [PATCH 0363/1082] Add id option --- docs/plugins/strangemood.rst | 4 ++++ plugins/strangemood.cpp | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/plugins/strangemood.rst b/docs/plugins/strangemood.rst index b7d87196e2..e9f0033783 100644 --- a/docs/plugins/strangemood.rst +++ b/docs/plugins/strangemood.rst @@ -29,6 +29,10 @@ Options Make the strange mood strike the selected unit instead of picking one randomly. Unit eligibility is still enforced (unless ``--force`` is also specified). +``--id`` + Make the strange mood strike the unit with the given id instead of picking one + randomly. Unit eligibility is still enforced (unless ``--force`` is also + specified). ``--type `` Force the mood to be of a particular type instead of choosing randomly based on happiness. Valid values are "fey", "secretive", "possessed", "fell", and diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 98b8b4b725..3bdd9431df 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -164,6 +164,18 @@ command_result df_strangemood (color_ostream &out, vector & parameters) return CR_WRONG_USAGE; else if(parameters[i] == "--force") force = true; + else if(parameters[i] == "--id") + { + i++; + if (i == parameters.size()) + { + out.printerr("No unit id specified!\n"); + return CR_WRONG_USAGE; + } + unit = df::unit::find(std::stoi(parameters[i])); + if (!unit) + return CR_FAILURE; + } else if(parameters[i] == "--unit") { unit = DFHack::Gui::getSelectedUnit(out); From 23a9e3c9d424c75f574345d42b4f4a69e79425ec Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 21 May 2025 13:45:44 -0500 Subject: [PATCH 0364/1082] Update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index abd1b845fc..1cf3d0d070 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -71,6 +71,7 @@ Template for new versions: - `spectate`: added prefer nicknamed units - `blueprint`: support for recording zones - `blueprint`: support for recording stockpile properties like names and stockpile links; does not yet support recording detailed contents configuration +- `strangemood`: support added for specifying unit id instead of selected unit or random one. ## Documentation From a8661d674f5b0d1696be52b8892999dd6f66b4ec Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 10 May 2025 00:49:39 -0500 Subject: [PATCH 0365/1082] lua/c++ interface code cleanup replace weird `operator new` override with `make_lua_userdata` template use placement `new` and a constructor for `DFRefHeader` --- library/LuaApi.cpp | 10 +++++----- library/LuaWrapper.cpp | 15 +++++++++------ library/include/LuaTools.h | 19 ++++++++++++++----- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1d77ebf15a..be010a359c 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -629,7 +629,7 @@ void Lua::Push(lua_State *L, const Screen::Pen &info) return; } - new (L) Pen(info); + Lua::make_lua_userdata(L, info); lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN); lua_setmetatable(L, -2); @@ -1128,7 +1128,7 @@ static int dfhack_random_init(lua_State *L) static int dfhack_random_new(lua_State *L) { - new (L) MersenneRNG(); + Lua::make_lua_userdata(L); lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_RANDOM_TOKEN); lua_setmetatable(L, -2); @@ -1212,19 +1212,19 @@ static int dfhack_random_perlin(lua_State *L) switch (size) { case 1: { - auto pdata = new (L) PerlinNoise1D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_1, 1); break; } case 2: { - auto pdata = new (L) PerlinNoise2D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_2, 1); break; } case 3: { - auto pdata = new (L) PerlinNoise3D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_3, 1); break; diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 8c4a17f8db..8a46e992f9 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -168,12 +168,15 @@ static void BuildTypeMetatable(lua_State *state, const type_identity *type); void LuaWrapper::push_object_ref(lua_State *state, void *ptr) { // stack: [metatable] - auto ref = (DFRefHeader*)lua_newuserdata(state, sizeof(DFRefHeader)); - ref->ptr = ptr; - ref->field_info = NULL; - ref->tag_ptr = NULL; - ref->tag_identity = NULL; - ref->tag_attr = NULL; + void* stg = lua_newuserdata(state, sizeof(DFRefHeader)); + auto ref = new (stg) DFRefHeader + { + .ptr = ptr, + .field_info = NULL, + .tag_ptr = NULL, + .tag_identity = NULL, + .tag_attr = NULL, + }; lua_swap(state); lua_setmetatable(state, -2); diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 429a8bfc6b..db40e76810 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -44,11 +44,6 @@ distribution. #include #include -/// Allocate a new user data object and push it on the stack -inline void *operator new (std::size_t size, lua_State *L) { - return lua_newuserdata(L, size); -} - namespace DFHack { class function_identity_base; struct MaterialInfo; @@ -67,6 +62,20 @@ namespace DFHack::Lua { */ DFHACK_EXPORT lua_State *Open(color_ostream &out, lua_State *state = NULL); + /** + * Allocate a lua userdata and construct a C++ object in that userdata's storage space + * The C++ object must be trivially destructible as lua GC will _not_ call the object's destructor + * be aware that the created userdata is left on the Lua stack as well as returned to the caller + */ + template + requires (std::is_trivially_destructible_v) + T* make_lua_userdata(lua_State* L, Args&&... args) + { + void* stg = lua_newuserdata(L, sizeof(T)); + T * obj = ::new (stg) T(std::forward(args)...); + return obj; + } + DFHACK_EXPORT void PushDFHack(lua_State *state); DFHACK_EXPORT void PushBaseGlobals(lua_State *state); From 776b4d421a5b4f54d73e6fd66a76034b4d31fd81 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 7 Jun 2025 14:34:04 -0500 Subject: [PATCH 0366/1082] remove unused variable --- library/LuaWrapper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 8a46e992f9..a0069fbbdc 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -169,7 +169,7 @@ void LuaWrapper::push_object_ref(lua_State *state, void *ptr) { // stack: [metatable] void* stg = lua_newuserdata(state, sizeof(DFRefHeader)); - auto ref = new (stg) DFRefHeader + new (stg) DFRefHeader { .ptr = ptr, .field_info = NULL, From 7c62106aac7b98432f4183cbf9ca642a1d26f2fc Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 25 Jun 2025 07:21:39 +0000 Subject: [PATCH 0367/1082] 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 d65c4ac560..eeb00caa9b 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit d65c4ac560264c4a0d69a802bc31b636c3da55fc +Subproject commit eeb00caa9bba4655e5095e1a767f4beee52bac8f From c38138192bdd06b6811754e4a6887d2ad50a834e Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 15 Jun 2025 19:53:27 -0700 Subject: [PATCH 0368/1082] Update plant.rst - Link to regrass tool --- docs/plugins/plant.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plugins/plant.rst b/docs/plugins/plant.rst index 51eedaf999..2a2d6327e0 100644 --- a/docs/plugins/plant.rst +++ b/docs/plugins/plant.rst @@ -9,7 +9,8 @@ Grow and remove shrubs or trees. Modes are ``list``, ``create``, ``grow``, and ``remove``. ``list`` prints a list of all valid shrub and sapling raw IDs. ``create`` allows the creation of new shrubs and saplings. ``grow`` adjusts the age of saplings and trees, allowing them to grow instantly. ``remove`` can -remove existing shrubs and saplings. +remove existing shrubs and saplings. For operating on grasses (such as bamboo) +use `regrass` instead. Usage ----- From dca9aded12c2b02bae4d57422050dd01cf6aae16 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 04:54:57 -0700 Subject: [PATCH 0369/1082] remove reference to unused SquadSupplies gui state --- library/modules/Gui.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index ff635ac418..f71654c024 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -762,11 +762,6 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.squad_supplies.open) { - newFocusString = baseFocus; - newFocusString += "/SquadSupplies"; - focusStrings.push_back(newFocusString); - } if (game->main_interface.squads.open) { newFocusString = baseFocus; newFocusString += "/Squads"; From 95dc5f083e7c83910319eed81fe8b81b5c146874 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:57:39 +0000 Subject: [PATCH 0370/1082] 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 f48fd9ef43..9c0222d1c1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f48fd9ef4369027499da28441adaf9c572431891 +Subproject commit 9c0222d1c17b66599ee57362c847ab78ee99723f From d7e65b07edad89df75d0a457aba5cc5d2c9f6efd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:12:08 -0700 Subject: [PATCH 0371/1082] add missing cases to timer switch --- library/modules/Units.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index ce135b477d..744a3979af 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2078,6 +2078,14 @@ static int32_t *getActionTimerPointer(df::unit_action *action) { return &action->data.dismount.timer; case unit_action_type::HoldItem: return &action->data.holditem.timer; + case unit_action_type::LoadRangedWeapon: + return &action->data.loadrangedweapon.movewait; + case unit_action_type::ShootRangedWeapon: + return &action->data.shootrangedweapon.movewait; + case unit_action_type::ThrowItem: + return &action->data.throwitem.movewait; + case unit_action_type::PostShootRecovery: + return &action->data.postshootrecovery.movewait; case unit_action_type::LeadAnimal: case unit_action_type::StopLeadAnimal: case unit_action_type::Jump: From fb6f8bb5d4e6d4f54d4e9ded1e2bf7a7ae39b14f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:22:27 -0700 Subject: [PATCH 0372/1082] remove reference to removed think_counter field --- plugins/timestream.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index eeb4d1115c..a5e5b04c80 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -341,7 +341,6 @@ static void decrement_counter(T *obj, FT T::*field, int32_t timeskip) { static void adjust_unit_counters(df::unit * unit, int32_t timeskip) { auto * c1 = &unit->counters; - decrement_counter(c1, &df::unit::T_counters::think_counter, timeskip); decrement_counter(c1, &df::unit::T_counters::job_counter, timeskip); decrement_counter(c1, &df::unit::T_counters::swap_counter, timeskip); decrement_counter(c1, &df::unit::T_counters::winded, timeskip); From 055930a83ebc00b99481159dbbeecc62cb1b805b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:31:45 -0700 Subject: [PATCH 0373/1082] bump version to 51.12 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 16 ++++++++++++++-- library/xml | 2 +- scripts | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d2d1c8ec9e..6c2e619ad3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.11") -set(DFHACK_RELEASE "r1.2") +set(DF_VERSION "51.12") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index abd1b845fc..9c4a419513 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,20 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 51.12-r1 + ## Fixes - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed - `getplants`: use updated formula for calculating whether plant growths are ripe @@ -72,8 +86,6 @@ Template for new versions: - `blueprint`: support for recording zones - `blueprint`: support for recording stockpile properties like names and stockpile links; does not yet support recording detailed contents configuration -## Documentation - ## API - ``Random`` module: added ``SplitmixRNG`` class, implements the Splitmix64 RNG used by Dwarf Fortress for "simple" randomness - ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys vanilla SHOW_IMP_QUALITY setting diff --git a/library/xml b/library/xml index 9c0222d1c1..9dac571e64 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9c0222d1c17b66599ee57362c847ab78ee99723f +Subproject commit 9dac571e64bb99663f753ba5fe625fc79d0b2aeb diff --git a/scripts b/scripts index e50474efbf..8a2c8e7b13 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e50474efbfa514fd0e4b63cd9a8c114ccf0bb392 +Subproject commit 8a2c8e7b13d703da672532d408e4250dd11a86c9 From cbbbde6c6ac20dfe793e2b5496fda8711794b319 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:40:10 +0000 Subject: [PATCH 0374/1082] Auto-update structures ref for 51.12 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 9dac571e64..ece08d36c4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9dac571e64bb99663f753ba5fe625fc79d0b2aeb +Subproject commit ece08d36c4f08dac9822a4ededa78a2f8b5532a9 From e3165854e80411eec584c74949c73000f2aa0dcb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:44:39 -0700 Subject: [PATCH 0375/1082] ignore "dest" install target --- build/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/build/.gitignore b/build/.gitignore index f25d10ca7b..11f87d8c3e 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -7,3 +7,4 @@ _CPack_Packages *.tar.* .cmake win64-cross +dest From a10a0888e79c0b4f826d0676d62fd8af88a77089 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:14:02 +0000 Subject: [PATCH 0376/1082] Auto-update structures ref for 51.12 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ece08d36c4..b299620731 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ece08d36c4f08dac9822a4ededa78a2f8b5532a9 +Subproject commit b299620731342affa32414de855673fa54fabe6f From ecb68285e4e463dd2650be7e7857d94f85d46825 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:29:51 +0000 Subject: [PATCH 0377/1082] Auto-update structures ref for 51.12 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index b299620731..f18388f097 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b299620731342affa32414de855673fa54fabe6f +Subproject commit f18388f097c752cbd54ac791d687237e0b88cb5b From 5a635afc2e76c9f5d2a08cdc656531504bc02dd3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 26 Jun 2025 14:44:02 -0500 Subject: [PATCH 0378/1082] bump visual studio version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c2e619ad3..361965522f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ endif() if(WIN32) set(MSVC_MIN_VER 1930) - set(MSVC_MAX_VER 1943) + set(MSVC_MAX_VER 1944) if(NOT MSVC) message(SEND_ERROR "No MSVC found! MSVC 2022 version ${MSVC_MIN_VER} to ${MSVC_MAX_VER} is required.") elseif((MSVC_VERSION LESS MSVC_MIN_VER) OR (MSVC_VERSION GREATER MSVC_MAX_VER)) From 64222555f1834a99e5d575cf2b308a4c25a33287 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 12:45:45 -0700 Subject: [PATCH 0379/1082] update to 51.12-r1.1 for itch support --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 361965522f..fafa5bdcc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.12") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 2d61c2083a785fde02d219da466e67f361a34004 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 13:15:43 -0700 Subject: [PATCH 0380/1082] add missing changelog entry for 1.1 --- docs/changelog.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9c4a419513..486cb911b8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,12 @@ Template for new versions: ## Removed +# 51.12-r1.1 + +## New Features + +- Compatibility with Itch release of DF 51.12 + # 51.12-r1 ## Fixes From 00aeceb937f0ba50d9342d05928b675af3feb98d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 16:56:58 -0700 Subject: [PATCH 0381/1082] bump version to 51.13-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fafa5bdcc0..d8c4144655 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.12") -set(DFHACK_RELEASE "r1.1") +set(DF_VERSION "51.13") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 486cb911b8..b90b39a332 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,12 @@ Template for new versions: ## Removed +# 51.13-r1 + +## New Features + +- Compatibility with DF 51.13 + # 51.12-r1.1 ## New Features From fac6d9b4b00baf4046bf3087134c09972f116469 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Fri, 27 Jun 2025 00:01:40 +0000 Subject: [PATCH 0382/1082] Auto-update structures ref for 51.13 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f18388f097..309450b08a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f18388f097c752cbd54ac791d687237e0b88cb5b +Subproject commit 309450b08ad4c7b20470a9833b7b251ea64d8639 From 8cc84672058102afe14e6196ee343d1c792ca4a4 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Fri, 27 Jun 2025 00:09:43 +0000 Subject: [PATCH 0383/1082] Auto-update structures ref for 51.13 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 309450b08a..ae7424d3f4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 309450b08ad4c7b20470a9833b7b251ea64d8639 +Subproject commit ae7424d3f40bf60b3906b5b9b472cb9a7209a3a8 From 4457a8cc3ed967bb4e393224df103a054f894048 Mon Sep 17 00:00:00 2001 From: Quietust Date: Sat, 31 May 2025 14:48:08 -0600 Subject: [PATCH 0384/1082] Add "animaltrap-reuse" tweak Makes it so animal traps generate "Release Small Creature" jobs instead of "Destroy Building" jobs, so a dwarf will transfer the caught vermin into an empty stockpiled trap and leave the built trap ready to be automatically filled with new bait. --- docs/changelog.txt | 1 + docs/plugins/tweak.rst | 3 + plugins/tweak/tweak.cpp | 3 + plugins/tweak/tweaks/animaltrap-reuse.h | 84 +++++++++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 plugins/tweak/tweaks/animaltrap-reuse.h diff --git a/docs/changelog.txt b/docs/changelog.txt index b90b39a332..fa559df8ea 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,7 @@ Template for new versions: ## New Tools ## New Features +- `tweak`: ``animaltrap-reuse``: make it so built animal traps automatically unload the vermin they catch into stockpiled animal traps, so that they can be automatically re-baited and reused ## Fixes diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index 87361ef670..ae42aecac7 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -38,6 +38,9 @@ Commands ``adamantine-cloth-wear`` Prevents adamantine clothing from wearing out while being worn (:bug:`6481`). +``animaltrap-reuse`` + Makes built animal traps unload caught vermin into stockpiled animal traps + so that they can be automatically re-baited and reused. ``craft-age-wear`` Fixes crafted items not wearing out over time (:bug:`6003`). With this tweak, items made from cloth and leather will gain a level of wear every 20 diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 74276bba1c..dd7edbf3e1 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -14,6 +14,7 @@ using std::string; using namespace DFHack; #include "tweaks/adamantine-cloth-wear.h" +#include "tweaks/animaltrap-reuse.h" #include "tweaks/craft-age-wear.h" #include "tweaks/eggs-fertile.h" #include "tweaks/fast-heat.h" @@ -53,6 +54,8 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector 1) && (contained_items[1]->item->getType() == df::item_type::VERMIN)) + { + auto trap = contained_items[0]->item; + auto vermin = contained_items[1]->item; + + // Make sure we don't already have a "Release Small Creature" job + for (size_t j = 0; j < jobs.size(); j++) + { + if (jobs[j]->job_type == df::job_type::ReleaseSmallCreature) + return; + // Also bail out if the player marked the building for destruction + if (jobs[j]->job_type == df::job_type::DestroyBuilding) + return; + } + + // Create the job + auto job = new df::job(); + Job::linkIntoWorld(job, true); + + job->job_type = df::job_type::ReleaseSmallCreature; + job->pos.x = centerx; + job->pos.y = centery; + job->pos.z = z; + + // Attach the vermin to the job + Job::attachJobItem(job, vermin, df::job_role_type::Hauled, -1, -1); + + // Link the job to the building + df::general_ref *ref = df::allocate(); + ref->setID(id); + job->general_refs.push_back(ref); + + jobs.push_back(job); + + // Hack: put the vermin inside the trap ITEM right away, otherwise the job will cancel. + // Normally, this doesn't happen until after the trap is deconstructed, but the game still + // seems to handle everything correctly and doesn't leave any bad references afterwards. + if (!Items::getContainer(vermin)) + { + // We can't use Items::moveToContainer, because that would remove it from the Building + // (and cause the game to no longer recognize the trap as being "full"). + // Instead, manually add the references and set the necessary bits. + + ref = df::allocate(); + ref->setID(trap->id); + vermin->general_refs.push_back(ref); + + ref = df::allocate(); + ref->setID(vermin->id); + trap->general_refs.push_back(ref); + + vermin->flags.bits.in_inventory = true; + trap->flags.bits.weight_computed = false; + // Don't set this flag here (even though it would normally get set), + // since the game doesn't clear it after the vermin gets taken out + // trap->flags.bits.container = true; + } + return; + } + INTERPOSE_NEXT(updateAction)(); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(animaltrap_reuse_hook, updateAction); From a94eb538bf3273609163491e5f7bb79c1357fc4c Mon Sep 17 00:00:00 2001 From: Quietust Date: Sat, 21 Jun 2025 10:09:00 -0600 Subject: [PATCH 0385/1082] Add "count" parameter to Items::createItem and update the "createitem" plugin to specify it instead of creating a bunch of individual items in a loop. --- docs/changelog.txt | 1 + library/LuaApi.cpp | 5 ++++- library/include/modules/Items.h | 2 +- library/modules/Items.cpp | 4 ++-- plugins/createitem.cpp | 16 +++++++--------- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b90b39a332..539b2c2240 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -90,6 +90,7 @@ Template for new versions: - `preserve-rooms`: don't warn when a room is assigned to a non-existent unit. this is now common behavior for DF when it keeps a room for an unloaded unit - fixed an overly restrictive type constraint that resulted in some object types being glossed as a boolean when passed as an argument from C++ to Lua - `plants`: will no longer generate a traceback when a filter is used +- `createitem`: multiple items should be properly created in stacks again ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1d77ebf15a..1d3f4075a2 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2461,8 +2461,11 @@ static int items_createItem(lua_State *state) auto mat_type = lua_tointeger(state, 4); auto mat_index = lua_tointeger(state, 5); bool no_floor = lua_toboolean(state, 6); + int count = lua_tointeger(state, 7); + if (count < 1) + count = 1; vector out_items; - Items::createItem(out_items, unit, item_type, item_subtype, mat_type, mat_index, no_floor); + Items::createItem(out_items, unit, item_type, item_subtype, mat_type, mat_index, no_floor, count); Lua::PushVector(state, out_items); return 1; } diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 6d068e8786..1189fd8bc2 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -173,7 +173,7 @@ DFHACK_EXPORT int getItemBaseValue(int16_t item_type, int16_t item_subtype, int1 DFHACK_EXPORT int getValue(df::item *item, df::caravan_state *caravan = NULL); DFHACK_EXPORT bool createItem(std::vector &out_items, df::unit *creator, df::item_type type, - int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor = false); + int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor = false, int32_t count = 1); // Returns true if the item is free from mandates, or false if mandates prevent trading the item. DFHACK_EXPORT bool checkMandates(df::item *item); diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index a161bea8a2..5b9e781721 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1752,7 +1752,7 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { } bool Items::createItem(vector &out_items, df::unit *unit, df::item_type item_type, - int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor) + int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor, int32_t count) { // Based on Quietust's plugins/createitem.cpp CHECK_NULL_POINTER(unit); auto pos = Units::getPosition(unit); @@ -1765,7 +1765,7 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t prod->mat_type = mat_type; prod->mat_index = mat_index; prod->probability = 100; - prod->count = 1; + prod->count = count; switch(item_type) { using namespace df::enums::item_type; diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index 1f4b582047..fba821b270 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -50,7 +50,7 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) { return CR_OK; } -bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_type, int32_t mat_index, +bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_type, int32_t mat_index, int32_t count, bool move_to_cursor = false, bool second_item = false) { // Special logic for making Gloves and Shoes in pairs bool is_gloves = (type == item_type::GLOVES); @@ -66,7 +66,7 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t bool on_floor = (container == NULL) && (building == NULL) && !move_to_cursor; vector out_items; - if (!Items::createItem(out_items, unit, type, subtype, mat_type, mat_index, !on_floor)) + if (!Items::createItem(out_items, unit, type, subtype, mat_type, mat_index, !on_floor, count)) return false; for (size_t i = 0; i < out_items.size(); i++) { @@ -105,7 +105,7 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t is_shoes = false; // If we asked for gloves/shoes and only got one (and we're making the first one), make another if ((is_gloves || is_shoes) && !second_item) - return makeItem(unit, type, subtype, mat_type, mat_index, move_to_cursor, true); + return makeItem(unit, type, subtype, mat_type, mat_index, count, move_to_cursor, true); return true; } @@ -435,12 +435,10 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { out.printerr("Previously selected building no longer exists - item will be placed on the floor.\n"); } - for (int i = 0; i < count; i++) { - if (!makeItem(unit, item_type, item_subtype, mat_type, mat_index, move_to_cursor, false)) - { - out.printerr("Failed to create item!\n"); - return CR_FAILURE; - } + if (!makeItem(unit, item_type, item_subtype, mat_type, mat_index, count, move_to_cursor, false)) + { + out.printerr("Failed to create item!\n"); + return CR_FAILURE; } return CR_OK; } From 910d0d48d91e740057c59f755986e0729e157408 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:30:45 +0000 Subject: [PATCH 0386/1082] Auto-update structures ref for 51.13-lua+filesystem-test-1 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ae7424d3f4..f306889012 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ae7424d3f40bf60b3906b5b9b472cb9a7209a3a8 +Subproject commit f30688901262aabef2c9dec0306e78a5b7350434 From c2d74714b6c5fb0c682c6f78a8f6c442af625a31 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 29 Jun 2025 07:19:34 +0000 Subject: [PATCH 0387/1082] 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 f306889012..ae7424d3f4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f30688901262aabef2c9dec0306e78a5b7350434 +Subproject commit ae7424d3f40bf60b3906b5b9b472cb9a7209a3a8 From 3ea3cb806040800ac838b571e0c142ce50bf55e7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:00:07 +0000 Subject: [PATCH 0388/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8a2c8e7b13..6f348f7d92 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8a2c8e7b13d703da672532d408e4250dd11a86c9 +Subproject commit 6f348f7d9267559e48ca6703d68eb234b4f8a8f1 From e76b960fc49a21f6e069822c4a21d9ea0abcf4e7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 2 Jul 2025 00:47:23 +0000 Subject: [PATCH 0389/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6f348f7d92..ba62d8e551 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6f348f7d9267559e48ca6703d68eb234b4f8a8f1 +Subproject commit ba62d8e5519e43fd777bca426ec62eb5bcd5ec9e From e89efb4f156aa1ded28b7759f1a7528ea530bf2c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Jul 2025 07:21:03 +0000 Subject: [PATCH 0390/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ba62d8e551..10a04ab91a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ba62d8e5519e43fd777bca426ec62eb5bcd5ec9e +Subproject commit 10a04ab91a50b357574b106df2cff96389e61ac3 From 62343fbb9c2ff9d0dce2c43e2f8a2cb88106d5ac Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 3 Jul 2025 07:53:57 -0500 Subject: [PATCH 0391/1082] `Units::getReadableName`: do not add commas to histfigs with no profession fixes #5474 --- docs/changelog.txt | 1 + library/modules/Units.cpp | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 539b2c2240..25e4e639b3 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession ## Misc Improvements diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 744a3979af..90fb8b593a 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1161,14 +1161,16 @@ static string formatReadableName(T *unit_or_hf, const string &prof_name, bool sk if (native_name.empty()) return prof_name; + std::string prof_suffix{ (prof_name.size() > 0) ? (", " + prof_name) : "" }; + if (skip_english) - return native_name + ", " + prof_name; + return native_name + prof_suffix; string english_name = Translation::translateName(visible_name, true, true); if (english_name.empty()) - return native_name + ", " + prof_name; + return native_name + prof_suffix; - return native_name + " \"" + english_name + "\", " + prof_name; + return native_name + " \"" + english_name + "\"" + prof_suffix; } string Units::getReadableName(df::historical_figure *hf, bool skip_english) { From d458d8ac6d0c12df74de194e524cd13092a704ba Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 3 Jul 2025 09:08:12 -0500 Subject: [PATCH 0392/1082] Update changelog.txt --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index a5e4a3fcab..e4ea864383 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,7 +54,6 @@ Template for new versions: ## New Tools ## New Features -- ``widgets.Slider``: new mouse-controlled single-headed slider widget ## Fixes @@ -65,6 +64,7 @@ Template for new versions: ## API ## Lua +- ``widgets.Slider``: new mouse-controlled single-headed slider widget ## Removed From 6707e20d9062483aa314c798fc21251dce9d2d81 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 3 Jul 2025 11:43:30 -0500 Subject: [PATCH 0393/1082] clean up the sliders and how they work. i have split out as much of the logic as possible. couldnt split out the `get_width_per_odx` bc we need the `frame_body` to not be nil. --- library/lua/gui/widgets/range_slider.lua | 128 +----------------- library/lua/gui/widgets/slide_core.lua | 163 +++++++++++++++++++++++ library/lua/gui/widgets/slider.lua | 70 +++++++--- 3 files changed, 224 insertions(+), 137 deletions(-) create mode 100644 library/lua/gui/widgets/slide_core.lua diff --git a/library/lua/gui/widgets/range_slider.lua b/library/lua/gui/widgets/range_slider.lua index f55f0e00f0..9b44e0fd54 100644 --- a/library/lua/gui/widgets/range_slider.lua +++ b/library/lua/gui/widgets/range_slider.lua @@ -1,17 +1,15 @@ -local Widget = require('gui.widgets.widget') - -local to_pen = dfhack.pen.parse +local core = require('gui.widgets.slide_core') -------------------------------- -- RangeSlider -------------------------------- ---@class widgets.RangeSlider.attrs: widgets.Widget.attrs ----@field num_stops integer ---@field get_left_idx_fn? function ---@field get_right_idx_fn? function ---@field on_left_change? fun(index: integer) ---@field on_right_change? fun(index: integer) +---@field is_single boolean ---@class widgets.RangeSlider.attrs.partial: widgets.RangeSlider.attrs @@ -22,28 +20,17 @@ local to_pen = dfhack.pen.parse ---@field super widgets.Widget ---@field ATTRS widgets.RangeSlider.attrs|fun(attributes: widgets.RangeSlider.attrs.partial) ---@overload fun(init_table: widgets.RangeSlider.initTable): self -RangeSlider = defclass(RangeSlider, Widget) +RangeSlider = defclass(RangeSlider, slide_core) RangeSlider.ATTRS{ - num_stops=DEFAULT_NIL, get_left_idx_fn=DEFAULT_NIL, get_right_idx_fn=DEFAULT_NIL, on_left_change=DEFAULT_NIL, on_right_change=DEFAULT_NIL, - is_single=DEFAULT_NIL + num_stops=DEFAULT_NIL, + is_single=false, } -function RangeSlider:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - -function RangeSlider:init() - if self.num_stops < 2 then error('too few RangeSlider stops') end - self.is_dragging_target = nil -- 'left', 'right', or 'both' - self.is_dragging_idx = nil -- offset from leftmost dragged tile -end - -local function rangeslider_get_width_per_idx(self) +function RangeSlider:get_width_per_idx() local min_value = (self.is_single) and 3 or 5 -- Single slider = 3, else 5 return math.max(min_value, (self.frame_body.width-7) // (self.num_stops-1)) end @@ -53,7 +40,7 @@ function RangeSlider:onInput(keys) local x = self:getMousePos() if not x then return false end local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() - local width_per_idx = rangeslider_get_width_per_idx(self) + local width_per_idx = self:get_width_per_idx() local left_pos = width_per_idx*(left_idx-1) local right_pos = width_per_idx*(right_idx-1) + 4 if x < left_pos then @@ -73,105 +60,4 @@ function RangeSlider:onInput(keys) return true end -local function rangeslider_do_drag(self, width_per_idx) - local x = self.frame_body:localXY(dfhack.screen.getMousePos()) - local cur_pos = x - self.is_dragging_idx - cur_pos = math.max(0, cur_pos) - cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) - local offset = self.is_dragging_target == 'right' and -2 or 1 - local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 - local new_left_idx, new_right_idx - if self.is_dragging_target == 'right' then - new_right_idx = new_idx - else - new_left_idx = new_idx - if self.is_dragging_target == 'both' then - new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() - if new_right_idx > self.num_stops then - return - end - end - end - if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then - if not new_right_idx and new_left_idx > self.get_right_idx_fn() then - self.on_right_change(new_left_idx) - end - self.on_left_change(new_left_idx) - end - if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then - if new_right_idx < self.get_left_idx_fn() then - self.on_left_change(new_right_idx) - end - self.on_right_change(new_right_idx) - end -end - -local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} - -function RangeSlider:onRenderBody(dc, rect) - local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() - local width_per_idx = rangeslider_get_width_per_idx(self) - -- draw track - dc:seek(1,0) - dc:char(nil, SLIDER_LEFT_END) - dc:char(nil, SLIDER_TRACK) - for stop_idx=1,self.num_stops-1 do - local track_stop_pen = SLIDER_TRACK_STOP_SELECTED - local track_pen = SLIDER_TRACK_SELECTED - if left_idx > stop_idx or right_idx < stop_idx then - track_stop_pen = SLIDER_TRACK_STOP - track_pen = SLIDER_TRACK - elseif right_idx == stop_idx then - track_pen = SLIDER_TRACK - end - dc:char(nil, track_stop_pen) - for i=2,width_per_idx do - dc:char(nil, track_pen) - end - end - if right_idx >= self.num_stops then - dc:char(nil, SLIDER_TRACK_STOP_SELECTED) - else - dc:char(nil, SLIDER_TRACK_STOP) - end - dc:char(nil, SLIDER_TRACK) - dc:char(nil, SLIDER_RIGHT_END) - - -- Draw tab(s) - if self.is_single then - -- Single slider: Draw one centered tab - dc:seek(width_per_idx * (left_idx-1) + 2) -- Center the tab - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - else - -- Dual slider: Draw left and right tabs separately - dc:seek(width_per_idx * (left_idx-1)) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - dc:seek(width_per_idx*(right_idx-1)+4) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - end - - -- Manage dragging - if self.is_dragging_target then - rangeslider_do_drag(self, width_per_idx) - end - if df.global.enabler.mouse_lbut_down == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil - end -end - return RangeSlider diff --git a/library/lua/gui/widgets/slide_core.lua b/library/lua/gui/widgets/slide_core.lua new file mode 100644 index 0000000000..450954b9c1 --- /dev/null +++ b/library/lua/gui/widgets/slide_core.lua @@ -0,0 +1,163 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- slide_core +-------------------------------- + +---@class widgets.slide_core.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field is_single boolean +---@field w integer + +---@class widgets.slide_core.attrs.partial: widgets.slide_core.attrs + +---@class widgets.slide_core.initTable: widgets.slide_core.attrs +---@field num_stops integer + +---@class widgets.slide_core: widgets.Widget, widgets.slide_core.attrs +---@field super widgets.Widget +---@field ATTRS widgets.slide_core.attrs|fun(attributes: widgets.slide_core.attrs.partial) +---@overload fun(init_table: widgets.slide_core.initTable): self +slide_core = defclass(slide_core, Widget) +slide_core.ATTRS{ + num_stops=DEFAULT_NIL, + is_single=DEFAULT_NIL, + w=DEFAULT_NIL +} + +function slide_core:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function slide_core:init() + local min_stops = self:get_min_stops() + if self.num_stops < min_stops then error(('too few stops, expected at least %s'):format(min_stops)) end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +function slide_core:get_min_stops() + return self.is_single and 1 or 2 +end + +local function do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = self.is_dragging_target == 'right' and -2 or 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + local new_left_idx, new_right_idx + if self.is_dragging_target == 'right' then + new_right_idx = new_idx + else + new_left_idx = new_idx + if self.is_dragging_target == 'both' then + if self.get_left_idx_fn ~= nil then + new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() + else + new_right_idx = new_left_idx + end + if new_right_idx > self.num_stops then + return + end + end + end + if self.get_idx_fn == nil then + if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then + if not new_right_idx and new_left_idx > self.get_right_idx_fn() then + self.on_right_change(new_left_idx) + end + self.on_left_change(new_left_idx) + end + if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then + if new_right_idx < self.get_left_idx_fn() then + self.on_left_change(new_right_idx) + end + self.on_right_change(new_right_idx) + end + else + if new_idx and new_idx ~= self.get_idx_fn() then + self.on_change(new_idx) + end + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function slide_core:onRenderBody(dc, rect) + local left_idx, right_idx + if self.get_idx_fn ~= nil then + left_idx = self.get_idx_fn() + right_idx = left_idx + else + left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + end + local width_per_idx = self:get_width_per_idx() + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx > stop_idx or right_idx < stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif right_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if right_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + + -- Draw tab(s) + if self.is_single then + -- Single slider: Draw one centered tab + dc:seek(width_per_idx * (left_idx-1) + 2) -- Center the tab + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + else + -- Dual slider: Draw left and right tabs separately + dc:seek(width_per_idx * (left_idx-1)) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + dc:seek(width_per_idx*(right_idx-1)+4) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + end + + -- Manage dragging + if self.is_dragging_target then + do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + +return slide_core diff --git a/library/lua/gui/widgets/slider.lua b/library/lua/gui/widgets/slider.lua index 23bf6f6202..65ce28a5c7 100644 --- a/library/lua/gui/widgets/slider.lua +++ b/library/lua/gui/widgets/slider.lua @@ -1,22 +1,60 @@ --- slider.lua -local RangeSlider = require('gui.widgets.range_slider') - -local Slider = defclass(Slider, RangeSlider) -Slider.ATTRS { - get_idx_fn = DEFAULT_NIL, -- Function to get the current index - on_change = DEFAULT_NIL, -- Function to handle index change - is_single = true +local core = require('gui.widgets.slide_core') + +-------------------------------- +-- Slider +-------------------------------- + +---@class widgets.Slider.attrs: widgets.Widget.attrs +---@field get_idx_fn? function +---@field on_change? fun(index: integer) +---@field is_single boolean + + +---@class widgets.Slider.attrs.partial: widgets.Slider.attrs + +---@class widgets.Slider.initTable: widgets.Slider.attrs +---@field num_stops integer + +---@class widgets.Slider: widgets.Widget, widgets.Slider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) +---@overload fun(init_table: widgets.Slider.initTable): self +Slider = defclass(Slider, slide_core) +Slider.ATTRS{ + get_idx_fn=DEFAULT_NIL, + on_change=DEFAULT_NIL, + num_stops=DEFAULT_NIL, + is_single=true, } -function Slider:init() - self.get_left_idx_fn = self.get_idx_fn - self.get_right_idx_fn = self.get_idx_fn - self.on_left_change = function(index) - if self.on_change then self.on_change(index) end - end - self.on_right_change = function(index) - if self.on_change then self.on_change(index) end +function Slider:get_width_per_idx() + local min_value = (self.is_single) and 3 or 5 -- Single slider = 3, else 5 + return math.max(min_value, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx, right_idx = self.get_idx_fn(), self.get_idx_fn() + local width_per_idx = self:get_width_per_idx() + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(right_idx-1) + 4 + if x < left_pos then + self.on_change(self.get_idx_fn() - 1) + elseif x < left_pos+3 then + self.is_dragging_target = 'left' + self.is_dragging_idx = x - left_pos + elseif x < right_pos then + self.is_dragging_target = 'both' + self.is_dragging_idx = x - left_pos + elseif x < right_pos+3 then + self.is_dragging_target = 'right' + self.is_dragging_idx = x - right_pos + else + self.on_change(self.get_idx_fn() + 1) end + return true end return Slider From f4057eee5745cf4972b95f248689d2ca741502f1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 3 Jul 2025 16:26:58 -0500 Subject: [PATCH 0394/1082] remove fake `curse` compound in `unitst` resolves an alignment issue in `unitst` --- library/RemoteTools.cpp | 24 +++++++++---------- library/modules/Units.cpp | 16 ++++++------- library/xml | 2 +- plugins/cursecheck.cpp | 50 +++++++++++++++++++-------------------- plugins/lua/sort.lua | 4 ++-- 5 files changed, 48 insertions(+), 48 deletions(-) diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index 3df55b847a..56b45eba01 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -336,22 +336,22 @@ void DFHack::describeUnit(BasicUnitInfo *info, df::unit *unit, } } - if (unit->curse.add_tags1.whole || - unit->curse.add_tags2.whole || - unit->curse.rem_tags1.whole || - unit->curse.rem_tags2.whole || - unit->curse.name_visible) + if (unit->uwss_add_caste_flag.whole || + unit->uwss_remove_caste_flag.whole || + unit->uwss_add_property.whole || + unit->uwss_remove_property.whole || + unit->uwss_use_display_name) { auto curse = info->mutable_curse(); - curse->set_add_tags1(unit->curse.add_tags1.whole); - curse->set_rem_tags1(unit->curse.rem_tags1.whole); - curse->set_add_tags2(unit->curse.add_tags2.whole); - curse->set_rem_tags2(unit->curse.rem_tags2.whole); + curse->set_add_tags1(unit->uwss_add_caste_flag.whole); + curse->set_rem_tags1(unit->uwss_add_property.whole); + curse->set_add_tags2(unit->uwss_remove_caste_flag.whole); + curse->set_rem_tags2(unit->uwss_remove_property.whole); - if (unit->curse.name_visible) - describeNameTriple(curse->mutable_name(), unit->curse.name, - unit->curse.name_plural, unit->curse.name_adjective); + if (unit->uwss_use_display_name) + describeNameTriple(curse->mutable_name(), unit->uwss_display_name_sing, + unit->uwss_display_name_plur, unit->uwss_display_name_adj); } for (size_t i = 0; i < unit->burrows.size(); i++) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 744a3979af..89a1cbf026 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -108,8 +108,8 @@ using df::global::gametype; using df::global::plotinfo; using df::global::world; -#define IS_ACTIVE_CASTE_FLAG(cf) !unit->curse.rem_tags1.bits.cf && \ -(unit->curse.add_tags1.bits.cf || casteFlagSet(unit->race, unit->caste, caste_raw_flags::cf)) +#define IS_ACTIVE_CASTE_FLAG(cf) !unit->uwss_remove_caste_flag.bits.cf && \ +(unit->uwss_add_caste_flag.bits.cf || casteFlagSet(unit->race, unit->caste, caste_raw_flags::cf)) // Common flags to exclude for fort members constexpr uint32_t exclude_flags1 = ( @@ -208,7 +208,7 @@ bool Units::isOwnRace(df::unit *unit) { bool Units::isAlive(df::unit *unit) { CHECK_NULL_POINTER(unit); - return !isDead(unit) && !unit->curse.add_tags1.bits.NOT_LIVING; + return !isDead(unit) && !unit->uwss_add_caste_flag.bits.NOT_LIVING; } bool Units::isDead(df::unit *unit) { @@ -606,7 +606,7 @@ bool Units::isInvader(df::unit *unit) { bool Units::isUndead(df::unit *unit, bool hiding_curse) { CHECK_NULL_POINTER(unit); - const auto &cb = unit->curse.add_tags1.bits; + const auto &cb = unit->uwss_add_caste_flag.bits; return unit->flags3.bits.ghostly || ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (hiding_curse || !isHidingCurse(unit))); } @@ -1014,7 +1014,7 @@ int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr auto &aobj = unit->body.physical_attrs[attr]; int value = max(0, aobj.value - aobj.soft_demotion); - if (auto mod = unit->curse.attr_change) + if (auto mod = unit->uwss_att_change) { int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; if (isHidingCurse(unit)) @@ -1033,7 +1033,7 @@ int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) { auto &aobj = soul->mental_attrs[attr]; int value = max(0, aobj.value - aobj.soft_demotion); - if (auto mod = unit->curse.attr_change) + if (auto mod = unit->uwss_att_change) { int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; if (isHidingCurse(unit)) @@ -1192,8 +1192,8 @@ string Units::getReadableName(df::unit *unit, bool skip_english) { CHECK_NULL_POINTER(unit); string prof_name = getProfessionName(unit, false, false, true); - if (unit->curse.name_visible) // Necromancer, etc. - prof_name += " " + unit->curse.name; + if (unit->uwss_use_display_name) // Necromancer, etc. + prof_name += " " + unit->uwss_display_name_sing; else if (isGhost(unit)) // TODO: Should be "Ghost" instead of "Ghostly Peasant" prof_name = "Ghostly " + prof_name; // TODO: impersonating deity/force diff --git a/library/xml b/library/xml index ae7424d3f4..672894f464 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ae7424d3f40bf60b3906b5b9b472cb9a7209a3a8 +Subproject commit 672894f464e84e1b3f8cfb911bf0ffe2af11695a diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index a8ec36f9ba..0762ad9c36 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -90,15 +90,15 @@ curses determineCurse(df::unit * unit) cursetype = curses::Ghost; // zombies: undead or hate life (according to ag), not bloodsuckers - if( (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE || unit->curse.add_tags1.bits.NOT_LIVING) - && !unit->curse.add_tags1.bits.BLOODSUCKER ) + if( (unit->uwss_add_caste_flag.bits.OPPOSED_TO_LIFE || unit->uwss_add_caste_flag.bits.NOT_LIVING) + && !unit->uwss_add_caste_flag.bits.BLOODSUCKER) cursetype = curses::Zombie; // necromancers: alive, don't eat, don't drink, don't age - if(!unit->curse.add_tags1.bits.NOT_LIVING - && unit->curse.add_tags1.bits.NO_EAT - && unit->curse.add_tags1.bits.NO_DRINK - && unit->curse.add_tags2.bits.NO_AGING + if (!unit->uwss_add_caste_flag.bits.NOT_LIVING + && unit->uwss_add_caste_flag.bits.NO_EAT + && unit->uwss_add_caste_flag.bits.NO_DRINK + && unit->uwss_add_property.bits.NO_AGING ) cursetype = curses::Necromancer; @@ -108,21 +108,21 @@ curses determineCurse(df::unit * unit) auto syndrome = df::syndrome::find(active_syndrome->type); if (syndrome) { for (auto classname : syndrome->syn_class) - if (classname && *classname == "WERECURSE") { - cursetype = curses::Werebeast; - break; - } + if (classname && *classname == "WERECURSE") { + cursetype = curses::Werebeast; + break; + } } } // vampires: bloodsucker (obvious enough) - if(unit->curse.add_tags1.bits.BLOODSUCKER) + if (unit->uwss_add_caste_flag.bits.BLOODSUCKER) cursetype = curses::Vampire; return cursetype; } -command_result cursecheck (color_ostream &out, vector & parameters) +command_result cursecheck(color_ostream& out, vector & parameters) { df::unit* selected_unit = Gui::getSelectedUnit(out, true); @@ -133,19 +133,19 @@ command_result cursecheck (color_ostream &out, vector & parameters) bool verbose = false; size_t cursecount = 0; - for(auto parameter : parameters) + for (auto parameter : parameters) { - if(parameter == "help" || parameter == "?") + if (parameter == "help" || parameter == "?") return CR_WRONG_USAGE; - if(parameter == "detail") + if (parameter == "detail") giveDetails = true; - if(parameter == "ids") + if (parameter == "ids") giveUnitID = true; - if(parameter == "nick") + if (parameter == "nick") giveNick = true; - if(parameter == "all") + if (parameter == "all") ignoreDead = false; - if(parameter == "verbose") + if (parameter == "verbose") { // verbose makes no sense without enabling details giveDetails = true; @@ -157,11 +157,11 @@ command_result cursecheck (color_ostream &out, vector & parameters) vector to_check; if (selected_unit) to_check.push_back(selected_unit); - for(df::unit *unit : to_check.size() ? to_check : world->units.all) + for (df::unit* unit : to_check.size() ? to_check : world->units.all) { // filter out all "living" units that are currently removed from play // don't spam all completely dead creatures if not explicitly wanted - if((!Units::isActive(unit) && !Units::isKilled(unit)) || (Units::isKilled(unit) && ignoreDead)) + if ((!Units::isActive(unit) && !Units::isKilled(unit)) || (Units::isKilled(unit) && ignoreDead)) { continue; } @@ -170,9 +170,9 @@ command_result cursecheck (color_ostream &out, vector & parameters) if (cursetype != curses::None) { - cursecount++; + cursecount++; - if(giveNick) + if (giveNick) { Units::setNickname(unit, curse_names[cursetype]); //"CURSED"); } @@ -196,8 +196,8 @@ command_result cursecheck (color_ostream &out, vector & parameters) if (verbose) { out << "Curse flags: " - << bitfield_to_string(unit->curse.add_tags1) << std::endl - << bitfield_to_string(unit->curse.add_tags2) << std::endl; + << bitfield_to_string(unit->uwss_add_caste_flag) << std::endl + << bitfield_to_string(unit->uwss_add_property) << std::endl; } } diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 5777d2b174..bdad4ae90e 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -43,7 +43,7 @@ local function get_active_idx_cache() end local function is_original_dwarf(unit) - return df.global.plotinfo.fortress_age == unit.curse.interaction.time_on_site // 10 + return df.global.plotinfo.fortress_age == unit.usable_interaction.time_on_site // 10 end local WAVE_END_GAP = 10000 @@ -53,7 +53,7 @@ local function get_most_recent_wave_oldest_active_idx(cache) for idx=#active_units-1,0,-1 do local unit = active_units[idx] if not dfhack.units.isCitizen(unit) then goto continue end - if oldest_unit and unit.curse.interaction.time_on_site - oldest_unit.curse.interaction.time_on_site > WAVE_END_GAP then + if oldest_unit and unit.usable_interaction.time_on_site - oldest_unit.usable_interaction.time_on_site > WAVE_END_GAP then return cache[oldest_unit.id] else oldest_unit = unit From 985e2832d93386ce02df29901f9a27b53a9695a6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 3 Jul 2025 16:59:38 -0500 Subject: [PATCH 0395/1082] repoint scripts --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 10a04ab91a..1b95334b2e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 10a04ab91a50b357574b106df2cff96389e61ac3 +Subproject commit 1b95334b2ee7d40a686be17bd343ecbf55504e4c From 4bf1b130be86acbc05e1ce15438dfe9c6720dfeb Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Jul 2025 22:34:01 +0000 Subject: [PATCH 0396/1082] 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 672894f464..ff2e060405 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 672894f464e84e1b3f8cfb911bf0ffe2af11695a +Subproject commit ff2e06040572a76afce7311ecffb61f403cc2fbb diff --git a/scripts b/scripts index 1b95334b2e..8a3b92bc43 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1b95334b2ee7d40a686be17bd343ecbf55504e4c +Subproject commit 8a3b92bc433722d1416b4b18ccd43bdad9307a8d From a561ea62eb7c5cc706d8d9154e5924bd117a8700 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:07:38 -0500 Subject: [PATCH 0397/1082] Update docs/dev/Lua API.rst Co-authored-by: Myk --- docs/dev/Lua API.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 665b069c66..639c0a9cac 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -6514,7 +6514,7 @@ Slider class This widget implements a mouse-interactable slider. The player can move the handle to set the value of the slider. The parent widget owns the slider value, and can control -it independently (e.g., with ``CycleHotkeyLabels``). If the value changes, the ``Slider`` +it independently (e.g., with a ``CycleHotkeyLabel``). If the value changes, the ``Slider`` appearance will adjust automatically. :num_stops: Used to specify the number of "notches" in the slider, the places From c5715533a09372ba6bdc96ea560ba5aad344fb74 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 7 Jul 2025 07:22:23 +0000 Subject: [PATCH 0398/1082] 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 eeb00caa9b..15add1726c 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit eeb00caa9bba4655e5095e1a767f4beee52bac8f +Subproject commit 15add1726c711b5b8c4d0ae5afe42b8f841d3af6 From 656e7c385409b15b6ecabc73fbdf6d35603c017c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:40:40 +0000 Subject: [PATCH 0399/1082] [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.33.0 → 0.33.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.0...0.33.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 937a9de80d..d79077b318 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.33.0 + rev: 0.33.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 058e036a554d822814ec510f2cc9a552747f8cae Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sun, 13 Jul 2025 16:31:27 +0800 Subject: [PATCH 0400/1082] Add arena mode focus strings for widgets --- library/modules/Gui.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index f71654c024..dedcc6cdbc 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -90,6 +90,7 @@ distribution. #include "df/viewscreen_dungeonmodest.h" #include "df/viewscreen_dwarfmodest.h" #include "df/viewscreen_legendsst.h" +#include "df/viewscreen_new_arenast.h" #include "df/viewscreen_new_regionst.h" #include "df/viewscreen_setupdwarfgamest.h" #include "df/viewscreen_titlest.h" @@ -188,6 +189,18 @@ DEFINE_GET_FOCUS_STRING_HANDLER(new_region) focusStrings.push_back(baseFocus); } +DEFINE_GET_FOCUS_STRING_HANDLER(new_arena) +{ + if (screen->raw_load) + focusStrings.push_back(baseFocus + "/Loading"); + else if (screen->doing_mods) + focusStrings.push_back(baseFocus + "/Mods"); + + if (focusStrings.empty()) + focusStrings.push_back(baseFocus); + +} + DEFINE_GET_FOCUS_STRING_HANDLER(choose_start_site) { if (screen->doing_site_finder) From 5d7968bdfa1fcc3fdd8736a90ef35de44f0f723f Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sun, 13 Jul 2025 16:42:17 +0800 Subject: [PATCH 0401/1082] Update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0fa2e9535a..49887c54ad 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Documentation ## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua From af44b489e5084eff25298a3a1b29a694af995628 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:12:32 +0000 Subject: [PATCH 0402/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8a3b92bc43..349321c9f1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8a3b92bc433722d1416b4b18ccd43bdad9307a8d +Subproject commit 349321c9f166670e317d53b7bcdfe5541493da1c From f5312cf6e417060fd3f62e582c64609b03741e09 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 22 Jul 2025 11:40:03 -0500 Subject: [PATCH 0403/1082] update structures to 52.01 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ff2e060405..4c8d4cc857 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ff2e06040572a76afce7311ecffb61f403cc2fbb +Subproject commit 4c8d4cc8577fcb8fdd3ac99e6156d9d719419bdc From 4034e8d0380091fb9a4d10e5d659fe230f660b61 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 22 Jul 2025 11:42:11 -0500 Subject: [PATCH 0404/1082] bump version to 52.01 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d8c4144655..0e1226cd81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.13") +set(DF_VERSION "52.01") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 101d25a44f0fe17236f81ce00b43e816a8d41698 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:50:06 +0000 Subject: [PATCH 0405/1082] Auto-update structures ref for 52.01 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 4c8d4cc857..1407dfd607 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 4c8d4cc8577fcb8fdd3ac99e6156d9d719419bdc +Subproject commit 1407dfd6076b6cc3ebb64aef17105042fd66e2ca From 749d3b036790daa8218e2b818234d90b7f667b15 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 22 Jul 2025 12:30:46 -0500 Subject: [PATCH 0406/1082] update changelogs for 52.01-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 0fa2e9535a..0fe8c6e9f4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -53,6 +53,24 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.01-r1 + +## New Tools + ## New Features - `tweak`: ``animaltrap-reuse``: make it so built animal traps automatically unload the vermin they catch into stockpiled animal traps, so that they can be automatically re-baited and reused diff --git a/library/xml b/library/xml index 1407dfd607..15442adf5c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1407dfd6076b6cc3ebb64aef17105042fd66e2ca +Subproject commit 15442adf5c0a3cf3c455a888553ff8da30f9b0cd diff --git a/scripts b/scripts index 349321c9f1..9a3cf1a1c1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 349321c9f166670e317d53b7bcdfe5541493da1c +Subproject commit 9a3cf1a1c13c0def58979f0c6b59aa3939af7099 From 242fee748aea55f45cbb048376b2228e7c111224 Mon Sep 17 00:00:00 2001 From: LightHardt Date: Wed, 23 Jul 2025 23:27:27 -0500 Subject: [PATCH 0407/1082] Add clear option to autoclothing that will unset a previosly set order --- docs/plugins/autoclothing.rst | 4 ++++ plugins/autoclothing.cpp | 32 +++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/plugins/autoclothing.rst b/docs/plugins/autoclothing.rst index 1ea1a9094f..02d13c83a7 100644 --- a/docs/plugins/autoclothing.rst +++ b/docs/plugins/autoclothing.rst @@ -22,6 +22,7 @@ Usage autoclothing autoclothing autoclothing + autoclothing clear ```` can be "cloth", "silk", "yarn", or "leather". The ```` can be anything your civilization can produce, such as "dress" or "mitten". @@ -43,6 +44,9 @@ Examples long as there is cloth available to make them out of). ``autoclothing cloth dress`` Displays the currently set number of cloth dresses chosen per citizen. +``autoclothing clear cloth "short skirt"`` + Unsets cloth short skirts from being made if previously enabled such as + in the first example Which should I enable: autoclothing or tailor? ---------------------------------------------- diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index fac721a5c5..96f81ecd2e 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -129,6 +129,15 @@ struct ClothingRequirement { bool SetFromParameters(color_ostream &out, vector ¶meters) { + if (parameters[0] == "clear") { // handle the clear case + if (!set_bitfield_field(&material_category, parameters[1], 1)) + out << "Unrecognized material type: " << parameters[1] << endl; + if (!setItem(parameters[2], this)) { + out << "Unrecognized item name or token: " << parameters[2] << endl; + return false; + } + return true; + } if (!set_bitfield_field(&material_category, parameters[0], 1)) out << "Unrecognized material type: " << parameters[0] << endl; if (!setItem(parameters[1], this)) { @@ -442,14 +451,20 @@ command_result autoclothing(color_ostream &out, vector ¶meters) bool settingSize = false; bool matchedExisting = false; if (parameters.size() > 2) { - try { - newRequirement.needed_per_citizen = std::stoi(parameters[2]); + if (parameters[0] == "clear") { + newRequirement.needed_per_citizen = 0; + settingSize = true; } - catch (const std::exception&) { - out << parameters[2] << " is not a valid number." << endl; - return CR_WRONG_USAGE; + else { + try { + newRequirement.needed_per_citizen = std::stoi(parameters[2]); + } + catch (const std::exception&) { + out << parameters[2] << " is not a valid number." << endl; + return CR_WRONG_USAGE; + } + settingSize = true; } - settingSize = true; } for (size_t i = clothingOrders.size(); i-- > 0;) { @@ -459,7 +474,10 @@ command_result autoclothing(color_ostream &out, vector ¶meters) if (settingSize) { if (newRequirement.needed_per_citizen == 0) { clothingOrders.erase(clothingOrders.begin() + i); - out << "Unset " << parameters[0] << " " << parameters[1] << endl; + if (parameters[0] == "clear") + out << "Unset " << parameters[1] << " " << parameters[2] << endl; + else + out << "Unset " << parameters[0] << " " << parameters[1] << endl; } else { clothingOrders[i] = newRequirement; From f8663942191f4b7856c2885dc237c8fc3055dd5b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 11:41:02 -0500 Subject: [PATCH 0408/1082] use proper save base path for cosaves fixes #5494 --- docs/changelog.txt | 1 + library/include/modules/DFSDL.h | 72 ++++++++++++++++++--------------- library/modules/DFSDL.cpp | 14 +++++++ library/modules/Persistence.cpp | 11 ++++- 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0fe8c6e9f4..1726e6b9a6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- Honor the "portable mode" preference setting for locating save folders. Fixes DFHack cosaves not working. ## Misc Improvements diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 0fcd17b322..8d12de1918 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -21,6 +21,7 @@ namespace DFHack 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 @@ -28,47 +29,52 @@ namespace DFHack * \ingroup grp_modules * \ingroup grp_dfsdl */ -namespace DFSDL +namespace DFHack::DFSDL { + /** + * Call this on DFHack init so we can load the SDL functions. Returns false on + * failure. + */ + bool init(DFHack::color_ostream& out); -/** - * Call this on DFHack init so we can load the SDL functions. Returns false on - * failure. - */ -bool init(DFHack::color_ostream &out); + /** + * Call this when DFHack is being unloaded. + */ + void cleanup(); -/** - * Call this when DFHack is being unloaded. - */ -void cleanup(); + DFHACK_EXPORT SDL_Surface* DFIMG_Load(const char* file); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceFrom(void* pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); + DFHACK_EXPORT int DFSDL_UpperBlit(SDL_Surface* src, const SDL_Rect* srcrect, SDL_Surface* dst, SDL_Rect* dstrect); + DFHACK_EXPORT SDL_Surface* DFSDL_ConvertSurface(SDL_Surface* src, const SDL_PixelFormat* fmt, uint32_t flags); + DFHACK_EXPORT void DFSDL_FreeSurface(SDL_Surface* surface); + // DFHACK_EXPORT int DFSDL_SemWait(SDL_sem *sem); + // DFHACK_EXPORT int DFSDL_SemPost(SDL_sem *sem); + DFHACK_EXPORT int DFSDL_PushEvent(SDL_Event* event); + DFHACK_EXPORT void DFSDL_free(void* ptr); + DFHACK_EXPORT SDL_PixelFormat* DFSDL_AllocFormat(uint32_t pixel_format); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); + DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char* title, const char* message, SDL_Window* window); -DFHACK_EXPORT SDL_Surface * DFIMG_Load(const char *file); -DFHACK_EXPORT SDL_Surface * DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); -DFHACK_EXPORT SDL_Surface * DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); -DFHACK_EXPORT int DFSDL_UpperBlit(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect); -DFHACK_EXPORT SDL_Surface * DFSDL_ConvertSurface(SDL_Surface *src, const SDL_PixelFormat *fmt, uint32_t flags); -DFHACK_EXPORT void DFSDL_FreeSurface(SDL_Surface *surface); -// DFHACK_EXPORT int DFSDL_SemWait(SDL_sem *sem); -// DFHACK_EXPORT int DFSDL_SemPost(SDL_sem *sem); -DFHACK_EXPORT int DFSDL_PushEvent(SDL_Event *event); -DFHACK_EXPORT void DFSDL_free(void *ptr); -DFHACK_EXPORT SDL_PixelFormat* DFSDL_AllocFormat(uint32_t pixel_format); -DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); -DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window); + // submitted and returned text is UTF-8 + // see wrapper functions below for cp-437 variants + DFHACK_EXPORT char* DFSDL_GetClipboardText(); + DFHACK_EXPORT int DFSDL_SetClipboardText(const char* text); -// submitted and returned text is UTF-8 -// see wrapper functions below for cp-437 variants -DFHACK_EXPORT char * DFSDL_GetClipboardText(); -DFHACK_EXPORT int DFSDL_SetClipboardText(const char *text); + DFHACK_EXPORT char* DFSDL_GetPrefPath(const char* org, const char* app); + DFHACK_EXPORT char* DFSDL_GetBasePath(); } -// System clipboard -- submitted and returned text must be in CP437 -DFHACK_EXPORT std::string getClipboardTextCp437(); -DFHACK_EXPORT bool setClipboardTextCp437(std::string text); +namespace DFHack +{ + + // System clipboard -- submitted and returned text must be in CP437 + DFHACK_EXPORT std::string getClipboardTextCp437(); + DFHACK_EXPORT bool setClipboardTextCp437(std::string text); -// interprets 0xa as newline instead of usual CP437 char -DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); -DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); + // interprets 0xa as newline instead of usual CP437 char + DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); + DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 832ffeb0be..536d1a5881 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -58,6 +58,8 @@ void (*g_SDL_free)(void *); SDL_PixelFormat* (*g_SDL_AllocFormat)(uint32_t pixel_format) = nullptr; SDL_Surface* (*g_SDL_CreateRGBSurfaceWithFormat)(uint32_t flags, int width, int height, int depth, uint32_t format) = nullptr; int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char *message, SDL_Window *window) = nullptr; +char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; +char* (*g_SDL_GetBasePath)() = nullptr; bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -101,6 +103,8 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_AllocFormat); bind(g_sdl_handle, SDL_CreateRGBSurfaceWithFormat); bind(g_sdl_handle, SDL_ShowSimpleMessageBox); + bind(g_sdl_handle, SDL_GetPrefPath); + bind(g_sdl_handle, SDL_GetBasePath); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -175,6 +179,16 @@ SDL_Surface* DFSDL::DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, return g_SDL_CreateRGBSurfaceWithFormat(flags, width, height, depth, format); } +char* DFSDL::DFSDL_GetPrefPath(const char* org, const char* app) +{ + return g_SDL_GetPrefPath(org, app); +} + +char* DFSDL::DFSDL_GetBasePath() +{ + return g_SDL_GetBasePath(); +} + int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window) { if (!g_SDL_ShowSimpleMessageBox) return -1; diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index 9e1dd4d018..e2db24d5fd 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -29,12 +29,14 @@ distribution. #include "LuaTools.h" #include "MemAccess.h" +#include "modules/DFSDL.h" #include "modules/Filesystem.h" #include "modules/Gui.h" #include "modules/Persistence.h" #include "modules/World.h" #include "df/world.h" +#include "df/init.h" #include @@ -184,7 +186,14 @@ static std::string filterSaveFileName(std::string s) { } static std::filesystem::path getSavePath(const std::string &world) { - return std::filesystem::path{} / "save" / world; + auto getsavebase = []() { + if (df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) + return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); + else + return DFSDL::DFSDL_GetBasePath(); + }; + std::filesystem::path base{ getsavebase() }; + return base / "save" / world; } static std::filesystem::path getSaveFilePath(const std::string &world, const std::string &name) { From 4f38870514b35c924ccdc74e8b9cc2a1334e2a18 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:07:51 +0000 Subject: [PATCH 0409/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 9a3cf1a1c1..1c6ef8eaae 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9a3cf1a1c13c0def58979f0c6b59aa3939af7099 +Subproject commit 1c6ef8eaae0b942722c6ad869153fabd8d299069 From 8f745d58d69c379998f9ecab8af0f68d79b7895e Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 24 Jul 2025 12:14:24 -0500 Subject: [PATCH 0410/1082] Add missing function `validateMaterialCategory` to the `clear` case for parameters --- plugins/autoclothing.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index 96f81ecd2e..a4516f4dce 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -136,6 +136,10 @@ struct ClothingRequirement { out << "Unrecognized item name or token: " << parameters[2] << endl; return false; } + else if (!validateMaterialCategory(this)) { + out << parameters[1] << " is not a valid material category for " << parameters[2] << endl; + return false; + } return true; } if (!set_bitfield_field(&material_category, parameters[0], 1)) From 30aa982b31ce465af8b5b18ab6790b945d8b0076 Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 24 Jul 2025 12:35:36 -0500 Subject: [PATCH 0411/1082] Update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0fe8c6e9f4..f9b4d82f9b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `autoclothing`: Added a `clear` option to unset previously set orders ## Documentation From 83f7b908613a6adcf51ea0445d1e2eecf7b8cb7f Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 24 Jul 2025 12:38:38 -0500 Subject: [PATCH 0412/1082] Update changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f9b4d82f9b..ae86e46667 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,7 +58,7 @@ Template for new versions: ## Fixes ## Misc Improvements -- `autoclothing`: Added a `clear` option to unset previously set orders +- `autoclothing`: Added a ``clear`` option to unset previously set orders ## Documentation From 2239953e16e524571a105f8b821f3af5a19ac9b0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 12:38:48 -0500 Subject: [PATCH 0413/1082] update version to 52.02 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e1226cd81..4901c0363a 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 "52.01") +set(DF_VERSION "52.02") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 1f31ec73e135a8179d3905453330ad95b3563697 Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 24 Jul 2025 12:43:41 -0500 Subject: [PATCH 0414/1082] realized all others start lowercase changing to conform --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ae86e46667..2acfe5c922 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,7 +58,7 @@ Template for new versions: ## Fixes ## Misc Improvements -- `autoclothing`: Added a ``clear`` option to unset previously set orders +- `autoclothing`: added a ``clear`` option to unset previously set orders ## Documentation From b7056fb287802b468f81594e580b4b9bba2d4e86 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:47:21 +0000 Subject: [PATCH 0415/1082] Auto-update structures ref for 52.02 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 15442adf5c..d2ebbc986d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 15442adf5c0a3cf3c455a888553ff8da30f9b0cd +Subproject commit d2ebbc986d275f3c68af30b63b024f376fd33f9e From 7a5ebe1a82894a496d4f6bf43d5d1b5781d08c6c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:48:44 +0000 Subject: [PATCH 0416/1082] 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 d2ebbc986d..fb88690959 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d2ebbc986d275f3c68af30b63b024f376fd33f9e +Subproject commit fb886909596f58612af71a41fe75b711b20a4120 From 849839b149b2fe0c41b25aa78be051c7e5992530 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:58:02 +0000 Subject: [PATCH 0417/1082] Auto-update structures ref for 52.02 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index fb88690959..ac168003b0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit fb886909596f58612af71a41fe75b711b20a4120 +Subproject commit ac168003b0f44775373e8a437e4db434619ed1d4 From eabfac36b0ef0800ab974030031189d18ea3b7ce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 13:32:43 -0500 Subject: [PATCH 0418/1082] changelog and modules for 52.02-r1 --- docs/changelog.txt | 20 +++++++++++++++++++- library/xml | 2 +- scripts | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1726e6b9a6..98f6f7e487 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,7 +56,25 @@ Template for new versions: ## New Features ## Fixes -- Honor the "portable mode" preference setting for locating save folders. Fixes DFHack cosaves not working. + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.02-r1 + +## New Tools + +## New Features + +## Fixes +- Honor the "portable mode" preference setting for locating save folders. Fixes DFHack cosaves not working in most cases. ## Misc Improvements diff --git a/library/xml b/library/xml index ac168003b0..e8c52ec34d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ac168003b0f44775373e8a437e4db434619ed1d4 +Subproject commit e8c52ec34dc7ea5979c3b655902b6816028c504a diff --git a/scripts b/scripts index 1c6ef8eaae..166b2378a9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1c6ef8eaae0b942722c6ad869153fabd8d299069 +Subproject commit 166b2378a95721f5bdf6d7575541772ac420b48a From 39cc8548ff4db3e18470cccea630da7144563d8f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 16:56:06 -0500 Subject: [PATCH 0419/1082] fix mod manager in appdata mode these changes allow the mod manager to work, even in appdata mode, accounting for structure changes in 52.01 --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 10 ++++++++++ library/LuaApi.cpp | 2 ++ library/LuaTypes.cpp | 3 ++- library/include/modules/Filesystem.h | 5 ++++- library/lua/script-manager.lua | 6 +++--- library/modules/Filesystem.cpp | 19 +++++++++++++++++++ library/modules/Persistence.cpp | 8 +------- 8 files changed, 42 insertions(+), 12 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 750670a79e..24cfa47f03 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- several fixes related to changes in file system handling in DF 52.01 ## Misc Improvements - `autoclothing`: added a ``clear`` option to unset previously set orders diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index e810a18440..e2d65d4465 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3206,6 +3206,16 @@ unless otherwise noted. following it for each entry. Set ``include_prefix`` to false if you don't want the ``path`` string prepended to the returned filenames. +* ``dfhack.filesystem.getBaseDir()`` + + Returns a directory to which DF (and thus DFHack) can save files. This will either + be DF's install directory, or the path returned by ``SDLGetPrefDir``, depending on whether + DF is in "portable mode" or not. + +* ``dfhack.filesystem.getInstallDir()`` + + Returns the the directory in which DF is installed. + Console API ----------- diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1d3f4075a2..10568f7ea0 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3159,6 +3159,8 @@ static const LuaWrapper::FunctionReg dfhack_filesystem_module[] = { WRAPM(Filesystem, isdir), WRAPM(Filesystem, mtime), WRAPM(Filesystem, canonicalize), + WRAPM(Filesystem, getInstallDir), + WRAPM(Filesystem, getBaseDir), {NULL, NULL} }; diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index 81914a7185..cdacd5ebfe 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -41,6 +41,7 @@ distribution. #include "PluginManager.h" #include "MiscUtils.h" +#include "modules/Filesystem.h" #include #include @@ -191,7 +192,7 @@ void df::stl_string_identity::lua_write(lua_State *state, int fname_idx, void *p void df::path_identity::lua_read(lua_State* state, int fname_idx, void* ptr) const { auto ppath = (std::filesystem::path*)ptr; - auto str = ppath->u8string(); + auto str = DFHack::Filesystem::as_string(*ppath); lua_pushlstring(state, (char*)str.data(), str.size()); } diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 7c68983a3a..407579926f 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -75,7 +75,7 @@ namespace DFHack { DFHACK_EXPORT int listdir_recursive(std::filesystem::path dir, std::map& files, int depth = 10, bool include_prefix = true) noexcept; DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; - inline std::string as_string(std::filesystem::path path) noexcept + inline std::string as_string(const std::filesystem::path path) noexcept { auto pStr = path.string(); if constexpr (std::filesystem::path::preferred_separator != '/') @@ -84,5 +84,8 @@ namespace DFHack { } return pStr; } + DFHACK_EXPORT std::filesystem::path getInstallDir() noexcept; + DFHACK_EXPORT std::filesystem::path getBaseDir() noexcept; + } } diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 9a1ecb1df0..72b0aae951 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -80,8 +80,8 @@ end -- this perhaps could/should be queried from the Steam API -- are there any installation configurations where this will be wrong, though? local WORKSHOP_MODS_PATH = '../../workshop/content/975370/' -local MODS_PATH = 'mods/' -local INSTALLED_MODS_PATH = 'data/installed_mods/' +local MODS_PATH = dfhack.filesystem.getBaseDir() .. 'mods/' +local INSTALLED_MODS_PATH = dfhack.filesystem.getBaseDir() .. 'data/installed_mods/' -- last instance of the same version of the same mod wins, so read them in this -- order (in increasing order of liklihood that players may have made custom @@ -214,7 +214,7 @@ function get_active_mods() local ol = df.global.world.object_loader for idx=0,#ol.object_load_order_id-1 do - local path = ol.object_load_order_src_dir[idx].value + local path = ol.object_load_order_src_dir[idx] table.insert(mods, { id=ol.object_load_order_id[idx].value, name=ol.object_load_order_name[idx].value, diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 55a7fd2194..3b1deb67ed 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -51,8 +51,11 @@ SOFTWARE. #include #include +#include "modules/DFSDL.h" #include "modules/Filesystem.h" +#include "df/init.h" + using namespace DFHack; static bool initialized = false; @@ -226,3 +229,19 @@ std::filesystem::path Filesystem::canonicalize(std::filesystem::path p) noexcept return p; } } + +std::filesystem::path Filesystem::getInstallDir() noexcept +{ + return std::filesystem::path{ DFSDL::DFSDL_GetBasePath() }; +} + +std::filesystem::path Filesystem::getBaseDir() noexcept +{ + auto getsavebase = []() { + if (df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) + return DFSDL::DFSDL_GetBasePath(); + else + return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); + }; + return std::filesystem::path{ getsavebase() }; +} diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index e2db24d5fd..cd67ff59dc 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -186,13 +186,7 @@ static std::string filterSaveFileName(std::string s) { } static std::filesystem::path getSavePath(const std::string &world) { - auto getsavebase = []() { - if (df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) - return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); - else - return DFSDL::DFSDL_GetBasePath(); - }; - std::filesystem::path base{ getsavebase() }; + auto base{ Filesystem::getBaseDir() }; return base / "save" / world; } From b8bbca83cadb13060c7a351c24385f7adfae421d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 09:45:39 -0500 Subject: [PATCH 0420/1082] second half of fixing mod manager --- library/include/DataIdentity.h | 21 +++++++++++++++++++++ library/include/LuaWrapper.h | 5 +++-- plugins/CMakeLists.txt | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 5aa256809a..e9fb5a9aa3 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -37,6 +37,7 @@ distribution. #include #include "DataDefs.h" +#include "LuaWrapper.h" namespace std { class condition_variable; @@ -113,6 +114,7 @@ namespace DFHack }; class DFHACK_EXPORT container_identity : public constructed_identity { + protected: const type_identity *item; const enum_identity *ienum; @@ -418,6 +420,25 @@ namespace df ct.insert(ct.begin()+idx, *(typename T::value_type*)item); return true; } + virtual bool lua_insert2(lua_State* state, int fname_idx, void* ptr, int idx, int val_index) const + { + using VT = typename T::value_type; + VT tmp{}; + auto id = (type_identity*)lua_touserdata(state, DFHack::LuaWrapper::UPVAL_ITEM_ID); + auto pitem = DFHack::LuaWrapper::get_object_internal(state, id, val_index, false); + bool useTemporary = (!pitem && id->isPrimitive()); + + if (useTemporary) + { + pitem = &tmp; + id->lua_write(state, fname_idx, pitem, val_index); + } + + if (id != item || !pitem) + DFHack::LuaWrapper::field_error(state, fname_idx, "incompatible object type", "insert"); + + return insert(ptr, idx, pitem); + } protected: virtual int item_count(void *ptr, CountMode) const { return (int)((T*)ptr)->size(); } diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 6d77af87db..67301b52cc 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -29,8 +29,6 @@ distribution. #include #include -#include "DataDefs.h" - #include #include @@ -41,6 +39,9 @@ distribution. namespace DFHack { struct FunctionReg; + + class function_identity_base; + namespace LuaWrapper { struct LuaToken; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 3ef551a49e..405a11baa0 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -36,7 +36,7 @@ option(BUILD_SUPPORTED "Build the supported plugins (reveal, probe, etc.)." ON) if(BUILD_SUPPORTED) dfhack_plugin(3dveins 3dveins.cpp) dfhack_plugin(army-controller-sanity army-controller-sanity.cpp) - dfhack_plugin(add-spatter add-spatter.cpp) + dfhack_plugin(add-spatter add-spatter.cpp LINK_LIBRARIES lua) dfhack_plugin(aquifer aquifer.cpp LINK_LIBRARIES lua) dfhack_plugin(autobutcher autobutcher.cpp LINK_LIBRARIES lua) dfhack_plugin(autochop autochop.cpp LINK_LIBRARIES lua) From 36a0b1f8aa4d41526978bd21518de223250d8191 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 09:56:38 -0500 Subject: [PATCH 0421/1082] add comment to plugin CMakeLists.txt --- plugins/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 405a11baa0..1f721ec713 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -32,6 +32,10 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) # proto file must be in the proto/ folder # dfhack_plugin(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename) +# warning: for complicated reasons, if a plugin uses a vmethod interpose, it must +# link to lua even if it doesn't otherwise use lua. we'll fix this someday, we promise. +# we apologize for the inconvenience. + option(BUILD_SUPPORTED "Build the supported plugins (reveal, probe, etc.)." ON) if(BUILD_SUPPORTED) dfhack_plugin(3dveins 3dveins.cpp) From bcea2c9b6021c5a212316879f443bcdab9dc1d5d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 10:02:09 -0500 Subject: [PATCH 0422/1082] didn't know about this one --- plugins/devel/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt index 210669a75d..c044fa1044 100644 --- a/plugins/devel/CMakeLists.txt +++ b/plugins/devel/CMakeLists.txt @@ -23,7 +23,7 @@ dfhack_plugin(tilesieve tilesieve.cpp) # dfhack_plugin(zoom zoom.cpp) if(UNIX) - dfhack_plugin(ref-index ref-index.cpp) + dfhack_plugin(ref-index ref-index.cpp LINK_LIBRARIES lua) endif() add_subdirectory(check-structures-sanity) From 1c0663c4bc04b53c4632b4395a370ec145038c1c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 10:18:33 -0500 Subject: [PATCH 0423/1082] add changelog also included a bit i forgot from #5514 --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 24cfa47f03..483e0b0570 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,8 +64,10 @@ Template for new versions: ## Documentation ## API +- ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) ## Lua +- ``insert``ing values into STL containers containing nonprimitive types is now supported ## Removed From 772b637ca7ce63e9c24c365145bdcc728163fde3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 10:33:51 -0500 Subject: [PATCH 0424/1082] Update changelog.txt --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 483e0b0570..bf01965a3c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,7 +67,7 @@ Template for new versions: - ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) ## Lua -- ``insert``ing values into STL containers containing nonprimitive types is now supported +- inserting values into STL containers containing nonprimitive types is now supported ## Removed From 253101b334bbf0683132ce90e53e15709f316629 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 26 Jul 2025 16:12:07 +0000 Subject: [PATCH 0425/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 166b2378a9..ca3ab763c5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 166b2378a95721f5bdf6d7575541772ac420b48a +Subproject commit ca3ab763c5f5d9c6d340bf4ee5db9ba213c718f0 From 81821ffdb23f67ca8939c881483a52ba6e708c30 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:07:47 +0000 Subject: [PATCH 0426/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ca3ab763c5..3cec9d6f11 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ca3ab763c5f5d9c6d340bf4ee5db9ba213c718f0 +Subproject commit 3cec9d6f116a7e4246723f0aaeb2dfe6b124a697 From aee124c3d3380979580f43e78c986f5bb49f7871 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 26 Jul 2025 13:49:43 -0500 Subject: [PATCH 0427/1082] use consistent base and install path determination for 52.xx compatibility when in appdata mode --- library/Core.cpp | 57 +++++++++++++++++++-------------- library/modules/Persistence.cpp | 3 +- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 1208650579..a2b48c992d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -108,11 +108,18 @@ size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector *dest) { std::lock_guard lock(script_path_mutex); dest->clear(); - std::filesystem::path df_path = this->p->getPath(); + std::filesystem::path df_pref_path = Filesystem::getBaseDir(); + std::filesystem::path df_install_path = Filesystem::getInstallDir(); for (auto & path : script_paths[0]) dest->emplace_back(path); - dest->push_back(df_path / CONFIG_PATH / "scripts"); + // should this be df_pref_path? probably + dest->push_back(getConfigPath() / "scripts"); if (df::global::world && isWorldLoaded()) { std::string save = World::ReadWorldFolder(); if (save.size()) - dest->emplace_back(df_path / "save" / save / "scripts"); + dest->emplace_back(df_pref_path / "save" / save / "scripts"); } - dest->emplace_back(df_path / "hack" / "scripts"); + dest->emplace_back(df_install_path / "hack" / "scripts"); for (auto & path : script_paths[2]) dest->emplace_back(path); for (auto & path : script_paths[1]) @@ -535,7 +544,7 @@ std::filesystem::path Core::findScript(std::string name) bool loadScriptPaths(color_ostream &out, bool silent = false) { - std::filesystem::path filename{ CONFIG_PATH / "script-paths.txt" }; + std::filesystem::path filename{ getConfigPath() / "script-paths.txt" }; std::ifstream file(filename); if (!file) { @@ -1372,11 +1381,11 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // load baseline defaults - core->loadScriptFile(out, CONFIG_PATH / "init" / "default.dfhack.init", false); + core->loadScriptFile(out, getConfigPath() / "init" / "default.dfhack.init", false); // load user overrides std::vector prefixes(1, "dfhack"); - loadScriptFiles(core, out, prefixes, CONFIG_PATH / "init"); + loadScriptFiles(core, out, prefixes, getConfigPath() / "init"); // show the terminal if requested auto L = DFHack::Core::getInstance().getLuaState(); @@ -1399,7 +1408,7 @@ static void fInitthread(IODATA * iod) // A thread function... for the interactive console. static void fIOthread(IODATA * iod) { - static const std::filesystem::path HISTORY_FILE = CONFIG_PATH / "dfhack.history"; + static const std::filesystem::path HISTORY_FILE = getConfigPath() / "dfhack.history"; Core * core = iod->core; PluginManager * plug_mgr = iod->plug_mgr; @@ -1763,16 +1772,16 @@ bool Core::InitSimulationThread() virtual_identity::Init(this); // create config directory if it doesn't already exist - if (!Filesystem::mkdir_recursive(CONFIG_PATH)) - con.printerr("Failed to create config directory: '%s'\n", CONFIG_PATH.c_str()); + if (!Filesystem::mkdir_recursive(getConfigPath())) + con.printerr("Failed to create config directory: '%s'\n", getConfigPath().c_str()); // copy over default config files if necessary std::map config_files; std::map default_config_files; - if (Filesystem::listdir_recursive(CONFIG_PATH, config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", CONFIG_PATH.c_str()); - else if (Filesystem::listdir_recursive(CONFIG_DEFAULTS_PATH, default_config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", CONFIG_DEFAULTS_PATH.c_str()); + if (Filesystem::listdir_recursive(getConfigPath(), config_files, 10, false) != 0) + con.printerr("Failed to list directory: '%s'\n", getConfigPath().c_str()); + else if (Filesystem::listdir_recursive(getConfigDefaultsPath(), default_config_files, 10, false) != 0) + con.printerr("Failed to list directory: '%s'\n", getConfigDefaultsPath().c_str()); else { // ensure all config file directories exist before we start copying files @@ -1780,7 +1789,7 @@ bool Core::InitSimulationThread() // skip over files if (!entry.second) continue; - std::filesystem::path dirname = CONFIG_PATH / entry.first; + std::filesystem::path dirname = getConfigPath() / entry.first; if (!Filesystem::mkdir_recursive(dirname)) con.printerr("Failed to create config directory: '%s'\n", dirname.c_str()); } @@ -1792,10 +1801,10 @@ bool Core::InitSimulationThread() continue; std::filesystem::path filename = entry.first; if (!config_files.count(filename)) { - std::filesystem::path src_file = CONFIG_DEFAULTS_PATH / filename; + std::filesystem::path src_file = getConfigDefaultsPath() / filename; if (!Filesystem::isfile(src_file)) continue; - std::filesystem::path dest_file = CONFIG_PATH / filename; + std::filesystem::path dest_file = getConfigPath() / filename; std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); if (!src.good() || !dest.good()) { @@ -2224,16 +2233,16 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve if (!df::global::world) return; - std::filesystem::path rawFolder = !isWorldLoaded() ? std::filesystem::path{} : std::filesystem::path{} / "save" / World::ReadWorldFolder() / "init"; + std::filesystem::path rawFolder = !isWorldLoaded() ? Filesystem::getInstallDir() : Filesystem::getBaseDir() / "save" / World::ReadWorldFolder() / "init"; auto i = table.find(event); if ( i != table.end() ) { const std::vector& set = i->second; // load baseline defaults - this->loadScriptFile(out, CONFIG_PATH / "init" / ("default." + set[0] + ".init"), false); + this->loadScriptFile(out, getConfigPath() / "init" / ("default." + set[0] + ".init"), false); - loadScriptFiles(this, out, set, CONFIG_PATH / "init"); + loadScriptFiles(this, out, set, getConfigPath() / "init"); loadScriptFiles(this, out, set, rawFolder); } diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index cd67ff59dc..e6a0d50126 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -186,8 +186,7 @@ static std::string filterSaveFileName(std::string s) { } static std::filesystem::path getSavePath(const std::string &world) { - auto base{ Filesystem::getBaseDir() }; - return base / "save" / world; + return Filesystem::getBaseDir() / "save" / world; } static std::filesystem::path getSaveFilePath(const std::string &world, const std::string &name) { From c6f3ebde5c10fa3f5a1d5b6139ec14e05baa7331 Mon Sep 17 00:00:00 2001 From: Quietust Date: Sat, 26 Jul 2025 13:15:44 -0600 Subject: [PATCH 0428/1082] dig-now - properly handle UNDIGGABLE tiles Fixes #5512 --- docs/changelog.txt | 1 + plugins/dig-now.cpp | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ef0b1b00da..730508f2d9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes - several fixes related to changes in file system handling in DF 52.01 +- `dig-now`: don't allow UNDIGGABLE stones to be excavated ## Misc Improvements - `autoclothing`: added a ``clear`` option to unset previously set orders diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index 16fe585157..48984e1b9f 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -450,12 +450,10 @@ static bool is_diggable(MapExtras::MapCache &map, const DFCoord &pos, break; } - if (mat == df::tiletype_material::FEATURE) { - // adamantine is the only is diggable feature - t_feature feature; - return map.BlockAtTile(pos)->GetLocalFeature(&feature) - && feature.type == feature_type::deep_special_tube; - } + MaterialInfo mi; + mi.decode(map.baseMaterialAt(pos)); + if (mi.material != nullptr && mi.material->flags.is_set(df::material_flags::UNDIGGABLE)) + return false; return true; } From 2a43c80b945050c34395df7e46772e4cf1070e6d Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 26 Jul 2025 14:18:17 -0700 Subject: [PATCH 0429/1082] script-manager.lua One last use of a as a --- library/lua/script-manager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 72b0aae951..74a31fd1d9 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -152,7 +152,6 @@ function get_mod_paths(installed_subdir, active_subdir) -- if a world is loaded, process active mods first, and lock to active version if dfhack.isWorldLoaded() then for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do - path = tostring(path.value) -- skip vanilla "mods" if not path:startswith(INSTALLED_MODS_PATH) then goto continue end local id = get_mod_id_and_version(path) From 4d7544735b32c6ef65fc2381469236fd846e8a8a Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 27 Jul 2025 13:35:31 +0200 Subject: [PATCH 0430/1082] extend implementations of `addToSquad` and `removeFromSquad` --- docs/changelog.txt | 1 + library/modules/Military.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index ef0b1b00da..071d6352ef 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## API - ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) +- expand the partial implementations of ``Military::addToSquad`` and ``Military::removeFromSquad`` ## Lua - inserting values into STL containers containing nonprimitive types is now supported diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 83cf240d19..b0c671af2d 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -5,6 +5,8 @@ #include "modules/Military.h" #include "modules/Translation.h" #include "modules/Units.h" +#include "df/activity_entry.h" +#include "df/activity_event.h" #include "df/building.h" #include "df/building_civzonest.h" #include "df/building_squad_infost.h" @@ -17,6 +19,7 @@ #include "df/history_event_remove_hf_entity_linkst.h" #include "df/entity_position.h" #include "df/entity_position_assignment.h" +#include "df/equipment_update.h" #include "df/plotinfost.h" #include "df/military_routinest.h" #include "df/squad.h" @@ -460,6 +463,15 @@ bool Military::addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos) unit->military.squad_position = squad_pos; add_soldier_entity_link(hf, squad, squad_pos); + + #define F(flag) df::equipment_update::mask_##flag + auto constexpr update_flags = F(weapon) | F(armor) | F(shoes) | F(shield) | F(helm) | F(gloves) + | F(ammo) | F(pants) | F(backpack) | F(quiver) | F(flask); + #undef F + + squad->ammo.update.whole |= update_flags; + df::global::plotinfo->equipment.update.whole |= update_flags; + return true; } @@ -489,6 +501,19 @@ bool Military::removeFromSquad(int32_t unit_id) unit->military.squad_id = -1; unit->military.squad_position = -1; + // abort individual training + if (unit->individual_drills.size()) { + unit->flags3.bits.verify_personal_training = true; + } + + // remove unit from squad activities + auto activity_entry = binsearch_in_vector(df::global::world->activities.all,&df::activity_entry::id,squad->activity); + if (activity_entry) { + for (auto const activity_event : activity_entry->events) { + activity_event->removeParticipant(unit->hist_figure_id, unit->id, false); + } + } + if (squad_pos == 0) // is unit a commander? remove_officer_entity_link(hf, squad); else From 9c51ad173ca702d3038b6cfcac640c3d40a14df1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:10:08 +0000 Subject: [PATCH 0431/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 3cec9d6f11..33159eee46 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3cec9d6f116a7e4246723f0aaeb2dfe6b124a697 +Subproject commit 33159eee46f0eca08a728893a3f7476d7e6d42d8 From 2264abb8ba91f1c408a68a860e66d02f159a32c0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 27 Jul 2025 12:25:18 -0500 Subject: [PATCH 0432/1082] 52.02-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 25 ++++++++++++++++++++++--- library/xml | 2 +- scripts | 2 +- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4901c0363a..ecdf3f68e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.02") -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 6c71e7027a..3807f7ffc8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,25 @@ Template for new versions: ## New Features ## Fixes -- several fixes related to changes in file system handling in DF 52.01 + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.02-r2 + +## New Tools + +## New Features + +## Fixes +- Several fixes related to changes in file system handling in DF 52.01 - `dig-now`: don't allow UNDIGGABLE stones to be excavated ## Misc Improvements @@ -66,11 +84,12 @@ Template for new versions: ## Documentation ## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` - ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) -- expand the partial implementations of ``Military::addToSquad`` and ``Military::removeFromSquad`` +- Expanded the partial implementations of ``Military::addToSquad`` and ``Military::removeFromSquad`` ## Lua -- inserting values into STL containers containing nonprimitive types is now supported +- Inserting values into STL containers containing nonprimitive types is now supported ## Removed diff --git a/library/xml b/library/xml index e8c52ec34d..3eb07ab8ab 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e8c52ec34dc7ea5979c3b655902b6816028c504a +Subproject commit 3eb07ab8abfb0d6a00f0758917c52736effbeb9b diff --git a/scripts b/scripts index 33159eee46..30993eef1c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 33159eee46f0eca08a728893a3f7476d7e6d42d8 +Subproject commit 30993eef1c6ad88279886b6863ad56bbc82f669b From 9a4d21225052bf6f81d21c2c2ea647e942e0543f Mon Sep 17 00:00:00 2001 From: Quietust Date: Sun, 27 Jul 2025 13:58:42 -0600 Subject: [PATCH 0433/1082] Update removeFromSquad to match in-game logic aside from the link removal stuff at the end, which might still be slightly different --- docs/changelog.txt | 1 + library/modules/Military.cpp | 59 +++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3807f7ffc8..a52fb495e5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: ## Documentation ## API +- Adjusted the logic inside ``Military::removeFromSquad`` to more closely match the game's own behavior ## Lua diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index b0c671af2d..d3fb436826 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -477,42 +477,53 @@ bool Military::addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos) bool Military::removeFromSquad(int32_t unit_id) { + // based on unitst::remove_squad_info df::unit *unit = df::unit::find(unit_id); if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) return false; - int32_t squad_id = unit->military.squad_id; - df::squad* squad = df::squad::find(squad_id); - if (squad == nullptr) - return false; + // abort individual training + if (unit->individual_drills.size()) + unit->flags3.bits.verify_personal_training = true; + int32_t squad_id = unit->military.squad_id; int32_t squad_pos = unit->military.squad_position; - df::squad_position* pos = vector_get(squad->positions, squad_pos); - if (pos == nullptr) - return false; - df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); - if (hf == nullptr) - return false; + df::squad* squad = df::squad::find(squad_id); + + if (squad) + { + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos) + { + // based on unitst::remove_squad_activity_info + FOR_ENUM_ITEMS(squad_event_type, i) + { + auto activity_entry = df::activity_entry::find(pos->activities[i]); + if (activity_entry) + { + auto activity_event = binsearch_in_vector(activity_entry->events, &df::activity_event::event_id, pos->events[i]); + if (activity_event) + { + activity_event->removeParticipant(unit->hist_figure_id, unit->id, false); + pos->activities[i] = -1; + pos->events[i] = -1; + } + } + } - // remove from squad information - pos->occupant = -1; + // remove from squad position + pos->occupant = -1; + } + } // remove from unit information unit->military.squad_id = -1; unit->military.squad_position = -1; - // abort individual training - if (unit->individual_drills.size()) { - unit->flags3.bits.verify_personal_training = true; - } - - // remove unit from squad activities - auto activity_entry = binsearch_in_vector(df::global::world->activities.all,&df::activity_entry::id,squad->activity); - if (activity_entry) { - for (auto const activity_event : activity_entry->events) { - activity_event->removeParticipant(unit->hist_figure_id, unit->id, false); - } - } + // remove entity squad link + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr || squad == nullptr) + return false; if (squad_pos == 0) // is unit a commander? remove_officer_entity_link(hf, squad); From d1f30167b403912a3a2349cf6fcbf0a1880f033d Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 3 Aug 2025 17:16:33 +0200 Subject: [PATCH 0434/1082] helper functions for creating jobs and checking whether units are ready to take jobs --- docs/changelog.txt | 2 + library/include/modules/Job.h | 7 +++ library/include/modules/Units.h | 13 +++++ library/modules/Job.cpp | 22 ++++++++ library/modules/Units.cpp | 90 +++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3807f7ffc8..99b9c6a9bf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,8 @@ Template for new versions: ## Documentation ## API +- ``Job``: new functions ``createLinked`` and ``assignToWorkshop`` +- ``Units``: new functions ``getFocusPenalty``, ``unbailableSocialActivity``, ``isJobAvailable`` ## Lua diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index a0754c4781..5f7ddcb11a 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -31,6 +31,7 @@ distribution. #include "Types.h" #include "DataDefs.h" +#include "df/building_workshopst.h" #include "df/item_type.h" #include "df/job_item_ref.h" @@ -94,7 +95,13 @@ namespace DFHack DFHACK_EXPORT void checkBuildingsNow(); DFHACK_EXPORT void checkDesignationsNow(); + // link the job into the global job list, passing ownership to DF DFHACK_EXPORT bool linkIntoWorld(df::job *job, bool new_id = true); + // create a job and immediately link it into the global job list + DFHACK_EXPORT df::job* createLinked(); + + // assign job to workshop, returns false if workshop already has the maximum of ten jobs + DFHACK_EXPORT bool assignToWorkshop(df::job *job, df::building_workshopst *workshop); // Flag this job's posting as "dead" and set its posting_index to -1 // If remove_all is true, flag all postings pointing to this job diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 0cba251644..fc33fd3b0a 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -39,12 +39,14 @@ distribution. #include "df/job_skill.h" #include "df/mental_attribute_type.h" #include "df/misc_trait_type.h" +#include "df/need_type.h" #include "df/physical_attribute_type.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" #include "df/unit_path_goal.h" #include +#include namespace df { struct activity_entry; @@ -339,6 +341,17 @@ DFHACK_EXPORT bool isGoalAchieved(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT df::activity_entry *getMainSocialActivity(df::unit *unit); DFHACK_EXPORT df::activity_event *getMainSocialEvent(df::unit *unit); +// get largest (i.e. most negative) focus penalty for a set of needs +using need_types_set = std::bitset; +DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, need_types_set need_types); +// get focused penalty for a single need +DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, df::need_type need_type); + +// unit has an unbailable social activity (e.g. "Socialize!") +DFHACK_EXPORT bool unbailableSocialActivity(df::unit *unit); +// unit can be assigned a job +DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool interrupt_social); + // Stress categories. 0 is highest stress, 6 is lowest. DFHACK_EXPORT extern const std::vector stress_cutoffs; DFHACK_EXPORT int getStressCategory(df::unit *unit); diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 96344ddd6f..5a34a19e01 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -38,6 +38,7 @@ distribution. #include "modules/References.h" #include "df/building.h" +#include "df/building_workshopst.h" #include "df/general_ref.h" #include "df/general_ref_unit_workerst.h" #include "df/general_ref_building_holderst.h" @@ -517,6 +518,27 @@ bool DFHack::Job::linkIntoWorld(df::job *job, bool new_id) } } +df::job* DFHack::Job::createLinked() +{ + auto job = new df::job(); + DFHack::Job::linkIntoWorld(job, true); + return job; +} + +bool assignToWorkshop(df::job *job, df::building_workshopst *workshop) +{ + CHECK_NULL_POINTER(job); + CHECK_NULL_POINTER(workshop); + + if (workshop->jobs.size() >= 10) { + return false; + } + job->pos = df::coord(workshop->centerx, workshop->centery, workshop->z); + DFHack::Job::addGeneralRef(job, df::general_ref_type::BUILDING_HOLDER, workshop->id); + workshop->jobs.push_back(job); + return true; +} + bool DFHack::Job::removePostings(df::job *job, bool remove_all) { using df::global::world; diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 89a1cbf026..4a5e3f5a0d 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -64,12 +64,15 @@ distribution. #include "df/interaction_profilest.h" #include "df/item.h" #include "df/job.h" +#include "df/need_type.h" #include "df/nemesis_record.h" #include "df/personality_goalst.h" +#include "df/personality_needst.h" #include "df/plotinfost.h" #include "df/proj_unitst.h" #include "df/reputation_profilest.h" #include "df/syndrome.h" +#include "df/squad.h" #include "df/tile_occupancy.h" #include "df/training_assignment.h" #include "df/unit.h" @@ -89,6 +92,7 @@ distribution. #include "df/world_site.h" #include +#include #include #include #include @@ -2017,6 +2021,92 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) { return entry->events[entry->events.size() - 1]; } +int32_t Units::getFocusPenalty(df::unit* unit, need_types_set need_types) { + CHECK_NULL_POINTER(unit); + + int max_penalty = INT_MAX; + auto& needs = unit->status.current_soul->personality.needs; + for (auto const need : needs) { + if (need_types.test(need->id)) { + max_penalty = min(max_penalty, need->focus_level); + } + } + return max_penalty; +} + +int32_t Units::getFocusPenalty(df::unit* unit, df::need_type need_type) { + auto need_types = need_types_set().set(need_type); + return getFocusPenalty(unit, need_types); +} + +// reverse engineered from unitst::have_unbailable_sp_activities (partial implementation) +bool Units::unbailableSocialActivity(df::unit *unit) +{ + // these can become constexpr with C++23 + static const need_types_set pray_needs = need_types_set() + .set(df::need_type::PrayOrMeditate); + + static const need_types_set socialize_needs = need_types_set() + .set(df::need_type::Socialize) + .set(df::need_type::BeCreative) + .set(df::need_type::Excitement) + .set(df::need_type::AdmireArt); + + static const need_types_set read_needs = need_types_set() + .set(df::need_type::ThinkAbstractly) + .set(df::need_type::LearnSomething); + + CHECK_NULL_POINTER(unit); + + if (unit->social_activities.empty()) { + return false; + } else if (unit->social_activities.size() > 1) { + return true; // is this even possible? + } + + auto activity = df::activity_entry::find(unit->social_activities[0]); + if (activity) { + using df::activity_entry_type; + switch (activity->type) { + case activity_entry_type::Socialize: + return getFocusPenalty(unit, socialize_needs) <= -10000; + case activity_entry_type::Prayer: + return getFocusPenalty(unit, pray_needs) <= -10000; + case activity_entry_type::Read: + return getFocusPenalty(unit, read_needs) <= -10000; + default: + // consider unhandled activities as uninterruptible + return true; + } + } + // this should never happen + return false; +} + +bool Units::isJobAvailable(df::unit *unit, bool preserve_social = false){ + if (unit->job.current_job) + return false; + if (unit->flags1.bits.caged || unit->flags1.bits.chained) + return false; + if (unit->individual_drills.size() > 0) { + if (unit->individual_drills.size() > 1) + return false; // this is even possible + auto activity = df::activity_entry::find(unit->individual_drills[0]); + if (activity && (activity->type == df::activity_entry_type::FillServiceOrder)) + return false; + } + if (unbailableSocialActivity(unit)) + return false; + if (preserve_social && unit->social_activities.size() > 0) + return false; + if (unit->military.squad_id != -1) { + auto squad = df::squad::find(unit->military.squad_id); + if (squad) + return squad->orders.size() == 0 && squad->activity == -1; + } + return true; +} + // 50000 and up is level 0, 25000 and up is level 1, etc. const vector Units::stress_cutoffs {50000, 25000, 10000, -10000, -25000, -50000, -100000}; From e6757c36a395e577741c2c303c14463bd811cd19 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 6 Aug 2025 18:42:25 -0500 Subject: [PATCH 0435/1082] cheap mitigation of #5538 this doesn't really resolve #5538 but it does mitigate it from a hang to potential misoperation --- docs/changelog.txt | 1 + plugins/lua/preserve-rooms.lua | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3807f7ffc8..fbf9959a38 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Features ## Fixes +- `preserve-rooms` will no longer hang on startup in the presence of a cycle in the replacement relationship of noble positions ## Misc Improvements diff --git a/plugins/lua/preserve-rooms.lua b/plugins/lua/preserve-rooms.lua index 8dd4e507af..3851920daf 100644 --- a/plugins/lua/preserve-rooms.lua +++ b/plugins/lua/preserve-rooms.lua @@ -320,9 +320,13 @@ local function get_codes(positions) if position.replaced_by == -1 then ensure_key(grouped, id)[id] = position else + -- replaced-by links may be cyclic. this code will behave "incorrectly" in the event + -- of a cycle but will not hang. a proper fix is still needed. see issue DFHack/dfhack#5538 local parent = positions[position.replaced_by] - while parent.replaced_by ~= -1 do + local counter = 0 + while parent.replaced_by ~= -1 and counter < 10 do parent = positions[parent.replaced_by] + counter = counter + 1 end ensure_key(grouped, parent.id)[id] = position end From bd6c6bd9139e3116957db3e620c3f49b83c63f1d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 7 Aug 2025 07:26:46 +0000 Subject: [PATCH 0436/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 30993eef1c..244ddec509 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 30993eef1c6ad88279886b6863ad56bbc82f669b +Subproject commit 244ddec5095cf8ea3e5553a96cd285dd13a33942 From 3a5eaa1a33166236fe258eb831316b99773b2b9a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 12:54:13 -0500 Subject: [PATCH 0437/1082] Bump to 52.03-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 19 +++++++++++++++++++ library/xml | 2 +- scripts | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ecdf3f68e8..fa6ac565c2 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 "52.02") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "52.03") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index fbf9959a38..2c4aa5c9f2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,25 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` + +## Lua + +## Removed + +# 52.03-1 + +## New Tools + +## New Features + ## Fixes - `preserve-rooms` will no longer hang on startup in the presence of a cycle in the replacement relationship of noble positions diff --git a/library/xml b/library/xml index 3eb07ab8ab..5ee7e9d47d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3eb07ab8abfb0d6a00f0758917c52736effbeb9b +Subproject commit 5ee7e9d47d86bb7b27f540197d4916ae971439e5 diff --git a/scripts b/scripts index 244ddec509..61e0e18195 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 244ddec5095cf8ea3e5553a96cd285dd13a33942 +Subproject commit 61e0e181953e70679d0ed4dfc7e9e64922ad0c07 From 0cc8ad1edb16ac98a74176d727bcda25bd78d2de Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:09:42 +0000 Subject: [PATCH 0438/1082] Auto-update structures ref for 52.03 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 5ee7e9d47d..23880e1a19 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 5ee7e9d47d86bb7b27f540197d4916ae971439e5 +Subproject commit 23880e1a19cc77e3aafdb642b291015390ccc01c From 26b65e0591a02645d513bc04dbe47a46e23698d3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 13:19:29 -0500 Subject: [PATCH 0439/1082] fix generate symbols CI --- .github/workflows/generate-symbols.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 9f4e32b146..f78da19e74 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -134,7 +134,7 @@ jobs: +quit tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_steam xml/symbols_gen_linux.sh ${{ inputs.version == 'auto' && '50.0' || inputs.version }} STEAM DF_steam - if [ "${{ inputs.version }}" = "auto" ]; then + if [ "${{ inputs.version }}" == "auto" ]; then while pgrep dwarfort; do echo "waiting for DF to exit" sleep 0.5 From 76a1ce24a6e388be63140143c74dc9bce4bffefe Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 14:48:50 -0500 Subject: [PATCH 0440/1082] update structures --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 23880e1a19..64ce0f1ced 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 23880e1a19cc77e3aafdb642b291015390ccc01c +Subproject commit 64ce0f1ced2b224168d7d898b4afa8f3b47405b3 From 82a0acec3aacd2f13031c72c9728032d6e166da0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 15:42:45 -0500 Subject: [PATCH 0441/1082] make `Filesystem::getBaseDir` fall back if df::global::init is missing --- library/modules/Filesystem.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 3b1deb67ed..7a6b09a500 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -238,7 +238,8 @@ std::filesystem::path Filesystem::getInstallDir() noexcept std::filesystem::path Filesystem::getBaseDir() noexcept { auto getsavebase = []() { - if (df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) + // assume portable mode is _on_ if init is missing + if (!df::global::init || df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) return DFSDL::DFSDL_GetBasePath(); else return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); From ad2625074503df7a3beea3bd411e73990e9a5443 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:56:33 +0000 Subject: [PATCH 0442/1082] Auto-update structures ref for 52.03 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 64ce0f1ced..7c4f3e32cd 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 64ce0f1ced2b224168d7d898b4afa8f3b47405b3 +Subproject commit 7c4f3e32cd9c03f39aefed4b6ad7f6adb8eea95b From c2435b0a571c8733e3f11b352a5c62d61d4a1b98 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 8 Aug 2025 21:49:24 -0500 Subject: [PATCH 0443/1082] fix `Job::getName` add fields added to `job` in the dye update --- docs/changelog.txt | 5 +++-- library/modules/Job.cpp | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2c4aa5c9f2..064e718106 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,19 +57,19 @@ Template for new versions: ## New Features ## Fixes +- job descriptions of mix dye job will display proper dye names ## Misc Improvements ## Documentation ## API -- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua ## Removed -# 52.03-1 +# 52.03-r1 ## New Tools @@ -83,6 +83,7 @@ Template for new versions: ## Documentation ## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 96344ddd6f..39ca701c23 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -655,6 +655,9 @@ std::string Job::getName(df::job *job) button->matgloss = job->mat_index; button->specflag = job->specflag; button->job_item_flag = job->material_category; + button->specdata = job->specdata; + button->art_specifier_id1 = job->art_spec.id; + button->art_specifier_id2 = job->art_spec.subid; button->text(&desc); delete button; From 24985eb3753aaa7ddb4919a75a6c467c00d304aa Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 9 Aug 2025 03:10:55 +0000 Subject: [PATCH 0444/1082] 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 7c4f3e32cd..462eeeb505 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7c4f3e32cd9c03f39aefed4b6ad7f6adb8eea95b +Subproject commit 462eeeb505c453ecbc7d2062fd4575feca34c34e From 02bd21828ea5e7b5622bafc22d9b56cab9d5d25a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 9 Aug 2025 03:01:07 -0500 Subject: [PATCH 0445/1082] update for 52.03-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 20 ++++++++++++++++++-- library/xml | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fa6ac565c2..a0875d2c32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.03") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 064e718106..76a203fc72 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,7 +35,6 @@ Template for new versions: ## Documentation ## API -- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua @@ -56,6 +55,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.03-r1.1 + +## New Tools + +## New Features + ## Fixes - job descriptions of mix dye job will display proper dye names @@ -83,7 +100,6 @@ Template for new versions: ## Documentation ## API -- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua diff --git a/library/xml b/library/xml index 462eeeb505..fc6dcd5aa0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 462eeeb505c453ecbc7d2062fd4575feca34c34e +Subproject commit fc6dcd5aa077a0d96d81bd2dfe89e24bed375472 From f031a14a4e622b7ba9ef6ad3af24923e4e324994 Mon Sep 17 00:00:00 2001 From: beofhelp <17787716+beofhelp@users.noreply.github.com> Date: Sat, 9 Aug 2025 13:43:05 +0000 Subject: [PATCH 0446/1082] fixed a typo in orders.rst --- docs/plugins/orders.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 378881d12c..d89475a328 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -151,7 +151,7 @@ game does not provide sufficient order conditions. Please enable ``automilk``, types of jobs. Note that the jugs are specifically made out of wood. This is so, as long as -you don't may any other "Tools" out of wood, you can have a stockpile just for +you don't make any other "Tools" out of wood, you can have a stockpile just for jugs by restricting a finished goods stockpile to only take wooden tools. Armok's additional note: "shleggings? Yes, From 46d8213f16058623a3c790b9026a67facce58750 Mon Sep 17 00:00:00 2001 From: definability <5962274+definability@users.noreply.github.com> Date: Sat, 16 Aug 2025 09:59:54 +0100 Subject: [PATCH 0447/1082] Fix #5554: Typo in plugins/lua/sort/places.lua Typo in the word "Barracks" --- plugins/lua/sort/places.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/sort/places.lua b/plugins/lua/sort/places.lua index d58f306584..f2588cc0ff 100644 --- a/plugins/lua/sort/places.lua +++ b/plugins/lua/sort/places.lua @@ -21,7 +21,7 @@ local zone_names = { [df.civzone_type.SandCollection] = 'Sand', [df.civzone_type.Office] = 'Office', [df.civzone_type.Dormitory] = 'Dormitory', - [df.civzone_type.Barracks] = 'Barrachs', + [df.civzone_type.Barracks] = 'Barracks', [df.civzone_type.ArcheryRange] = 'Archery Range', [df.civzone_type.Dump] = 'Garbage Dump', [df.civzone_type.AnimalTraining] = 'Animal Training', From b258565046908a18b69c376d225d450cde5251da Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 16 Aug 2025 18:04:39 +0200 Subject: [PATCH 0448/1082] export job helpers and provide documentation --- docs/changelog.txt | 2 ++ docs/dev/Lua API.rst | 28 +++++++++++++++++++++++++++- library/LuaApi.cpp | 26 ++++++++++++++++++++++++++ library/include/modules/Units.h | 6 +++--- library/modules/Job.cpp | 2 +- library/modules/Units.cpp | 14 +++++++------- 6 files changed, 66 insertions(+), 12 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 667bbe0425..6a9ee5ec0d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,8 @@ Template for new versions: ## Lua +- New functions: ``dfhack.jobs.createLinked``, ``dfhack.jobs.assignToWorkshop``, ``dfhack.units.getFocusPenalty``, ``dfhack.units.unbailableSocialActivity``, and ``dfhack.units.isJobAvailable`` + ## Removed # 52.03-r1.1 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index e2d65d4465..ea6a23183d 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1313,6 +1313,10 @@ Job module Creates a deep copy of the given job. +* ``dfhack.job.createLinked()`` + + Create a job and immediately link it into the global job list. + * ``dfhack.job.printJobDetails(job)`` Prints info about the job. @@ -1338,6 +1342,12 @@ Job module Searches for a specific_ref with the given type. +* ``dfhack.job.assignToWorkshop(job, workshop)`` + + Assign job to workshop (i.e. establish the bidirectional link between the job + and the workshop). Does nothing and returns ``false`` if the workshop already + has the maximum of ten jobs. + * ``dfhack.job.getHolder(job)`` Returns the building holding the job. @@ -1628,7 +1638,7 @@ Units module Returns true if the unit is within a box defined by the specified coordinates. -``dfhack.units.getUnitsInBox(pos1, pos2[, filter])`` +* ``dfhack.units.getUnitsInBox(pos1, pos2[, filter])`` * ``dfhack.units.getUnitsInBox(x1,y1,z1,x2,y2,z2[,filter])`` Returns a table of all units within the specified coordinates. @@ -1894,6 +1904,22 @@ Units module Return the ``df.activity_entry`` or ``df.activity_event`` representing the unit's current social activity. +* ``dfhack.units.hasUnbailableSocialActivity(unit)`` + + Unit has an uninterruptible social activity (e.g. a purple "Socialize!"). + +* ``dfhack.units.isJobAvailable(unit [, interrupt_social])`` + + Check whether a unit can be assigned to (i.e. is looking for) a job. Will + return ``true`` if the unit is engaged in "green" social activities, unless + the boolean ``interrupt_social`` is true. + +* ``dfhack.units.getFocusPenalty(unit, need_type [, need_type, ...])`` + + Get largest (i.e. most negative) focus penalty associated to a collection of + ``df.need_type`` arguments. Returns a number strictly greater than 400 if the + unit does not have any of the requested needs. + * ``dfhack.units.getStressCategory(unit)`` Returns a number from 0-6 indicating stress. 0 is most stressed; 6 is least. diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index a34439a2ae..192fc7beb5 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -69,6 +69,7 @@ distribution. #include "df/building_civzonest.h" #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" +#include "df/building_workshopst.h" #include "df/burrow.h" #include "df/caravan_state.h" #include "df/construction.h" @@ -116,6 +117,7 @@ distribution. #include #include #include +#include namespace DFHack { DBG_DECLARE(core, luaapi, DebugCategory::LINFO); @@ -1902,6 +1904,8 @@ static const LuaWrapper::FunctionReg dfhack_job_module[] = { WRAPM(Job,disconnectJobItem), WRAPM(Job,disconnectJobGeneralRef), WRAPM(Job,removeJob), + WRAPM(Job,createLinked), + WRAPM(Job,assignToWorkshop), WRAPN(is_equal, jobEqual), WRAPN(is_item_equal, jobItemEqual), { NULL, NULL } @@ -2128,6 +2132,8 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, setGroupActionTimers), WRAPM(Units, getUnitByNobleRole), WRAPM(Units, unassignTrainer), + WRAPM(Units, hasUnbailableSocialActivity), + WRAPM(Units, isJobAvailable), { NULL, NULL } }; @@ -2321,6 +2327,25 @@ static int units_getProfessionName(lua_State *L) { return 1; } +int32_t units_getFocusPenalty(lua_State *L) { + auto unit = Lua::GetDFObject(L, 1); + Units::need_type_set needs; + auto top = lua_gettop(L); + if (top < 2) { + luaL_argerror(L, 2, "Expected at least one need type"); + } else { + for (int i = 2; i <= top; ++i) { + try { + needs.set(luaL_checkint(L, i)); + } catch (const std::out_of_range &e) { + luaL_argerror(L, i, "Expected a need type"); + } + } + Lua::Push(L, Units::getFocusPenalty(unit, needs)); + } + return 1; +} + static const luaL_Reg dfhack_units_funcs[] = { { "getPosition", units_getPosition }, { "getOuterContainerRef", units_getOuterContainerRef }, @@ -2335,6 +2360,7 @@ static const luaL_Reg dfhack_units_funcs[] = { { "getReadableName", units_getReadablename }, { "getVisibleName", units_getVisibleName }, { "getProfessionName", units_getProfessionName }, + { "getFocusPenalty", units_getFocusPenalty }, { NULL, NULL } }; diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index fc33fd3b0a..a105930438 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -342,13 +342,13 @@ DFHACK_EXPORT df::activity_entry *getMainSocialActivity(df::unit *unit); DFHACK_EXPORT df::activity_event *getMainSocialEvent(df::unit *unit); // get largest (i.e. most negative) focus penalty for a set of needs -using need_types_set = std::bitset; -DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, need_types_set need_types); +using need_type_set = std::bitset; +DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, need_type_set need_types); // get focused penalty for a single need DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, df::need_type need_type); // unit has an unbailable social activity (e.g. "Socialize!") -DFHACK_EXPORT bool unbailableSocialActivity(df::unit *unit); +DFHACK_EXPORT bool hasUnbailableSocialActivity(df::unit *unit); // unit can be assigned a job DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool interrupt_social); diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index e0bb857ad8..3c70807325 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -525,7 +525,7 @@ df::job* DFHack::Job::createLinked() return job; } -bool assignToWorkshop(df::job *job, df::building_workshopst *workshop) +bool DFHack::Job::assignToWorkshop(df::job *job, df::building_workshopst *workshop) { CHECK_NULL_POINTER(job); CHECK_NULL_POINTER(workshop); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index d42087f7c8..086a2532f7 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2023,7 +2023,7 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) { return entry->events[entry->events.size() - 1]; } -int32_t Units::getFocusPenalty(df::unit* unit, need_types_set need_types) { +int32_t Units::getFocusPenalty(df::unit* unit, need_type_set need_types) { CHECK_NULL_POINTER(unit); int max_penalty = INT_MAX; @@ -2037,24 +2037,24 @@ int32_t Units::getFocusPenalty(df::unit* unit, need_types_set need_types) { } int32_t Units::getFocusPenalty(df::unit* unit, df::need_type need_type) { - auto need_types = need_types_set().set(need_type); + auto need_types = need_type_set().set(need_type); return getFocusPenalty(unit, need_types); } // reverse engineered from unitst::have_unbailable_sp_activities (partial implementation) -bool Units::unbailableSocialActivity(df::unit *unit) +bool Units::hasUnbailableSocialActivity(df::unit *unit) { // these can become constexpr with C++23 - static const need_types_set pray_needs = need_types_set() + static const need_type_set pray_needs = need_type_set() .set(df::need_type::PrayOrMeditate); - static const need_types_set socialize_needs = need_types_set() + static const need_type_set socialize_needs = need_type_set() .set(df::need_type::Socialize) .set(df::need_type::BeCreative) .set(df::need_type::Excitement) .set(df::need_type::AdmireArt); - static const need_types_set read_needs = need_types_set() + static const need_type_set read_needs = need_type_set() .set(df::need_type::ThinkAbstractly) .set(df::need_type::LearnSomething); @@ -2097,7 +2097,7 @@ bool Units::isJobAvailable(df::unit *unit, bool preserve_social = false){ if (activity && (activity->type == df::activity_entry_type::FillServiceOrder)) return false; } - if (unbailableSocialActivity(unit)) + if (hasUnbailableSocialActivity(unit)) return false; if (preserve_social && unit->social_activities.size() > 0) return false; From b307ef62ca09f5572d733c6d42d482b67b0338c7 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:21:49 +0200 Subject: [PATCH 0449/1082] fix unused variable warning --- library/LuaApi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 192fc7beb5..1989b9a78b 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2337,7 +2337,7 @@ int32_t units_getFocusPenalty(lua_State *L) { for (int i = 2; i <= top; ++i) { try { needs.set(luaL_checkint(L, i)); - } catch (const std::out_of_range &e) { + } catch ([[maybe_unused]] const std::out_of_range &e) { luaL_argerror(L, i, "Expected a need type"); } } From 80f084b44ad1206cee529244a8f1017f5dc1ab4a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 17 Aug 2025 07:19:07 +0000 Subject: [PATCH 0450/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 61e0e18195..b36c739619 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 61e0e181953e70679d0ed4dfc7e9e64922ad0c07 +Subproject commit b36c7396196beeaaacdf518e96e23ea106d425d6 From 00faba712abd1b9687974d653a505b0f75da2b24 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 17 Aug 2025 12:06:10 +0200 Subject: [PATCH 0451/1082] Update Authors.rst --- docs/about/Authors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index e8030e881a..8743b75a19 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -43,6 +43,7 @@ Cameron Ewell Ozzatron Carter Bray Qartar Chris Dombroski cdombroski Chris Parsons chrismdp +Christian Doczkal chdoc cjhammel cjhammel Clayton Hughes Clément Vuchener cvuchener From 7af372a3254e55354e899690f48c92858e3f6d4f Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 17 Aug 2025 16:32:22 +0200 Subject: [PATCH 0452/1082] allow limiting egg protection to nestboxes inside a designated burrow --- docs/changelog.txt | 2 + docs/plugins/nestboxes.rst | 7 ++++ plugins/nestboxes.cpp | 75 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6a9ee5ec0d..96a7887d92 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,8 @@ Template for new versions: ## New Features +- `nestboxes`: allow limiting egg protection to nestboxes inside a designated burrow + ## Fixes - ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession diff --git a/docs/plugins/nestboxes.rst b/docs/plugins/nestboxes.rst index 3c07cc30ca..a8c67dda33 100644 --- a/docs/plugins/nestboxes.rst +++ b/docs/plugins/nestboxes.rst @@ -10,9 +10,16 @@ This plugin will automatically scan for and forbid fertile eggs incubating in a nestbox so that dwarves won't come to collect them for eating. The eggs will hatch normally, even when forbidden. +You can control which eggs are collected and which eggs are protected by placing +the nestboxes whose contents should be protected inside a burrow and running +``nestboxes burrow ``. The default behavior of protecting all +fertile eggs in nest boxes can be reestablished by running ``nestboxes all``. + Usage ----- :: enable nestboxes + nestboxes burrow + nestbox all diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp index b935c34f81..bce36f9103 100644 --- a/plugins/nestboxes.cpp +++ b/plugins/nestboxes.cpp @@ -1,6 +1,7 @@ #include "Debug.h" #include "PluginManager.h" +#include "modules/Burrows.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/Persistence.h" @@ -8,6 +9,7 @@ #include "df/buildingitemst.h" #include "df/building_nest_boxst.h" +#include "df/burrow.h" #include "df/item.h" #include "df/item_eggst.h" #include "df/unit.h" @@ -34,6 +36,7 @@ static PersistentDataItem config; enum ConfigValues { CONFIG_IS_ENABLED = 0, + CONFIG_BURROW = 1 }; static const int32_t CYCLE_TICKS = 7; // need to react quickly when eggs are laid/unforbidden @@ -41,9 +44,16 @@ static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle static void do_cycle(color_ostream &out); +static command_result do_command(color_ostream &out, std::vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { DEBUG(control,out).print("initializing %s\n", plugin_name); + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Protect fertile eggs incubating in a nestbox.", + do_command)); + return CR_OK; } @@ -108,6 +118,65 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } +///////////////////////////////////////////////////// +// configuration logic +// + +static void setBurrow(color_ostream &out, const string &burrow_name){ + auto burrow = Burrows::findByName(burrow_name); + if (burrow) { + config.set_int(CONFIG_BURROW, burrow->id); + } else { + config.set_int(CONFIG_BURROW, -1); + } +} + +static df::burrow* getBurrow(color_ostream &out){ + int id = config.get_int(CONFIG_BURROW); + auto burrow = df::burrow::find(id); + if (!burrow) { + config.set_int(CONFIG_BURROW, -1); + } + return burrow; +} + +static void printStatus(color_ostream &out){ + if (!is_enabled) + out.print("%s is disabled\n", plugin_name); + else { + out.print("%s is enabled\n", plugin_name); + auto burrow = getBurrow(out); + if (burrow) + { + out.print("only protecting eggs inside burrow: %s\n", burrow->name.c_str()); + } + else + { + out.print("protecting all fertile eggs\n"); + } + } +} + +static command_result do_command(color_ostream &out, std::vector ¶meters){ + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + if (parameters.size() == 0 || parameters[0] == "status") + { + printStatus(out); + return CR_OK; + } else if (parameters.size() > 1 && parameters[0] == "burrow") { + setBurrow(out, parameters[1]); + return CR_OK; + } else if (parameters[0] == "all") { + config.set_int(CONFIG_BURROW, -1); + return CR_OK; + } else { + return CR_WRONG_USAGE; + } +} ///////////////////////////////////////////////////// // cycle logic // @@ -118,6 +187,9 @@ static void do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; + // see if egg protection is limited to a burrow + auto burrow = getBurrow(out); + for (df::building_nest_boxst *nb : world->buildings.other.NEST_BOX) { for (auto &contained_item : nb->contained_items) { if (contained_item->use_mode == df::building_item_role_type::PERM) @@ -126,6 +198,9 @@ static void do_cycle(color_ostream &out) { bool fertile = item->egg_flags.bits.fertile; if (item->flags.bits.forbid == fertile) continue; + if (burrow && !Burrows::isAssignedTile(burrow, Items::getPosition(item))) { + continue; + } item->flags.bits.forbid = fertile; if (fertile && item->flags.bits.in_job) { // cancel any job involving the egg From bc9290d78d1c944b1948247e176132b0f4cdc572 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 15:08:31 -0500 Subject: [PATCH 0453/1082] fix off-by-one in furniture stockpile export fixes #5557 --- docs/changelog.txt | 1 + plugins/stockpiles/StockpileSerializer.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b30979b781..ce71b3edb8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## Fixes - ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession +- `stockpiles`: fixed off-by-one error in exporting furniture stockpiles ## Misc Improvements diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 6abab93baf..1be2ae550c 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -1741,7 +1741,7 @@ bool StockpileSettingsSerializer::write_furniture(color_ostream& out, StockpileS all = false; continue; } - string f_type(type_traits::key_table[i]); + string f_type{ENUM_KEY_STR(furniture_type, furniture_type(i))}; furniture->add_type(f_type); DEBUG(log, out).print("furniture_type %zd is %s\n", i, f_type.c_str()); } From cb2f9a71ebca247132e9d4cae58f720ff90c89ef Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:02:50 -0500 Subject: [PATCH 0454/1082] add dye automation to tailor --- docs/plugins/tailor.rst | 14 ++ plugins/lua/tailor.lua | 8 + plugins/tailor.cpp | 333 ++++++++++++++++++++++++++++++++++------ 3 files changed, 308 insertions(+), 47 deletions(-) diff --git a/docs/plugins/tailor.rst b/docs/plugins/tailor.rst index c1732a741d..b915afb264 100644 --- a/docs/plugins/tailor.rst +++ b/docs/plugins/tailor.rst @@ -28,6 +28,7 @@ Usage tailor now tailor materials [ ...] tailor confiscate [true|false] + tailor dye [true|false] By default, ``tailor`` will prefer using materials in this order:: @@ -48,6 +49,11 @@ too precious to routinely make into cloth. ``tailor`` does not support modded "cloth" types which utilize custom reactions for making clothing out of those cloth types. +If dye management is enabled, ``tailor`` will also issue orders to dye cloth +as needed to fulfill fortress clothing requirements, and will issue orders to +manufacture dyes required to fulfill the dye cloth orders. Dye management is +disabled by default. + Examples -------- @@ -69,3 +75,11 @@ Caveats Modded cloth-like materials are not supported because custom reactions do not support being sized for non-dwarf races. The game only supports sizing the built-in default make-clothing or make-armor reactions. + +Dye automation will not not issue orders to mill dyes that are made at a +millstone or quern because Dwarf Fortress does not currently support +"mill plant" orders for a specified material. + +At present, dye automation uses any available dye and cloth will be dyed +with whatever color dye can be found or made. The ability to specify color +preferences may be added in the future. \ No newline at end of file diff --git a/plugins/lua/tailor.lua b/plugins/lua/tailor.lua index 46f3e712d9..d32a4b7570 100644 --- a/plugins/lua/tailor.lua +++ b/plugins/lua/tailor.lua @@ -17,6 +17,7 @@ end function status() dfhack.print(('tailor is %s'):format(isEnabled() and "enabled" or "disabled")) print((' %s confiscating tattered clothing'):format(tailor_getConfiscate() and "and" or "but not")) + print(('tailor %s automating dye'):format(tailor_getAutomateDye() and "is" or "is not")) print('materials preference order:') for _,name in ipairs(tailor_getMaterialPreferences()) do print((' %s'):format(name)) @@ -38,6 +39,11 @@ function setConfiscate(opt) tailor_setConfiscate(fl) end +function setAutomateDye(opt) + local fl = argparse.boolean(opt[1], "set dye") + tailor_setAutomateDye(fl) +end + function parse_commandline(...) local args, opts = {...}, {} local positionals = process_args(opts, args) @@ -55,6 +61,8 @@ function parse_commandline(...) setMaterials(positionals) elseif command == 'confiscate' then setConfiscate(positionals) + elseif command == 'dye' then + setAutomateDye(positionals) else return false end diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index ef9b77c67a..dde351f9dc 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -2,6 +2,8 @@ * Tailor plugin. Automatically manages keeping your dorfs clothed. */ +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -12,7 +14,9 @@ #include "modules/Units.h" #include "modules/World.h" +#include "df/building.h" #include "df/creature_raw.h" +#include "df/descriptor_color.h" #include "df/historical_entity.h" #include "df/item.h" #include "df/item_flags.h" @@ -25,6 +29,10 @@ #include "df/manager_order.h" #include "df/material.h" #include "df/plotinfost.h" +#include "df/reaction.h" +#include "df/reaction_reagent.h" +#include "df/reaction_reagent_itemst.h" +#include "df/reaction_product_itemst.h" #include "df/unit.h" #include "df/world.h" @@ -46,7 +54,9 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; +static const string CONFIG_KEY_2 = string(plugin_name) + "/config1"; static PersistentDataItem config; +static PersistentDataItem config2; enum ConfigValues { CONFIG_IS_ENABLED = 0, @@ -58,6 +68,11 @@ enum ConfigValues { CONFIG_CONFISCATE = 6 }; +enum Config2Values +{ + CONFIG_AUTOMATE_DYE = 0 +}; + static const int32_t CYCLE_TICKS = 1231; // one day static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle @@ -144,11 +159,17 @@ class Tailor { bool confiscate = true; + bool automate_dye = false; + public: void set_confiscate(bool f){ confiscate = f; } bool get_confiscate() { return confiscate; } + void set_automate_dye(bool f) { automate_dye = f; } + + bool get_automate_dye() { return automate_dye; } + void sync_material_order() { material_order.clear(); @@ -426,45 +447,232 @@ class Tailor { DF2CONSOLE(m.name).c_str(), o->amount_left); } } + } + } - if (race == -1) - continue; // -1 means that the race of the worker will determine the size made; we must ignore these jobs + using jiqt = decltype(df::reaction_reagent_itemst::quantity); - int size = world->raws.creatures.all[race]->adultsize; + int get_reaction_max_count(df::reaction* r, jiqt lim = std::numeric_limits::max()) + { + jiqt max = lim; - auto tt = jobTypeMap.find(o->job_type); - if (tt == jobTypeMap.end()) + for (auto rr : r->reagents) + { + if (rr->getType() == df::reaction_reagent_type::item) { - continue; + max = std::min(get_reagent_max_count(virtual_cast(rr), max), max); + } + } + + return max; + } + + int get_reagent_max_count(df::reaction_reagent_itemst* r, jiqt lim = std::numeric_limits::max()) + { + df::reaction_reagent_itemst* t = df::allocate(); + *t = *r; + jiqt orig = t->quantity; + + std::vector tbc; + std::vector toc; + std::vector trc; + std::vector tcc; + + auto test = [&] (jiqt m) { + t->quantity = orig * m; + return t->have_enough_from_precalc_info(NULL, &tbc, &toc, &trc, &tcc, -1); + }; + + jiqt max = lim / orig; + + jiqt lo = 0; + jiqt hi = max; + + // exit fast when 0 + if (test(1)) + { + lo = 1; + + while (hi > lo + 1) + { + // can't do (hi+lo)/2 because hi+lo may overflow! + jiqt mult = hi / 2 + lo / 2; + if (mult == lo) mult += 1; + if (test(mult)) + lo = mult; + else + hi = mult; + } + } + + delete t; + + return lo; + } + + int count_dyeables(df::item_type ty) + { + auto reagent = df::allocate(); + reagent->item_subtype = -1; + reagent->mat_type = -1; + reagent->mat_index = -1; + reagent->flags2.bits.dyeable = true; + reagent->has_tool_use = df::tool_uses::NONE; + if (ty == df::item_type::SKIN_TANNED) + { + reagent->quantity = 1; + reagent->min_dimension = 1; + } + else + { + reagent->quantity = 10000; + reagent->min_dimension = 10000; + } + reagent->item_type = ty; + return get_reagent_max_count(reagent); + } + + int count_dyes() + { + auto reagent = df::allocate(); + reagent->item_type = df::item_type::NONE; + reagent->item_subtype = -1; + reagent->mat_type = -1; + reagent->mat_index = -1; + reagent->flags1.bits.unrotten = true; + reagent->flags2.bits.dye = true; + reagent->has_tool_use = df::tool_uses::NONE; + reagent->quantity = 1; + int count = get_reagent_max_count(reagent); + delete reagent; + return count; + } + + using color_type = decltype(MaterialInfo::material->powder_dye); + + static auto product_is_dye (df::reaction_product* r) -> bool + { + if (r->getType() == df::reaction_product_type::item) + { + auto rr = virtual_cast (r); + auto mat = MaterialInfo(rr->mat_type, rr->mat_index); + return mat.material && mat.material->flags.is_set(df::enums::material_flags::IS_DYE); + } + return false; + }; + + void order_dye_from_reaction(df::reaction* r, int c = 1) + { + std::string descr; + auto dye = std::ranges::find_if(r->products, product_is_dye); + assert(dye != r->products.end()); + auto pp = virtual_cast(*dye); + assert(pp != nullptr); + auto mat = MaterialInfo(pp->mat_type, pp->mat_index); + color_type color = mat.material->powder_dye; + + auto& color_name = world->raws.descriptors.colors[color]->name; + pp->getDescription(&descr); + + get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); + INFO(cycle).print("tailor: ordered %d %s\n", c, descr); + } + + void order_dye_cloth(int c = 1) + { + get_or_create_order(c, df::job_type::DyeCloth, -1, -1, 0); + } + + void make_dyes(int count) + { + auto reaction_produces_dye = [] (df::reaction* r) { + return std::ranges::any_of(r->products, product_is_dye); + }; + + for (auto r : std::ranges::views::filter(world->raws.reactions.reactions, reaction_produces_dye)) + { + auto& reaction_name = r->name; + + int max = get_reaction_max_count(r,1); + + if (max > 0) + { + order_dye_from_reaction(r, max); } - needed[std::make_pair(tt->second, size)] -= o->amount_left; - TRACE(cycle).print("tailor: existing order for %d %s of size %d detected\n", - o->amount_left, - ENUM_KEY_STR(job_type, o->job_type).c_str(), - size); + count = std::max(0, count - max); + if (count <= 0) + break; } + } + + int count_dye_cloth_orders() + { + auto f = [] (df::manager_order* o) { + return o->job_type == df::job_type::DyeCloth; + }; + int sum = 0; + for (auto o : std::ranges::views::filter(world->manager_orders.all, f)) + { + sum += o->amount_left; + } + return sum; } - static df::manager_order * get_existing_order(df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat) { - for (auto order : world->manager_orders.all) { - if (order->job_type == ty && - order->item_type == df::item_type::NONE && - order->item_subtype == sub && - order->mat_type == -1 && - order->mat_index == -1 && - order->specdata.hist_figure_id == hfid && - order->material_category.whole == mcat.whole && - order->frequency == df::workquota_frequency_type::OneTime) - return order; + using oqt = decltype(df::manager_order::amount_total); + + static df::manager_order* get_or_create_order(oqt c, df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat, std::string custom_reaction = "") + { + auto f = [&] (df::manager_order* order) { + return order->job_type == ty && + order->item_type == df::item_type::NONE && + order->item_subtype == sub && + order->mat_type == -1 && + order->mat_index == -1 && + order->specdata.race == hfid && + order->material_category.whole == mcat.whole && + order->frequency == df::workquota_frequency_type::OneTime && + order->reaction_name == custom_reaction; + }; + + auto orderIt = std::ranges::find_if(world->manager_orders.all, f); + + if (orderIt != world->manager_orders.all.end()) + { + auto o = *orderIt; + if (o->amount_left > 0) + { + o->amount_left = std::max(c, o->amount_left); + o->amount_total = std::max(c, o->amount_total); + } + return o; } - return NULL; + + auto order = new df::manager_order; + order->job_type = ty; + order->item_type = df::item_type::NONE; + order->item_subtype = sub; + order->reaction_name = custom_reaction; + order->specdata.race = hfid; + order->material_category = mcat; + order->mat_type = -1; + order->mat_index = -1; + order->amount_left = c; + order->amount_total = c; + order->status.bits.validated = false; + order->status.bits.active = false; + order->id = world->manager_orders.manager_order_next_id++; + + world->manager_orders.all.push_back(order); + + return order; } int place_orders() { int ordered = 0; + int skipped = 0; auto entity = world->entities.all[plotinfo->civ_id]; for (auto& o : orders) @@ -550,32 +758,11 @@ class Tailor { c = supply[m] - res; TRACE(cycle).print("tailor: order reduced from %d to %d to protect reserves of %s\n", count, c, DF2CONSOLE(m.name).c_str()); + skipped += (count - c); } supply[m] -= c; - auto order = get_existing_order(ty, sub, sizes[size], m.job_material); - if (order) { - if (order->amount_total > 0) { - order->amount_left += c; - order->amount_total += c; - } - } else { - order = new df::manager_order; - order->job_type = ty; - order->item_type = df::item_type::NONE; - order->item_subtype = sub; - order->mat_type = -1; - order->mat_index = -1; - order->amount_left = c; - order->amount_total = c; - order->status.bits.validated = false; - order->status.bits.active = false; - order->id = world->manager_orders.manager_order_next_id++; - order->specdata.hist_figure_id = sizes[size]; - order->material_category = m.job_material; - - world->manager_orders.all.push_back(order); - } + auto order = get_or_create_order(c, ty, sub, sizes[size], m.job_material); INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", order->id, @@ -590,12 +777,39 @@ class Tailor { } else { - TRACE(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); + skipped += count; + INFO(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); } } } } + + if (skipped > 0) + { + INFO(cycle).print("tailor: %d item%s not ordered due to a lack of materials\n", skipped, skipped != 1 ? "s" : ""); + + if (automate_dye) + { + int available_dyes = count_dyes(); + int available_dyable_cloth = count_dyeables(df::item_type::CLOTH); + + int to_dye = std::max(0, std::min(skipped, std::min(count_dyes(), count_dyeables(df::item_type::CLOTH))) - count_dye_cloth_orders()); + if (to_dye > 0) + { + INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); + order_dye_cloth(to_dye); + } + + int dyes_to_make = std::max(0, skipped - available_dyes - to_dye); + if (dyes_to_make > 0) + { + INFO(cycle).print("tailor: ordering up to %d dyes\n", dyes_to_make); + make_dyes(dyes_to_make); + } + } + } + return ordered; } @@ -671,16 +885,27 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { config = World::AddPersistentSiteData(CONFIG_KEY); config.set_bool(CONFIG_IS_ENABLED, is_enabled); config.set_bool(CONFIG_CONFISCATE, true); + config.set_bool(CONFIG_AUTOMATE_DYE, false); } // transition existing saves to CONFIG_CONFISCATE=true if (config.get_int(CONFIG_CONFISCATE) < 0) { DEBUG(control,out).print("found existing configuration with CONFIG_CONFISCATE unset, initializing to true\n"); config.set_bool(CONFIG_CONFISCATE, true); } + + config2 = World::GetPersistentSiteData(CONFIG_KEY_2); + // transition existing saves to CONFIG_AUTOMATE_DYE=false + if (config2.get_int(CONFIG_AUTOMATE_DYE) < 0) + { + DEBUG(control, out).print("found existing configuration with CONFIG_AUTOMATE_DYE unset, initializing to false\n"); + config2.set_bool(CONFIG_AUTOMATE_DYE, false); + } + is_enabled = config.get_bool(CONFIG_IS_ENABLED); DEBUG(control,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); tailor_instance->set_confiscate(config.get_bool(CONFIG_CONFISCATE)); + tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); DEBUG(control,out).print("loading persisted confiscation state: %s\n", tailor_instance->get_confiscate() ? "true" : "false"); tailor_instance->sync_material_order(); @@ -776,6 +1001,18 @@ static bool tailor_getConfiscate(color_ostream& out) return tailor_instance->get_confiscate(); } +static void tailor_setAutomateDye(color_ostream& out, bool enable) +{ + DEBUG(control, out).print("%s automation of dye\n", enable ? "enabling" : "disabling"); + config2.set_bool(CONFIG_AUTOMATE_DYE, enable); + tailor_instance->set_automate_dye(enable); +} + +static bool tailor_getAutomateDye(color_ostream& out) +{ + return tailor_instance->get_automate_dye(); +} + static int tailor_getMaterialPreferences(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) @@ -793,6 +1030,8 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(tailor_setMaterialPreferences), DFHACK_LUA_FUNCTION(tailor_setConfiscate), DFHACK_LUA_FUNCTION(tailor_getConfiscate), + DFHACK_LUA_FUNCTION(tailor_setAutomateDye), + DFHACK_LUA_FUNCTION(tailor_getAutomateDye), DFHACK_LUA_END }; From 078741e21040f7a193ebc10688d4df9bcd87e226 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:09:15 -0500 Subject: [PATCH 0455/1082] Update StockpileSerializer.cpp remove no longer used type alias --- plugins/stockpiles/StockpileSerializer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 1be2ae550c..efac3e8bad 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -1731,7 +1731,6 @@ static bool furniture_mat_is_allowed(const MaterialInfo& mi) { bool StockpileSettingsSerializer::write_furniture(color_ostream& out, StockpileSettings::FurnitureSet* furniture) { using df::enums::furniture_type::furniture_type; - using type_traits = df::enum_traits; auto & pfurniture = mSettings->furniture; bool all = true; From 59238a31d07542fe074cfac9343adb3eff216236 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:13:25 -0500 Subject: [PATCH 0456/1082] Update tailor.cpp Clean up unused variables, fix type error --- plugins/tailor.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index dde351f9dc..f5e4a27207 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -436,8 +436,6 @@ class Tailor { if (f == jobTypeMap.end()) continue; - int race = o->specdata.hist_figure_id; - for (auto& m : all_materials) { if (o->material_category.whole == m.job_material.whole) @@ -571,11 +569,10 @@ class Tailor { auto mat = MaterialInfo(pp->mat_type, pp->mat_index); color_type color = mat.material->powder_dye; - auto& color_name = world->raws.descriptors.colors[color]->name; pp->getDescription(&descr); get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); - INFO(cycle).print("tailor: ordered %d %s\n", c, descr); + INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr.c_str())); } void order_dye_cloth(int c = 1) @@ -591,8 +588,6 @@ class Tailor { for (auto r : std::ranges::views::filter(world->raws.reactions.reactions, reaction_produces_dye)) { - auto& reaction_name = r->name; - int max = get_reaction_max_count(r,1); if (max > 0) @@ -792,9 +787,9 @@ class Tailor { if (automate_dye) { int available_dyes = count_dyes(); - int available_dyable_cloth = count_dyeables(df::item_type::CLOTH); + int available_dyeable_cloth = count_dyeables(df::item_type::CLOTH); - int to_dye = std::max(0, std::min(skipped, std::min(count_dyes(), count_dyeables(df::item_type::CLOTH))) - count_dye_cloth_orders()); + int to_dye = std::max(0, std::min(skipped, std::min(count_dyes(), available_dyeable_cloth)) - count_dye_cloth_orders()); if (to_dye > 0) { INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); From 734623bec5afc314805e82fc3ce1c12066705656 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:14:28 -0500 Subject: [PATCH 0457/1082] whitespace --- plugins/tailor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index f5e4a27207..0780667170 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -1033,4 +1033,4 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(tailor_getMaterialPreferences), DFHACK_LUA_END -}; +}; \ No newline at end of file From 28dabce6082f31301f8c9ca6fbfb13a5f53040dd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:15:26 -0500 Subject: [PATCH 0458/1082] actual whitespace --- docs/plugins/tailor.rst | 2 +- plugins/tailor.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/tailor.rst b/docs/plugins/tailor.rst index b915afb264..9e59957810 100644 --- a/docs/plugins/tailor.rst +++ b/docs/plugins/tailor.rst @@ -82,4 +82,4 @@ millstone or quern because Dwarf Fortress does not currently support At present, dye automation uses any available dye and cloth will be dyed with whatever color dye can be found or made. The ability to specify color -preferences may be added in the future. \ No newline at end of file +preferences may be added in the future. diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 0780667170..f5e4a27207 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -1033,4 +1033,4 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(tailor_getMaterialPreferences), DFHACK_LUA_END -}; \ No newline at end of file +}; From e53224d2570c897e9fe5074c3707a4ff6050190a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:18:43 -0500 Subject: [PATCH 0459/1082] More minor cleanup --- plugins/tailor.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index f5e4a27207..6bb63f6ee8 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -567,12 +567,11 @@ class Tailor { auto pp = virtual_cast(*dye); assert(pp != nullptr); auto mat = MaterialInfo(pp->mat_type, pp->mat_index); - color_type color = mat.material->powder_dye; pp->getDescription(&descr); get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); - INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr.c_str())); + INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); } void order_dye_cloth(int c = 1) From a93ace4e87ab17c6686a3312cc481668ee3a0af0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:21:50 -0500 Subject: [PATCH 0460/1082] one more unused variable gcc, why can't you tell me all of them at once? --- plugins/tailor.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 6bb63f6ee8..513e1a9543 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -566,7 +566,6 @@ class Tailor { assert(dye != r->products.end()); auto pp = virtual_cast(*dye); assert(pp != nullptr); - auto mat = MaterialInfo(pp->mat_type, pp->mat_index); pp->getDescription(&descr); From ee34213f72eea6610f16f2a8ec7d53a39d701a64 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:23:49 +0000 Subject: [PATCH 0461/1082] 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 fc6dcd5aa0..6af6484430 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit fc6dcd5aa077a0d96d81bd2dfe89e24bed375472 +Subproject commit 6af6484430d633f059a40998d4e8b3a2c0cc863f diff --git a/plugins/stonesense b/plugins/stonesense index 15add1726c..fba957f269 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 15add1726c711b5b8c4d0ae5afe42b8f841d3af6 +Subproject commit fba957f2699987cfec89cc1f18552bb7fc07f1eb diff --git a/scripts b/scripts index b36c739619..6749ada0aa 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b36c7396196beeaaacdf518e96e23ea106d425d6 +Subproject commit 6749ada0aa923935d2997f0ea8c3ca58d532a4d0 From 0f5b65b8af3bc22d05507e17bd29bbbdb972ca98 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:45:02 +0000 Subject: [PATCH 0462/1082] 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 6af6484430..d84e070851 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 6af6484430d633f059a40998d4e8b3a2c0cc863f +Subproject commit d84e07085151f3b177fe257232322d7010c5a2e5 diff --git a/scripts b/scripts index 6749ada0aa..e2e976e3f4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6749ada0aa923935d2997f0ea8c3ca58d532a4d0 +Subproject commit e2e976e3f4a17f16d7d0e8d0c8fb09adaf4aa5dc From a2d0f94f60e070f776a44642376cb7790e4405ed Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 18 Aug 2025 10:00:08 -0500 Subject: [PATCH 0463/1082] bump to 52.03-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a0875d2c32..e150f7255a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.03") -set(DFHACK_RELEASE "r1.1") -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 3347184e0b297ef7aec4e588163a8c6019c8d403 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 18 Aug 2025 11:13:48 -0500 Subject: [PATCH 0464/1082] update changelog to conform to prerelease --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ce71b3edb8..dc0f1d296e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,8 +54,8 @@ Template for new versions: ## New Tools ## New Features - - `nestboxes`: allow limiting egg protection to nestboxes inside a designated burrow +- `tailor`: tailor now provides optional dye automation ## Fixes - ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession From 6febec7b0bab9e098a2a6ff6cd371651cf802ef8 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:19:37 +0000 Subject: [PATCH 0465/1082] 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 fba957f269..78ea0e4906 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit fba957f2699987cfec89cc1f18552bb7fc07f1eb +Subproject commit 78ea0e4906af869c7b6b2ab3b64d833953e2ab2f From 867da3e0b63f63e2a25dd8757aac3fd2bbcf61ec Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 19 Aug 2025 09:32:51 -0500 Subject: [PATCH 0466/1082] tailor: fixes from testing add missing binding for second config persistence item improve logging --- plugins/tailor.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 513e1a9543..bfcd4f0934 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -771,7 +771,7 @@ class Tailor { else { skipped += count; - INFO(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); + DEBUG(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); } } @@ -786,15 +786,19 @@ class Tailor { { int available_dyes = count_dyes(); int available_dyeable_cloth = count_dyeables(df::item_type::CLOTH); + int dye_cloth_orders = count_dye_cloth_orders(); - int to_dye = std::max(0, std::min(skipped, std::min(count_dyes(), available_dyeable_cloth)) - count_dye_cloth_orders()); + DEBUG(cycle).print("tailor: available dyes = %d, available dyeable cloth = %d, dye cloth orders = %d\n", + available_dyes, available_dyeable_cloth, dye_cloth_orders); + int to_dye = std::min(skipped, std::min(available_dyes, available_dyeable_cloth) - dye_cloth_orders); + DEBUG(cycle).print("tailor: to dye = %d\n", to_dye); if (to_dye > 0) { INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); order_dye_cloth(to_dye); } - int dyes_to_make = std::max(0, skipped - available_dyes - to_dye); + int dyes_to_make = available_dyes - to_dye; if (dyes_to_make > 0) { INFO(cycle).print("tailor: ordering up to %d dyes\n", dyes_to_make); @@ -872,6 +876,7 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { DFhackCExport command_result plugin_load_site_data (color_ostream &out) { cycle_timestamp = 0; config = World::GetPersistentSiteData(CONFIG_KEY); + config2 = World::GetPersistentSiteData(CONFIG_KEY_2); if (!config.isValid()) { DEBUG(control,out).print("no config found in this save; initializing\n"); From 0f389d195f35aa02bbccaf3d0c485392132054b2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:20:37 +0000 Subject: [PATCH 0467/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e2e976e3f4..4a950bd5b2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e2e976e3f4a17f16d7d0e8d0c8fb09adaf4aa5dc +Subproject commit 4a950bd5b2b9dae26fdfed14e24afda7b7f2a4b1 From 56f9a23def0803be26dcb30ba803c967e116cbe7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 19 Aug 2025 19:16:12 -0500 Subject: [PATCH 0468/1082] tailor: logging tweaks, configuration --- plugins/tailor.cpp | 65 ++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index bfcd4f0934..9f658fff13 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -546,6 +546,8 @@ class Tailor { return count; } + using oqt = decltype(df::manager_order::amount_total); + using color_type = decltype(MaterialInfo::material->powder_dye); static auto product_is_dye (df::reaction_product* r) -> bool @@ -559,7 +561,7 @@ class Tailor { return false; }; - void order_dye_from_reaction(df::reaction* r, int c = 1) + oqt order_dye_from_reaction(df::reaction* r, int c = 1) { std::string descr; auto dye = std::ranges::find_if(r->products, product_is_dye); @@ -569,13 +571,16 @@ class Tailor { pp->getDescription(&descr); - get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); - INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); + auto [_, n] = get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); + if (n > 0) + INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); + return n; } - void order_dye_cloth(int c = 1) + oqt order_dye_cloth(int c = 1) { - get_or_create_order(c, df::job_type::DyeCloth, -1, -1, 0); + auto [_, n] = get_or_create_order(c, df::job_type::DyeCloth, -1, -1, 0); + return n; } void make_dyes(int count) @@ -590,7 +595,7 @@ class Tailor { if (max > 0) { - order_dye_from_reaction(r, max); + max = order_dye_from_reaction(r, max); } count = std::max(0, count - max); @@ -613,9 +618,7 @@ class Tailor { return sum; } - using oqt = decltype(df::manager_order::amount_total); - - static df::manager_order* get_or_create_order(oqt c, df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat, std::string custom_reaction = "") + static std::pair get_or_create_order(oqt c, df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat, std::string custom_reaction = "") { auto f = [&] (df::manager_order* order) { return order->job_type == ty && @@ -634,12 +637,15 @@ class Tailor { if (orderIt != world->manager_orders.all.end()) { auto o = *orderIt; + int chg = 0; if (o->amount_left > 0) { + int prev = o->amount_left; o->amount_left = std::max(c, o->amount_left); o->amount_total = std::max(c, o->amount_total); + chg = o->amount_left - prev; } - return o; + return {o, chg}; } auto order = new df::manager_order; @@ -659,7 +665,7 @@ class Tailor { world->manager_orders.all.push_back(order); - return order; + return {order, c}; } int place_orders() @@ -755,18 +761,21 @@ class Tailor { } supply[m] -= c; - auto order = get_or_create_order(c, ty, sub, sizes[size], m.job_material); - - INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", - order->id, - c, - bitfield_to_string(order->material_category).c_str(), - DF2CONSOLE((c > 1) ? name_p : name_s).c_str(), - DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1]).c_str() - ); + auto [order,n] = get_or_create_order(c, ty, sub, sizes[size], m.job_material); - count -= c; - ordered += c; + if (n > 0) + { + INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", + order->id, + n, + bitfield_to_string(order->material_category).c_str(), + DF2CONSOLE((c > 1) ? name_p : name_s).c_str(), + DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1]).c_str() + ); + + count -= n; + ordered += n; + } } else { @@ -794,8 +803,9 @@ class Tailor { DEBUG(cycle).print("tailor: to dye = %d\n", to_dye); if (to_dye > 0) { - INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); - order_dye_cloth(to_dye); + int dyed = order_dye_cloth(to_dye); + if (dyed > 0) + INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); } int dyes_to_make = available_dyes - to_dye; @@ -883,7 +893,12 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { config = World::AddPersistentSiteData(CONFIG_KEY); config.set_bool(CONFIG_IS_ENABLED, is_enabled); config.set_bool(CONFIG_CONFISCATE, true); - config.set_bool(CONFIG_AUTOMATE_DYE, false); + } + if (!config2.isValid()) + { + DEBUG(control, out).print("no extended config found in this save; initializing\n"); + config2 = World::AddPersistentSiteData(CONFIG_KEY_2); + config2.set_bool(CONFIG_AUTOMATE_DYE, false); } // transition existing saves to CONFIG_CONFISCATE=true if (config.get_int(CONFIG_CONFISCATE) < 0) { From 2aa6ab03b42b47fd632506d4f0ba8dc97b9adeed Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 19 Aug 2025 19:19:13 -0500 Subject: [PATCH 0469/1082] make gcc happier --- plugins/tailor.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 9f658fff13..7409c0eb7c 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -573,7 +573,9 @@ class Tailor { auto [_, n] = get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); if (n > 0) + { INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); + } return n; } @@ -805,7 +807,9 @@ class Tailor { { int dyed = order_dye_cloth(to_dye); if (dyed > 0) + { INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); + } } int dyes_to_make = available_dyes - to_dye; From c7e066eee49b532d9a1559d929ac7185ceb5a4e6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 20 Aug 2025 18:48:12 -0500 Subject: [PATCH 0470/1082] tailor: fix configuration transition --- plugins/tailor.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 7409c0eb7c..c66d314431 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -54,7 +54,7 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; -static const string CONFIG_KEY_2 = string(plugin_name) + "/config1"; +static const string CONFIG_KEY_2 = string(plugin_name) + "/config_2"; static PersistentDataItem config; static PersistentDataItem config2; @@ -885,8 +885,6 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { return CR_OK; } - - DFhackCExport command_result plugin_load_site_data (color_ostream &out) { cycle_timestamp = 0; config = World::GetPersistentSiteData(CONFIG_KEY); @@ -910,7 +908,6 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { config.set_bool(CONFIG_CONFISCATE, true); } - config2 = World::GetPersistentSiteData(CONFIG_KEY_2); // transition existing saves to CONFIG_AUTOMATE_DYE=false if (config2.get_int(CONFIG_AUTOMATE_DYE) < 0) { @@ -922,9 +919,12 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DEBUG(control,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); tailor_instance->set_confiscate(config.get_bool(CONFIG_CONFISCATE)); - tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); DEBUG(control,out).print("loading persisted confiscation state: %s\n", tailor_instance->get_confiscate() ? "true" : "false"); + tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); + DEBUG(control, out).print("loading persisted dye automation state: %s\n", + tailor_instance->get_automate_dye() ? "true" : "false"); + tailor_instance->sync_material_order(); return CR_OK; From a2a768c1df35f533eb6db2c9f375171962682de5 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:49:43 +0200 Subject: [PATCH 0471/1082] two minor fixes for #5535 --- docs/dev/Lua API.rst | 5 +++-- library/include/modules/Units.h | 2 +- library/modules/Units.cpp | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index d31a8296aa..86e90da9c1 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1908,11 +1908,12 @@ Units module Unit has an uninterruptible social activity (e.g. a purple "Socialize!"). -* ``dfhack.units.isJobAvailable(unit [, interrupt_social])`` +* ``dfhack.units.isJobAvailable(unit [, preserve_social])`` Check whether a unit can be assigned to (i.e. is looking for) a job. Will return ``true`` if the unit is engaged in "green" social activities, unless - the boolean ``interrupt_social`` is true. + the boolean ``preserve_social`` is true. Will never interrupt uninterruptible + social activities (e.g. a purple "Socialize!"). * ``dfhack.units.getFocusPenalty(unit, need_type [, need_type, ...])`` diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index a105930438..b13a75d6a3 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -350,7 +350,7 @@ DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, df::need_type need_type); // unit has an unbailable social activity (e.g. "Socialize!") DFHACK_EXPORT bool hasUnbailableSocialActivity(df::unit *unit); // unit can be assigned a job -DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool interrupt_social); +DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool preserve_social); // Stress categories. 0 is highest stress, 6 is lowest. DFHACK_EXPORT extern const std::vector stress_cutoffs; diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 086a2532f7..d8bfebdc97 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2090,6 +2090,8 @@ bool Units::isJobAvailable(df::unit *unit, bool preserve_social = false){ return false; if (unit->flags1.bits.caged || unit->flags1.bits.chained) return false; + if (Units::getSpecificRef(unit, df::specific_ref_type::ACTIVITY)) + return false; if (unit->individual_drills.size() > 0) { if (unit->individual_drills.size() > 1) return false; // this is even possible From 7ca63173c3730257c83d89d51cca0294a6ba1c5b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 22 Aug 2025 03:01:14 +0000 Subject: [PATCH 0472/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 4a950bd5b2..93bdca9d4d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4a950bd5b2b9dae26fdfed14e24afda7b7f2a4b1 +Subproject commit 93bdca9d4d234ab982867b8e71ad052a82fb9421 From 430678d7a46ce3c0ceb5a158eb0411eb92b8a0f7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 Aug 2025 00:36:55 -0500 Subject: [PATCH 0473/1082] update for 52.03-r2 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 19 ++++++++++++++++++- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e150f7255a..9ed54be78a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.03") -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 dc0f1d296e..7e27d7f4c9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -53,6 +53,24 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.03-r2 + +## New Tools + ## New Features - `nestboxes`: allow limiting egg protection to nestboxes inside a designated burrow - `tailor`: tailor now provides optional dye automation @@ -70,7 +88,6 @@ Template for new versions: - ``Units``: new functions ``getFocusPenalty``, ``unbailableSocialActivity``, ``isJobAvailable`` ## Lua - - New functions: ``dfhack.jobs.createLinked``, ``dfhack.jobs.assignToWorkshop``, ``dfhack.units.getFocusPenalty``, ``dfhack.units.unbailableSocialActivity``, and ``dfhack.units.isJobAvailable`` ## Removed diff --git a/library/xml b/library/xml index d84e070851..e4993ceef7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d84e07085151f3b177fe257232322d7010c5a2e5 +Subproject commit e4993ceef7907351fd2ac6f9bd3a9e5ff24699db diff --git a/plugins/stonesense b/plugins/stonesense index 78ea0e4906..08102c59ef 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 78ea0e4906af869c7b6b2ab3b64d833953e2ab2f +Subproject commit 08102c59ef1c76b1cebe3e582be51f0ac763de0e diff --git a/scripts b/scripts index 93bdca9d4d..539120522a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 93bdca9d4d234ab982867b8e71ad052a82fb9421 +Subproject commit 539120522a808220e4ab23538bad5cfcd979211e From 95e41a0bf612398dc84b0148876258c52ed75bf7 Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 24 Aug 2025 01:18:41 -0400 Subject: [PATCH 0474/1082] Update changelog syntax reference, and move out the changelog template --- docs/changelog.txt | 29 ++++++++++++++++------------- docs/dev/Documentation.rst | 4 ++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7e27d7f4c9..fc0031b4fe 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -1,26 +1,30 @@ === Scroll down for changes ===[[[ -The text below is included in docs/dev/Documentation.rst - see that file for more details on the changelog setup. -This is kept in this file as a quick syntax reference. +For information on how to edit/build the changelogs, see `docs/html/docs/dev/Documentation.html` or `docs/dev/Documentation.rst`. -===help +The text between the `syntax-reference` markers is included in `docs/dev/Documentation.rst`, so it must be valid RST. +It is kept in this file as a quick syntax reference. -changelog.txt uses a syntax similar to RST, with a few special sequences: +===syntax-reference-start + +The changelogs use a syntax similar to RST, with a few special sequences: - ``===`` indicates the start of a comment - ``#`` indicates the start of a release name (do not include "DFHack") -- ``##`` indicates the start of a section name (this must be listed in ``gen_changelog.py``) +- ``##`` indicates the start of a section name (which must be listed in ``docs/sphinx_extensions/dfhack/changelog.py``) - ``-`` indicates the start of a changelog entry. **Note:** an entry currently must be only one line. -- ``:`` (colon followed by space) separates the name of a feature from a description of a change to that feature. - Changes made to the same feature are grouped if they end up in the same section. -- ``:\`` (colon, backslash, space) avoids the above behavior -- ``- @`` (the space is optional) indicates the start of an entry that should only be displayed in NEWS-dev.rst. - Use this sparingly, e.g. for immediate fixes to one development build in another development build that - are not of interest to users of stable builds only. +- ``:`` (followed by space) separates the name of a feature from a description of a change to that feature. + - Changes made to the same feature are grouped if they end up in the same section. +- ``:\`` (followed by space) avoids the above behavior +- ``- @`` (the space is optional) indicates the start of an entry that should only be displayed in ``NEWS-dev.rst``. + - Use this sparingly, e.g. for immediate fixes to one development build in another development build that + are not of interest to users of stable builds only. - Three ``[`` characters indicate the start of a block (possibly a comment) that spans multiple lines. Three ``]`` characters indicate the end of such a block. -- ``!`` immediately before a phrase set up to be replaced (see gen_changelog.py) stops that occurrence from being replaced. +- ``!`` immediately before a configured replacement (see ``docs/sphinx_extensions/dfhack/changelog.py``) stops that occurrence from being replaced. + +===syntax-reference-end Template for new versions: @@ -40,7 +44,6 @@ Template for new versions: ## Removed -===end ]]] ================================================================================ diff --git a/docs/dev/Documentation.rst b/docs/dev/Documentation.rst index 13eaf42da0..19a243f31c 100644 --- a/docs/dev/Documentation.rst +++ b/docs/dev/Documentation.rst @@ -515,8 +515,8 @@ Changelog syntax ---------------- .. include:: /docs/changelog.txt - :start-after: ===help - :end-before: ===end + :start-after: ===syntax-reference-start + :end-before: ===syntax-reference-end .. _docs-ci: From a308c260a6634711cfe03012d91cc85fa1e271f7 Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 24 Aug 2025 01:24:09 -0400 Subject: [PATCH 0475/1082] Remove unnecessary blank lines from output when building docs under CMake --- CMakeLists.txt | 2 +- docs/build.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ed54be78a..eeb548f7f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -523,7 +523,7 @@ if(BUILD_DOCS) add_custom_command(OUTPUT ${SPHINX_OUTPUT} COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/docs/build.py" - ${SPHINX_BUILD_TARGETS} --sphinx="${SPHINX_EXECUTABLE}" -- -q -W + ${SPHINX_BUILD_TARGETS} --sphinx="${SPHINX_EXECUTABLE}" --quiet -- -W DEPENDS ${SPHINX_DEPS} COMMENT "Building documentation with Sphinx" ) diff --git a/docs/build.py b/docs/build.py index bfb6780b23..bf0dd9e488 100755 --- a/docs/build.py +++ b/docs/build.py @@ -62,6 +62,8 @@ def output_format(s): help='Sphinx executable to run [environment variable: SPHINX; default: "sphinx-build"]') parser.add_argument('-j', '--jobs', type=str, default=os.environ.get('JOBS', 'auto'), help='Number of Sphinx threads to run [environment variable: JOBS; default: "auto"]') + parser.add_argument('-q', '--quiet', action='store_true', + help='Disable most output on stdout (also passed to sphinx-build)') parser.add_argument('--debug', action='store_true', help='Log commands that are run, etc.') parser.add_argument('--offline', action='store_true', @@ -91,6 +93,8 @@ def output_format(s): command = [args.sphinx] + OUTPUT_FORMATS[format_name].args + ['-j', args.jobs] if args.clean: command += ['-E'] + if args.quiet: + command += ['-q'] command += forward_args if args.debug: @@ -98,4 +102,5 @@ def output_format(s): print('Running:', command) subprocess.run(command, check=True, env=sphinx_env) - print('') + if not args.quiet: + print('') From 548087db149e4b206a7719ea60725b1e71743b01 Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 24 Aug 2025 01:52:03 -0400 Subject: [PATCH 0476/1082] Rearrange/clarify use of docs/build.py --- docs/dev/Documentation.rst | 67 ++++++++++++++++++++++-------------- docs/dev/compile/Options.rst | 18 +++++++--- 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/docs/dev/Documentation.rst b/docs/dev/Documentation.rst index 19a243f31c..12637af0bf 100644 --- a/docs/dev/Documentation.rst +++ b/docs/dev/Documentation.rst @@ -418,6 +418,8 @@ Once you have pip available, you can install Sphinx with the following command:: Note that this may require opening a new (admin) command prompt if you just installed pip from the same command prompt. +.. _docs-build: + Building the documentation ========================== @@ -427,16 +429,18 @@ Sphinx to build the docs: Using CMake ----------- -See our page on `build options ` +See our page on `build options `. -Running Sphinx manually ------------------------ +Using the documentation build script +------------------------------------ You can also build the documentation without running CMake - this is faster if -you only want to rebuild the documentation regardless of any code changes. The -``docs/build.py`` script will build the documentation in any specified formats -(HTML only by default) using the same command that CMake runs when building the -docs. Run the script with ``--help`` to see additional options. +you only want to rebuild the documentation regardless of any code changes. + +The recommended approach is the ``docs/build.py`` script. This is the same +script that CMake uses internally, which wraps Sphinx with a few additional +options to handle common cases and can build multiple documentation formats with +a single invocation. Examples: @@ -449,37 +453,50 @@ Examples: * ``docs/build.py --clean`` Build HTML and force a clean build (all source files are re-read) -The resulting documentation will be stored in ``docs/html`` and/or ``docs/text``. - -Alternatively, you can run Sphinx manually with:: - - sphinx-build . docs/html - -or, to build plain-text output:: +* ``docs/build.py --help`` + Display a full list of available options - sphinx-build -b text . docs/text - -Sphinx has many options to enable clean builds, parallel builds, logging, and -more - run ``sphinx-build --help`` for details. If you specify a different -output path, be warned that Sphinx may overwrite existing files in the output -folder. Also be aware that when running ``sphinx-build`` directly, the -``docs/html`` folder may be polluted with intermediate build files that normally -get written in the cmake ``build`` directory. +The resulting documentation will be stored in ``docs/html`` and/or ``docs/text`` +(or generally, a subfolder of ``docs/`` named after the requested output format(s)). Building a PDF version ---------------------- ReadTheDocs automatically builds a PDF version of the documentation (available under the "Downloads" section when clicking on the release selector). If you -want to build a PDF version locally, you will need ``pdflatex``, which is part +want to build a PDF version locally, you will need the ``pdflatex`` command, which is part of a TeX distribution. The following command will then build a PDF, located in ``docs/pdf/latex/DFHack.pdf``, with default options:: docs/build.py pdf -Alternatively, you can run Sphinx manually with:: +Running Sphinx manually +----------------------- + +If ``docs/build.py`` does not support what you need, you can also run Sphinx +manually. This is primarily useful for low-level debugging. + +For a good starting point, add the ``--debug`` argument to your call to +``docs/build.py``. This will cause the script to print out the Sphinx command(s) +that it is running. + +Some examples: + +* ``sphinx-build . docs/html`` + Build the HTML docs (equivalent to ``docs/build.py``) + +* ``sphinx-build -b text . docs/text`` + Build the plain text docs (equivalent to ``docs/build.py text``) - sphinx-build -M latexpdf . docs/pdf +* ``sphinx-build -M latexpdf . docs/pdf`` + Build the PDF docs + +Sphinx has many options to enable clean builds, parallel builds, logging, and +more - run ``sphinx-build --help`` for details. If you specify a different +output path, be warned that Sphinx may overwrite existing files in the output +folder. Also be aware that when running ``sphinx-build`` directly, the +``docs/html`` folder may be polluted with intermediate build files that normally +get written in the cmake ``build`` directory. .. _build-changelog: diff --git a/docs/dev/compile/Options.rst b/docs/dev/compile/Options.rst index b314e9db27..462fd1f1ca 100644 --- a/docs/dev/compile/Options.rst +++ b/docs/dev/compile/Options.rst @@ -136,16 +136,26 @@ Usage:: Documentation ============= -If you need to build documentation. Documentation can be built as HTML, and PDF, -but there are also plain text files generated for in-game. +If you need to build `documentation `. -Variable: ``BUILD_DOCS`` +.. note:: + + These options are primarily useful for verifying that the end-to-end process + for building and packaging the documentation is working as expected. For + iterating on documentation changes, `faster alternatives ` are + available. + +Variables: + +* ``BUILD_DOCS``: enables the default documentation build +* ``BUILD_DOCS_NO_HTML``: disables the HTML documentation build (only builds the text documentation used in-game) Usage:: cmake .. -DBUILD_DOCS:bool=ON cmake .. -DBUILD_DOCS=1 - + cmake .. -DBUILD_DOCS_NO_HTML:bool=ON + cmake .. -DBUILD_DOCS_NO_HTML=1 The generated documentation is stored in ``docs/html`` and ``docs/text`` (respectively) in the root DFHack folder, and they will both be installed to ``hack/docs`` when you From 26fcf3694595bae7cf13304a42243aa75979fe4c Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 24 Aug 2025 02:11:41 -0400 Subject: [PATCH 0477/1082] Add some basic troubleshooting tips for the docs build --- docs/dev/Documentation.rst | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/dev/Documentation.rst b/docs/dev/Documentation.rst index 12637af0bf..0ba61c404d 100644 --- a/docs/dev/Documentation.rst +++ b/docs/dev/Documentation.rst @@ -67,6 +67,8 @@ with relevant tags. These are used to compile indices and generate cross-links b commands, both in the HTML documents and in-game. See the list of available `tag-list` and think about which categories your new tool belongs in. +.. _docs-links: + Links ----- @@ -498,6 +500,48 @@ folder. Also be aware that when running ``sphinx-build`` directly, the ``docs/html`` folder may be polluted with intermediate build files that normally get written in the cmake ``build`` directory. +Troubleshooting +=============== + +Sphinx errors are typically printed by Sphinx, so ensure that you are not silencing Sphinx output. + +When built with ``docs/build.py`` or CMake, errors are also logged to +``build/docs//sphinx-warnings.txt`` (for instance, if you are building +the HTML docs, ``build/docs/html/sphinx-warnings.txt``). + + +"undefined label" +----------------- + +Typical causes: + +* You have used single backticks for an inline code snippet, where double backticks should be used instead (see `docs-links`):: + + `this is an invalid inline code snippet (actually a link)` + ``this is a valid inline code snippet`` + +* You are attempting to link to a section/label, but either it does not have a label defined or you have spelled it incorrectly:: + + .. my-label: + + This is where the link should go to. + + ... + + This is `a valid link to the earlier label `. So is `my-label`. + +"toctree contains reference to document that doesn't have a title" +------------------------------------------------------------------ + +Due to the nature of our autogenerated documentation, this can sometimes occur +when switching between branches that have different autogenerated files, and can +result in autogenerated documentation (e.g. for individual tools) being missing +from the table of contents, or links failing to generate. + +The quickest resolution is a clean docs build:: + + docs/build.py --clean + .. _build-changelog: Building the changelogs From 67d4d915d6f5c18116eb4b8870db6aea7b97af49 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 28 Aug 2025 20:05:49 +0000 Subject: [PATCH 0478/1082] 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 e4993ceef7..bd52dfb23f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e4993ceef7907351fd2ac6f9bd3a9e5ff24699db +Subproject commit bd52dfb23f28e40350f88fe3c81e63f5edc38f89 From e83fce914640ab93d2dbaf18eedf7926950dc8f2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 28 Aug 2025 15:05:52 -0500 Subject: [PATCH 0479/1082] bump version to 52.04 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ed54be78a..cab5c6a8ce 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 "52.03") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "52.04") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From a7d8acafa8d9afb5ff91a34e45d36a688b1d03b3 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 28 Aug 2025 20:15:41 +0000 Subject: [PATCH 0480/1082] Auto-update structures ref for 52.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index bd52dfb23f..7b691d256f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit bd52dfb23f28e40350f88fe3c81e63f5edc38f89 +Subproject commit 7b691d256f9427036e7ff24fa795a0f9334739e7 From f37f004fa673d5e1219fdb560a12acb84902c5c5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 28 Aug 2025 15:56:59 -0500 Subject: [PATCH 0481/1082] changelog for 52.04-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7e27d7f4c9..a2e558aeb2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,24 @@ Template for new versions: ## Removed +# 52.04-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 52.03-r2 ## New Tools From 5d5288aa2b1bcb6cf72d56b2baed91d16a5ebcf5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 28 Aug 2025 16:09:17 -0500 Subject: [PATCH 0482/1082] make changelog nonempty --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index a2e558aeb2..f6421c7d89 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -72,6 +72,7 @@ Template for new versions: ## New Tools ## New Features +- Compatibility with DF 52.04 ## Fixes From cba221330e1e6e580b8c4cb47af95ed394c9e40f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:38:28 +0000 Subject: [PATCH 0483/1082] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/python-jsonschema/check-jsonschema: 0.33.2 → 0.33.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.2...0.33.3) --- .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 d79077b318..28599a14ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: # shared across repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.33.3 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 34f5bf8217b1eeb447d7788654242e634731ceca Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Mon, 1 Sep 2025 20:00:31 -0700 Subject: [PATCH 0484/1082] Remove unused declaration in Burrows.cpp --- library/modules/Burrows.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/modules/Burrows.cpp b/library/modules/Burrows.cpp index 3c641e79b7..6c8802469c 100644 --- a/library/modules/Burrows.cpp +++ b/library/modules/Burrows.cpp @@ -99,8 +99,6 @@ bool Burrows::isAssignedUnit(df::burrow *burrow, df::unit *unit) void Burrows::setAssignedUnit(df::burrow *burrow, df::unit *unit, bool enable) { - using df::global::plotinfo; - CHECK_NULL_POINTER(unit); CHECK_NULL_POINTER(burrow); From d5ffeb3e9181450fff3775f3c84c5b82e70348b3 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Tue, 2 Sep 2025 11:57:24 -0700 Subject: [PATCH 0485/1082] Comment out unused declarations in remotefortressreader.cpp --- plugins/remotefortressreader/remotefortressreader.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index 697eb3fe92..a7bfccd7ce 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -1679,8 +1679,8 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques continue; } - using df::global::cur_year; - using df::global::cur_year_tick; + //using df::global::cur_year; + //using df::global::cur_year_tick; send_unit->set_age(Units::getAge(unit, false)); From 2ef698c24de7f5ebec81da4d10a2821d81229575 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 7 Sep 2025 12:02:32 -0700 Subject: [PATCH 0486/1082] zone.lua pits, ponds, and cages: no trader-owned small pets. --- docs/changelog.txt | 1 + plugins/lua/zone.lua | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f6421c7d89..d07fb869bc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- small animals owned by traders can no longer be assigned to pits, ponds, and cages. ## Misc Improvements diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index 92940ef48e..7dce4aee14 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -602,7 +602,9 @@ local function is_assignable_unit(unit) end local function is_assignable_item(item) - -- all vermin/small pets are assignable + -- small pets owned by traders are not assignable + if item.flags.trader then return false end + -- other vermin/small pets are assignable return true end From be8594a6bfd4a900ec3d4b04cd1d98db1b1fae38 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 7 Sep 2025 16:53:47 -0500 Subject: [PATCH 0487/1082] Revert "Prevent assignment of trader-owned small pets (items, not units) to pits, ponds, and cages." --- docs/changelog.txt | 1 - plugins/lua/zone.lua | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index d07fb869bc..f6421c7d89 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,7 +56,6 @@ Template for new versions: ## New Features ## Fixes -- small animals owned by traders can no longer be assigned to pits, ponds, and cages. ## Misc Improvements diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index 7dce4aee14..92940ef48e 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -602,9 +602,7 @@ local function is_assignable_unit(unit) end local function is_assignable_item(item) - -- small pets owned by traders are not assignable - if item.flags.trader then return false end - -- other vermin/small pets are assignable + -- all vermin/small pets are assignable return true end From 9a0455e1280bce6f4b58081a38aaa59c39da94bd Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 10 Sep 2025 11:58:57 -0700 Subject: [PATCH 0488/1082] plugins/lua/zone.lua fix: overlay doesn't run when invalid pasture While the overlay is processing all animals to see where they're assigned, if it encounters an animal assigned to a nonexistent civzone or to a building that isn't a civzone, it errors out. This patch makes the overlay more robust in these cases. Note that the overlay will not display the relevant animal, nor will it attempt to repair the invalid assignment. --- plugins/lua/zone.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index 92940ef48e..2923f8e333 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -989,7 +989,9 @@ local function get_zone_status(unit_or_vermin, bld_assignments) return PASTURE_STATUS.ASSIGNED_HERE.value else local civzone = df.building.find(assigned_zone_ref.building_id) - if civzone.type == df.civzone_type.Pen then + if not civzone or not df.building_civzonest:is_instance(civzone) then + return PASTURE_STATUS.NONE.value + elseif civzone.type == df.civzone_type.Pen then return PASTURE_STATUS.PASTURED.value elseif civzone.type == df.civzone_type.Pond then return PASTURE_STATUS.PITTED.value @@ -1150,7 +1152,9 @@ local function get_cage_status(unit_or_vermin, bld_assignments) local assigned_zone_ref = get_general_ref(unit_or_vermin, df.general_ref_type.BUILDING_CIVZONE_ASSIGNED) if assigned_zone_ref then local civzone = df.building.find(assigned_zone_ref.building_id) - if civzone.type == df.civzone_type.Pen then + if not civzone or not df.building_civzonest:is_instance(civzone) then + return CAGE_STATUS.NONE.value + elseif civzone.type == df.civzone_type.Pen then return CAGE_STATUS.PASTURED.value elseif civzone.type == df.civzone_type.Pond then return CAGE_STATUS.PITTED.value From 6fce14efc4469b9451a8b09442a6551b07655d69 Mon Sep 17 00:00:00 2001 From: SilasD Date: Thu, 11 Sep 2025 15:11:10 -0700 Subject: [PATCH 0489/1082] Update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f6421c7d89..fd85c18af5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. ## Misc Improvements From 516ade1ef354fc15b3c5a558c2cc830b2871eeef Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 15 Sep 2025 07:19:48 +0000 Subject: [PATCH 0490/1082] 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 7b691d256f..4358ea5e4d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7b691d256f9427036e7ff24fa795a0f9334739e7 +Subproject commit 4358ea5e4d92a8b13b17adb58da773ab66db8cf6 diff --git a/scripts b/scripts index 539120522a..a91c23c51a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 539120522a808220e4ab23538bad5cfcd979211e +Subproject commit a91c23c51a5d7be64b89d35e915677f2319026d6 From 8df7c427551c850e882481b5b7ecaa5555db1b34 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 23 Sep 2025 07:19:36 +0000 Subject: [PATCH 0491/1082] 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 08102c59ef..525024cf72 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 08102c59ef1c76b1cebe3e582be51f0ac763de0e +Subproject commit 525024cf72244cd6c384b723a0b7a34a50b336ae From fdb1db08ccf068c0e19fe8a40eb146e220ac055d Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Thu, 25 Sep 2025 06:57:38 -0400 Subject: [PATCH 0492/1082] use noexcept std::filesystem overloads --- library/Core.cpp | 9 ++++++++- library/modules/Filesystem.cpp | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index a2b48c992d..bbe6195351 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -535,7 +535,14 @@ std::filesystem::path Core::findScript(std::string name) getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { - std::filesystem::path path = std::filesystem::weakly_canonical(*it / name); + std::error_code ec; + std::filesystem::path path = std::filesystem::weakly_canonical(*it / name, ec); + if (ec) + { + con.printerr("Error loading ''%s' (%s)\n", (*it / name).string().c_str(), ec.message().c_str()); + continue; + } + if (Filesystem::isfile(path)) return path; } diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 7a6b09a500..8f0f86cb8b 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -154,17 +154,20 @@ bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status bool Filesystem::exists (std::filesystem::path path) noexcept { - return std::filesystem::exists(path); + std::error_code ec; + return std::filesystem::exists(path, ec); } bool Filesystem::isfile(std::filesystem::path path) noexcept { - return std::filesystem::exists(path) && std::filesystem::is_regular_file(path); + std::error_code ec; + return std::filesystem::exists(path, ec) && std::filesystem::is_regular_file(path, ec); } bool Filesystem::isdir (std::filesystem::path path) noexcept { - return std::filesystem::exists(path) && std::filesystem::is_directory(path); + std::error_code ec; + return std::filesystem::exists(path, ec) && std::filesystem::is_directory(path, ec); } std::time_t Filesystem::mtime (std::filesystem::path path) noexcept From 838c3786ee7748c7afba8b2dac24961893fd4b7d Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:49:18 -0400 Subject: [PATCH 0493/1082] fix typo --- library/Core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index bbe6195351..a6fc71fb2b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -539,7 +539,7 @@ std::filesystem::path Core::findScript(std::string name) std::filesystem::path path = std::filesystem::weakly_canonical(*it / name, ec); if (ec) { - con.printerr("Error loading ''%s' (%s)\n", (*it / name).string().c_str(), ec.message().c_str()); + con.printerr("Error loading '%s' (%s)\n", (*it / name).string().c_str(), ec.message().c_str()); continue; } From 06fd987d8e00f5b2081668352ad721bb4f28161f Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:27:50 -0400 Subject: [PATCH 0494/1082] implement suggested changes --- library/modules/Filesystem.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 8f0f86cb8b..6d49c6a3b7 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -155,19 +155,30 @@ bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status bool Filesystem::exists (std::filesystem::path path) noexcept { std::error_code ec; - return std::filesystem::exists(path, ec); + auto r = std::filesystem::exists(path, ec); + if (ec) + return false; + return r; } bool Filesystem::isfile(std::filesystem::path path) noexcept { std::error_code ec; - return std::filesystem::exists(path, ec) && std::filesystem::is_regular_file(path, ec); + // is_regular_file() also checks for existence. + auto r = std::filesystem::is_regular_file(path, ec); + if (ec) + return false; + return r; } bool Filesystem::isdir (std::filesystem::path path) noexcept { std::error_code ec; - return std::filesystem::exists(path, ec) && std::filesystem::is_directory(path, ec); + // is_directory() also checks for existence. + auto r = std::filesystem::is_directory(path, ec); + if (ec) + return false; + return r; } std::time_t Filesystem::mtime (std::filesystem::path path) noexcept From 2f54afa068c751f98496aac2a36aa323ea10462b Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Thu, 25 Sep 2025 21:43:11 -0400 Subject: [PATCH 0495/1082] add changelog entry --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index fd85c18af5..8ff5b73e6c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## Fixes - `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. +- improved file system handling: gracefully handle errors from operations, preventing crashes. ## Misc Improvements From ba349ba66dcb3a3fe3589a63ecac190b5b0b3620 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 26 Sep 2025 07:19:11 +0000 Subject: [PATCH 0496/1082] 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 4358ea5e4d..d8ebde11e7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 4358ea5e4d92a8b13b17adb58da773ab66db8cf6 +Subproject commit d8ebde11e7d66f4f033e801d3737e49f94d5cf80 diff --git a/scripts b/scripts index a91c23c51a..046b99aa87 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit a91c23c51a5d7be64b89d35e915677f2319026d6 +Subproject commit 046b99aa87ed69561050ace3f6273a63f2a23ee1 From 77455eeadb47782186c85435b8fc8cbfa9d76eef Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:27:21 +0000 Subject: [PATCH 0497/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 046b99aa87..14e4a34e15 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 046b99aa87ed69561050ace3f6273a63f2a23ee1 +Subproject commit 14e4a34e158071a2796669e63d8eb02bb2b9f408 From 6cdf702e8119fc89b48acaf3784cdfac1e834d4a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 26 Sep 2025 11:28:15 -0500 Subject: [PATCH 0498/1082] prep for prerelease --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 463fa229da..d50c8ee722 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.04") -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) diff --git a/docs/changelog.txt b/docs/changelog.txt index fb59ad3912..b1a11493ea 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,8 +59,8 @@ Template for new versions: ## New Features ## Fixes -- `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. - improved file system handling: gracefully handle errors from operations, preventing crashes. +- `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. ## Misc Improvements From 3e2c2a5445eb76418ffddc3f7aa17d2c6124e4f7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:28:12 +0000 Subject: [PATCH 0499/1082] 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 d8ebde11e7..57443cdc32 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d8ebde11e7d66f4f033e801d3737e49f94d5cf80 +Subproject commit 57443cdc32a48a8a2e11b91be5200346b9d7c7e8 From 8825c9824db89efc0c89ba12be83f4c8498314e9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 30 Sep 2025 14:07:44 -0500 Subject: [PATCH 0500/1082] Revert "Merge pull request #4781 from Crystalwarrior/tombstone-linger" This reverts commit 345abc1404ca6849434221dee26c8493d989ac1f, reversing changes made to ba349ba66dcb3a3fe3589a63ecac190b5b0b3620. --- docs/about/Removed.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 24e0562642..5f73b86be3 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -10,12 +10,6 @@ work (e.g. links from the `changelog`). :local: :depth: 1 -.. _linger: - -linger -====== -Merged into `bodyswap` as ``bodyswap linger``. - .. _adv-rumors: adv-rumors From c406ff971b20cff92a664dae5a4d32f70f719792 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 1 Oct 2025 10:42:10 -0500 Subject: [PATCH 0501/1082] Update watch-df-release.yml remove beta and experimental branches as they currently do not exist --- .github/workflows/watch-df-release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index 77f4947781..50950140d1 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -21,11 +21,11 @@ jobs: # steam_branch: leave blank if no DFHack steam push is desired include: - df_steam_branch: public - - df_steam_branch: beta - - df_steam_branch: experimental - structures_ref: experimental - dfhack_ref: experimental - steam_branch: experimental +# - df_steam_branch: beta +# - df_steam_branch: experimental +# structures_ref: experimental +# dfhack_ref: experimental +# steam_branch: experimental steps: - name: Fetch state uses: actions/cache/restore@v4 From e16df6f94112c31b8e5958354c1a9dbd1fc1372e Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:00:55 +0000 Subject: [PATCH 0502/1082] Auto-update structures ref for 52.05 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 57443cdc32..df0ec27701 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 57443cdc32a48a8a2e11b91be5200346b9d7c7e8 +Subproject commit df0ec27701a2aeaa1ccfb81323f4be55b78e1c10 From 7c268fd33500ef0685b21b12d1f623dedd7c04ba Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 1 Oct 2025 11:02:53 -0500 Subject: [PATCH 0503/1082] Update CMakeLists.txt for 52.05-r1 --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d50c8ee722..3446e24784 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "52.04") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DF_VERSION "52.05") +set(DFHACK_RELEASE "r1") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From d4122bed934eccf7e49961f2872e0772c7bf71c4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:23:31 +0000 Subject: [PATCH 0504/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 14e4a34e15..9b325f16fd 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 14e4a34e158071a2796669e63d8eb02bb2b9f408 +Subproject commit 9b325f16fd64838a032ccf168303cfc08cc59d24 From b6872194f9f1ca4643064d8defd9137f8d981959 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 1 Oct 2025 11:43:43 -0500 Subject: [PATCH 0505/1082] Update changelog and scripts for 52.05-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ scripts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b1a11493ea..f09a12f7e4 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 + +# 52.05-r1 + +## New Tools + +## New Features + ## Fixes - improved file system handling: gracefully handle errors from operations, preventing crashes. - `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. diff --git a/scripts b/scripts index 9b325f16fd..c6e4e62404 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9b325f16fd64838a032ccf168303cfc08cc59d24 +Subproject commit c6e4e62404fbff15783d1e94393177119fd2beb7 From e003f20fe19b74f3fdf695f4fac2f879846d39d6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 2 Oct 2025 16:23:52 -0500 Subject: [PATCH 0506/1082] add link to DF wiki for DF Lua API --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f09a12f7e4..897dc98d1f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: ## Misc Improvements ## Documentation +- added a clarification link to DF's Lua API documentation to the DFHack Lua API documentation, as a way to reduce end-user confusion ## API diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 86e90da9c1..a4a039f409 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -25,6 +25,13 @@ It does not describe all of the utility functions implemented by Lua files located in :file:`hack/lua/*` (:file:`library/lua/*` in the git repo). +.. admonition:: Is this the DF or DFHack Lua API? + :class: warning + + This document describes the Lua API provided by DFHack, not + the Lua API provided by Dwarf Fortress. For information about DF's Lua API, see + :wiki:`Lua scripting` + on the Dwarf Fortress Wiki. .. contents:: Contents :local: From a82d35fd90e42aa9bbd1bacf23f10f6287918348 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Oct 2025 22:16:55 -0500 Subject: [PATCH 0507/1082] add missing `const` qualifiers this is basically catching some cases missed in #4702; in addition, these changes are necessary to allow for the type identities defined by codegen to be `const` --- library/LuaTools.cpp | 6 +++--- library/LuaWrapper.cpp | 2 +- library/include/DataDefs.h | 4 ++-- library/include/LuaTools.h | 4 ++-- library/include/LuaWrapper.h | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 95f8017dd8..81bcdd50ca 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1077,7 +1077,7 @@ bool DFHack::Lua::Require(color_ostream &out, lua_State *state, } static bool doAssignDFObject(color_ostream *out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact, bool perr, bool signal) { if (signal) @@ -1124,13 +1124,13 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, } bool DFHack::Lua::AssignDFObject(color_ostream &out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact_type, bool perr) { return doAssignDFObject(&out, state, type, target, val_index, exact_type, perr, false); } -void DFHack::Lua::CheckDFAssign(lua_State *state, type_identity *type, +void DFHack::Lua::CheckDFAssign(lua_State *state, const type_identity *type, void *target, int val_index, bool exact_type) { doAssignDFObject(NULL, state, type, target, val_index, exact_type, false, true); diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index a0069fbbdc..ef37bd61ee 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -132,7 +132,7 @@ bool LuaWrapper::LookupTypeInfo(lua_State *state, bool in_method) return true; } -void LuaWrapper::LookupInTable(lua_State *state, void *id, LuaToken *tname) +void LuaWrapper::LookupInTable(lua_State *state, const void *id, LuaToken *tname) { lua_rawgetp(state, LUA_REGISTRYINDEX, tname); lua_rawgetp(state, -1, id); diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index e8e54fac41..76e74c9791 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -257,8 +257,8 @@ namespace DFHack }; struct struct_field_info_extra { - enum_identity *index_enum; - type_identity *ref_target; + const enum_identity *index_enum; + const type_identity *ref_target; const char *union_tag_field; const char *union_tag_attr; const char *original_name; diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index db40e76810..9831841ca2 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -140,14 +140,14 @@ namespace DFHack::Lua { * Return behavior is of SafeCall below. */ DFHACK_EXPORT bool AssignDFObject(color_ostream &out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact_type = false, bool perr = true); /** * Assign the value at val_index to the target of given identity using df.assign(). * Otherwise throws an error. */ - DFHACK_EXPORT void CheckDFAssign(lua_State *state, type_identity *type, + DFHACK_EXPORT void CheckDFAssign(lua_State *state, const type_identity *type, void *target, int val_index, bool exact_type = false); template concept df_object = requires() diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 67301b52cc..1b8be705ed 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -167,7 +167,7 @@ namespace LuaWrapper { const char *ctx, bool allow_type = false, bool keep_metatable = false); - void LookupInTable(lua_State *state, void *id, LuaToken *tname); + void LookupInTable(lua_State *state, const void *id, LuaToken *tname); void SaveInTable(lua_State *state, void *node, LuaToken *tname); void SaveTypeInfo(lua_State *state, void *node); From c31f28f6f2f0da3887fd682f709059cbe4122fe8 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 5 Oct 2025 15:48:52 -0700 Subject: [PATCH 0508/1082] script-manager.lua mitigate #5591 not searching scripts_modactive --- library/lua/script-manager.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 74a31fd1d9..80774c53e8 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -152,6 +152,7 @@ function get_mod_paths(installed_subdir, active_subdir) -- if a world is loaded, process active mods first, and lock to active version if dfhack.isWorldLoaded() then for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do + path = dfhack.filesystem.getBaseDir() .. path -- skip vanilla "mods" if not path:startswith(INSTALLED_MODS_PATH) then goto continue end local id = get_mod_id_and_version(path) From ca827d332f00bf63d70aa086b7bb2f2eb84b8a62 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 5 Oct 2025 16:13:32 -0700 Subject: [PATCH 0509/1082] update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f09a12f7e4..c3d50b9e7f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- modding: the ``scripts_modactive`` and ``scripts_modinstalled`` subdirectories of mods will be searched for scripts as per the modding-guide documentation ## Misc Improvements From 96936a7466597d3c407809c69013171d708f1167 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 5 Oct 2025 18:26:18 -0700 Subject: [PATCH 0510/1082] update changelog.txt again --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index c3d50b9e7f..28b6b42fad 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,7 +59,7 @@ Template for new versions: ## New Features ## Fixes -- modding: the ``scripts_modactive`` and ``scripts_modinstalled`` subdirectories of mods will be searched for scripts as per the modding-guide documentation +- `script-manager`: the ``scripts_modactive`` and ``scripts_modinstalled`` folders of a script-enabled mod will be properly added to the script path search list ## Misc Improvements From 6b1ede0b0335f85e5ab14bfd5b023cf3495df8c2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 6 Oct 2025 07:20:06 +0000 Subject: [PATCH 0511/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index c6e4e62404..6cd5936717 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit c6e4e62404fbff15783d1e94393177119fd2beb7 +Subproject commit 6cd5936717c24455b42944d31b27b0bbe9a6533d From 43ff7893aa6307a693c98ec9742095ccc13dd269 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:32:25 +0000 Subject: [PATCH 0512/1082] [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.33.3 → 0.34.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.3...0.34.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 28599a14ae..1bca6a9289 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.33.3 + rev: 0.34.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 48ae6a4b98183ad9ed40f00b5e61fdc10f1592b1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 9 Oct 2025 05:37:50 +0000 Subject: [PATCH 0513/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6cd5936717..2cc976a029 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6cd5936717c24455b42944d31b27b0bbe9a6533d +Subproject commit 2cc976a0295071ba2668a89738d55db24f7820f8 From cd0a4146af8658ced20edc5f8c87b0cd0947f492 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 6 Oct 2025 11:22:16 -0500 Subject: [PATCH 0514/1082] Update CMakeLists.txt for 52.05-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3446e24784..b145db0bf8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.05") -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 e3bf5f73c334f2b039bf9fa9769f60e9147ff50e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Oct 2025 10:08:43 -0500 Subject: [PATCH 0515/1082] Changelogs for 52.05-r2 --- docs/changelog.txt | 18 ++++++++++++++++++ scripts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e2253c7f43..14a9ba0ca3 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 + +# 52.05-r2 + +## New Tools + +## New Features + ## Fixes - `script-manager`: the ``scripts_modactive`` and ``scripts_modinstalled`` folders of a script-enabled mod will be properly added to the script path search list diff --git a/scripts b/scripts index 2cc976a029..ae0ca6eefb 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 2cc976a0295071ba2668a89738d55db24f7820f8 +Subproject commit ae0ca6eefb1297ca909eebb58b7706e95e223d98 From ec428d988ac4bbb1a81083faada4576ae1c8a5db Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Oct 2025 11:25:11 -0500 Subject: [PATCH 0516/1082] CMakeLists.txt for 52.05-r2 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b145db0bf8..8e63238f89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.05") -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) From 7b9347d0b78298bd705f9c47cca14d888da37e85 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 10 Oct 2025 20:13:59 -0500 Subject: [PATCH 0517/1082] add more missing `const` qualifiers --- library/DataDefs.cpp | 6 +++--- library/include/DataDefs.h | 14 +++++++------- library/include/MiscUtils.h | 2 +- library/include/modules/Gui.h | 2 +- library/modules/Gui.cpp | 5 +++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 5d2386282e..50be583459 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -232,8 +232,8 @@ const std::string df::buffer_container_identity::getFullName(const type_identity } union_identity::union_identity(size_t size, const TAllocateFn alloc, - compound_identity *scope_parent, const char *dfhack_name, - struct_identity *parent, const struct_field_info *fields) + const compound_identity *scope_parent, const char *dfhack_name, + const struct_identity *parent, const struct_field_info *fields) : struct_identity(size, alloc, scope_parent, dfhack_name, parent, fields) { } @@ -350,7 +350,7 @@ virtual_identity *virtual_identity::find(void *vtable) return NULL; } -void virtual_identity::adjust_vtable(virtual_ptr obj, virtual_identity *main) +void virtual_identity::adjust_vtable(virtual_ptr obj, const virtual_identity *main) const { if (vtable_ptr) { *(void**)obj = vtable_ptr; diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 76e74c9791..6a5695809d 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -326,8 +326,8 @@ namespace DFHack class DFHACK_EXPORT union_identity : public struct_identity { public: union_identity(size_t size, TAllocateFn alloc, - compound_identity *scope_parent, const char *dfhack_name, - struct_identity *parent, const struct_field_info *fields); + const compound_identity *scope_parent, const char *dfhack_name, + const struct_identity *parent, const struct_field_info *fields); virtual identity_type type() const { return IDTYPE_UNION; } @@ -418,13 +418,13 @@ namespace DFHack template static P get_vmethod_ptr(P selector); public: - bool can_instantiate() { return can_allocate(); } - virtual_ptr instantiate() { return can_instantiate() ? (virtual_ptr)do_allocate() : NULL; } + bool can_instantiate() const { return can_allocate(); } + virtual_ptr instantiate() const { return can_instantiate() ? (virtual_ptr)do_allocate() : NULL; } static virtual_ptr clone(virtual_ptr obj); public: // Strictly for use in virtual class constructors - void adjust_vtable(virtual_ptr obj, virtual_identity *main); + void adjust_vtable(virtual_ptr obj, const virtual_identity *main) const; }; template @@ -536,10 +536,10 @@ namespace df struct identity_traits {}; template - requires requires () { { &T::_identity } -> std::convertible_to; } + requires requires () { { &T::_identity } -> std::convertible_to; } struct identity_traits { static const bool is_primitive = false; - static compound_identity *get() { return &T::_identity; } + static const compound_identity *get() { return &T::_identity; } }; template diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 2b9426202d..2ff04282e4 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -400,7 +400,7 @@ typename T::mapped_type findPrefixInMap( #endif template -inline bool static_add_to_map(CT *pmap, typename CT::key_type key, typename CT::mapped_type value) { +inline bool static_add_to_map(CT *pmap, const typename CT::key_type key, const typename CT::mapped_type value) { (*pmap)[key] = value; return true; } diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 222779a8ee..63939fc247 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -211,7 +211,7 @@ namespace DFHack /// Get the current top-level view-screen DFHACK_EXPORT df::viewscreen *getCurViewscreen(bool skip_dismissed = false); - DFHACK_EXPORT df::viewscreen *getViewscreenByIdentity(virtual_identity &id, int n = 1); + DFHACK_EXPORT df::viewscreen *getViewscreenByIdentity(const virtual_identity &id, int n = 1); /// Get the top-most underlying DF viewscreen (not owned by DFHack) DFHACK_EXPORT df::viewscreen *getDFViewscreen(bool skip_dismissed = false, df::viewscreen *top = NULL); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index dedcc6cdbc..b5df3ca990 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -152,9 +152,10 @@ static std::string getNameChunk(virtual_identity *id, int start, int end) */ typedef void (*getFocusStringsHandler)(std::string &str, std::vector &strList, df::viewscreen *screen); -static std::map getFocusStringsHandlers; +static std::map getFocusStringsHandlers; #define VIEWSCREEN(name) df::viewscreen_##name##st + #define DEFINE_GET_FOCUS_STRING_HANDLER(screen_type) \ static void getFocusStrings_##screen_type(const std::string &baseFocus, std::vector &focusStrings, VIEWSCREEN(screen_type) *screen);\ DFHACK_STATIC_ADD_TO_MAP(\ @@ -2718,7 +2719,7 @@ df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed) return ws; } -df::viewscreen *Gui::getViewscreenByIdentity (virtual_identity &id, int n) +df::viewscreen *Gui::getViewscreenByIdentity (const virtual_identity &id, int n) { bool limit = (n > 0); df::viewscreen *screen = Gui::getCurViewscreen(); From 73c103bf20958eff9d56f90ad1a451a3249903e7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 13 Oct 2025 07:20:41 +0000 Subject: [PATCH 0518/1082] 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 df0ec27701..27c83f81f1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit df0ec27701a2aeaa1ccfb81323f4be55b78e1c10 +Subproject commit 27c83f81f16f904c626cf80f581d658e85206a0b diff --git a/scripts b/scripts index ae0ca6eefb..80d61d16d4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ae0ca6eefb1297ca909eebb58b7706e95e223d98 +Subproject commit 80d61d16d467a4d2bb1c301b22cd067bf4b4992e From 0a2a35343fbfc77a4d2f1ab98754ff61c2c2eb75 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 16 Oct 2025 07:20:33 +0000 Subject: [PATCH 0519/1082] 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 27c83f81f1..9e3924fa79 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 27c83f81f16f904c626cf80f581d658e85206a0b +Subproject commit 9e3924fa7967078cebdf5890908df909569f7f5f From 60b5ce3824c14627e71487f50998f333ded0b813 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Oct 2025 06:06:45 -0500 Subject: [PATCH 0520/1082] more `const`ifying! this gets rid of the `mutable`s in `compound_identity`, `struct_identity`, and `virtual_identity` and makes, I think, finally, all `type_identity` objects `const` everywhere --- library/DataDefs.cpp | 183 +++++++++++++++++++++++------------- library/LuaWrapper.cpp | 2 +- library/VTableInterpose.cpp | 39 ++++---- library/include/DataDefs.h | 90 ++++++++++++------ library/modules/Gui.cpp | 4 +- 5 files changed, 200 insertions(+), 118 deletions(-) diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 50be583459..5c187290f7 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -27,6 +27,7 @@ distribution. #include #include #include +#include #include "MemAccess.h" #include "Core.h" @@ -86,27 +87,43 @@ void *enum_identity::do_allocate() const { return p; } -/* The order of global object constructor calls is - * undefined between compilation units. Therefore, - * this list has to be plain data, so that it gets - * initialized by the loader in the initial mmap. - */ -compound_identity *compound_identity::list = NULL; -std::vector compound_identity::top_scope; +/****** COMPOUND IDENTITIES ******/ -compound_identity::compound_identity(size_t size, TAllocateFn alloc, - const compound_identity *scope_parent, const char *dfhack_name) - : constructed_identity(size, alloc), dfhack_name(dfhack_name), scope_parent(const_cast(scope_parent)) // fixme +decltype(compound_identity::list) compound_identity::list = nullptr; +decltype(compound_identity::parent_map) compound_identity::parent_map = nullptr; +decltype(compound_identity::children_map) compound_identity::children_map = nullptr; +decltype(compound_identity::top_scope) compound_identity::top_scope = nullptr; + +void compound_identity::ensure_compound_identity_init() { - next = list; list = this; + static std::once_flag compound_identity_init_flag; + std::call_once(compound_identity_init_flag, [] () { + list = new (std::remove_pointer::type)(); + parent_map = new (std::remove_pointer::type)(); + children_map = new (std::remove_pointer::type)(); + top_scope = new std::vector(); + }); } -void compound_identity::doInit(Core *) +compound_identity::compound_identity(size_t size, TAllocateFn alloc, + const compound_identity* scope_parent, const char* dfhack_name) + : constructed_identity(size, alloc), dfhack_name(dfhack_name), scope_parent(scope_parent) { + ensure_compound_identity_init(); + if (scope_parent) - scope_parent->scope_children.push_back(this); + { + (*parent_map)[this] = scope_parent; + (*children_map)[scope_parent].push_back(this); + } else - top_scope.push_back(this); + (*top_scope).push_back(this); + + list->push_back(this); +} + +void compound_identity::doInit(Core *) const +{ } const std::string compound_identity::getFullName() const @@ -124,10 +141,10 @@ void compound_identity::Init(Core *core) if (!known_mutex) known_mutex = new std::mutex(); - // This cannot be done in the constructors, because - // they are called in an undefined order. - for (compound_identity *p = list; p; p = p->next) - p->doInit(core); + // do any late initialization required for compound identities + + for (auto ci : *list) + ci->doInit(core); } bitfield_identity::bitfield_identity(size_t size, @@ -176,27 +193,44 @@ enum_identity::ComplexData::ComplexData(std::initializer_list values) } } +/****** STRUCT IDENTITIES ******/ + +decltype(struct_identity::parent_map) struct_identity::parent_map = nullptr; +decltype(struct_identity::children_map) struct_identity::children_map = nullptr; + +void struct_identity::ensure_struct_identity_init() +{ + static std::once_flag struct_identity_init_flag; + std::call_once(struct_identity_init_flag, []() { + parent_map = new (std::remove_pointer::type)(); + children_map = new (std::remove_pointer::type)(); + }); +} + + + struct_identity::struct_identity(size_t size, TAllocateFn alloc, const compound_identity *scope_parent, const char *dfhack_name, const struct_identity *parent, const struct_field_info *fields) : compound_identity(size, alloc, scope_parent, dfhack_name), - parent(const_cast(parent)), has_children(false), fields(fields) + fields(fields) { + ensure_struct_identity_init(); + if (parent) + { + (*parent_map)[this] = parent; + (*children_map)[parent].push_back(this); + } } -void struct_identity::doInit(Core *core) +void struct_identity::doInit(Core *core) const { compound_identity::doInit(core); - - if (parent) { - parent->children.push_back(this); - parent->has_children = true; - } } bool struct_identity::is_subclass(const struct_identity *actual) const { - if (!has_children && actual != this) + if (!hasChildren() && actual != this) return false; for (; actual; actual = actual->getParent()) @@ -238,13 +272,32 @@ union_identity::union_identity(size_t size, const TAllocateFn alloc, { } +/****** VIRTUAL IDENTITIES ******/ + +decltype(virtual_identity::name_lookup) virtual_identity::name_lookup = nullptr; +decltype(virtual_identity::known) virtual_identity::known = nullptr; +decltype(virtual_identity::vtable_ptr_map) virtual_identity::vtable_ptr_map = nullptr; +decltype(virtual_identity::interpose_list_map) virtual_identity::interpose_list_map = nullptr; + +void virtual_identity::ensure_virtual_identity_init() +{ + static std::once_flag virtual_identity_init_flag; + std::call_once(virtual_identity_init_flag, []() { + name_lookup = new (std::remove_pointer::type)(); + known = new (std::remove_pointer::type)(); + vtable_ptr_map = new (std::remove_pointer::type)(); + interpose_list_map = new (std::remove_pointer::type)(); + }); +} + virtual_identity::virtual_identity(size_t size, const TAllocateFn alloc, const char *dfhack_name, const char *original_name, const virtual_identity *parent, const struct_field_info *fields, bool is_plugin) : struct_identity(size, alloc, NULL, dfhack_name, parent, fields), original_name(original_name), - vtable_ptr(NULL), is_plugin(is_plugin) + is_plugin(is_plugin) { + ensure_virtual_identity_init(); // Plugins are initialized after Init was called, so they need to be added to the name table here if (is_plugin) { @@ -252,15 +305,10 @@ virtual_identity::virtual_identity(size_t size, const TAllocateFn alloc, } } -/* Vtable name to identity lookup. */ -static std::map name_lookup; - -/* Vtable pointer to identity lookup. */ -std::map virtual_identity::known; - virtual_identity::~virtual_identity() { // Remove interpose entries, so that they don't try accessing this object later + auto& interpose_list = (*interpose_list_map)[this]; for (auto it = interpose_list.begin(); it != interpose_list.end(); ++it) if (it->second) it->second->on_host_delete(this); @@ -269,75 +317,79 @@ virtual_identity::~virtual_identity() // Remove global lookup table entries if we're from a plugin if (is_plugin) { - name_lookup.erase(getOriginalName()); + (*name_lookup).erase(getOriginalName()); - if (vtable_ptr) - known.erase(vtable_ptr); + if (vtable_ptr()) + (*known).erase(vtable_ptr()); } } -void virtual_identity::doInit(Core *core) +void virtual_identity::doInit(Core *core) const { struct_identity::doInit(core); auto vtname = getOriginalName(); - name_lookup[vtname] = this; + (*name_lookup)[vtname] = this; - vtable_ptr = core->vinfo->getVTable(vtname); + auto vtable_ptr = core->vinfo->getVTable(vtname); if (vtable_ptr) - known[vtable_ptr] = this; + { + (*known)[vtable_ptr] = this; + (*vtable_ptr_map)[this] = vtable_ptr; + } } -virtual_identity *virtual_identity::find(const std::string &name) +const virtual_identity *virtual_identity::find(std::string_view name) { - auto name_it = name_lookup.find(name); + auto name_it = (*name_lookup).find(name); - return (name_it != name_lookup.end()) ? name_it->second : NULL; + return (name_it != (*name_lookup).end()) ? name_it->second : nullptr; } -virtual_identity *virtual_identity::get(virtual_ptr instance_ptr) +const virtual_identity *virtual_identity::get(virtual_ptr instance_ptr) { - if (!instance_ptr) return NULL; + if (!instance_ptr) return nullptr; return find(get_vtable(instance_ptr)); } -virtual_identity *virtual_identity::find(void *vtable) +const virtual_identity *virtual_identity::find(void *vtable) { if (!vtable || !known_mutex) - return NULL; + return nullptr; + + ensure_virtual_identity_init(); // Actually, a reader/writer lock would be sufficient, // since the table is only written once per class. std::lock_guard lock(*known_mutex); - std::map::iterator it = known.find(vtable); - - if (it != known.end()) + auto it = (*known).find(vtable); + if (it != (*known).end()) return it->second; // If using a reader/writer lock, re-grab as write here, and recheck Core &core = Core::getInstance(); std::string name = core.p->doReadClassName(vtable); - auto name_it = name_lookup.find(name); - if (name_it != name_lookup.end()) { - virtual_identity *p = name_it->second; + auto name_it = (*name_lookup).find(name); + if (name_it != (*name_lookup).end()) { + const virtual_identity *p = name_it->second; - if (p->vtable_ptr && p->vtable_ptr != vtable) { + if (p->vtable_ptr() && p->vtable_ptr() != vtable) { std::cerr << "Conflicting vtable ptr for class '" << p->getName() << "': found 0x" << std::hex << uintptr_t(vtable) - << ", previous 0x" << uintptr_t(p->vtable_ptr) << std::dec << std::endl; + << ", previous 0x" << uintptr_t(p->vtable_ptr()) << std::dec << std::endl; abort(); - } else if (!p->vtable_ptr) { + } else if (!p->vtable_ptr()) { uintptr_t pv = uintptr_t(vtable); pv -= Core::getInstance().vinfo->getRebaseDelta(); std::cerr << "" << std::endl; } - known[vtable] = p; - p->vtable_ptr = vtable; + (*known)[vtable] = p; + (*vtable_ptr_map)[p] = vtable; return p; } @@ -346,14 +398,15 @@ virtual_identity *virtual_identity::find(void *vtable) << std::hex << uintptr_t(vtable) << std::dec << std::endl; } - known[vtable] = NULL; - return NULL; + (*known).erase(vtable); + return nullptr; } void virtual_identity::adjust_vtable(virtual_ptr obj, const virtual_identity *main) const { - if (vtable_ptr) { - *(void**)obj = vtable_ptr; + auto vp = vtable_ptr(); + if (vp) { + *(void**)obj = vp; return; } @@ -366,10 +419,10 @@ void virtual_identity::adjust_vtable(virtual_ptr obj, const virtual_identity *ma virtual_ptr virtual_identity::clone(virtual_ptr obj) { - virtual_identity *id = get(obj); - if (!id) return NULL; + const virtual_identity *id = get(obj); + if (!id) return nullptr; virtual_ptr copy = id->instantiate(); - if (!copy) return NULL; + if (!copy) return nullptr; id->do_copy(copy, obj); return copy; } diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index ef37bd61ee..823d542b79 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -221,7 +221,7 @@ void LuaWrapper::push_object_internal(lua_State *state, const type_identity *typ if (type->type() == IDTYPE_CLASS) { - virtual_identity *class_vid = virtual_identity::get(virtual_ptr(ptr)); + const virtual_identity *class_vid = virtual_identity::get(virtual_ptr(ptr)); if (class_vid) type = class_vid; } diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 797161240e..3c2a4a50fc 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -210,7 +210,7 @@ void DFHack::addr_to_method_pointer_(void *pptr, void *addr) void *virtual_identity::get_vmethod_ptr(int idx) const { assert(idx >= 0); - void **vtable = (void**)vtable_ptr; + void **vtable = (void**)vtable_ptr(); if (!vtable) return NULL; return vtable[idx]; } @@ -218,7 +218,7 @@ void *virtual_identity::get_vmethod_ptr(int idx) const bool virtual_identity::set_vmethod_ptr(MemoryPatcher &patcher, int idx, void *ptr) const { assert(idx >= 0); - void **vtable = (void**)vtable_ptr; + void **vtable = (void**)vtable_ptr(); if (!vtable) return NULL; return patcher.write(&vtable[idx], &ptr, sizeof(void*)); } @@ -326,16 +326,13 @@ VMethodInterposeLinkBase::~VMethodInterposeLinkBase() VMethodInterposeLinkBase *VMethodInterposeLinkBase::get_first_interpose(const virtual_identity *id) { - auto pitem = id->interpose_list.find(vmethod_idx); - if (pitem == id->interpose_list.end()) - return NULL; - auto item = pitem->second; + auto item = id->get_interpose(vmethod_idx); if (!item) - return NULL; + return nullptr; if (item->host != id) - return NULL; + return nullptr; while (item->prev && item->prev->host == id) item = item->prev; @@ -364,7 +361,7 @@ bool VMethodInterposeLinkBase::find_child_hosts(const virtual_identity *cur, voi child_next.insert(base); found = true; } - else if (child->vtable_ptr) + else if (child->vtable_ptr()) { void *cptr = child->get_vmethod_ptr(vmethod_idx); if (cptr != vmptr) @@ -401,7 +398,7 @@ void VMethodInterposeLinkBase::on_host_delete(const virtual_identity *from) { // Otherwise, drop the link to that child: assert(child_hosts.count(from) != 0 && - from->interpose_list[vmethod_idx] == this); // while mutating this gets cleaned up below so machts nichts + from->get_interpose(vmethod_idx) == this); // while mutating this gets cleaned up below so machts nichts // Find and restore the original vmethod ptr auto last = this; @@ -413,7 +410,7 @@ void VMethodInterposeLinkBase::on_host_delete(const virtual_identity *from) // Unlink the chains child_hosts.erase(from); - from->interpose_list.erase(vmethod_idx); + from->set_interpose(vmethod_idx,nullptr); } } @@ -427,7 +424,7 @@ bool VMethodInterposeLinkBase::apply(bool enable) if (is_applied()) return true; - if (!host->vtable_ptr) + if (!host->vtable_ptr()) { std::cerr << "VMethodInterposeLinkBase::apply(" << enable << "): " << name() << ": no vtable pointer: " << host->getName() << endl; @@ -435,10 +432,10 @@ bool VMethodInterposeLinkBase::apply(bool enable) } // Retrieve the current vtable entry - auto l = host->interpose_list.find(vmethod_idx); + auto l = host->get_interpose(vmethod_idx); - VMethodInterposeLinkBase* old_link = (l != host->interpose_list.end()) ? (l->second) : nullptr; - VMethodInterposeLinkBase* next_link = NULL; + VMethodInterposeLinkBase* old_link = (l != nullptr) ? l : nullptr; + VMethodInterposeLinkBase* next_link = nullptr; while (old_link && old_link->host == host && old_link->priority > priority) { @@ -473,7 +470,7 @@ bool VMethodInterposeLinkBase::apply(bool enable) if (next_link) next_link->prev = this; else - host->interpose_list[vmethod_idx] = this; + host->set_interpose(vmethod_idx,this); child_hosts.clear(); child_next.clear(); @@ -532,9 +529,9 @@ bool VMethodInterposeLinkBase::apply(bool enable) for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it) { auto nhost = *it; - assert(nhost->interpose_list[vmethod_idx] == old_link); // acceptable due to assign below + assert(nhost->get_interpose(vmethod_idx) == old_link); // acceptable due to assign below nhost->set_vmethod_ptr(patcher, vmethod_idx, interpose_method); - nhost->interpose_list[vmethod_idx] = this; + nhost->set_interpose(vmethod_idx, this); } return true; @@ -573,7 +570,7 @@ void VMethodInterposeLinkBase::remove() MemoryPatcher patcher; // Remove from the list in the identity and vtable - host->interpose_list[vmethod_idx] = prev; + host->set_interpose(vmethod_idx, prev); host->set_vmethod_ptr(patcher, vmethod_idx, saved_chain); for (auto it = child_next.begin(); it != child_next.end(); ++it) @@ -589,8 +586,8 @@ void VMethodInterposeLinkBase::remove() for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it) { auto nhost = *it; - assert(nhost->interpose_list[vmethod_idx] == this); // acceptable due to assign below - nhost->interpose_list[vmethod_idx] = prev; + assert(nhost->get_interpose(vmethod_idx) == this); // acceptable due to assign below + nhost->set_interpose(vmethod_idx,prev); nhost->set_vmethod_ptr(patcher, vmethod_idx, saved_chain); if (prev) prev->child_hosts.insert(nhost); diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 6a5695809d..9d1eb4c664 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -24,6 +24,7 @@ distribution. #pragma once +#include #include #include #include @@ -134,28 +135,30 @@ namespace DFHack }; class DFHACK_EXPORT compound_identity : public constructed_identity { - static compound_identity *list; - mutable compound_identity *next; + static std::list* list; + static std::map* parent_map; + static std::map>* children_map; + static std::vector* top_scope; const char *dfhack_name; - mutable compound_identity *scope_parent; - mutable std::vector scope_children; - static std::vector top_scope; + const compound_identity *const scope_parent; + + static void ensure_compound_identity_init(); protected: compound_identity(size_t size, TAllocateFn alloc, const compound_identity *scope_parent, const char *dfhack_name); - virtual void doInit(Core *core); + virtual void doInit(Core *core) const; public: const char *getName() const { return dfhack_name; } virtual const std::string getFullName() const; - const compound_identity *getScopeParent() const { return scope_parent; } - const std::vector &getScopeChildren() const { return scope_children; } - static const std::vector &getTopScope() { return top_scope; } + const compound_identity *getScopeParent() const { return (*parent_map)[this]; } + const std::vector &getScopeChildren() const { return (*children_map)[this]; } + static const std::vector &getTopScope() { return *top_scope; } static void Init(Core *core); }; @@ -286,14 +289,15 @@ namespace DFHack }; class DFHACK_EXPORT struct_identity : public compound_identity { - mutable struct_identity *parent; - mutable std::vector children; - bool has_children; + static std::map* parent_map; + static std::map>* children_map; const struct_field_info *fields; + static void ensure_struct_identity_init(); + protected: - virtual void doInit(Core *core); + virtual void doInit(Core *core) const override; public: struct_identity(size_t size, TAllocateFn alloc, @@ -302,9 +306,9 @@ namespace DFHack virtual identity_type type() const { return IDTYPE_STRUCT; } - const struct_identity *getParent() const { return parent; } - const std::vector &getChildren() const { return children; } - bool hasChildren() const { return has_children; } + const struct_identity *getParent() const { return (*parent_map)[this]; } + const std::vector &getChildren() const { return (*children_map)[this]; } + bool hasChildren() const { return (*children_map)[this].size() > 0; } const struct_field_info *getFields() const { return fields; } @@ -361,27 +365,55 @@ namespace DFHack class MemoryPatcher; class DFHACK_EXPORT virtual_identity : public struct_identity { - static std::map known; + public: + using interpose_t = VMethodInterposeLinkBase*; + using interpose_list_t = std::map; - const char *original_name; + private: + static std::map> *name_lookup; + static std::map* known; + static std::map* vtable_ptr_map; + static std::map* interpose_list_map; - mutable void *vtable_ptr; + const char *original_name; bool is_plugin; friend class VMethodInterposeLinkBase; - mutable std::map interpose_list; + + static void ensure_virtual_identity_init(); protected: - virtual void doInit(Core *core); + virtual void doInit(Core *core) const override; static void *get_vtable(virtual_ptr instance_ptr) { return *(void**)instance_ptr; } - bool can_allocate() const { return struct_identity::can_allocate() && (vtable_ptr != NULL); } + void* vtable_ptr() const + { + auto& lst = (*vtable_ptr_map); + auto it = lst.find(this); + return it != lst.end() ? it->second : nullptr; + } + + bool can_allocate() const { return struct_identity::can_allocate() && (vtable_ptr() != nullptr); } void *get_vmethod_ptr(int index) const; bool set_vmethod_ptr(MemoryPatcher &patcher, int index, void *ptr) const; + interpose_list_t& get_interpose_list() const { return (*interpose_list_map)[this]; } + interpose_t get_interpose(int index) const { + auto &lst = get_interpose_list(); + auto it = lst.find(index); + return it != lst.end() ? it->second : nullptr; + } + void set_interpose(int index, interpose_t link) const { + auto &lst = get_interpose_list(); + if (link) + lst[index] = link; + else + lst.erase(index); + } + public: virtual_identity(size_t size, const TAllocateFn alloc, const char *dfhack_name, const char *original_name, @@ -394,16 +426,16 @@ namespace DFHack const char *getOriginalName() const { return original_name ? original_name : getName(); } public: - static virtual_identity *get(virtual_ptr instance_ptr); + static const virtual_identity *get(virtual_ptr instance_ptr); - static virtual_identity *find(void *vtable); - static virtual_identity *find(const std::string &name); + static const virtual_identity *find(void *vtable); + static const virtual_identity *find(std::string_view name); bool is_instance(virtual_ptr instance_ptr) const { if (!instance_ptr) return false; - if (vtable_ptr) { + if (vtable_ptr()) { void *vtable = get_vtable(instance_ptr); - if (vtable == vtable_ptr) return true; + if (vtable == vtable_ptr()) return true; if (!hasChildren()) return false; } return is_subclass(get(instance_ptr)); @@ -411,8 +443,8 @@ namespace DFHack bool is_direct_instance(virtual_ptr instance_ptr) const { if (!instance_ptr) return false; - return vtable_ptr ? (vtable_ptr == get_vtable(instance_ptr)) - : (this == get(instance_ptr)); + auto vp = vtable_ptr(); + return vp ? (vp == get_vtable(instance_ptr)) : (this == get(instance_ptr)); } template static P get_vmethod_ptr(P selector); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index b5df3ca990..3808fa0271 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -135,7 +135,7 @@ static df::layer_object_listst *getLayerList(df::viewscreen_layer *layer, int id } */ -static std::string getNameChunk(virtual_identity *id, int start, int end) +static std::string getNameChunk(const virtual_identity *id, int start, int end) { if (!id) return "UNKNOWN"; @@ -970,7 +970,7 @@ std::vector Gui::getFocusStrings(df::viewscreen* top) } } - if (virtual_identity *id = virtual_identity::get(top)) + if (const virtual_identity *id = virtual_identity::get(top)) { std::string name = getNameChunk(id, 11, 2); From 2a8c7897418427f94afbb5e348dea27ab3020b75 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Oct 2025 06:24:36 -0500 Subject: [PATCH 0521/1082] update docs --- docs/dev/data-identity.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/dev/data-identity.rst b/docs/dev/data-identity.rst index 855a6db26c..ae7076941d 100644 --- a/docs/dev/data-identity.rst +++ b/docs/dev/data-identity.rst @@ -123,18 +123,12 @@ Type identity object lifetime and mutability ============================================ *Most* instances of ``type_identity`` are statically constructed and immutable and are thus ``const static`` when constructed. -All ``type_identity`` pointers should be declared ``const``. -Due to the use of lazy initialization, ``compound_identity`` and its subclasses have a few fields that are marked as ``mutable`` -(so that the ``parent`` and ``child`` members can be updated as additional instances are constructed). -In addition, ``virtual_identity`` has two fields that are ``mutable`` due to the dual use of this type to implement -DFHack's vmethod interpose system. -Due to this dual purpose, it is important that there be at most one ``virtual_identity`` object per virtual class, -although this is not enforced at present. +All ``type_identity`` pointers should be declared ``const``. Due to ``virtual_identity``'s role in implementing +DFHack's vmethod interpose system, it is important that there be at most one ``virtual_identity`` object per virtual class. Having more than one ``struct_identity`` object for the same type might also potentially lead to misoperation. In general, there should be a one to one correspondence between ``type_identity`` objects and C++ types -(with the special case that ``global_identity`` has no corresponding type). -As far as we know, for any type other than ``virtual_identity``, +(with the special case that ``global_identity`` has no corresponding type). As far as we know, for any type other than ``virtual_identity``, violations of this constraint will not lead to misoperation, but this constraint should not be lightly violated. The Lua/C++ interface does, in a handful of places, assume that it can compare ``type_identity`` pointers to determine if they reference the same type, but as far as we know all of these instances will fall From c6b55d4da33a2ae21ea2fda4339735d331962080 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 22 Oct 2025 20:34:21 -0500 Subject: [PATCH 0522/1082] simplify the allocator current code has three different allocator functions, the generic one, one labeled "nodel" and one labeled "noassign" the `noassign` allocator just mimics the behavior of the basic one when the type in question is not copy-assignable and is only being used for types that are not copy-assignable so the default allocator will work _exactly_ the same anyway the `nodel` version is entirely unused thus, this PR removes the `nodel` and `noassign` versions and folds `try_assign` into the allocator so that all the logic is in one place also add in-code documentation --- library/DataIdentity.cpp | 2 +- library/include/DataDefs.h | 77 +++++++++++++++++++--------------- library/include/DataIdentity.h | 2 +- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index a385915d92..1bbeb64e77 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -19,7 +19,7 @@ namespace df { #define INTEGER_IDENTITY_TRAITS(type, name) NUMBER_IDENTITY_TRAITS(integer, type, name) #define FLOAT_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(float, type, #type) #define OPAQUE_IDENTITY_TRAITS_NAME(name, ...) \ - const opaque_identity identity_traits<__VA_ARGS__ >::identity(sizeof(__VA_ARGS__), allocator_noassign_fn<__VA_ARGS__ >, name) + const opaque_identity identity_traits<__VA_ARGS__ >::identity(sizeof(__VA_ARGS__), allocator_fn<__VA_ARGS__ >, name) #define OPAQUE_IDENTITY_TRAITS(...) OPAQUE_IDENTITY_TRAITS_NAME(#__VA_ARGS__, __VA_ARGS__ ) INTEGER_IDENTITY_TRAITS(char, "char"); diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 9d1eb4c664..7fac3c0c5e 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -72,7 +72,7 @@ namespace DFHack PTRFLAG_HAS_BAD_POINTERS = 2, }; - typedef void *(*TAllocateFn)(void*,const void*); + using TAllocateFn = void *(*)(void*, const void*); class DFHACK_EXPORT type_identity { const size_t size; @@ -122,10 +122,10 @@ namespace DFHack constructed_identity(size_t size, const TAllocateFn alloc) : type_identity(size), allocator(alloc) {}; - virtual bool can_allocate() const { return (allocator != NULL); } - virtual void *do_allocate() const { return allocator(NULL,NULL); } + virtual bool can_allocate() const { return (allocator != nullptr); } + virtual void *do_allocate() const { return allocator(nullptr,nullptr); } virtual bool do_copy(void *tgt, const void *src) const { return allocator(tgt,src) == tgt; } - virtual bool do_destroy(void *obj) const { return allocator(NULL,obj) == obj; } + virtual bool do_destroy(void *obj) const { return allocator(nullptr,obj) == obj; } public: virtual bool isPrimitive() const { return false; } virtual bool isConstructed() const { return true; } @@ -526,42 +526,53 @@ namespace df using DFHack::DfLinkedList; using DFHack::DfOtherVectors; + /* + * + * Allocator functions are used to allocate, deallocate, and copy-assign objects + * + * When out is non-null, the object pointed to by in is copy-assigned over the object + * pointed to by out, if possible. When assignment is possible, out is returned. + * When assignment is not possible, nothing is done and nullptr is returned. + * The type must be copy-assignable for this to work; move-assignment is not + * supported. Callers can determine if the assignment succeeded by checking for the + * return value matching out (or simply being not null). + * + * When only in is non-null, the object pointed to by in is destroyed and deallocated, + * and in is returned. Note that the return value points to deallocated memory + * and should not be dereferenced. + * + * When both out and in are null, a new object is constructed and returned. + * + * Calling an allocator function with out non-null and in null is undefined behavior. + * + */ + template concept copy_assignable = std::assignable_from && std::assignable_from; template - void* allocator_try_assign(void *out, const void *in) { - if constexpr (copy_assignable) { - *(T*)out = *(const T*)in; - return out; - } - else { - // assignment is not possible; do nothing - return nullptr; + void *allocator_fn(void *out, const void *in) { + if (out) + { + if constexpr (copy_assignable) + { + *(T*)out = *(const T*)in; + return out; + } + else + { + return nullptr; + } } - } - + else if (in) + { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" - template - void *allocator_fn(void *out, const void *in) { - if (out) { return allocator_try_assign(out, in); } - else if (in) { delete (T*)in; return (T*)in; } - else return new T(); - } + delete (T*)in; #pragma GCC diagnostic pop - - template - void *allocator_nodel_fn(void *out, const void *in) { - if (out) { *(T*)out = *(const T*)in; return out; } - else if (in) { return NULL; } - else return new T(); - } - - template - void *allocator_noassign_fn(void *out, const void *in) { - if (out) { return NULL; } - else if (in) { delete (T*)in; return (T*)in; } - else return new T(); + return (T*)in; + } + else + return new T(); } template diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index e9fb5a9aa3..2c302264d9 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -638,7 +638,7 @@ namespace df static opaque_identity *get() { using type = std::shared_ptr; static std::string name = std::string("shared_ptr<") + typeid(T).name() + ">"; - static opaque_identity identity(sizeof(type), allocator_noassign_fn, name); + static opaque_identity identity(sizeof(type), allocator_fn, name); return &identity; } }; From 321173c6617999c29364af61d759449d80daf2a7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 23 Oct 2025 07:20:30 +0000 Subject: [PATCH 0523/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 80d61d16d4..f72ccf6f3b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 80d61d16d467a4d2bb1c301b22cd067bf4b4992e +Subproject commit f72ccf6f3b83cdaa8d424d68b7f2b0cdd552f242 From df1bdc3dcb4bbd9bcba9e805aea92262e0c66b49 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 24 Oct 2025 07:20:00 +0000 Subject: [PATCH 0524/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index f72ccf6f3b..e80d330103 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f72ccf6f3b83cdaa8d424d68b7f2b0cdd552f242 +Subproject commit e80d33010309fcbad0b10ff05218f21600f62751 From 3911fca9e7a7f47bca1c4b6ffdf312607cf92c15 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 24 Oct 2025 09:22:31 -0400 Subject: [PATCH 0525/1082] Add stockpiles plugin support for dyes Enables support for "dyed", "undyed", and "color" fields for armor, cloth, leather, and finished goods. --- docs/changelog.txt | 1 + plugins/stockpiles/StockpileSerializer.cpp | 128 ++++++++++++++++++++- plugins/stockpiles/proto/stockpiles.proto | 12 ++ 3 files changed, 136 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 14a9ba0ca3..fcd16dc002 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `stockpiles`: add support for managing the dyed, undyed, and color filter settings. ## Documentation diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index efac3e8bad..756421d9d4 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -12,6 +12,7 @@ #include "df/building_stockpilest.h" #include "df/creature_raw.h" #include "df/caste_raw.h" +#include "df/descriptor_color.h" #include "df/inorganic_raw.h" #include "df/item_quality.h" #include @@ -497,6 +498,57 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, } } +static bool serialize_list_color(color_ostream& out, FuncWriteExport add_value, const vector* colors) { + bool all = true; + if (!colors) { + DEBUG(log, out).print("serialize_list_color: list null\n"); + return all; + } + for (size_t i = 0; i < colors->size(); ++i) { + if (!colors->at(i)) { + all = false; + continue; + } + add_value(world->raws.descriptors.colors[i]->id); + } + return all; +} + +// Find the index of a named color, returning SIZE_MAX on unknown. +static size_t find_color_by_token(const string& token) { + auto& colors = world->raws.descriptors.colors; + size_t num_colors = colors.size(); + for (size_t idx = 0; idx < num_colors; ++ idx) { + if (colors[idx]->id == token) { + return idx; + } + } + return SIZE_MAX; +} + +static void unserialize_list_color(color_ostream& out, const char* subcat, bool all, char val, const vector& filters, + FuncReadImport read_value, int32_t list_size, vector& pile_list) { + size_t num_elems = world->raws.descriptors.colors.size(); + pile_list.resize(num_elems, '\0'); + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + set_filter_elem(out, subcat, filters, val, world->raws.descriptors.colors[idx]->id, idx, pile_list[idx]); + } + return; + } + + for (int32_t idx = 0; idx < list_size; ++idx) { + const string& value = read_value(idx); + + size_t color = find_color_by_token(value); + if (color == SIZE_MAX) { + WARN(log, out).print("unknown color %s", value.c_str()); + continue; + } + set_filter_elem(out, subcat, filters, val, value, color, pile_list[color]); + } +} + static bool serialize_list_item_type(color_ostream& out, FuncItemAllowed is_allowed, FuncWriteExport add_value, const vector& list) { using df::enums::item_type::item_type; @@ -1059,10 +1111,12 @@ static bool armor_mat_is_allowed(const MaterialInfo& mi) { bool StockpileSettingsSerializer::write_armor(color_ostream& out, StockpileSettings::ArmorSet* armor) { auto & parmor = mSettings->armor; - bool all = parmor.unusable && parmor.usable; + bool all = parmor.unusable && parmor.usable && parmor.dyed && parmor.undyed; armor->set_unusable(parmor.unusable); armor->set_usable(parmor.usable); + armor->set_dyed(parmor.dyed); + armor->set_undyed(parmor.undyed); // armor type all = serialize_list_itemdef(out, @@ -1125,6 +1179,9 @@ bool StockpileSettingsSerializer::write_armor(color_ostream& out, StockpileSetti all = serialize_list_quality(out, [&](const string& token) { armor->add_quality_total(token); }, parmor.quality_total) && all; + all = serialize_list_color(out, [&](const string& token) { armor->add_color(token); }, + &parmor.color) && all; + return all; } @@ -1148,6 +1205,9 @@ void StockpileSettingsSerializer::read_armor(color_ostream& out, DeserializeMode parmor.mats.clear(); quality_clear(parmor.quality_core); quality_clear(parmor.quality_total); + parmor.dyed = false; + parmor.undyed = false; + parmor.color.clear(); }, [&](bool all, char val) { auto & barmor = mBuffer.armor(); @@ -1195,6 +1255,13 @@ void StockpileSettingsSerializer::read_armor(color_ostream& out, DeserializeMode unserialize_list_quality(out, "total", all, val, filters, [&](const size_t& idx) -> const string& { return barmor.quality_total(idx); }, barmor.quality_total_size(), parmor.quality_total); + + unserialize_list_color(out, "color", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.color(idx); }, + barmor.color_size(), parmor.color); + + set_flag(out, "dyed", filters, all, val, barmor.dyed(), parmor.dyed); + set_flag(out, "undyed", filters, all, val, barmor.undyed(), parmor.undyed); }); } @@ -1266,7 +1333,9 @@ void StockpileSettingsSerializer::read_bars_blocks(color_ostream& out, Deseriali } bool StockpileSettingsSerializer::write_cloth(color_ostream& out, StockpileSettings::ClothSet* cloth) { - bool all = true; + bool all = mSettings->cloth.dyed && mSettings->cloth.undyed; + cloth->set_dyed(mSettings->cloth.dyed); + cloth->set_undyed(mSettings->cloth.undyed); all = serialize_list_organic_mat(out, [&](const string& token) { cloth->add_thread_silk(token); }, @@ -1300,6 +1369,10 @@ bool StockpileSettingsSerializer::write_cloth(color_ostream& out, StockpileSetti [&](const string& token) { cloth->add_cloth_metal(token); }, &mSettings->cloth.cloth_metal, organic_mat_category::MetalThread) && all; + all = serialize_list_color(out, + [&](const string& token) { cloth->add_color(token); }, + &mSettings->cloth.color) && all; + return all; } @@ -1319,6 +1392,9 @@ void StockpileSettingsSerializer::read_cloth(color_ostream& out, DeserializeMode pcloth.cloth_plant.clear(); pcloth.cloth_yarn.clear(); pcloth.cloth_metal.clear(); + pcloth.dyed = false; + pcloth.undyed = false; + pcloth.color.clear(); }, [&](bool all, char val) { auto & bcloth = mBuffer.cloth(); @@ -1354,6 +1430,13 @@ void StockpileSettingsSerializer::read_cloth(color_ostream& out, DeserializeMode unserialize_list_organic_mat(out, "cloth/metal", all, val, filters, [&](size_t idx) -> string { return bcloth.cloth_metal(idx); }, bcloth.cloth_metal_size(), pcloth.cloth_metal, organic_mat_category::MetalThread); + + unserialize_list_color(out, "cloth/color", all, val, filters, + [&](size_t idx) -> string { return bcloth.color(idx); }, + bcloth.color_size(), pcloth.color); + + set_flag(out, "dyed", filters, all, val, bcloth.dyed(), pcloth.dyed); + set_flag(out, "undyed", filters, all, val, bcloth.undyed(), pcloth.undyed); }); } @@ -1425,7 +1508,11 @@ static bool finished_goods_mat_is_allowed(const MaterialInfo& mi) { } bool StockpileSettingsSerializer::write_finished_goods(color_ostream& out, StockpileSettings::FinishedGoodsSet* finished_goods) { - bool all = serialize_list_item_type(out, + bool all = mSettings->leather.dyed && mSettings->leather.undyed; + finished_goods->set_dyed(mSettings->finished_goods.dyed); + finished_goods->set_undyed(mSettings->finished_goods.undyed); + + all = serialize_list_item_type(out, finished_goods_type_is_allowed, [&](const string& token) { finished_goods->add_type(token); }, mSettings->finished_goods.type); @@ -1447,6 +1534,10 @@ bool StockpileSettingsSerializer::write_finished_goods(color_ostream& out, Stock [&](const string& token) { finished_goods->add_quality_total(token); }, mSettings->finished_goods.quality_total) && all; + all = serialize_list_color(out, + [&](const string& token) { finished_goods->add_color(token); }, + &mSettings->finished_goods.color) && all; + return all; } @@ -1463,6 +1554,9 @@ void StockpileSettingsSerializer::read_finished_goods(color_ostream& out, Deseri pfinished_goods.mats.clear(); quality_clear(pfinished_goods.quality_core); quality_clear(pfinished_goods.quality_total); + pfinished_goods.dyed = false; + pfinished_goods.undyed = false; + pfinished_goods.color.clear(); }, [&](bool all, char val) { auto & bfinished_goods = mBuffer.finished_goods(); @@ -1486,6 +1580,13 @@ void StockpileSettingsSerializer::read_finished_goods(color_ostream& out, Deseri unserialize_list_quality(out, "total", all, val, filters, [&](const size_t& idx) -> const string& { return bfinished_goods.quality_total(idx); }, bfinished_goods.quality_total_size(), pfinished_goods.quality_total); + + unserialize_list_color(out, "color", all, val, filters, + [&](const size_t& idx) -> const string& { return bfinished_goods.color(idx); }, + bfinished_goods.color_size(), pfinished_goods.color); + + set_flag(out, "dyed", filters, all, val, bfinished_goods.dyed(), pfinished_goods.dyed); + set_flag(out, "undyed", filters, all, val, bfinished_goods.undyed(), pfinished_goods.undyed); }); } @@ -1934,9 +2035,17 @@ void StockpileSettingsSerializer::read_gems(color_ostream& out, DeserializeMode } bool StockpileSettingsSerializer::write_leather(color_ostream& out, StockpileSettings::LeatherSet* leather) { - return serialize_list_organic_mat(out, + bool all = mSettings->leather.dyed && mSettings->leather.undyed; + leather->set_dyed(mSettings->leather.dyed); + leather->set_undyed(mSettings->leather.undyed); + + all = serialize_list_organic_mat(out, [&](const string& id) { leather->add_mats(id); }, - &mSettings->leather.mats, organic_mat_category::Leather); + &mSettings->leather.mats, organic_mat_category::Leather) && all; + all = serialize_list_color(out, + [&](const string& id) { leather->add_color(id); }, + &mSettings->leather.color) && all; + return all; } void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMode mode, const vector& filters) { @@ -1948,6 +2057,9 @@ void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMo mSettings->flags.mask_leather, [&]() { pleather.mats.clear(); + pleather.color.clear(); + pleather.dyed = false; + pleather.undyed = false; }, [&](bool all, char val) { auto & bleather = mBuffer.leather(); @@ -1955,6 +2067,12 @@ void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMo unserialize_list_organic_mat(out, "", all, val, filters, [&](size_t idx) -> string { return bleather.mats(idx); }, bleather.mats_size(), pleather.mats, organic_mat_category::Leather); + unserialize_list_color(out, "", all, val, filters, + [&](size_t idx) -> string { return bleather.color(idx); }, + bleather.color_size(), pleather.color); + + set_flag(out, "dyed", filters, all, val, bleather.dyed(), pleather.dyed); + set_flag(out, "undyed", filters, all, val, bleather.undyed(), pleather.undyed); }); } diff --git a/plugins/stockpiles/proto/stockpiles.proto b/plugins/stockpiles/proto/stockpiles.proto index 681e7d9279..49a6d1c42e 100644 --- a/plugins/stockpiles/proto/stockpiles.proto +++ b/plugins/stockpiles/proto/stockpiles.proto @@ -100,10 +100,16 @@ message StockpileSettings { repeated string mats = 3; repeated string quality_core = 4; repeated string quality_total = 5; + optional bool dyed = 7; + optional bool undyed = 8; + repeated string color = 9; } message LeatherSet { optional bool all = 2; repeated string mats = 1; + optional bool dyed = 3; + optional bool undyed = 4; + repeated string color = 5; } message ClothSet { optional bool all = 9; @@ -115,6 +121,9 @@ message StockpileSettings { repeated string cloth_plant = 6; repeated string cloth_yarn = 7; repeated string cloth_metal = 8; + optional bool dyed = 10; + optional bool undyed = 11; + repeated string color = 12; } message WoodSet { optional bool all = 2; @@ -145,6 +154,9 @@ message StockpileSettings { repeated string quality_total = 10; optional bool usable = 11; optional bool unusable = 12; + optional bool dyed = 14; + optional bool undyed = 15; + repeated string color = 16; } message CorpsesSet { optional bool all = 1; From 2a332ebbfe3ec2f28e1a2a2aa40937945b1a4fda Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 24 Oct 2025 21:36:31 -0400 Subject: [PATCH 0526/1082] Correct logic errors --- plugins/stockpiles/StockpileSerializer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 756421d9d4..ad76977f1a 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -499,7 +499,7 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, } static bool serialize_list_color(color_ostream& out, FuncWriteExport add_value, const vector* colors) { - bool all = true; + bool all = world->raws.descriptors.colors.size() == colors->size(); if (!colors) { DEBUG(log, out).print("serialize_list_color: list null\n"); return all; @@ -1508,14 +1508,14 @@ static bool finished_goods_mat_is_allowed(const MaterialInfo& mi) { } bool StockpileSettingsSerializer::write_finished_goods(color_ostream& out, StockpileSettings::FinishedGoodsSet* finished_goods) { - bool all = mSettings->leather.dyed && mSettings->leather.undyed; + bool all = mSettings->finished_goods.dyed && mSettings->finished_goods.undyed; finished_goods->set_dyed(mSettings->finished_goods.dyed); finished_goods->set_undyed(mSettings->finished_goods.undyed); all = serialize_list_item_type(out, finished_goods_type_is_allowed, [&](const string& token) { finished_goods->add_type(token); }, - mSettings->finished_goods.type); + mSettings->finished_goods.type) && all; all = serialize_list_material(out, finished_goods_mat_is_allowed, From 374df0e264407ad733dee93ad4bc915e0bf357d1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 27 Oct 2025 16:03:02 -0500 Subject: [PATCH 0527/1082] 52.05-r3rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e63238f89..e9686d6b5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.05") -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 5180882301238b164ba87f4222598a3b4f8db382 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:20:01 +0000 Subject: [PATCH 0528/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e80d330103..412ae1a979 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e80d33010309fcbad0b10ff05218f21600f62751 +Subproject commit 412ae1a979fb1e634ce29d7b87c17bb38fb22c6c From 576e8396dae8914d3bca321ab48dfdf9de3b1a7e Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:36:25 +0000 Subject: [PATCH 0529/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 412ae1a979..ede81fd3a0 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 412ae1a979fb1e634ce29d7b87c17bb38fb22c6c +Subproject commit ede81fd3a00f4104b16a4b913b17f3019af89c44 From b372657cfe2a0dd41b2189decfd1388ab35372e3 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:21:07 +0000 Subject: [PATCH 0530/1082] 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 9e3924fa79..d7630adb7b 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9e3924fa7967078cebdf5890908df909569f7f5f +Subproject commit d7630adb7b548ce7bce8cf1a0d3a7648b870d5b2 From 77683efb41d2815e55c9207140873523501a7a0c Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:53:19 +0000 Subject: [PATCH 0531/1082] Auto-update structures ref for 53.01 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index d7630adb7b..014e2e43d1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d7630adb7b548ce7bce8cf1a0d3a7648b870d5b2 +Subproject commit 014e2e43d148fd04f309a47ab8d866539aced726 From 2a3906b7d0906089e01b2b3949c25ab4e4779500 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:00:24 +0000 Subject: [PATCH 0532/1082] Auto-update structures ref for 53.01 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 014e2e43d1..812cec185f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 014e2e43d148fd04f309a47ab8d866539aced726 +Subproject commit 812cec185f5666d40049958257368e624c7c01ec From 76d7bde153fac9eec0264d084ca573dbcd98696e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Nov 2025 11:51:25 -0600 Subject: [PATCH 0533/1082] update version to 53.01-r1 --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9686d6b5c..9f471490c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "52.05") -set(DFHACK_RELEASE "r3rc1") -set(DFHACK_PRERELEASE TRUE) +set(DF_VERSION "53.01") +set(DFHACK_RELEASE "r1") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 9a73da3d1604abc24ce02e382aaa420b47557c01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:00:28 +0000 Subject: [PATCH 0534/1082] [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.34.0 → 0.34.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.0...0.34.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 1bca6a9289..70277354ad 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.34.0 + rev: 0.34.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From dec140ef815ea8bfd5f0ec44e6463ddcd2a9d95a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:08:26 +0000 Subject: [PATCH 0535/1082] 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 812cec185f..fa6fb6e515 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 812cec185f5666d40049958257368e624c7c01ec +Subproject commit fa6fb6e5159344c383736f9f5819d9e48c97d356 From 7fc09e34daff5b2c39098cbf12841faf953bb65a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:38:41 +0000 Subject: [PATCH 0536/1082] 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 fa6fb6e515..a2604299ce 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit fa6fb6e5159344c383736f9f5819d9e48c97d356 +Subproject commit a2604299ce4321dd5111b374c69af783db8969e9 From 1dead6c15d913eecd22938f031ae619a009a6216 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Nov 2025 15:00:12 -0600 Subject: [PATCH 0537/1082] Changelog for 53.01-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ scripts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index fcd16dc002..bc72a8e33a 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.01-r1 + +## New Tools + +## New Features + +## Fixes + ## Misc Improvements - `stockpiles`: add support for managing the dyed, undyed, and color filter settings. diff --git a/scripts b/scripts index ede81fd3a0..370778c997 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ede81fd3a00f4104b16a4b913b17f3019af89c44 +Subproject commit 370778c997cafc1721a4ae635159c21b84d95e8c From f1dc866ccf3c1b96860897fdea3085a43e86e9b4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Nov 2025 15:24:58 -0600 Subject: [PATCH 0538/1082] update global_table --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a2604299ce..c7da8a7376 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a2604299ce4321dd5111b374c69af783db8969e9 +Subproject commit c7da8a737657117ff5c8752b7ca08bb5392e02e0 From c9af9f44f297b7b70c08b8a77a7aa7004ac22072 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 4 Nov 2025 07:20:31 +0000 Subject: [PATCH 0539/1082] 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 c7da8a7376..8808adbdf5 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit c7da8a737657117ff5c8752b7ca08bb5392e02e0 +Subproject commit 8808adbdf5b1073e4eb7c55de0eb96a5b512cfed diff --git a/scripts b/scripts index 370778c997..f71c1568f3 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 370778c997cafc1721a4ae635159c21b84d95e8c +Subproject commit f71c1568f3af618fc542f1af2028dc89ba383f7a From 7123b8c8b49bb3bca8733733904a4f16532a88af Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:03:32 +0000 Subject: [PATCH 0540/1082] 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 8808adbdf5..3833a11445 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 8808adbdf5b1073e4eb7c55de0eb96a5b512cfed +Subproject commit 3833a11445168f67cffd35b678e47ec86c35e091 From e0951fdafd18636f6170574b82b12b2ad50f6940 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 4 Nov 2025 11:04:36 -0600 Subject: [PATCH 0541/1082] add `gps` to checked globals --- docs/changelog.txt | 1 + library/Core.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bc72a8e33a..04b6fd6fd1 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- Core: added ``gps`` (``graphicst``) to the set of globals whose sizes must agree for DFHack to pass initialization checks ## Documentation diff --git a/library/Core.cpp b/library/Core.cpp index a6fc71fb2b..b927d49cc4 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1682,6 +1682,7 @@ bool Core::InitMainThread() { { "world", sizeof(df::world) }, { "game", sizeof(df::gamest) }, { "plotinfo", sizeof(df::plotinfost) }, + { "gps", sizeof(df::graphic) }, }; for (auto& gte : *df::global::global_table) From dd94cff18e91c8d022656cd6924a82a26b7db804 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:23:03 +0000 Subject: [PATCH 0542/1082] 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 3833a11445..0c03527312 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3833a11445168f67cffd35b678e47ec86c35e091 +Subproject commit 0c035273128c300aa77014e92315b09136e65de9 From 0c2902ab12ecf301ea1674a376690cadc678d82e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 4 Nov 2025 11:23:58 -0600 Subject: [PATCH 0543/1082] cmake for 53.02-r1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f471490c6..e394d7d349 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.01") +set(DF_VERSION "53.02") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From e8bba788f855909679186c912a73757a52233234 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:31:01 +0000 Subject: [PATCH 0544/1082] Auto-update structures ref for 53.02 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 0c03527312..99cb51ac84 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 0c035273128c300aa77014e92315b09136e65de9 +Subproject commit 99cb51ac849a17652dbb74efa757df5b5716a9ba From 8ef275bc2c7a6eb8f0a516138292df2bc7918d43 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 4 Nov 2025 11:50:23 -0600 Subject: [PATCH 0545/1082] changelog for 53.02-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 04b6fd6fd1..537686fb9a 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.02-r1 + +## New Tools + +## New Features + +## Fixes + ## Misc Improvements - Core: added ``gps`` (``graphicst``) to the set of globals whose sizes must agree for DFHack to pass initialization checks From a8e4cb173842ea23d631107d804c2568631f4c75 Mon Sep 17 00:00:00 2001 From: Quietust Date: Tue, 4 Nov 2025 15:29:34 -0600 Subject: [PATCH 0546/1082] Update cleanconst to skip reinforced constructions since those are built with 2 items plus reinforcements, and it's unclear whether the game would properly restore those during deconstruction. --- docs/changelog.txt | 1 + plugins/cleanconst.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 537686fb9a..3f338221a6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## New Features ## Fixes +- `cleanconst`: do not attempt to clean Reinforced constructions ## Misc Improvements diff --git a/plugins/cleanconst.cpp b/plugins/cleanconst.cpp index b72806cd9b..895f182e76 100644 --- a/plugins/cleanconst.cpp +++ b/plugins/cleanconst.cpp @@ -45,6 +45,10 @@ command_result df_cleanconst(color_ostream &out, vector & parameters) if (cons->flags.bits.no_build_item) continue; + // Skip reinforced constructions as well + if (cons->flags.bits.reinforced) + continue; + // only destroy the item if the construction claims to be made of the exact same thing if (item->getType() != cons->item_type || item->getSubtype() != cons->item_subtype || From 8f517802a6ce36860d954790a783797e0928153b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 5 Nov 2025 00:17:58 -0500 Subject: [PATCH 0547/1082] Add support for new siege update buildings --- docs/changelog.txt | 2 ++ library/lua/dfhack/buildings.lua | 26 +++++++++++++++- plugins/lua/buildingplan/planneroverlay.lua | 34 +++++++++++++++++++-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 537686fb9a..0037d2515a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,8 +59,10 @@ Template for new versions: ## New Features ## Fixes +- `buildingplan`: Building costs for reinforced walls are now correct. ## Misc Improvements +- `buildingplan`: Added support for bolt throwers and siege engine rotation. ## Documentation diff --git a/library/lua/dfhack/buildings.lua b/library/lua/dfhack/buildings.lua index 7fff00dcf2..5ea3b1f758 100644 --- a/library/lua/dfhack/buildings.lua +++ b/library/lua/dfhack/buildings.lua @@ -130,7 +130,6 @@ local building_inputs = { vector_id=df.job_item_vector_id.PIPE_SECTION } }, - [df.building_type.Construction] = { { flags2={ building_material=true, non_economic=true } } }, [df.building_type.Hatch] = { { item_type=df.item_type.HATCH_COVER, @@ -353,6 +352,26 @@ local siegeengine_input = { quantity=3 } }, + [df.siegeengine_type.BoltThrower] = { + { + item_type=df.item_type.BOLT_THROWER_PARTS, + vector_id=df.job_item_vector_id.BOLT_THROWER_PARTS, + }, + { + item_type=df.item_type.BIN, + vector_id=df.job_item_vector_id.BIN, + }, + { + name='mechanism', + item_type=df.item_type.TRAPPARTS, + vector_id=df.job_item_vector_id.TRAPPARTS, + }, + { + name='chain', + item_type=df.item_type.CHAIN, + vector_id=df.job_item_vector_id.CHAIN + }, + }, } --[[ Functions for lookup in tables. ]] @@ -380,6 +399,11 @@ local function get_inputs_by_type(type,subtype,custom) return trap_inputs[subtype] elseif type == df.building_type.SiegeEngine then return siegeengine_input[subtype] + elseif type == df.building_type.Construction then + if subtype == df.construction_type.ReinforcedWall then + return { { flags2={ building_material=true, non_economic=true }, quantity=2 }, { flags3={ metal=true }, item_type=df.item_type.BAR, vector_id=df.job_item_vector_id.BAR } } + end + return { { flags2={ building_material=true, non_economic=true } } } else return building_inputs[type] end diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f92dc95d84..72987590fb 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -119,6 +119,10 @@ local function is_construction() return uibs.building_type == df.building_type.Construction end +local function is_siege_engine() + return uibs.building_type == df.building_type.SiegeEngine +end + local function tile_is_construction(pos) local tt = dfhack.maps.getTileType(pos) if not tt then return false end @@ -605,7 +609,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'), @@ -743,6 +747,24 @@ function PlannerOverlay:init() buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) end, }, + widgets.CycleHotkeyLabel { + view_id='siege_facing', + frame = {b=4, l=1, w=28}, + key='CUSTOM_T', + key_back='CUSTOM_SHIFT_T', + label='Facing:', + visible=is_siege_engine, + options={ + {label='North',value=0}, + {label='Northeast',value=1}, + {label='East',value=2}, + {label='Southeast',value=3}, + {label='South',value=4}, + {label='Southwest',value=5}, + {label='West',value=6}, + {label='Northwest',value=7}, + }, + }, widgets.ToggleHotkeyLabel { view_id='empty', frame={b=4, l=1, w=22}, @@ -829,7 +851,7 @@ function PlannerOverlay:init() } local divider_widget = widgets.Divider{ - frame={t=10, l=0, r=0, h=1}, + frame={t=11, l=0, r=0, h=1}, frame_style=gui.FRAME_INTERIOR_MEDIUM, visible=self:callback('is_not_minimized'), } @@ -1307,10 +1329,16 @@ function PlannerOverlay:place_building(placement_data, chosen_items) if is_stairs() then subtype = self:get_stairs_subtype(pos, pd) end + local fields = {} + if is_siege_engine() then + local val = self.subviews.siege_facing:getOptionValue() + fields.facing = val + fields.resting_orientation = val + end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, width=pd.width, height=pd.height, - direction=uibs.direction, filters=filters} + direction=uibs.direction, filters=filters, fields=fields} if err then -- it's ok if some buildings fail to build goto continue From dc7057fb22722fe06be5bdd4742064ac1ea0449d Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 5 Nov 2025 08:42:18 -0500 Subject: [PATCH 0548/1082] Use vanilla direction interface for siege engine facing direction --- plugins/lua/buildingplan/planneroverlay.lua | 29 +++++---------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 72987590fb..f0fbe17de4 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -239,6 +239,7 @@ local direction_panel_types = utils.invert{ df.building_type.WaterWheel, df.building_type.AxleHorizontal, df.building_type.Rollers, + df.building_type.SiegeEngine, } local function has_direction_panel() @@ -609,7 +610,7 @@ function PlannerOverlay:init() local main_panel = widgets.Panel{ view_id='main', - frame={t=1, l=0, r=0, h=15}, + 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'), @@ -747,24 +748,6 @@ function PlannerOverlay:init() buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) end, }, - widgets.CycleHotkeyLabel { - view_id='siege_facing', - frame = {b=4, l=1, w=28}, - key='CUSTOM_T', - key_back='CUSTOM_SHIFT_T', - label='Facing:', - visible=is_siege_engine, - options={ - {label='North',value=0}, - {label='Northeast',value=1}, - {label='East',value=2}, - {label='Southeast',value=3}, - {label='South',value=4}, - {label='Southwest',value=5}, - {label='West',value=6}, - {label='Northwest',value=7}, - }, - }, widgets.ToggleHotkeyLabel { view_id='empty', frame={b=4, l=1, w=22}, @@ -851,7 +834,7 @@ function PlannerOverlay:init() } local divider_widget = widgets.Divider{ - frame={t=11, l=0, r=0, h=1}, + frame={t=10, l=0, r=0, h=1}, frame_style=gui.FRAME_INTERIOR_MEDIUM, visible=self:callback('is_not_minimized'), } @@ -1331,9 +1314,9 @@ function PlannerOverlay:place_building(placement_data, chosen_items) end local fields = {} if is_siege_engine() then - local val = self.subviews.siege_facing:getOptionValue() - fields.facing = val - fields.resting_orientation = val + 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, From a939fc06fafb05c54b4c31c8b5f96eceb4f69660 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 6 Nov 2025 11:28:05 -0500 Subject: [PATCH 0549/1082] Fix boltthrower building size --- library/modules/Buildings.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index b7aeb55996..e5a0af116b 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -615,6 +615,23 @@ bool Buildings::getCorrectSize(df::coord2d &size, df::coord2d ¢er, return false; case SiegeEngine: + { + using namespace df::enums::siegeengine_type; + + switch((df::siegeengine_type)subtype) + { + case df::siegeengine_type::BoltThrower: + size = df::coord2d(1, 1); + center = df::coord2d(0, 0); + break; + default: + size = df::coord2d(3,3); + center = df::coord2d(1,1); + break; + } + return false; + } + case Windmill: case Wagon: size = df::coord2d(3,3); From 86cf16bd17a4be6a2c843ae2e171a4471db1151a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 7 Nov 2025 12:00:46 -0600 Subject: [PATCH 0550/1082] 53.02-r2 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e394d7d349..8edb346fe1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.02") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 8c8e39fb5a0dd6afe088827ebc52923f8fbb7ee1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 7 Nov 2025 12:16:03 -0600 Subject: [PATCH 0551/1082] changelog for 53.02-r2 --- docs/changelog.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 888f6350c5..bb3c7b769c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,7 +33,6 @@ Template for new versions: ## New Features ## Fixes -- `cleanconst`: do not attempt to clean Reinforced constructions ## Misc Improvements @@ -59,8 +58,27 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.02-r2 + +## New Tools + +## New Features + ## Fixes - `buildingplan`: Building costs for reinforced walls are now correct. +- `cleanconst`: do not attempt to clean Reinforced constructions ## Misc Improvements - `buildingplan`: Added support for bolt throwers and siege engine rotation. From bd9fc845861c7673d84791844bcea27afab14328 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 6 Nov 2025 14:38:59 -0600 Subject: [PATCH 0552/1082] update workflows to use gcc 11/13/15 instead of gcc 10/12, because it's not 2023 anymore --- .github/workflows/build-linux.yml | 2 +- .github/workflows/test.yml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3bc931305a..c2d946e300 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -66,7 +66,7 @@ on: default: false gcc-ver: type: string - default: "10" + default: "11" jobs: build-linux64: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a73bb3f82d..323d1a9883 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,9 +59,11 @@ jobs: fail-fast: false matrix: include: - - gcc: 10 + - gcc: 11 # baseline compatibility with ubuntu LTS 22.04 plugins: "default" - - gcc: 12 + - gcc: 13 # future compatibility with ubuntu LTS 24.04 + plugins: "all" + - gcc: 15 # cutting-edge plugins: "all" test-windows: From b30d606a2fc5670fcbf7040e104942f2568dbc7b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 6 Nov 2025 14:41:32 -0600 Subject: [PATCH 0553/1082] reduce due to limitations of LTS 22.04 --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 323d1a9883..a428435941 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,9 +61,7 @@ jobs: include: - gcc: 11 # baseline compatibility with ubuntu LTS 22.04 plugins: "default" - - gcc: 13 # future compatibility with ubuntu LTS 24.04 - plugins: "all" - - gcc: 15 # cutting-edge + - gcc: 12 # highest available in ubuntu 22.04 plugins: "all" test-windows: From 66807805d2148016aef06246cee338e538643296 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 6 Nov 2025 15:12:19 -0600 Subject: [PATCH 0554/1082] correct references to gcc-10 --- .github/workflows/test.yml | 2 +- docs/dev/github-workflows.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a428435941..175a1adf48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: dfhack_repo: ${{ inputs.dfhack_repo }} dfhack_ref: ${{ inputs.dfhack_ref }} os: ubuntu - compiler: gcc-10 + compiler: gcc-11 plugins: default config: default diff --git a/docs/dev/github-workflows.rst b/docs/dev/github-workflows.rst index fa1c36b66c..ea71e1a2b6 100644 --- a/docs/dev/github-workflows.rst +++ b/docs/dev/github-workflows.rst @@ -46,8 +46,8 @@ tuned our build and test workflows to minimize spurious cache misses and keep the fast path fast. Caches are namespaced by key prefixes, and we have one key prefix per build -context. For example, release builds on gcc-10 are kept in one cache namespace, -whereas test builds on gcc-10 are kept separate. MSVC release and test builds +context. For example, release builds on gcc-11 are kept in one cache namespace, +whereas test builds on gcc-11 are kept separate. MSVC release and test builds similarly have their own namespaces. Each cache has a maximum size that is enforced by the business logic that writes the cache data. From 324281d194621748a126b1e0d64a640463a13ed0 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:02:41 +0000 Subject: [PATCH 0555/1082] Auto-update structures ref for 53.03 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 99cb51ac84..1bc08a85ea 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 99cb51ac849a17652dbb74efa757df5b5716a9ba +Subproject commit 1bc08a85ea0038ac8ba2ca5d6203dddf7d840036 From 240414cc9e863007016340bef043cf03613bee88 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:10:32 +0000 Subject: [PATCH 0556/1082] 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 1bc08a85ea..145f317340 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1bc08a85ea0038ac8ba2ca5d6203dddf7d840036 +Subproject commit 145f317340ac9327854318c7ba758ee2af5e1928 From 30374d04db2d8591491703750628aa0ddb7c76d8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 10 Nov 2025 12:25:34 -0600 Subject: [PATCH 0557/1082] 53.03-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8edb346fe1..f14ed88878 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.02") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "53.03") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index bb3c7b769c..9ea0853484 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,25 @@ Template for new versions: ## Removed +# 53.03-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements +- Release builds for Linux are now compiled with gcc 11 + +## Documentation + +## API + +## Lua + +## Removed + # 53.02-r2 ## New Tools From fb2a14001cdbc1803d6be63cc3059dfc32bb3499 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Mon, 10 Nov 2025 23:20:39 -0500 Subject: [PATCH 0558/1082] use nullptr in Core --- library/Core.cpp | 30 +++++++++++++++--------------- library/include/Core.h | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b927d49cc4..0a47430d3b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -103,7 +103,7 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = NULL); +static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = nullptr); size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder); namespace DFHack { @@ -210,7 +210,7 @@ thread_local int CommandDepthCounter::depth = 0; void Core::cheap_tokenise(std::string const& input, std::vector& output) { - std::string *cur = NULL; + std::string *cur = nullptr; size_t i = 0; // Check the first non-space character @@ -241,7 +241,7 @@ void Core::cheap_tokenise(std::string const& input, std::vector& ou { unsigned char c = input[i]; if (isspace(c)) { - cur = NULL; + cur = nullptr; } else { if (!cur) { output.push_back(""); @@ -280,7 +280,7 @@ static void fHKthread(IODATA * iodata) { Core * core = iodata->core; PluginManager * plug_mgr = iodata->plug_mgr; - if(plug_mgr == 0 || core == 0) + if(!plug_mgr || !core) { std::cerr << "Hotkey thread has croaked." << std::endl; return; @@ -1424,7 +1424,7 @@ static void fIOthread(IODATA * iod) main_history.load(HISTORY_FILE.c_str()); Console & con = core->getConsole(); - if (plug_mgr == 0) + if (plug_mgr == nullptr) { con.printerr("Something horrible happened in Core's constructor...\n"); return; @@ -1496,7 +1496,7 @@ Core::Core() : toolCount{0} { // init the console. This must be always the first step! - plug_mgr = 0; + plug_mgr = nullptr; errorstate = false; vinfo = 0; memset(&(s_mods), 0, sizeof(s_mods)); @@ -1504,10 +1504,10 @@ Core::Core() : // set up hotkey capture suppress_duplicate_keyboard_events = true; hotkey_set = NO; - last_world_data_ptr = NULL; - last_local_map_ptr = NULL; + last_world_data_ptr = nullptr; + last_local_map_ptr = nullptr; last_pause_state = false; - top_viewscreen = NULL; + top_viewscreen = nullptr; color_ostream::log_errors_to_stderr = true; }; @@ -1531,7 +1531,7 @@ void Core::fatal (std::string output, const char * title) std::cout << "DFHack fatal error: " << out.str() << std::endl; if (!title) title = "DFHack error!"; - DFSDL::DFSDL_ShowSimpleMessageBox(0x10 /* SDL_MESSAGEBOX_ERROR */, title, out.str().c_str(), NULL); + DFSDL::DFSDL_ShowSimpleMessageBox(0x10 /* SDL_MESSAGEBOX_ERROR */, title, out.str().c_str(), nullptr); bool is_headless = bool(getenv("DFHACK_HEADLESS")); if (is_headless) @@ -2004,7 +2004,7 @@ void Core::doUpdate(color_ostream &out) Lua::Core::Reset(out, "DF code execution"); // find the current viewscreen - df::viewscreen *screen = NULL; + df::viewscreen *screen = nullptr; if (df::global::gview) { screen = &df::global::gview->view; @@ -2230,7 +2230,7 @@ namespace DFHack { } void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event event) { - static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable(0, + static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable(nullptr, (int)SC_WORLD_LOADED, "onLoad", "onLoadWorld", "onWorldLoaded", "", (int)SC_WORLD_UNLOADED, "onUnload", "onUnloadWorld", "onWorldUnloaded", "", (int)SC_MAP_LOADED, "onMapLoad", "onLoadMap", "", @@ -2330,7 +2330,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) else { char timebuf[30]; - time_t rawtime = time(NULL); + time_t rawtime = time(nullptr); struct tm * timeinfo = localtime(&rawtime); strftime(timebuf, sizeof(timebuf), "[%Y-%m-%dT%H:%M:%S%z] ", timeinfo); evtlog << timebuf; @@ -2416,7 +2416,7 @@ int Core::Shutdown ( void ) if(plug_mgr) { delete plug_mgr; - plug_mgr = 0; + plug_mgr = nullptr; } // invalidate all modules allModules.clear(); @@ -2998,7 +2998,7 @@ bool Process::patchMemory(void *target, const void* src, size_t count) #define MODULE_GETTER(TYPE) \ TYPE * Core::get##TYPE() \ { \ - if(errorstate) return NULL;\ + if(errorstate) return nullptr;\ if(!s_mods.p##TYPE)\ {\ std::unique_ptr mod = create##TYPE();\ diff --git a/library/include/Core.h b/library/include/Core.h index 383a0a9c95..49aaa50430 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -200,8 +200,8 @@ namespace DFHack std::filesystem::path getHackPath(); - bool isWorldLoaded() { return (last_world_data_ptr != NULL); } - bool isMapLoaded() { return (last_local_map_ptr != NULL && last_world_data_ptr != NULL); } + bool isWorldLoaded() { return (last_world_data_ptr != nullptr); } + bool isMapLoaded() { return (last_local_map_ptr != nullptr && last_world_data_ptr != nullptr); } static df::viewscreen *getTopViewscreen(); @@ -252,7 +252,7 @@ namespace DFHack void operator=(Core const&); // Don't implement // report error to user while failing - void fatal (std::string output, const char * title = NULL); + void fatal (std::string output, const char * title = nullptr); // 1 = fatal failure bool errorstate; From e94b51918463e3636e075e3d60a0a3af29fe875b Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Mon, 10 Nov 2025 23:37:52 -0500 Subject: [PATCH 0559/1082] Remove unused sortable struct --- library/Core.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b927d49cc4..253f84683f 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -301,21 +301,6 @@ static void fHKthread(IODATA * iodata) } } -struct sortable -{ - bool recolor; - std::string name; - std::string description; - //FIXME: Nuke when MSVC stops failing at being C++11 compliant - sortable(bool recolor_,const std::string& name_,const std::string & description_): recolor(recolor_), name(name_), description(description_){}; - bool operator <(const sortable & rhs) const - { - if( name < rhs.name ) - return true; - return false; - }; -}; - static std::string dfhack_version_desc() { std::stringstream s; From 31244ad82a6d83c9a91f5ffaa6fd6adaa3534cff Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 00:17:45 -0500 Subject: [PATCH 0560/1082] prefer lambdas over std::bind --- library/Core.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b927d49cc4..17754e6884 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -330,7 +330,7 @@ static std::string dfhack_version_desc() return s.str(); } -static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, std::vector& pargs) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, std::span pargs) { if (!lua_checkstack(state, pargs.size()+10)) return false; @@ -338,21 +338,23 @@ static bool init_run_script(color_ostream &out, lua_State *state, const std::str lua_getfield(state, -1, "run_script"); lua_remove(state, -2); lua_pushstring(state, pcmd.c_str()); - for (auto& arg : pargs) + for (const auto& arg : pargs) lua_pushstring(state, arg.c_str()); return true; } -static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) +static command_result runLuaScript(color_ostream &out, std::string name, std::span args) { - using namespace std::placeholders; - auto init_fn = std::bind(init_run_script, _1, _2, name, args); + auto init_fn = [n = std::move(name), args](color_ostream& out, lua_State* state) -> bool { + return init_run_script(out, state, n, args); + }; + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_fn); return ok ? CR_OK : CR_FAILURE; } -static bool init_enable_script(color_ostream &out, lua_State *state, std::string& name, bool enable) +static bool init_enable_script(color_ostream &out, lua_State *state, const std::string& name, bool enable) { if (!lua_checkstack(state, 4)) return false; @@ -364,10 +366,12 @@ static bool init_enable_script(color_ostream &out, lua_State *state, std::string return true; } -static command_result enableLuaScript(color_ostream &out, std::string name, bool state) +static command_result enableLuaScript(color_ostream &out, std::string name, bool enabled) { - using namespace std::placeholders; - auto init_fn = std::bind(init_enable_script, _1, _2, name, state); + auto init_fn = [n = std::move(name), enabled](color_ostream& out, lua_State* state) -> bool { + return init_enable_script(out, state, n, enabled); + }; + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_fn); return ok ? CR_OK : CR_FAILURE; From 31e3caecabd83c1f9b081aef461a7f5207935bff Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 02:02:55 -0500 Subject: [PATCH 0561/1082] attempt to use more modern c++20 --- library/Core.cpp | 184 +++++++++++++++++++++-------------------- library/include/Core.h | 2 +- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 17754e6884..0fa91ccee4 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -74,6 +74,7 @@ distribution. #include #include #include +#include #include #include #include @@ -806,11 +807,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s bool reload = (first == "reload"); if (parts.size()) { - for (auto p = parts.begin(); p != parts.end(); p++) + for (const auto& p : parts) { - if (p->size() && (*p)[0] == '-') + if (p.empty() && p[0] == '-') { - if (p->find('a') != std::string::npos) + if (p.find('a') != std::string::npos) all = true; } } @@ -826,15 +827,15 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else { - for (auto p = parts.begin(); p != parts.end(); p++) + for (auto& p : parts) { - if (!p->size() || (*p)[0] == '-') + if (p.empty() || p[0] == '-') continue; - if (load && !plug_mgr->load(*p)) + if (load && !plug_mgr->load(p)) ret = CR_FAILURE; - else if (unload && !plug_mgr->unload(*p)) + else if (unload && !plug_mgr->unload(p)) ret = CR_FAILURE; - else if (reload && !plug_mgr->reload(*p)) + else if (reload && !plug_mgr->reload(p)) ret = CR_FAILURE; } } @@ -854,16 +855,15 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if(parts.size()) { - for (size_t i = 0; i < parts.size(); i++) + for (auto& part : parts) { - std::string part = parts[i]; if (part.find('\\') != std::string::npos) { con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); - for (size_t j = 0; j < part.size(); j++) + for (char& c : part) { - if (part[j] == '\\') - part[j] = '/'; + if (c == '\\') + c = '/'; } } @@ -902,9 +902,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else { - for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it) + for (auto& [key, plug] : *plug_mgr) { - Plugin * plug = it->second; + if (!plug) + continue; if (!plug->can_be_enabled()) continue; con.print( @@ -929,13 +930,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s con.print(header_format, "Name", "State", "Cmds", "Enabled"); plug_mgr->refresh(); - for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it) + for (auto& [key, plug] : *plug_mgr) { - Plugin * plug = it->second; if (!plug) continue; - if (parts.size() && std::find(parts.begin(), parts.end(), plug->getName()) == parts.end()) + + if (std::ranges::find(parts, plug->getName()) == parts.end()) continue; + color_value color; switch (plug->getState()) { @@ -1012,9 +1014,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s std::string keystr = parts[1]; if (parts[0] == "set") ClearKeyBindings(keystr); - for (int i = parts.size()-1; i >= 2; i--) + // for (int i = parts.size()-1; i >= 2; i--) + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - if (!AddKeyBinding(keystr, parts[i])) { + if (!AddKeyBinding(keystr, part)) { con.printerr("Invalid key spec: %s\n", keystr.c_str()); break; } @@ -1022,10 +1025,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (parts.size() >= 2 && parts[0] == "clear") { - for (size_t i = 1; i < parts.size(); i++) + // for (size_t i = 1; i < parts.size(); i++) + for (const auto& part : parts | std::views::drop(1)) { - if (!ClearKeyBindings(parts[i])) { - con.printerr("Invalid key spec: %s\n", parts[i].c_str()); + if (!ClearKeyBindings(part)) { + con.printerr("Invalid key spec: %s\n", part.c_str()); break; } } @@ -1035,8 +1039,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s std::vector list = ListKeyBindings(parts[1]); if (list.empty()) con << "No bindings." << std::endl; - for (size_t i = 0; i < list.size(); i++) - con << " " << list[i] << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; } else { @@ -1121,9 +1125,9 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else if (first == "kill-lua") { bool force = false; - for (auto it = parts.begin(); it != parts.end(); ++it) + for (const auto& part : parts) { - if (*it == "force") + if (part == "force") force = true; } if (!Lua::Interrupt(force)) @@ -1189,14 +1193,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s con << "Unrecognized event name: " << parts[1] << std::endl; return CR_WRONG_USAGE; } - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) + for (const auto& state_script : state_change_scripts) { - if (!parts[1].size() || (it->event == sc_event_id(parts[1]))) + if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) { - con.print("%s (%s): %s%s\n", sc_event_name(it->event).c_str(), - it->save_specific ? "save-specific" : "global", - it->save_specific ? "/raw/" : "/", - it->path.c_str()); + con.print("%s (%s): %s%s\n", sc_event_name(state_script.event).c_str(), + state_script.save_specific ? "save-specific" : "global", + state_script.save_specific ? "/raw/" : "/", + state_script.path.c_str()); } } return CR_OK; @@ -1216,9 +1220,9 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); StateChangeScript script(evt, parts[2], save_specific); - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) + for (const auto& state_script : state_change_scripts) { - if (script == *it) + if (script == state_script) { con << "Script already registered" << std::endl; return CR_FAILURE; @@ -1448,7 +1452,7 @@ static void fIOthread(IODATA * iod) while (true) { - std::string command = ""; + std::string command; int ret; while ((ret = con.lineedit("[DFHack]# ",command, main_history)) == Console::RETRY); @@ -1812,7 +1816,7 @@ bool Core::InitSimulationThread() if (entry.second) continue; std::filesystem::path filename = entry.first; - if (!config_files.count(filename)) { + if (!config_files.contains(filename)) { std::filesystem::path src_file = getConfigDefaultsPath() / filename; if (!Filesystem::isfile(src_file)) continue; @@ -1855,7 +1859,7 @@ bool Core::InitSimulationThread() plug_mgr->init(); std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); - IODATA *temp = new IODATA; + auto *temp = new IODATA; temp->core = this; temp->plug_mgr = plug_mgr; @@ -1890,13 +1894,13 @@ bool Core::InitSimulationThread() if (raw[offset] == '"') { offset++; - size_t next = raw.find("\"", offset); + size_t next = raw.find('"', offset); args.push_back(raw.substr(offset, next - offset)); offset = next + 2; } else { - size_t next = raw.find(" ", offset); + size_t next = raw.find(' ', offset); if (next == std::string::npos) { args.push_back(raw.substr(offset)); @@ -1915,9 +1919,9 @@ bool Core::InitSimulationThread() if (first.length() > 0 && first[0] == '+') { std::vector cmd; - for (it++; it != args.end(); it++) { - const std::string & arg = *it; - if (arg.length() > 0 && arg[0] == '+') + for (const auto& arg : args) + { + if (!arg.empty() && arg[0] == '+') { break; } @@ -1927,9 +1931,9 @@ bool Core::InitSimulationThread() if (runCommand(con, first.substr(1), cmd) != CR_OK) { std::cerr << "Error running command: " << first.substr(1); - for (auto it2 = cmd.begin(); it2 != cmd.end(); it2++) + for (const auto& arg : cmd) { - std::cerr << " \"" << *it2 << "\""; + std::cerr << " \"" << arg << "\""; } std::cerr << "\n"; } @@ -1953,7 +1957,7 @@ bool Core::setHotkeyCmd( std::string cmd ) // access command std::lock_guard lock(HotkeyMutex); hotkey_set = SET; - hotkey_cmd = cmd; + hotkey_cmd = std::move(cmd); HotkeyCond.notify_all(); return true; } @@ -2170,43 +2174,44 @@ void Core::onUpdate(color_ostream &out) perf_counters.incCounter(perf_counters.update_lua_ms, step_start_ms); } -void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { +static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { std::vector files; DFHack::Filesystem::listdir(folder, files); - for ( auto f : files) { + for ( const auto& f : files) { if (f.stem().string().starts_with(prefix) && f.extension() == suffix) result.push_back(f); } - return; } size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder) { static const std::string suffix = ".init"; std::vector scriptFiles; - for ( size_t a = 0; a < prefix.size(); a++ ) { - getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles); + for ( const auto& p : prefix ) { + getFilesWithPrefixAndSuffix(folder, p, ".init", scriptFiles); } - std::sort(scriptFiles.begin(), scriptFiles.end(), - [](const std::filesystem::path& a, const std::filesystem::path& b) { - return a.stem() < b.stem(); - }); + + std::ranges::sort(scriptFiles, + [](const std::filesystem::path& a, const std::filesystem::path& b) { + return a.stem() < b.stem(); + }); + size_t result = 0; - for ( size_t a = 0; a < scriptFiles.size(); a++ ) { + for ( const auto& file : scriptFiles) { result++; - core->loadScriptFile(out, folder / scriptFiles[a], false); + core->loadScriptFile(out, folder / file, false); } return result; } namespace DFHack { namespace X { - typedef state_change_event Key; - typedef std::vector Val; - typedef std::pair Entry; - typedef std::vector EntryVector; - typedef std::map InitVariationTable; + using Key = state_change_event; + using Val = std::vector; + using Entry = std::pair; + using EntryVector = std::vector; + using InitVariationTable = std::map; - EntryVector computeInitVariationTable(void* none, ...) { + static EntryVector computeInitVariationTable(void* none, ...) { va_list list; va_start(list,none); EntryVector result; @@ -2227,7 +2232,7 @@ namespace DFHack { return result; } - InitVariationTable getTable(const EntryVector& vec) { + static InitVariationTable getTable(const EntryVector& vec) { return InitVariationTable(vec.begin(),vec.end()); } } @@ -2258,17 +2263,17 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve loadScriptFiles(this, out, set, rawFolder); } - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) + for (const auto& script : state_change_scripts) { - if (it->event == event) + if (script.event == event) { - if (!it->save_specific) + if (!script.save_specific) { - loadScriptFile(out, it->path, false); + loadScriptFile(out, script.path, false); } - else if (it->save_specific && isWorldLoaded()) + else if (script.save_specific && isWorldLoaded()) { - loadScriptFile(out, rawFolder / it->path, false); + loadScriptFile(out, rawFolder / script.path, false); } } } @@ -2278,7 +2283,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) { using df::global::gametype; static md5wrapper md5w; - static std::string ostype = ""; + static std::string ostype; if (!ostype.size()) { @@ -2480,7 +2485,7 @@ bool Core::DFH_ncurses_key(int key) return ncurses_wgetch(key, dummy); } -bool Core::getSuppressDuplicateKeyboardEvents() { +bool Core::getSuppressDuplicateKeyboardEvents() const { return suppress_duplicate_keyboard_events; } @@ -2605,8 +2610,8 @@ bool Core::SelectHotkey(int sym, int modifiers) // Check the internal keybindings std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - auto &binding = bindings[i]; + //for (int i = bindings.size()-1; i >= 0; --i) { + for (const auto& binding : bindings | std::views::reverse) { DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); if (binding.modifiers != modifiers) { @@ -2662,8 +2667,8 @@ bool Core::SelectHotkey(int sym, int modifiers) setHotkeyCmd(cmd); return true; } - else - return false; + + return false; } static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus) @@ -2684,13 +2689,13 @@ static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string // ugh, ugly for (;;) { - if (keyspec.size() > 6 && keyspec.substr(0, 6) == "Shift-") { + if (keyspec.size() > 6 && keyspec.starts_with("Shift-")) { *pmod |= 1; keyspec = keyspec.substr(6); - } else if (keyspec.size() > 5 && keyspec.substr(0, 5) == "Ctrl-") { + } else if (keyspec.size() > 5 && keyspec.starts_with("Ctrl-")) { *pmod |= 2; keyspec = keyspec.substr(5); - } else if (keyspec.size() > 4 && keyspec.substr(0, 4) == "Alt-") { + } else if (keyspec.size() > 4 && keyspec.starts_with("Alt-")) { *pmod |= 4; keyspec = keyspec.substr(4); } else @@ -2709,13 +2714,13 @@ static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { *psym = SDLK_F1 + (keyspec[1]-'1'); return true; - } else if (keyspec.size() == 3 && keyspec.substr(0, 2) == "F1" && keyspec[2] >= '0' && keyspec[2] <= '2') { + } else if (keyspec.size() == 3 && keyspec.starts_with("F1") && keyspec[2] >= '0' && keyspec[2] <= '2') { *psym = SDLK_F10 + (keyspec[2]-'0'); return true; - } else if (keyspec.size() == 6 && keyspec.substr(0, 5) == "MOUSE" && keyspec[5] >= '4' && keyspec[5] <= '9') { + } else if (keyspec.size() == 6 && keyspec.starts_with("MOUSE") && keyspec[5] >= '4' && keyspec[5] <= '9') { *psym = SDLK_F13 + (keyspec[5]-'4'); return true; - } else if (keyspec.size() == 7 && keyspec.substr(0, 6) == "MOUSE1" && keyspec[5] >= '0' && keyspec[5] <= '5') { + } else if (keyspec.size() == 7 && keyspec.starts_with("MOUSE1") && keyspec[5] >= '0' && keyspec[5] <= '5') { *psym = SDLK_F19 + (keyspec[5]-'0'); return true; } else if (keyspec == "Enter") { @@ -2754,9 +2759,9 @@ bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) { std::vector focus_strings; split_string(&focus_strings, raw_focus, "|"); - for (size_t i = 0; i < focus_strings.size(); i++) + for (const auto& fs : focus_strings) { - if (!AddKeyBinding(raw_spec + "@" + focus_strings[i], cmdline)) + if (!AddKeyBinding(raw_spec + "@" + fs, cmdline)) return false; } return true; @@ -2838,7 +2843,7 @@ bool Core::RemoveAlias(const std::string &name) bool Core::IsAlias(const std::string &name) { std::lock_guard lock(alias_mutex); - return aliases.find(name) != aliases.end(); + return aliases.contains(name); } bool Core::RunAlias(color_ostream &out, const std::string &name, @@ -2903,10 +2908,9 @@ bool ClassNameCheck::operator() (Process *p, void * ptr) const { void ClassNameCheck::getKnownClassNames(std::vector &names) { - std::set::iterator it = known_class_names.begin(); - - for (; it != known_class_names.end(); it++) - names.push_back(*it); + for(const auto& kcn : known_class_names) { + names.push_back(kcn); + } } MemoryPatcher::MemoryPatcher(Process *p_) : p(p_) @@ -2922,7 +2926,7 @@ MemoryPatcher::~MemoryPatcher() bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write) { - uint8_t *sptr = (uint8_t*)target; + auto *sptr = (uint8_t*)target; uint8_t *eptr = sptr + count; // Find the valid memory ranges diff --git a/library/include/Core.h b/library/include/Core.h index 383a0a9c95..50cfd8909b 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -180,7 +180,7 @@ namespace DFHack std::filesystem::path findScript(std::string name); void getScriptPaths(std::vector *dest); - bool getSuppressDuplicateKeyboardEvents(); + bool getSuppressDuplicateKeyboardEvents() const; void setSuppressDuplicateKeyboardEvents(bool suppress); void setMortalMode(bool value); void setArmokTools(const std::vector &tool_names); From a5717a66d6d0a5bf18079ce129511f1a37e6c363 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 02:21:26 -0500 Subject: [PATCH 0562/1082] add missing include --- library/Core.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Core.cpp b/library/Core.cpp index 0fa91ccee4..1fee8000e6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -75,6 +75,7 @@ distribution. #include #include #include +#include #include #include #include From f9fae1391af320e690cdce0ab1edcbac577ddde5 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 02:43:41 -0500 Subject: [PATCH 0563/1082] fix typo --- library/Core.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 1fee8000e6..ef271b980d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -80,7 +80,6 @@ distribution. #include #include #include -#include #include #include #include @@ -810,7 +809,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { for (const auto& p : parts) { - if (p.empty() && p[0] == '-') + if (p.empty()) + continue; + + if (p[0] == '-') { if (p.find('a') != std::string::npos) all = true; From c454de10d85e36d7e377e010d3bc274ab17225b1 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:12:25 -0500 Subject: [PATCH 0564/1082] use size() as before --- library/Core.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 3371746995..7bd11562e3 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -794,10 +794,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { for (const auto& p : parts) { - if (p.empty()) - continue; - - if (p[0] == '-') + if (p.size() && p[0] == '-') { if (p.find('a') != std::string::npos) all = true; From 16d7e985a3d60c61b7f1bc8d90a242e4786b8686 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:59:57 -0500 Subject: [PATCH 0565/1082] typo: missing parts.size() test for 'plug' without arguments. use std::ranges::end() iterator. --- library/Core.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 7bd11562e3..14d82a30aa 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -895,7 +895,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s con.print( "%21s %-3s%s\n", - (plug->getName()+":").c_str(), + (key+":").c_str(), plug->is_enabled() ? "on" : "off", plug->can_set_enabled() ? "" : " (controlled internally)" ); @@ -920,7 +920,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (!plug) continue; - if (std::ranges::find(parts, plug->getName()) == parts.end()) + if (parts.size() && std::ranges::find(parts, key) == std::ranges::end(parts)) continue; color_value color; From a52f36b895c89a3d2221ecf1ad54b4147ca4cc38 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:49:30 -0500 Subject: [PATCH 0566/1082] mostly add utility functions for backslash handling to MiscUtils.h --- library/Core.cpp | 32 ++++++++++++-------------------- library/include/MiscUtils.h | 13 +++++++++++++ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 14d82a30aa..b49784db4b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -105,7 +105,7 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = nullptr); -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder); +static size_t loadScriptFiles(Core* core, color_ostream& out, std::span prefix, const std::filesystem::path& folder); namespace DFHack { @@ -316,7 +316,7 @@ static std::string dfhack_version_desc() return s.str(); } -static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, std::span pargs) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, const std::span pargs) { if (!lua_checkstack(state, pargs.size()+10)) return false; @@ -329,7 +329,7 @@ static bool init_run_script(color_ostream &out, lua_State *state, const std::str return true; } -static command_result runLuaScript(color_ostream &out, std::string name, std::span args) +static command_result runLuaScript(color_ostream &out, std::string name, const std::span args) { auto init_fn = [n = std::move(name), args](color_ostream& out, lua_State* state) -> bool { return init_run_script(out, state, n, args); @@ -673,14 +673,14 @@ void tags_helper(color_ostream &con, const std::string &tag) { } } -void ls_helper(color_ostream &con, const std::vector ¶ms) { +static void ls_helper(color_ostream &con, const std::span params) { std::vector filter; bool skip_tags = false; bool show_dev_commands = false; - std::string exclude_strs = ""; + std::string exclude_strs; bool in_exclude = false; - for (auto str : params) { + for (const auto& str : params) { if (in_exclude) exclude_strs = str; else if (str == "--notags") @@ -733,14 +733,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (first.empty()) return CR_NOT_IMPLEMENTED; - if (first.find('\\') != std::string::npos) + if (has_backslashes(first)) { con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", first.c_str()); - for (size_t i = 0; i < first.size(); i++) - { - if (first[i] == '\\') - first[i] = '/'; - } + replace_backslashes_with_forwardslashes(first); } // let's see what we actually got @@ -842,14 +838,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { for (auto& part : parts) { - if (part.find('\\') != std::string::npos) + if (has_backslashes(part)) { con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); - for (char& c : part) - { - if (c == '\\') - c = '/'; - } + replace_backslashes_with_forwardslashes(part); } part = GetAliasCommand(part, true); @@ -920,7 +912,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (!plug) continue; - if (parts.size() && std::ranges::find(parts, key) == std::ranges::end(parts)) + if (parts.size() && std::ranges::find(parts, key) == parts.end()) continue; color_value color; @@ -2168,7 +2160,7 @@ static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, con } } -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder) { +size_t loadScriptFiles(Core* core, color_ostream& out, const std::span prefix, const std::filesystem::path& folder) { static const std::string suffix = ".init"; std::vector scriptFiles; for ( const auto& p : prefix ) { diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 2ff04282e4..1d703b2f83 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -475,6 +475,19 @@ static inline std::string &trim(std::string &s) { return ltrim(rtrim(s)); } +static inline bool has_backslashes(const std::string_view str) +{ + return (str.find('\\') != std::string::npos); +} + +static inline void replace_backslashes_with_forwardslashes(std::string& str) +{ + for (auto& c : str) { + if (c == '\\') + c = '/'; + } +} + enum struct NumberFormatType : int32_t { DEFAULT = 0, ENGLISH, From 6ffc9d87129d5b1cce7ecf749e7167c41a86e3b2 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:00:44 -0500 Subject: [PATCH 0567/1082] try to cleanup unused includes from headers --- library/include/BitArray.h | 3 --- library/include/Core.h | 1 - library/include/DataDefs.h | 2 -- library/include/DataFuncs.h | 4 ---- library/include/DataIdentity.h | 4 +--- library/include/Error.h | 1 - library/include/LuaTools.h | 3 +-- library/include/LuaWrapper.h | 5 ----- library/include/MemAccess.h | 1 - library/include/MiscUtils.h | 1 - library/include/PluginManager.h | 1 - library/include/Types.h | 2 -- library/include/VTableInterpose.h | 3 ++- library/include/modules/DFSDL.h | 1 - library/include/modules/EventManager.h | 2 -- library/include/modules/Gui.h | 3 --- library/include/modules/Items.h | 2 -- library/include/modules/Job.h | 3 --- library/include/modules/Maps.h | 2 -- library/include/modules/Materials.h | 2 -- library/include/modules/Random.h | 3 --- library/include/modules/Screen.h | 3 --- library/include/modules/Translation.h | 4 ++-- library/include/modules/World.h | 4 +--- library/modules/DFSDL.cpp | 1 + library/modules/EventManager.cpp | 1 + plugins/autofarm.cpp | 1 + plugins/buildingplan/itemfilter.h | 2 ++ plugins/burrow.cpp | 2 ++ plugins/eventful.cpp | 1 + plugins/filltraffic.cpp | 1 + plugins/getplants.cpp | 2 ++ plugins/pathable.cpp | 2 ++ plugins/reveal.cpp | 1 + 34 files changed, 21 insertions(+), 53 deletions(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index bad77d7935..15ec84a62f 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -23,14 +23,11 @@ distribution. */ #pragma once -#include "Export.h" #include "Error.h" #include #include #include #include -#include -#include #include namespace DFHack { diff --git a/library/include/Core.h b/library/include/Core.h index 49aaa50430..bced326fd1 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -37,7 +37,6 @@ distribution. #include #include #include -#include #include #include #include diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 7fac3c0c5e..477496673f 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -26,10 +26,8 @@ distribution. #include #include -#include #include #include -#include #include #include diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index 0b297a68e4..ab9bb78a26 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -24,10 +24,6 @@ distribution. #pragma once -#include -#include -#include -#include #include #include "ColorText.h" diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 2c302264d9..8a9e3c406b 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -26,11 +26,9 @@ distribution. #include #include -#include #include -#include #include -#include +#include #include #include #include diff --git a/library/include/Error.h b/library/include/Error.h index a4624d5f72..550e1b0198 100644 --- a/library/include/Error.h +++ b/library/include/Error.h @@ -25,7 +25,6 @@ distribution. #pragma once #include -#include #include #include "Export.h" diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 9831841ca2..93853468e4 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -26,8 +26,8 @@ distribution. #include #include -#include #include +#include #include #include #include @@ -39,7 +39,6 @@ distribution. #include "DataDefs.h" #include "df/interface_key.h" -#include "df/interfacest.h" #include #include diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 1b8be705ed..7576be7a11 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -24,11 +24,6 @@ distribution. #pragma once -#include -#include -#include -#include - #include #include diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 95f7e256d1..5ef352a99e 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -29,7 +29,6 @@ distribution. #define PROCESS_H_INCLUDED #include "Export.h" -#include #include #include #include diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 2ff04282e4..fd78523210 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -33,7 +33,6 @@ distribution. #include #include #include -#include #include #include #include diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index 50b9b0447d..1eccc7a79a 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -25,7 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Hooks.h" #include "ColorText.h" #include "MiscUtils.h" #include diff --git a/library/include/Types.h b/library/include/Types.h index 84408ecf2a..f367946e56 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -30,12 +30,10 @@ distribution. #include #include "Export.h" - #include "DataDefs.h" #include "df/general_ref_type.h" #include "df/specific_ref_type.h" -#include "df/language_name_type.h" namespace df { struct building; diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h index 7575ee18c2..9dff74d7e5 100644 --- a/library/include/VTableInterpose.h +++ b/library/include/VTableInterpose.h @@ -25,7 +25,8 @@ distribution. #pragma once #include "DataDefs.h" -#include "DataIdentity.h" + +#include namespace DFHack { diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 8d12de1918..ff8e81ab2c 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -1,6 +1,5 @@ #pragma once -#include "Error.h" #include "Export.h" #include "ColorText.h" diff --git a/library/include/modules/EventManager.h b/library/include/modules/EventManager.h index e0a74bed92..a2c546a755 100644 --- a/library/include/modules/EventManager.h +++ b/library/include/modules/EventManager.h @@ -1,10 +1,8 @@ #pragma once -#include "Core.h" #include "Export.h" #include "ColorText.h" #include "PluginManager.h" -#include "Console.h" #include "DataDefs.h" #include "df/unit_inventory_item.h" diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 63939fc247..761e948bb0 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -25,8 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Module.h" -#include "BitArray.h" #include "ColorText.h" #include "Types.h" #include "DataDefs.h" @@ -34,7 +32,6 @@ distribution. #include "modules/GuiHooks.h" #include "df/announcement_type.h" -#include "df/report_zoom_type.h" #include "df/unit_report_type.h" namespace df { diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 1189fd8bc2..b4235df11d 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -28,8 +28,6 @@ distribution. */ #include "DataDefs.h" #include "Export.h" -#include "MemAccess.h" -#include "Module.h" #include "Types.h" #include "modules/Materials.h" diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index 5f7ddcb11a..25c357bec7 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -27,7 +27,6 @@ distribution. #define CL_MOD_JOB #include "Export.h" -#include "Module.h" #include "Types.h" #include "DataDefs.h" @@ -35,8 +34,6 @@ distribution. #include "df/item_type.h" #include "df/job_item_ref.h" -#include - namespace df { struct job; diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index 839989237e..052dbe3aab 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -31,8 +31,6 @@ distribution. #define CL_MOD_MAPS #include "Export.h" -#include "Module.h" -#include "BitArray.h" #include "modules/Materials.h" diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 90907b8a41..2c3be4e731 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -31,8 +31,6 @@ distribution. */ #include "Export.h" #include "Module.h" -#include "Types.h" -#include "BitArray.h" #include "DataDefs.h" #include "df/craft_material_class.h" diff --git a/library/include/modules/Random.h b/library/include/modules/Random.h index 8ed703be79..b8e5de26bf 100644 --- a/library/include/modules/Random.h +++ b/library/include/modules/Random.h @@ -31,9 +31,6 @@ distribution. */ #include "Export.h" -#include "Module.h" -#include "Types.h" - #include "DataDefs.h" namespace DFHack diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h index 3989460bb6..1a6ab569ad 100644 --- a/library/include/modules/Screen.h +++ b/library/include/modules/Screen.h @@ -25,9 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Module.h" -#include "BitArray.h" -#include "ColorText.h" #include "Types.h" #include "DataDefs.h" diff --git a/library/include/modules/Translation.h b/library/include/modules/Translation.h index 2709dbf6fa..2860c3db87 100644 --- a/library/include/modules/Translation.h +++ b/library/include/modules/Translation.h @@ -31,9 +31,9 @@ distribution. */ #include "Export.h" -#include "Module.h" -#include "Types.h" #include "DataDefs.h" +#include "Types.h" +#include "df/language_name_type.h" namespace df { struct language_name; diff --git a/library/include/modules/World.h b/library/include/modules/World.h index b03170b5fa..a82ceef9a4 100644 --- a/library/include/modules/World.h +++ b/library/include/modules/World.h @@ -32,12 +32,10 @@ distribution. */ #include "Export.h" -#include "Module.h" #include "modules/Persistence.h" -#include - #include "DataDefs.h" + namespace df { struct tile_bitmask; diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 536d1a5881..9da2bd8057 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -1,3 +1,4 @@ +#include "Error.h" #include "Internal.h" #include "modules/DFSDL.h" diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 5a24f1174a..d504e74c3d 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -2,6 +2,7 @@ #include "Console.h" #include "Debug.h" #include "VTableInterpose.h" +#include "MemAccess.h" #include "modules/Buildings.h" #include "modules/Constructions.h" diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index 571cd13608..f961bc13a7 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -22,6 +22,7 @@ #include "df/unit.h" #include "df/world.h" +#include #include using namespace DFHack; diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 8a1f67d3cf..95a216c302 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -7,6 +7,8 @@ #include "df/dfhack_material_category.h" #include "df/item_quality.h" +#include + class ItemFilter { public: ItemFilter(); diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp index d06e95efb6..b016014d0c 100644 --- a/plugins/burrow.cpp +++ b/plugins/burrow.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" diff --git a/plugins/eventful.cpp b/plugins/eventful.cpp index 0ed06d9284..8dd3bbc85d 100644 --- a/plugins/eventful.cpp +++ b/plugins/eventful.cpp @@ -22,6 +22,7 @@ #include "df/unit_wound.h" #include "df/world.h" +#include #include #include #include diff --git a/plugins/filltraffic.cpp b/plugins/filltraffic.cpp index c8b0a209e3..f16e8bdd83 100644 --- a/plugins/filltraffic.cpp +++ b/plugins/filltraffic.cpp @@ -1,6 +1,7 @@ // Wide-area traffic designation utility. // Flood-fill from cursor or fill entire map. +#include #include //For toupper(). #include //for min(). #include diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 29c6655fc9..18570fda95 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -21,6 +21,8 @@ #include "df/world_object_data.h" #include "df/world_site.h" +#include + using std::string; using std::vector; using std::set; diff --git a/plugins/pathable.cpp b/plugins/pathable.cpp index 3bf9e2c010..45a5827df6 100644 --- a/plugins/pathable.cpp +++ b/plugins/pathable.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "Error.h" #include "PluginManager.h" diff --git a/plugins/reveal.cpp b/plugins/reveal.cpp index 1feb72bbc6..e5bd2479f5 100644 --- a/plugins/reveal.cpp +++ b/plugins/reveal.cpp @@ -16,6 +16,7 @@ #include "df/map_block.h" #include "df/world.h" +#include #include using std::string; From 3156695bbd48bab2af6089129f5a26679c96684a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 13 Nov 2025 13:05:35 -0600 Subject: [PATCH 0568/1082] use `unordered_map` instead of `map` for lookasides some of these are used in fairly hot code, and so using `unordered_map` should improve performance --- library/include/DataDefs.h | 46 +++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 7fac3c0c5e..a067b8e24a 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -31,7 +31,9 @@ distribution. #include #include #include +#include #include +#include #include "BitArray.h" #include "Export.h" @@ -136,8 +138,8 @@ namespace DFHack class DFHACK_EXPORT compound_identity : public constructed_identity { static std::list* list; - static std::map* parent_map; - static std::map>* children_map; + static std::unordered_map* parent_map; + static std::unordered_map>* children_map; static std::vector* top_scope; const char *dfhack_name; @@ -204,7 +206,7 @@ namespace DFHack class DFHACK_EXPORT enum_identity : public compound_identity { public: struct ComplexData { - std::map value_index_map; + std::unordered_map value_index_map; std::vector index_value_map; ComplexData(std::initializer_list values); size_t size() const { @@ -289,8 +291,8 @@ namespace DFHack }; class DFHACK_EXPORT struct_identity : public compound_identity { - static std::map* parent_map; - static std::map>* children_map; + static std::unordered_map* parent_map; + static std::unordered_map>* children_map; const struct_field_info *fields; @@ -355,6 +357,30 @@ namespace DFHack virtual void build_metatable(lua_State *state) const; }; + namespace + { + template + struct overload : Bases ... + { + using is_transparent = void; + using Bases::operator() ...; + }; + + struct char_pointer_hash + { + auto operator()(const char* ptr) const noexcept + { + return std::hash{}(ptr); + } + }; + + using transparent_string_hash = overload< + std::hash, + std::hash, + char_pointer_hash + >; + } + #ifdef _MSC_VER using virtual_ptr = void*; #else @@ -367,13 +393,13 @@ namespace DFHack class DFHACK_EXPORT virtual_identity : public struct_identity { public: using interpose_t = VMethodInterposeLinkBase*; - using interpose_list_t = std::map; + using interpose_list_t = std::unordered_map; private: - static std::map> *name_lookup; - static std::map* known; - static std::map* vtable_ptr_map; - static std::map* interpose_list_map; + static std::unordered_map> *name_lookup; + static std::unordered_map* known; + static std::unordered_map* vtable_ptr_map; + static std::unordered_map* interpose_list_map; const char *original_name; From c94555901e2a845f126e1eb0d62f7d0fb6e23e0f Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 13 Nov 2025 21:27:28 -0500 Subject: [PATCH 0569/1082] Require empty bins when constructing bolt throwers --- docs/changelog.txt | 1 + library/lua/dfhack/buildings.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ea0853484..f91fe08e44 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `buildingplan`: Bolt throwers will no longer be constructed using populated bins. ## Misc Improvements diff --git a/library/lua/dfhack/buildings.lua b/library/lua/dfhack/buildings.lua index 5ea3b1f758..ae4d624360 100644 --- a/library/lua/dfhack/buildings.lua +++ b/library/lua/dfhack/buildings.lua @@ -358,6 +358,7 @@ local siegeengine_input = { vector_id=df.job_item_vector_id.BOLT_THROWER_PARTS, }, { + flags1={ empty=true }, item_type=df.item_type.BIN, vector_id=df.job_item_vector_id.BIN, }, From 0871519d7c9af8a9b31fa5327a05c550f742218e Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 5 Nov 2025 21:11:49 -0500 Subject: [PATCH 0570/1082] Initial implementation for a linux crashlog --- library/CMakeLists.txt | 1 + library/Core.cpp | 10 ++++++ library/Crashlog.cpp | 81 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 library/Crashlog.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 895ae6eb86..fa5c482bf6 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -136,6 +136,7 @@ endif() set(MAIN_SOURCES_LINUX ${CONSOLE_SOURCES} + Crashlog.cpp ) set(MAIN_SOURCES_DARWIN diff --git a/library/Core.cpp b/library/Core.cpp index faec04db61..42c7dda2cb 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1540,6 +1540,11 @@ bool Core::InitMainThread() { Filesystem::init(); + #ifdef LINUX_BUILD + extern void dfhack_crashlog_init(); + dfhack_crashlog_init(); + #endif + // Re-route stdout and stderr again - DF seems to set up stdout and // stderr.txt on Windows as of 0.43.05. Also, log before switching files to // make it obvious what's going on if someone checks the *.txt files. @@ -2375,6 +2380,11 @@ void Core::onStateChange(color_ostream &out, state_change_event event) int Core::Shutdown ( void ) { + #ifdef LINUX_BUILD + extern void dfhack_crashlog_shutdown(); + dfhack_crashlog_shutdown(); + #endif + if(errorstate) return true; errorstate = 1; diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp new file mode 100644 index 0000000000..fa2ec6dad5 --- /dev/null +++ b/library/Crashlog.cpp @@ -0,0 +1,81 @@ +#include "DFHackVersion.h" +#include +#include +#include +#include +#include + +#include + +const int BT_ENTRY_MAX = 25; +int bt_entries = 0; +void* bt[BT_ENTRY_MAX]; +int crash_signal = 0; + +std::binary_semaphore crashlog_ready{0}; +std::binary_semaphore crashlog_complete{0}; + +std::thread crashlog_thread; +volatile bool shutdown = false; + +extern "C" void dfhack_crashlog_handle_signal(int sig) { + crash_signal = sig; + bt_entries = backtrace(bt, BT_ENTRY_MAX); + + // Signal saving of crashlog and wait for completion + crashlog_ready.release(); + crashlog_complete.acquire(); + std::quick_exit(1); +} + +void dfhack_save_crashlog() { + char** backtrace_strings = backtrace_symbols(bt, bt_entries); + if (!backtrace_strings) { + // Something has gone terribly wrong + return; + } + std::filesystem::path crashlog_path = "./crash.txt"; + std::ofstream crashlog(crashlog_path); + + crashlog << "Dwarf Fortress has crashed!" << "\n"; + crashlog << "DwarfFortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; + + for (int i = 0; i < bt_entries; i++) { + crashlog << i << "> " << backtrace_strings[i] << "\n"; + } + + free(backtrace_strings); +} + +void dfhack_crashlog_thread() { + // Wait for crash or shutdown signal + crashlog_ready.acquire(); + if (shutdown) + return; + + dfhack_save_crashlog(); + crashlog_complete.release(); + std::quick_exit(1); +} + +const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; +namespace DFHack { +void dfhack_crashlog_init() { + for (int signal : desired_signals) { + std::signal(signal, dfhack_crashlog_handle_signal); + } + + // Ensure the library is initialized to avoid AsyncSignal-Unsafe init during crash + int _ = backtrace(bt, 1); + + crashlog_thread = std::thread(dfhack_crashlog_thread); +} + +void dfhack_crashlog_shutdown() { + shutdown = true; + crashlog_ready.release(); + crashlog_thread.join(); + return; +} +} From 093896d2d8e21be084a951465192c16392a10cd9 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 6 Nov 2025 11:11:32 -0500 Subject: [PATCH 0571/1082] Improve linux crashlog functionality and safety --- library/Crashlog.cpp | 119 ++++++++++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 34 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index fa2ec6dad5..2c8ac0f17f 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,47 +1,94 @@ #include "DFHackVersion.h" #include #include -#include #include #include #include const int BT_ENTRY_MAX = 25; -int bt_entries = 0; -void* bt[BT_ENTRY_MAX]; -int crash_signal = 0; +struct CrashInfo { + int backtrace_entries = 0; + void* backtrace[BT_ENTRY_MAX]; + int signal = 0; +}; -std::binary_semaphore crashlog_ready{0}; -std::binary_semaphore crashlog_complete{0}; +CrashInfo crash_info; + +/* + * As of c++17 the only safe stdc++ methods are plain lock-free atomic methods + * This sadly means that using std::semaphore *could* cause issues according to the standard. + */ +std::atomic_bool crashed = false; +std::atomic_bool crashlog_ready = false; +std::atomic_bool crashlog_complete = false; + +void flag_set(std::atomic_bool &atom) { + atom.store(true); + atom.notify_all(); +} +void flag_wait(std::atomic_bool &atom) { + atom.wait(false); +} std::thread crashlog_thread; -volatile bool shutdown = false; +bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - crash_signal = sig; - bt_entries = backtrace(bt, BT_ENTRY_MAX); + if (crashed.exchange(true)) { + // Crashlog already produced, bail thread. + std::quick_exit(1); + } + crash_info.signal = sig; + crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); // Signal saving of crashlog and wait for completion - crashlog_ready.release(); - crashlog_complete.acquire(); + flag_set(crashlog_ready); + flag_wait(crashlog_complete); std::quick_exit(1); } +void dfhack_crashlog_handle_terminate() { + dfhack_crashlog_handle_signal(0); +} + +std::string signal_name(int sig) { + switch (sig) { + case SIGINT: + return "SIGINT"; + case SIGILL: + return "SIGILL"; + case SIGABRT: + return "SIGABRT"; + case SIGFPE: + return "SIGFPE"; + case SIGSEGV: + return "SIGSEGV"; + case SIGTERM: + return "SIGTERM"; + } + return ""; +} + void dfhack_save_crashlog() { - char** backtrace_strings = backtrace_symbols(bt, bt_entries); + char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); if (!backtrace_strings) { - // Something has gone terribly wrong + // Allocation failed, give up return; } std::filesystem::path crashlog_path = "./crash.txt"; std::ofstream crashlog(crashlog_path); - crashlog << "Dwarf Fortress has crashed!" << "\n"; - crashlog << "DwarfFortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; + crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; - for (int i = 0; i < bt_entries; i++) { + std::string signal = signal_name(crash_info.signal); + if (!signal.empty()) { + crashlog << "Signal " << signal << "\n"; + } + + for (int i = 0; i < crash_info.backtrace_entries; i++) { crashlog << i << "> " << backtrace_strings[i] << "\n"; } @@ -49,33 +96,37 @@ void dfhack_save_crashlog() { } void dfhack_crashlog_thread() { - // Wait for crash or shutdown signal - crashlog_ready.acquire(); - if (shutdown) + // Wait for activation signal + flag_wait(crashlog_ready); + if (shutdown) // Shutting down gracefully, end thread. return; dfhack_save_crashlog(); - crashlog_complete.release(); + + flag_set(crashlog_complete); std::quick_exit(1); } const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { -void dfhack_crashlog_init() { - for (int signal : desired_signals) { - std::signal(signal, dfhack_crashlog_handle_signal); - } + void dfhack_crashlog_init() { + for (int signal : desired_signals) { + std::signal(signal, dfhack_crashlog_handle_signal); + } + std::set_terminate(dfhack_crashlog_handle_terminate); - // Ensure the library is initialized to avoid AsyncSignal-Unsafe init during crash - int _ = backtrace(bt, 1); + // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 + // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s + // Using it here ensures it is loaded before use in the signal handler. + int _ = backtrace(crash_info.backtrace, 1); - crashlog_thread = std::thread(dfhack_crashlog_thread); -} + crashlog_thread = std::thread(dfhack_crashlog_thread); + } -void dfhack_crashlog_shutdown() { - shutdown = true; - crashlog_ready.release(); - crashlog_thread.join(); - return; -} + void dfhack_crashlog_shutdown() { + shutdown = true; + flag_set(crashlog_ready); + crashlog_thread.join(); + return; + } } From 6dabe6adb454f550ca30849494ce21ad095fea3d Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 13 Nov 2025 22:21:28 -0500 Subject: [PATCH 0572/1082] Generate filepath using timestamp --- library/Crashlog.cpp | 53 +++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 2c8ac0f17f..e73a5e8b48 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,8 +1,9 @@ #include "DFHackVersion.h" #include -#include +#include #include #include +#include #include @@ -41,7 +42,7 @@ extern "C" void dfhack_crashlog_handle_signal(int sig) { } crash_info.signal = sig; crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); - + // Signal saving of crashlog and wait for completion flag_set(crashlog_ready); flag_wait(crashlog_complete); @@ -70,27 +71,49 @@ std::string signal_name(int sig) { return ""; } +std::filesystem::path get_crashlog_path() { + std::time_t time = std::time(nullptr); + std::tm* tm = std::localtime(&time); + + std::string timestamp = "unknown"; + if (tm) { + char stamp[64]; + std::size_t out = strftime(&stamp[0], 63, "%Y-%m-%d-%H-%M-%S", tm); + if (out != 0) + timestamp = stamp; + } + + std::filesystem::path dir = "crashlog"; + std::error_code err; + std::filesystem::create_directories(dir, err); + + std::filesystem::path log_path = dir / ("crash_" + timestamp + ".txt"); + return log_path; +} + void dfhack_save_crashlog() { char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); if (!backtrace_strings) { // Allocation failed, give up return; } - std::filesystem::path crashlog_path = "./crash.txt"; - std::ofstream crashlog(crashlog_path); + try { + std::filesystem::path crashlog_path = get_crashlog_path(); + std::ofstream crashlog(crashlog_path); - crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; - crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; - crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; + crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; + crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; - std::string signal = signal_name(crash_info.signal); - if (!signal.empty()) { - crashlog << "Signal " << signal << "\n"; - } + std::string signal = signal_name(crash_info.signal); + if (!signal.empty()) { + crashlog << "Signal " << signal << "\n"; + } - for (int i = 0; i < crash_info.backtrace_entries; i++) { - crashlog << i << "> " << backtrace_strings[i] << "\n"; - } + for (int i = 0; i < crash_info.backtrace_entries; i++) { + crashlog << i << "> " << backtrace_strings[i] << "\n"; + } + } catch (...) {} free(backtrace_strings); } @@ -118,7 +141,7 @@ namespace DFHack { // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s // Using it here ensures it is loaded before use in the signal handler. - int _ = backtrace(crash_info.backtrace, 1); + [[maybe_unused]] int _ = backtrace(crash_info.backtrace, 1); crashlog_thread = std::thread(dfhack_crashlog_thread); } From 0d4bc8ac4e092e7f21481c69179d189c0c0cfbeb Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 13 Nov 2025 23:45:59 -0500 Subject: [PATCH 0573/1082] Remove signal and terminate handlers on shutdown --- library/Crashlog.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index e73a5e8b48..c3ab5d4d67 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -130,13 +130,15 @@ void dfhack_crashlog_thread() { std::quick_exit(1); } +std::terminate_handler term_handler = nullptr; + const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { void dfhack_crashlog_init() { for (int signal : desired_signals) { std::signal(signal, dfhack_crashlog_handle_signal); } - std::set_terminate(dfhack_crashlog_handle_terminate); + term_handler = std::set_terminate(dfhack_crashlog_handle_terminate); // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s @@ -147,6 +149,11 @@ namespace DFHack { } void dfhack_crashlog_shutdown() { + for (int signal : desired_signals) { + std::signal(signal, SIG_DFL); + } + std::set_terminate(term_handler); + shutdown = true; flag_set(crashlog_ready); crashlog_thread.join(); From 25ac663f5a9327a1cac825fab5069e3f263783e7 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 09:20:06 -0500 Subject: [PATCH 0574/1082] Use eventfd to wait instead of cvar for as-safety --- library/Crashlog.cpp | 54 ++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index c3ab5d4d67..7286d83232 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include const int BT_ENTRY_MAX = 25; struct CrashInfo { @@ -16,13 +18,11 @@ struct CrashInfo { CrashInfo crash_info; -/* - * As of c++17 the only safe stdc++ methods are plain lock-free atomic methods - * This sadly means that using std::semaphore *could* cause issues according to the standard. - */ -std::atomic_bool crashed = false; -std::atomic_bool crashlog_ready = false; -std::atomic_bool crashlog_complete = false; +std::atomic crashed = false; +std::atomic crashlog_ready = false; + +// Use eventfd for async-signal safe waiting +int crashlog_complete = -1; void flag_set(std::atomic_bool &atom) { atom.store(true); @@ -32,20 +32,32 @@ void flag_wait(std::atomic_bool &atom) { atom.wait(false); } +void signal_crashlog_complete() { + if (crashlog_complete == -1) + return; + uint64_t v = 1; + write(crashlog_complete, &v, sizeof(v)); +} + std::thread crashlog_thread; -bool shutdown = false; +volatile bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - if (crashed.exchange(true)) { - // Crashlog already produced, bail thread. + if (shutdown || crashed.exchange(true) || crashlog_ready.load()) { + // Ensure the signal handler doesn't try to write a crashlog + // whilst the crashlog thread is unavailable. std::quick_exit(1); } crash_info.signal = sig; crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); - // Signal saving of crashlog and wait for completion + // Signal saving of crashlog flag_set(crashlog_ready); - flag_wait(crashlog_complete); + // Wait for completion via eventfd read, if fd isn't valid, bail + if (crashlog_complete != -1) { + [[maybe_unused]] uint64_t _; + read(crashlog_complete, &_, sizeof(_)); + } std::quick_exit(1); } @@ -125,8 +137,7 @@ void dfhack_crashlog_thread() { return; dfhack_save_crashlog(); - - flag_set(crashlog_complete); + signal_crashlog_complete(); std::quick_exit(1); } @@ -135,6 +146,11 @@ std::terminate_handler term_handler = nullptr; const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { void dfhack_crashlog_init() { + // Initialize eventfd flag + crashlog_complete = eventfd(0, EFD_CLOEXEC); + + crashlog_thread = std::thread(dfhack_crashlog_thread); + for (int signal : desired_signals) { std::signal(signal, dfhack_crashlog_handle_signal); } @@ -144,19 +160,23 @@ namespace DFHack { // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s // Using it here ensures it is loaded before use in the signal handler. [[maybe_unused]] int _ = backtrace(crash_info.backtrace, 1); - - crashlog_thread = std::thread(dfhack_crashlog_thread); } void dfhack_crashlog_shutdown() { + shutdown = true; for (int signal : desired_signals) { std::signal(signal, SIG_DFL); } std::set_terminate(term_handler); - shutdown = true; + // Shutdown the crashlog thread. flag_set(crashlog_ready); crashlog_thread.join(); + + // If the signal handler is somehow running whilst here, let it terminate + signal_crashlog_complete(); + if (crashlog_complete != -1) + close(crashlog_complete); // Close fd return; } } From cf28bd362121eb87bcaa8ea69463ae43039fe0e2 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 09:41:38 -0500 Subject: [PATCH 0575/1082] More maybe_unused --- library/Crashlog.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 7286d83232..22bd40cf31 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -36,7 +36,7 @@ void signal_crashlog_complete() { if (crashlog_complete == -1) return; uint64_t v = 1; - write(crashlog_complete, &v, sizeof(v)); + [[maybe_unused]] auto _ = write(crashlog_complete, &v, sizeof(v)); } std::thread crashlog_thread; @@ -55,8 +55,8 @@ extern "C" void dfhack_crashlog_handle_signal(int sig) { flag_set(crashlog_ready); // Wait for completion via eventfd read, if fd isn't valid, bail if (crashlog_complete != -1) { - [[maybe_unused]] uint64_t _; - read(crashlog_complete, &_, sizeof(_)); + [[maybe_unused]] uint64_t v; + [[maybe_unused]] auto _ = read(crashlog_complete, &v, sizeof(v)); } std::quick_exit(1); } From 8fbaedf20932c1710e267fcbd0a5a5bf61985ff1 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 21:48:12 -0500 Subject: [PATCH 0576/1082] Use atomic bool for shutdown flag --- library/Crashlog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 22bd40cf31..c560666f99 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -20,6 +20,7 @@ CrashInfo crash_info; std::atomic crashed = false; std::atomic crashlog_ready = false; +std::atomic shutdown = false; // Use eventfd for async-signal safe waiting int crashlog_complete = -1; @@ -40,10 +41,9 @@ void signal_crashlog_complete() { } std::thread crashlog_thread; -volatile bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - if (shutdown || crashed.exchange(true) || crashlog_ready.load()) { + if (shutdown.load() || crashed.exchange(true) || crashlog_ready.load()) { // Ensure the signal handler doesn't try to write a crashlog // whilst the crashlog thread is unavailable. std::quick_exit(1); @@ -133,7 +133,7 @@ void dfhack_save_crashlog() { void dfhack_crashlog_thread() { // Wait for activation signal flag_wait(crashlog_ready); - if (shutdown) // Shutting down gracefully, end thread. + if (shutdown.load()) // Shutting down gracefully, end thread. return; dfhack_save_crashlog(); @@ -163,7 +163,7 @@ namespace DFHack { } void dfhack_crashlog_shutdown() { - shutdown = true; + shutdown.exchange(true); for (int signal : desired_signals) { std::signal(signal, SIG_DFL); } From c6736649a493b37463e81ac8379c4ce1b2629652 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 15 Nov 2025 19:01:52 -0600 Subject: [PATCH 0577/1082] df structure support implement `operator==`, `operator<`, and `std::hash` for `labor_kitchen_interface_food_key` --- library/include/df/custom/.gitignore | 1 + .../hash/labor_kitchen_interface_food_key.h | 16 ++++++++++++++++ .../labor_kitchen_interface_food_key.custom.inc | 11 +++++++++++ 3 files changed, 28 insertions(+) create mode 100644 library/include/df/custom/.gitignore create mode 100644 library/include/df/custom/hash/labor_kitchen_interface_food_key.h create mode 100644 library/include/df/custom/labor_kitchen_interface_food_key.custom.inc diff --git a/library/include/df/custom/.gitignore b/library/include/df/custom/.gitignore new file mode 100644 index 0000000000..96e79c167d --- /dev/null +++ b/library/include/df/custom/.gitignore @@ -0,0 +1 @@ +!*.h diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h new file mode 100644 index 0000000000..3ca9676725 --- /dev/null +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -0,0 +1,16 @@ +template<> +struct std::hash { + auto operator()(const labor_kitchen_interface_food_key &a) const -> size_t { + struct thing { + int16_t t; + int16_t st; + int32_t x; + } thing{ + .t = a.type; + .st = a.subtype, + .x = static_cast(a.mat) ^ (static_cast(a.matg)) + }; + + return hash()(bit_cast(thing)); + } + }; diff --git a/library/include/df/custom/labor_kitchen_interface_food_key.custom.inc b/library/include/df/custom/labor_kitchen_interface_food_key.custom.inc new file mode 100644 index 0000000000..59796a69ba --- /dev/null +++ b/library/include/df/custom/labor_kitchen_interface_food_key.custom.inc @@ -0,0 +1,11 @@ + bool operator<(const labor_kitchen_interface_food_key &b) const { + if (type Date: Sat, 15 Nov 2025 19:27:07 -0600 Subject: [PATCH 0578/1082] Rename labor_kitchen_interface_food_key.custom.inc to labor_kitchen_interface_food_key.methods.inc correct file name --- ...ey.custom.inc => labor_kitchen_interface_food_key.methods.inc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename library/include/df/custom/{labor_kitchen_interface_food_key.custom.inc => labor_kitchen_interface_food_key.methods.inc} (100%) diff --git a/library/include/df/custom/labor_kitchen_interface_food_key.custom.inc b/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc similarity index 100% rename from library/include/df/custom/labor_kitchen_interface_food_key.custom.inc rename to library/include/df/custom/labor_kitchen_interface_food_key.methods.inc From e2eef85656df1621799b27fecafc40a03be59bdf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 15 Nov 2025 19:35:40 -0600 Subject: [PATCH 0579/1082] add missing namespace --- .../include/df/custom/hash/labor_kitchen_interface_food_key.h | 2 +- .../df/custom/labor_kitchen_interface_food_key.methods.inc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h index 3ca9676725..4880f385fb 100644 --- a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -1,5 +1,5 @@ template<> -struct std::hash { +struct std::hash<> { auto operator()(const labor_kitchen_interface_food_key &a) const -> size_t { struct thing { int16_t t; diff --git a/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc b/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc index 59796a69ba..a1de2fec13 100644 --- a/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc +++ b/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc @@ -1,4 +1,4 @@ - bool operator<(const labor_kitchen_interface_food_key &b) const { + bool operator<(const df::labor_kitchen_interface_food_key &b) const { if (type Date: Sat, 15 Nov 2025 19:41:31 -0600 Subject: [PATCH 0580/1082] Update labor_kitchen_interface_food_key.h grr forgot to save --- .../include/df/custom/hash/labor_kitchen_interface_food_key.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h index 4880f385fb..4386621a6a 100644 --- a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -1,6 +1,6 @@ template<> struct std::hash<> { - auto operator()(const labor_kitchen_interface_food_key &a) const -> size_t { + auto operator()(const df::labor_kitchen_interface_food_key &a) const -> size_t { struct thing { int16_t t; int16_t st; From 1df6c49c3b815d70e1b4bee3bafc8d80ff111364 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Nov 2025 21:33:20 -0500 Subject: [PATCH 0581/1082] Various updates for v53 items/buildings --- docs/changelog.txt | 3 +++ docs/guides/quickfort-user-guide.rst | 2 ++ library/modules/Items.cpp | 1 + plugins/autolabor/laborstatemap.h | 6 ++++++ plugins/blueprint.cpp | 24 ++++++++++++++++++++++-- plugins/suspendmanager.cpp | 5 ++++- 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ea0853484..d235191e3e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,8 +59,11 @@ Template for new versions: ## New Features ## Fixes +- `suspendmanager`: treat reinforced walls as a blocking construction and buildable platform ## Misc Improvements +- `blueprint`: support for reinforced walls and bolt throwers +- `autolabor`: support for new dying and siege-related labors ## Documentation diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index a5b39c529b..dae03c3aba 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -2298,8 +2298,10 @@ Symbol Type Properties ``ek`` kiln ``en`` magma kiln ``ib`` ballista +``it`` bolt thrower ``ic`` catapult ``Cw`` wall +``CW`` reinforced wall ``Cf`` floor ``Cr`` ramp ``Cu`` up stair diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 5b9e781721..b3d327881e 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1200,6 +1200,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, break; case CATAPULTPARTS: case BALLISTAPARTS: + case BOLT_THROWER_PARTS: case TRAPPARTS: value = 30; break; diff --git a/plugins/autolabor/laborstatemap.h b/plugins/autolabor/laborstatemap.h index a9472bcc2e..3757011d63 100644 --- a/plugins/autolabor/laborstatemap.h +++ b/plugins/autolabor/laborstatemap.h @@ -282,6 +282,12 @@ const dwarf_state dwarf_states[] = { dwarf_state::OTHER /* HeistItem */, dwarf_state::OTHER /* InterrogateSubject */, dwarf_state::OTHER /* AcceptHeistItem */, + dwarf_state::BUSY /* StoreSquadEquipmentItem */, + dwarf_state::BUSY /* MixDye */, + dwarf_state::BUSY /* DyeLeather */, + dwarf_state::BUSY /* ConstructBoltThrowerParts */, + dwarf_state::BUSY /* LoadBoltThrower */, + dwarf_state::BUSY /* FireBoltThrower */, }; #define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 26caf25816..98b4c1c155 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -16,6 +16,7 @@ #include "TileTypes.h" #include "modules/Buildings.h" +#include "modules/Constructions.h" #include "modules/Filesystem.h" #include "modules/Gui.h" #include "modules/Maps.h" @@ -33,6 +34,7 @@ #include "df/building_trapst.h" #include "df/building_water_wheelst.h" #include "df/building_workshopst.h" +#include "df/construction.h" #include "df/engraving.h" #include "df/entity_position.h" #include "df/tile_bitmask.h" @@ -610,6 +612,7 @@ static const char * get_construction_str(df::building *b) { case construction_type::TrackRampNEW: return "trackrampNEW"; case construction_type::TrackRampSEW: return "trackrampSEW"; case construction_type::TrackRampNSEW: return "trackrampNSEW"; + case construction_type::ReinforcedWall: return "CW"; case construction_type::NONE: default: return "~"; @@ -632,6 +635,13 @@ static const char * get_constructed_track_str(df::tiletype *tt, return cache(str); } +static const char * get_constructed_wall_str(df::coord pos) { + df::construction *c = Constructions::findAtTile(pos); + if (!c || !(c->flags.bits.reinforced)) + return "Cw"; + return "CW"; +} + static const char * get_constructed_floor_str(df::tiletype *tt) { if (tileSpecial(*tt) != df::tiletype_special::TRACK) return "Cf"; @@ -653,7 +663,7 @@ static const char * get_tile_construct(color_ostream &out, const df::coord &pos, return NULL; switch (tileShape(*tt)) { - case tiletype_shape::WALL: return "Cw"; + case tiletype_shape::WALL: return get_constructed_wall_str(pos); case tiletype_shape::FLOOR: return get_constructed_floor_str(tt); case tiletype_shape::RAMP: return get_constructed_ramp_str(tt); case tiletype_shape::FORTIFICATION: return "CF"; @@ -732,7 +742,17 @@ static const char * get_bridge_str(df::building *b) { static const char * get_siege_str(df::building *b) { df::building_siegeenginest *se = virtual_cast(b); - return !se || se->type == df::siegeengine_type::Catapult ? "ic" : "ib"; + if (!se) + return "ic"; + + switch(se->type) { + case df::siegeengine_type::Catapult: return "ic"; + case df::siegeengine_type::Ballista: return "ib"; + case df::siegeengine_type::BoltThrower: return "it"; + default: + return "ic"; + } + } static const char * get_workshop_str(df::building *b) { diff --git a/plugins/suspendmanager.cpp b/plugins/suspendmanager.cpp index d63d6a99c9..0756e10832 100644 --- a/plugins/suspendmanager.cpp +++ b/plugins/suspendmanager.cpp @@ -113,11 +113,13 @@ using df::coord; // impassible constructions static const std::bitset<64> construction_impassible = std::bitset<64>() .set(construction_type::Wall) + .set(construction_type::ReinforcedWall) .set(construction_type::Fortification); // constructions requiring same support as walls static const std::bitset<64> construction_wall_support = std::bitset<64>() .set(construction_type::Wall) + .set(construction_type::ReinforcedWall) .set(construction_type::Fortification) .set(construction_type::UpStair) .set(construction_type::UpDownStair); @@ -395,7 +397,8 @@ class SuspendManager { // other tiles can become suitable if a wall is being constructed below auto below = Buildings::findAtTile(coord(pos.x,pos.y,pos.z-1)); if (below && below->getType() == df::building_type::Construction && - below->getSubtype() == construction_type::Wall) + (below->getSubtype() == construction_type::Wall || + below->getSubtype() == construction_type::ReinforcedWall)) return true; return false; From b91188208eb7a00f87253da60b26242f6589bde0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 15 Nov 2025 20:49:08 -0600 Subject: [PATCH 0582/1082] take three? --- .../hash/labor_kitchen_interface_food_key.h | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h index 4386621a6a..b1fdd73fdb 100644 --- a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -1,16 +1,26 @@ -template<> -struct std::hash<> { - auto operator()(const df::labor_kitchen_interface_food_key &a) const -> size_t { - struct thing { - int16_t t; - int16_t st; - int32_t x; - } thing{ - .t = a.type; - .st = a.subtype, - .x = static_cast(a.mat) ^ (static_cast(a.matg)) - }; +#include +#include +#include "df/labor_kitchen_interface_food_key.h" - return hash()(bit_cast(thing)); +namespace std +{ + template<> + struct hash + { + auto operator()(const df::labor_kitchen_interface_food_key& a) const -> size_t + { + struct thing + { + int16_t t; + int16_t st; + int32_t x; + } thing{ + .t = a.type, + .st = a.subtype, + .x = static_cast(a.mat) ^ (static_cast(a.matg)) + }; + + return hash()(std::bit_cast(thing)); } }; +} From 7b3e3157963dfc981b43efbc64d7dfab4b249b8d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 15 Nov 2025 21:01:51 -0600 Subject: [PATCH 0583/1082] add include guard to labor_kitchen_interface_food_key.h --- .../include/df/custom/hash/labor_kitchen_interface_food_key.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h index b1fdd73fdb..5f2d0d2b33 100644 --- a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include "df/labor_kitchen_interface_food_key.h" From 1edf08d4ec6fa2b275f14b4ea24d104d4f99b136 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:00:22 -0500 Subject: [PATCH 0584/1082] use std::string_view. eliminate C-varargs when building state_change_event table. --- library/Core.cpp | 71 ++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b49784db4b..685703eb33 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -316,23 +316,23 @@ static std::string dfhack_version_desc() return s.str(); } -static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, const std::span pargs) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string_view pcmd, const std::span pargs) { if (!lua_checkstack(state, pargs.size()+10)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "run_script"); lua_remove(state, -2); - lua_pushstring(state, pcmd.c_str()); + lua_pushlstring(state, pcmd.data(), pcmd.size()); for (const auto& arg : pargs) lua_pushstring(state, arg.c_str()); return true; } -static command_result runLuaScript(color_ostream &out, std::string name, const std::span args) +static command_result runLuaScript(color_ostream &out, const std::string_view name, const std::span args) { - auto init_fn = [n = std::move(name), args](color_ostream& out, lua_State* state) -> bool { - return init_run_script(out, state, n, args); + auto init_fn = [name, args](color_ostream& out, lua_State* state) -> bool { + return init_run_script(out, state, name, args); }; bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_fn); @@ -340,22 +340,22 @@ static command_result runLuaScript(color_ostream &out, std::string name, const s return ok ? CR_OK : CR_FAILURE; } -static bool init_enable_script(color_ostream &out, lua_State *state, const std::string& name, bool enable) +static bool init_enable_script(color_ostream &out, lua_State *state, const std::string_view name, bool enable) { if (!lua_checkstack(state, 4)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "enable_script"); lua_remove(state, -2); - lua_pushstring(state, name.c_str()); + lua_pushlstring(state, name.data(), name.size()); lua_pushboolean(state, enable); return true; } -static command_result enableLuaScript(color_ostream &out, std::string name, bool enabled) +static command_result enableLuaScript(color_ostream &out, const std::string_view name, bool enabled) { - auto init_fn = [n = std::move(name), enabled](color_ostream& out, lua_State* state) -> bool { - return init_enable_script(out, state, n, enabled); + auto init_fn = [name, enabled](color_ostream& out, lua_State* state) -> bool { + return init_enable_script(out, state, name, enabled); }; bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_fn); @@ -677,7 +677,7 @@ static void ls_helper(color_ostream &con, const std::span par std::vector filter; bool skip_tags = false; bool show_dev_commands = false; - std::string exclude_strs; + std::string_view exclude_strs; bool in_exclude = false; for (const auto& str : params) { @@ -2188,24 +2188,31 @@ namespace DFHack { using EntryVector = std::vector; using InitVariationTable = std::map; - static EntryVector computeInitVariationTable(void* none, ...) { - va_list list; - va_start(list,none); + template + concept VariationTableTypes = std::same_as || std::convertible_to; + + template + static EntryVector computeInitVariationTable(Ts&&... ts) { + using Arg = std::variant; + std::vector args{ Arg{std::forward(ts)}... }; + + Key current_key = SC_UNKNOWN; EntryVector result; - while(true) { - Key key = (Key)va_arg(list,int); - if ( key == SC_UNKNOWN ) - break; - Val val; - while (true) { - const char *v = va_arg(list, const char *); - if (!v || !v[0]) - break; - val.emplace_back(v); + Val val; + for (auto& arg : args) { + if (std::holds_alternative(arg)) { + if (current_key != SC_UNKNOWN && !val.empty()) { + result.emplace_back(current_key, val); + val.clear(); + } + current_key = std::get(arg); + } else if (std::holds_alternative(arg)) { + auto str = std::get(arg); + if (!str.empty()) + val.emplace_back(str); } - result.push_back(Entry(key,val)); } - va_end(list); + return result; } @@ -2216,12 +2223,12 @@ namespace DFHack { } void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event event) { - static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable(nullptr, - (int)SC_WORLD_LOADED, "onLoad", "onLoadWorld", "onWorldLoaded", "", - (int)SC_WORLD_UNLOADED, "onUnload", "onUnloadWorld", "onWorldUnloaded", "", - (int)SC_MAP_LOADED, "onMapLoad", "onLoadMap", "", - (int)SC_MAP_UNLOADED, "onMapUnload", "onUnloadMap", "", - (int)SC_UNKNOWN + static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable( + SC_WORLD_LOADED, "onLoad", "onLoadWorld", "onWorldLoaded", + SC_WORLD_UNLOADED, "onUnload", "onUnloadWorld", "onWorldUnloaded", + SC_MAP_LOADED, "onMapLoad", "onLoadMap", + SC_MAP_UNLOADED, "onMapUnload", "onUnloadMap", + SC_UNKNOWN )); if (!df::global::world) From a6ed266f701b929f80ef5d8d6669739ee878743e Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:21:06 +0000 Subject: [PATCH 0585/1082] 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 525024cf72..f69c2c9182 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 525024cf72244cd6c384b723a0b7a34a50b336ae +Subproject commit f69c2c91821330899d795e52edd6655f1dc61477 diff --git a/scripts b/scripts index f71c1568f3..d286ffc6f2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f71c1568f3af618fc542f1af2028dc89ba383f7a +Subproject commit d286ffc6f27de862e0298fded0f4464b4842bf18 From 648045289e97a89c459d0cfb79edf9bfe2f3f032 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 12:32:37 -0600 Subject: [PATCH 0586/1082] adjustments for change in `announcement_alert_type` enum for compatibility with DFHack/df-structures#859 --- library/modules/Gui.cpp | 2 ++ library/xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3808fa0271..1e079da598 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1957,6 +1957,8 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, df::report auto alert_type = announcement_alert_type::NONE; switch (slot) { + case unit_report_type::NONE: /* should never happen? */ + return false; case unit_report_type::Combat: world->status.flags.bits.combat = true; alert_type = announcement_alert_type::COMBAT; diff --git a/library/xml b/library/xml index 145f317340..01c3774730 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 145f317340ac9327854318c7ba758ee2af5e1928 +Subproject commit 01c3774730e2c89b2a4a7894c03f43a91b8db0a7 From bde3891297ee9459c400b156b8fb9f69591662fd Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:09:23 +0000 Subject: [PATCH 0587/1082] 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 01c3774730..7ba825f763 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 01c3774730e2c89b2a4a7894c03f43a91b8db0a7 +Subproject commit 7ba825f763f414d90314d4e5b6d4563d8389a98a diff --git a/scripts b/scripts index d286ffc6f2..0936c5369e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d286ffc6f27de862e0298fded0f4464b4842bf18 +Subproject commit 0936c5369e389f36811d71094c3a0f0b4435637b From 48fd7dba0104e3be6f031dc57aa7c9df42f2b109 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 13:31:37 -0600 Subject: [PATCH 0588/1082] update RFR for changes to siegeengines for compatibility with DFHack/df-structures#860 --- docs/changelog.txt | 1 + library/xml | 2 +- .../remotefortressreader/building_reader.cpp | 25 ++++++++++++++----- .../proto/RemoteFortressReader.proto | 6 ++++- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7d5a3e6db3..dfdce58d2b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: ## Fixes - `buildingplan`: Bolt throwers will no longer be constructed using populated bins. +- `RemoteFortressReader`: updated siege engine facing enums for new diagonal directions - `suspendmanager`: treat reinforced walls as a blocking construction and buildable platform ## Misc Improvements diff --git a/library/xml b/library/xml index 7ba825f763..33bb9b6c08 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7ba825f763f414d90314d4e5b6d4563d8389a98a +Subproject commit 33bb9b6c08c661ef30b9bcc4ae652ec2e09bbd8f diff --git a/plugins/remotefortressreader/building_reader.cpp b/plugins/remotefortressreader/building_reader.cpp index ebf0359afd..62475ce1aa 100644 --- a/plugins/remotefortressreader/building_reader.cpp +++ b/plugins/remotefortressreader/building_reader.cpp @@ -560,18 +560,31 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re auto facing = actual->facing; switch (facing) { - case df::building_siegeenginest::Left: - remote_build->set_direction(WEST); - break; - case df::building_siegeenginest::Up: + using enum df::enums::siegeengine_orientation::siegeengine_orientation; + case North: remote_build->set_direction(NORTH); break; - case df::building_siegeenginest::Right: + case Northeast: + remote_build->set_direction(NORTHEAST); + break; + case East: remote_build->set_direction(EAST); break; - case df::building_siegeenginest::Down: + case Southeast: + remote_build->set_direction(SOUTHEAST); + break; + case South: remote_build->set_direction(SOUTH); break; + case Southwest: + remote_build->set_direction(SOUTHWEST); + break; + case West: + remote_build->set_direction(WEST); + break; + case Northwest: + remote_build->set_direction(NORTHWEST); + break; default: break; } diff --git a/plugins/remotefortressreader/proto/RemoteFortressReader.proto b/plugins/remotefortressreader/proto/RemoteFortressReader.proto index a092a12097..b7dc7bdc88 100644 --- a/plugins/remotefortressreader/proto/RemoteFortressReader.proto +++ b/plugins/remotefortressreader/proto/RemoteFortressReader.proto @@ -141,7 +141,11 @@ enum BuildingDirection EAST = 1; SOUTH = 2; WEST = 3; - NONE = 4; + NORTHEAST = 4; + SOUTHEAST = 5; + SOUTHWEST = 6; + NORTHWEST = 7; + NONE = 8; } enum TileDigDesignation From f8cff802562bb7ce004ad2c42df5db2ad38eee0e Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:44:40 +0000 Subject: [PATCH 0589/1082] 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 33bb9b6c08..07bd0e8e16 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 33bb9b6c08c661ef30b9bcc4ae652ec2e09bbd8f +Subproject commit 07bd0e8e160bf5e4b62cd6dc3ba643fcd7dac836 diff --git a/plugins/stonesense b/plugins/stonesense index f69c2c9182..7a82390f42 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f69c2c91821330899d795e52edd6655f1dc61477 +Subproject commit 7a82390f42f017dc961afa1a2d0376896aac7829 diff --git a/scripts b/scripts index 0936c5369e..5dc42f9985 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 0936c5369e389f36811d71094c3a0f0b4435637b +Subproject commit 5dc42f99852a295e03ca5fc538dd2b139c1e0453 From 6ec91c7e0724b3aa684b3b1118e2f491058b4f38 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 15:40:28 -0600 Subject: [PATCH 0590/1082] make itch downloader more permissive on matching this allows it to match adownloadable if there's crud after `windows` or `linux` (e.g. if a file is called `dwarf_fortress_53_04_windows(1).zip `) --- .github/workflows/generate-symbols.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index f78da19e74..18a99b9db8 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -164,7 +164,7 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="dwarf_fortress_${minor}_${patch}_linux.tar.bz2" + fname="dwarf_fortress_${minor}_${patch}_linux*.tar.bz2" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" tar xjf "kitfoxgames/dwarf-fortress/files/${fname}" -C DF_itch tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_itch @@ -272,7 +272,7 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="dwarf_fortress_${minor}_${patch}_windows.zip" + fname="dwarf_fortress_${minor}_${patch}_windows*.zip" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" unzip -d DF_itch "kitfoxgames/dwarf-fortress/files/${fname}" xml/symbols_gen_windows.sh ${{ inputs.version }} ITCH DF_itch From a829c98a28207314f648c06ada0d4232864ea2a4 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:49:00 +0000 Subject: [PATCH 0591/1082] Auto-update structures ref for 53.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 07bd0e8e16..20b33a7d9e 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 07bd0e8e160bf5e4b62cd6dc3ba643fcd7dac836 +Subproject commit 20b33a7d9e4ec2224a6877ce3eb0a568299d9567 From 64df95f2dac24e30bdae7fd377e902ff5d1683d5 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:54:35 +0000 Subject: [PATCH 0592/1082] Auto-update structures ref for 53.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 20b33a7d9e..ec38149428 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 20b33a7d9e4ec2224a6877ce3eb0a568299d9567 +Subproject commit ec38149428af0b5780c81a3fe77706b346c8c2d0 From 0701719515d6119bda78fb3e03c769a4fc8ad02f Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:58:04 +0000 Subject: [PATCH 0593/1082] Auto-update structures ref for 53.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ec38149428..757f87b85d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ec38149428af0b5780c81a3fe77706b346c8c2d0 +Subproject commit 757f87b85df2a22aa342a986b9609febd60ae734 From fb163fe3a3871d059b077151392a03fc64700e97 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 16:14:24 -0600 Subject: [PATCH 0594/1082] updates for 53.04-r1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 20 +++++++++++++++++++- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f14ed88878..4530bfb2f5 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.03") +set(DF_VERSION "53.04") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index dfdce58d2b..ac68d3b1df 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,14 +58,32 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.04-r1 + +## New Tools + +## New Features + ## Fixes - `buildingplan`: Bolt throwers will no longer be constructed using populated bins. - `RemoteFortressReader`: updated siege engine facing enums for new diagonal directions - `suspendmanager`: treat reinforced walls as a blocking construction and buildable platform ## Misc Improvements -- `blueprint`: support for reinforced walls and bolt throwers - `autolabor`: support for new dying and siege-related labors +- `blueprint`: support for reinforced walls and bolt throwers ## Documentation diff --git a/library/xml b/library/xml index 757f87b85d..6c70c75ed0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 757f87b85df2a22aa342a986b9609febd60ae734 +Subproject commit 6c70c75ed08975d963ecbb669d946f403831d3c9 diff --git a/plugins/stonesense b/plugins/stonesense index 7a82390f42..9213b47546 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 7a82390f42f017dc961afa1a2d0376896aac7829 +Subproject commit 9213b475463c39251e93a5d7c93d1b0f38a0f537 diff --git a/scripts b/scripts index 5dc42f9985..72f90ad582 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 5dc42f99852a295e03ca5fc538dd2b139c1e0453 +Subproject commit 72f90ad5824fa532525ab123eda8fe91851178ff From f628ce8850547e6744a646392f0377240d4d5715 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 18 Nov 2025 00:42:13 +0000 Subject: [PATCH 0595/1082] 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 6c70c75ed0..48077a042d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 6c70c75ed08975d963ecbb669d946f403831d3c9 +Subproject commit 48077a042dff14de22d682de3afc6d9157203c81 From 7f647fca3e34666c21b3a8130a8440f522e75726 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 18:46:11 -0600 Subject: [PATCH 0596/1082] 53.04-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4530bfb2f5..787d58a75a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.04") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index ac68d3b1df..99903dadbb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,25 @@ Template for new versions: ## Removed +# 53.04-r1.1 + +## New Tools + +## New Features + +## Fixes +- fixed misalignment in ``widgets::unit_list`` + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 53.04-r1 ## New Tools From 7e865e9ff7939c7e2205b2943fa3955e8c129728 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 23:09:43 -0600 Subject: [PATCH 0597/1082] changes needed to implement unordered maps and sets see DFHack/df-structures#862 --- library/include/DataIdentity.h | 16 ++++++++- .../include/df/custom/hash/texture_fullid.h | 34 +++++++++++++++++++ .../df/custom/texture_fullid.methods.inc | 26 ++++++++++++++ library/modules/Gui.cpp | 7 ++-- library/xml | 2 +- 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 library/include/df/custom/hash/texture_fullid.h create mode 100644 library/include/df/custom/texture_fullid.methods.inc diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 8a9e3c406b..ed4ca06ecd 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -29,9 +29,10 @@ distribution. #include #include #include +#include +#include #include #include -#include #include #include "DataDefs.h" @@ -748,6 +749,11 @@ namespace df static const container_identity *get(); }; + template struct identity_traits > + { + static const container_identity* get(); + }; + template<> struct identity_traits > { static const bit_array_identity identity; static const bit_container_identity *get() { return &identity; } @@ -831,6 +837,14 @@ namespace df return &identity; } + template + inline const container_identity* identity_traits >::get() + { + using container = std::set; + static const ro_stl_container_identity identity("unordered_set", identity_traits::get()); + return &identity; + } + template inline const container_identity *identity_traits>::get() { using container = std::map; diff --git a/library/include/df/custom/hash/texture_fullid.h b/library/include/df/custom/hash/texture_fullid.h new file mode 100644 index 0000000000..25eff39cf3 --- /dev/null +++ b/library/include/df/custom/hash/texture_fullid.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include "df/texture_fullid.h" + +template<> +struct std::hash +{ + size_t operator()(df::texture_fullid const &t) const noexcept + { +// for some reason, bay12 used different hash methods on windows vs linux +#ifdef WIN32 + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(std::bit_cast(std::make_pair(t.r, t.g))); + h^=u_hash(std::bit_cast(std::make_pair(t.b, t.br)))<<1; + h^=u_hash(std::bit_cast(std::make_pair(t.bg, t.bb)))<<2; + h^=std::hash{}(t.flag.whole); + return h; +#else + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(t.r); + h^=u_hash(t.g)<<1; + h^=u_hash(t.b)<<2; + h^=u_hash(t.br)<<3; + h^=u_hash(t.bg)<<4; + h^=u_hash(t.bb)<<5; + h^=std::hash{}(t.flag.whole)<<6; + return h; +#endif + } +}; \ No newline at end of file diff --git a/library/include/df/custom/texture_fullid.methods.inc b/library/include/df/custom/texture_fullid.methods.inc new file mode 100644 index 0000000000..ff57b2f4c5 --- /dev/null +++ b/library/include/df/custom/texture_fullid.methods.inc @@ -0,0 +1,26 @@ +auto operator== (const texture_fullid &other) const { + return ( + this->texpos == other.texpos && + this->r == other.r && + this->g == other.g && + this->b == other.b && + this->br == other.br && + this->bg == other.bg && + this->bb == other.bb && + this->flag.whole == other.flag.whole + ); +} + +auto operator< (const texture_fullid &other) const { + if (this->texpos < other.texpos) return true; + if (this->r < other.r) return true; + if (this->g < other.g) return true; + if (this->b < other.b) return true; + if (this->br < other.br) return true; + if (this->bg < other.bg) return true; + if (this->bb < other.bb) return true; + if (this->flag.whole < other.flag.whole) return true; + return false; +} + + diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 1e079da598..fae55ce9d7 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2686,10 +2686,9 @@ void Gui::MTB_set_width(df::markup_text_boxst *mtb, int32_t n_width) df::widget * Gui::getWidget(df::widget_container *container, string name) { CHECK_NULL_POINTER(container); // ensure the compiler catches the change if we ever fix the template parameters - std::map & orig_field = container->children_by_name; - auto children_by_name = reinterpret_cast> *>(&orig_field); - if (children_by_name->contains(name)) - return (*children_by_name)[name].get(); + auto & children_by_name = container->children_by_name; + if (children_by_name.contains(name)) + return (children_by_name)[name].get(); return NULL; } diff --git a/library/xml b/library/xml index 48077a042d..461cd5a7e6 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 48077a042dff14de22d682de3afc6d9157203c81 +Subproject commit 461cd5a7e6773ea89e2795e6ef6cef1d3abdffc9 From c628dde44670d0a2948bfefab3a80e26cc70d9bd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 23:18:47 -0600 Subject: [PATCH 0598/1082] whitespacery --- .../include/df/custom/hash/texture_fullid.h | 38 +++++++++---------- .../df/custom/texture_fullid.methods.inc | 2 - 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/library/include/df/custom/hash/texture_fullid.h b/library/include/df/custom/hash/texture_fullid.h index 25eff39cf3..a1e4606e33 100644 --- a/library/include/df/custom/hash/texture_fullid.h +++ b/library/include/df/custom/hash/texture_fullid.h @@ -7,28 +7,28 @@ template<> struct std::hash { - size_t operator()(df::texture_fullid const &t) const noexcept + size_t operator()(df::texture_fullid const &t) const noexcept { // for some reason, bay12 used different hash methods on windows vs linux #ifdef WIN32 - size_t h=std::hash{}(t.texpos); - auto u_hash=std::hash{}; - h^=u_hash(std::bit_cast(std::make_pair(t.r, t.g))); - h^=u_hash(std::bit_cast(std::make_pair(t.b, t.br)))<<1; - h^=u_hash(std::bit_cast(std::make_pair(t.bg, t.bb)))<<2; - h^=std::hash{}(t.flag.whole); - return h; + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(std::bit_cast(std::make_pair(t.r, t.g))); + h^=u_hash(std::bit_cast(std::make_pair(t.b, t.br)))<<1; + h^=u_hash(std::bit_cast(std::make_pair(t.bg, t.bb)))<<2; + h^=std::hash{}(t.flag.whole); + return h; #else - size_t h=std::hash{}(t.texpos); - auto u_hash=std::hash{}; - h^=u_hash(t.r); - h^=u_hash(t.g)<<1; - h^=u_hash(t.b)<<2; - h^=u_hash(t.br)<<3; - h^=u_hash(t.bg)<<4; - h^=u_hash(t.bb)<<5; - h^=std::hash{}(t.flag.whole)<<6; - return h; + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(t.r); + h^=u_hash(t.g)<<1; + h^=u_hash(t.b)<<2; + h^=u_hash(t.br)<<3; + h^=u_hash(t.bg)<<4; + h^=u_hash(t.bb)<<5; + h^=std::hash{}(t.flag.whole)<<6; + return h; #endif } -}; \ No newline at end of file +}; diff --git a/library/include/df/custom/texture_fullid.methods.inc b/library/include/df/custom/texture_fullid.methods.inc index ff57b2f4c5..12e21f6735 100644 --- a/library/include/df/custom/texture_fullid.methods.inc +++ b/library/include/df/custom/texture_fullid.methods.inc @@ -22,5 +22,3 @@ auto operator< (const texture_fullid &other) const { if (this->flag.whole < other.flag.whole) return true; return false; } - - From 353f16395a627133b0a3c8a7942fe87cb2fef2eb Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 23:31:20 -0600 Subject: [PATCH 0599/1082] Update xml --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 461cd5a7e6..00e1c5ea00 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 461cd5a7e6773ea89e2795e6ef6cef1d3abdffc9 +Subproject commit 00e1c5ea0071b716afad5a4140dc43e0ed7a8fbb From ade165ada438092b89c77040f02495304445de00 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 18 Nov 2025 07:43:06 +0000 Subject: [PATCH 0600/1082] 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 00e1c5ea00..40c9ae5c13 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 00e1c5ea0071b716afad5a4140dc43e0ed7a8fbb +Subproject commit 40c9ae5c13bc0b63dfb3cbf36efb95d3b20faabe From b033dc13d907c7bb1cae59cdd25afef0ace9a499 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:32:13 +0000 Subject: [PATCH 0601/1082] Auto-update structures ref for 53.05 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 40c9ae5c13..f8f7002cea 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 40c9ae5c13bc0b63dfb3cbf36efb95d3b20faabe +Subproject commit f8f7002cea696ebab9ee1c83d2da23150e6cc93c From 61b725faebe2aa78065baff7cbad582b69b5aed5 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:34:53 +0000 Subject: [PATCH 0602/1082] Auto-update structures ref for 53.05 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f8f7002cea..2bd33eba05 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f8f7002cea696ebab9ee1c83d2da23150e6cc93c +Subproject commit 2bd33eba052f6ea3f65f4a22670029f2311b07b4 From bd1b453a05c7fc4cdc3ab3b6399a3ac91deb92b4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 07:59:08 -0600 Subject: [PATCH 0603/1082] changes for 53.05-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 787d58a75a..4824ad4ed1 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.04") -set(DFHACK_RELEASE "r1.1") +set(DF_VERSION "53.05") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 99903dadbb..13d6f1bc4d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,24 @@ Template for new versions: ## Removed +# 53.05-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 53.04-r1.1 ## New Tools From dca7d8d48bfd28d6293f253e48ffe24836467853 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 08:10:42 -0600 Subject: [PATCH 0604/1082] changes for 53.05-r1 --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 13d6f1bc4d..8551676b26 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -75,6 +75,7 @@ Template for new versions: ## New Tools ## New Features +- compatibility with 53.05 ## Fixes From 04cd9718235af5e3f2538c4c415881041f54dcb1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 10:55:08 -0600 Subject: [PATCH 0605/1082] update `sort` for changes in df `unit_list` --- library/include/DataIdentity.h | 2 +- plugins/sort.cpp | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index ed4ca06ecd..4bd4976250 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -840,7 +840,7 @@ namespace df template inline const container_identity* identity_traits >::get() { - using container = std::set; + using container = std::unordered_set; static const ro_stl_container_identity identity("unordered_set", identity_traits::get()); return &identity; } diff --git a/plugins/sort.cpp b/plugins/sort.cpp index add54688c4..9b91f1118c 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -25,7 +27,7 @@ namespace DFHack { DBG_DECLARE(sort, log, DebugCategory::LINFO); } -using item_or_unit = std::pair; +using item_or_unit = std::variant; using filter_vec_type = std::vector>; // recreated here since our autogenerated df::sort_entry lacks template params @@ -44,8 +46,8 @@ static bool probing = false; static bool probe_result = false; static bool do_filter(const char *module_name, const char *fn_name, const item_or_unit &elem) { - if (elem.second) return true; - auto unit = (df::unit *)elem.first; + if (std::holds_alternative(elem)) return true; + auto unit = std::get(elem); if (probing) { TRACE(log).print("probe successful\n"); @@ -78,11 +80,11 @@ static bool do_work_animal_assignment_filter(item_or_unit elem) { static int32_t our_filter_idx(df::widget_unit_list *unitlist) { if (world->units.active.empty()) - return true; + return -1; df::unit *u = world->units.active[0]; // any unit will do; we just need a sentinel if (!u) - return true; + return -1; probing = true; probe_result = false; @@ -91,8 +93,8 @@ static int32_t our_filter_idx(df::widget_unit_list *unitlist) { filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); TRACE(log).print("probing for our filter function\n"); - for (auto fn : *filter_vec) { - fn(std::make_pair(u, false)); + for (auto& fn : *filter_vec) { + fn(item_or_unit(u)); if (probe_result) { TRACE(log).print("found our filter function at idx %d\n", idx); break; @@ -144,13 +146,13 @@ static df::widget_unit_list * get_work_animal_assignment_unit_list() { // static bool sort_proxy(const item_or_unit &a, const item_or_unit &b) { - if (a.second || b.second) + if (std::holds_alternative(a) || std::holds_alternative(b)) return true; bool ret = true; color_ostream &out = Core::getInstance().getConsole(); Lua::CallLuaModuleFunction(out, "plugins.sort", "do_sort", - std::make_tuple((df::unit *)a.first, (df::unit *)b.first), + std::make_tuple(std::get(a), std::get(b)), 1, [&](lua_State *L){ ret = lua_toboolean(L, 1); } From ab4bb52ebdcab82f876d82a7e2132b4c29aac9e6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 11:37:57 -0600 Subject: [PATCH 0606/1082] enable msvc c4062 fixes #5641 --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4824ad4ed1..a9bb70aa15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,9 @@ if(MSVC) # Enable C5038 - This is equivalent to gcc's -Werror=reorder, which is enabled by default by gcc -Wall add_compile_options("/w15038") + # Enable C4062 - Warns about missing enum case in switch statement, equivalent to gcc -Wswitch + add_compile_options("/w14062") + # MSVC panics if an object file contains more than 65,279 sections. this # happens quite frequently with code that uses templates, such as vectors. add_compile_options("/bigobj") From 6ca9bab763b7eed4582b0ffd2f6fa30b7ac4cabf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 17:19:47 -0600 Subject: [PATCH 0607/1082] `sort`: make removal of our filter functions more reliable avoids a crash on df exit caused by a dangled pointer left in the df data structure Co-Authored-By: Nicholas McDaniel --- docs/changelog.txt | 1 + plugins/sort.cpp | 67 +++++++++++++++++++--------------------------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8551676b26..46f1a718e6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements diff --git a/plugins/sort.cpp b/plugins/sort.cpp index 9b91f1118c..7706750fef 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -1,4 +1,5 @@ #include +#include #include "Debug.h" #include "LuaTools.h" @@ -13,6 +14,7 @@ #include "df/widget_unit_list.h" #include "df/world.h" + using std::vector; using std::string; @@ -28,7 +30,8 @@ namespace DFHack { } using item_or_unit = std::variant; -using filter_vec_type = std::vector>; +using filter_func = bool(item_or_unit); +using filter_vec_type = std::vector>; // recreated here since our autogenerated df::sort_entry lacks template params struct sort_entry { @@ -42,19 +45,10 @@ static const string DFHACK_SORT_IDENT = "dfhack_sort"; // filter logic // -static bool probing = false; -static bool probe_result = false; - static bool do_filter(const char *module_name, const char *fn_name, const item_or_unit &elem) { if (std::holds_alternative(elem)) return true; auto unit = std::get(elem); - if (probing) { - TRACE(log).print("probe successful\n"); - probe_result = true; - return false; - } - bool ret = true; color_ostream &out = Core::getInstance().getConsole(); Lua::CallLuaModuleFunction(out, module_name, fn_name, std::make_tuple(unit), @@ -78,35 +72,28 @@ static bool do_work_animal_assignment_filter(item_or_unit elem) { return do_filter("plugins.sort.info", "do_work_animal_assignment_filter", elem); } -static int32_t our_filter_idx(df::widget_unit_list *unitlist) { - if (world->units.active.empty()) - return -1; - - df::unit *u = world->units.active[0]; // any unit will do; we just need a sentinel - if (!u) - return -1; - - probing = true; - probe_result = false; +static int32_t our_filter_idx(filter_func* filter, df::widget_unit_list* unitlist) +{ int32_t idx = 0; - filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); + filter_vec_type* filter_vec = reinterpret_cast(&unitlist->filter_func); TRACE(log).print("probing for our filter function\n"); - for (auto& fn : *filter_vec) { - fn(item_or_unit(u)); - if (probe_result) { + + for (auto& fn : *filter_vec) + { + auto t = fn.target(); + if (t && *t == filter) + { TRACE(log).print("found our filter function at idx %d\n", idx); - break; + return idx; } ++idx; } - - probing = false; - return probe_result ? idx : -1; + return -1; } -static df::widget_unit_list * get_squad_unit_list() { +static df::widget_unit_list* get_squad_unit_list() { return virtual_cast( Gui::getWidget(&game->main_interface.unit_selector, "Unit selector")); } @@ -182,8 +169,8 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector= 0) { DEBUG(log,out).print("removing %s filter function\n", which); filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); @@ -202,22 +189,22 @@ static void remove_sort_function(color_ostream &out, const char *which, df::widg DFhackCExport command_result plugin_shutdown(color_ostream &out) { if (auto unitlist = get_squad_unit_list()) { - remove_filter_function(out, "squad", unitlist); + remove_filter_function(out, do_squad_filter, "squad", unitlist); remove_sort_function(out, "squad", unitlist); } if (auto unitlist = get_interrogate_unit_list("Open cases")) - remove_filter_function(out, "open cases interrogate", unitlist); + remove_filter_function(out, do_justice_filter, "open cases interrogate", unitlist); if (auto unitlist = get_interrogate_unit_list("Cold cases")) - remove_filter_function(out, "cold cases interrogate", unitlist); + remove_filter_function(out, do_justice_filter, "cold cases interrogate", unitlist); if (auto unitlist = get_convict_unit_list("Open cases")) - remove_filter_function(out, "open cases convict", unitlist); + remove_filter_function(out, do_justice_filter, "open cases convict", unitlist); if (auto unitlist = get_convict_unit_list("Cold cases")) - remove_filter_function(out, "cold cases convict", unitlist); + remove_filter_function(out, do_justice_filter, "cold cases convict", unitlist); if (auto unitlist = get_work_animal_assignment_unit_list()) - remove_filter_function(out, "work animal assignment", unitlist); + remove_filter_function(out, do_work_animal_assignment_filter, "work animal assignment", unitlist); return CR_OK; } @@ -228,7 +215,7 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) { static void sort_set_squad_filter_fn(color_ostream &out) { auto unitlist = get_squad_unit_list(); - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_squad_filter, unitlist) == -1) { DEBUG(log).print("adding squad filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_squad_filter); @@ -240,7 +227,7 @@ static void sort_set_squad_filter_fn(color_ostream &out) { } static void sort_set_justice_filter_fn(color_ostream &out, df::widget_unit_list *unitlist) { - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_justice_filter, unitlist) == -1) { DEBUG(log).print("adding justice filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_justice_filter); @@ -249,7 +236,7 @@ static void sort_set_justice_filter_fn(color_ostream &out, df::widget_unit_list } static void sort_set_work_animal_assignment_filter_fn(color_ostream &out, df::widget_unit_list *unitlist) { - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_work_animal_assignment_filter, unitlist) == -1) { DEBUG(log).print("adding work animal assignment filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_work_animal_assignment_filter); From 7cd54dd3cca3d5edcc0a2078f6db63b1a3dea666 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 19 Nov 2025 19:08:09 -0600 Subject: [PATCH 0608/1082] rewrite `BitArray` to be a little less crusty and no malloc/free/realloc neither --- library/include/BitArray.h | 216 ++++++++++++++++++++------------- library/include/DataDefs.h | 2 +- library/include/DataIdentity.h | 2 +- library/include/RemoteTools.h | 2 +- 4 files changed, 135 insertions(+), 87 deletions(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 15ec84a62f..dae604f659 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -27,60 +27,100 @@ distribution. #include #include #include + +#include #include #include + namespace DFHack { template class BitArray { + private: + // note that these are mandated by the implementation of flagarrayst in DF code, and must be exactly as below + using buffer_type = unsigned char; + using size_type = int32_t; + + buffer_type* _bits; + size_type _size; + + void resize(size_type newsize, const BitArray* replacement) + { + if (newsize == _size) + return; + + if (newsize == 0) + { + delete[] _bits; + _bits = nullptr; + _size = 0; + return; + } + + buffer_type* old_data = _bits; + + _bits = new buffer_type[newsize]; + + buffer_type* copysrc = replacement ? replacement->_bits : old_data; + size_type copysize = replacement ? replacement->_size : _size; + + if (copysrc) + std::memcpy(_bits, copysrc, std::min(copysize, newsize)); + + if (newsize > _size) + std::memset(_bits + _size, 0, newsize - _size); + + delete[] old_data; + + _size = newsize; + } + + void extend(T index) + { + size_type newsize = (index / 8) + 1; + if (newsize > _size) + resize(newsize); + } + public: - BitArray() : bits(NULL), size(0) {} - BitArray(const BitArray &other) : bits(NULL), size(0) + BitArray() : _bits(nullptr), _size(0) {} + BitArray(const BitArray &other) : _bits(nullptr), _size(0) { - *this = other; + resize(other._size, &other); } ~BitArray() { - free(bits); + delete [] _bits; } - explicit BitArray(T last) : bits(NULL), size(0) { + explicit BitArray(T last) : _bits(nullptr), _size(0) { extend(last); } - explicit BitArray(unsigned bytes) : bits(NULL), size(0) { + explicit BitArray(unsigned bytes) : _bits(nullptr), _size(0) { resize(bytes); } - void clear_all ( void ) + size_type size() const { return _size; } + buffer_type* bits() const { return _bits; } + + void resize( size_type newsize ) { - if(bits) - memset(bits, 0, size); + resize(newsize, nullptr); } - void resize (unsigned newsize) + + void clear_all ( void ) { - if (newsize == size) - return; - uint8_t* mem = (uint8_t *) realloc(bits, newsize); - if(!mem && newsize != 0) - throw std::bad_alloc(); - bits = mem; - if (newsize > size) - memset(bits+size, 0, newsize-size); - size = newsize; - } - BitArray &operator= (const BitArray &other) - { - resize(other.size); - memcpy(bits, other.bits, size); - return *this; + if(_bits) + memset(_bits, 0, _size); } - void extend (T index) + + BitArray& operator= (const BitArray& other) { - unsigned newsize = (index / 8) + 1; - if (newsize > size) - resize(newsize); + resize(other._size, &other); + return *this; } + void set (T index, bool value = true) { if(!value) @@ -88,71 +128,74 @@ namespace DFHack clear(index); return; } - uint32_t byte = index / 8; + size_type byte = index / 8; extend(index); - //if(byte < size) - { - uint8_t bit = 1 << (index % 8); - bits[byte] |= bit; - } + uint8_t bit = 1 << (index % 8); + _bits[byte] |= bit; } + void clear (T index) { - uint32_t byte = index / 8; - if(byte < size) + size_type byte = index / 8; + if(byte < _size) { uint8_t bit = 1 << (index % 8); - bits[byte] &= ~bit; + _bits[byte] &= ~bit; } } + void toggle (T index) { - uint32_t byte = index / 8; + size_type byte = index / 8; extend(index); - //if(byte < size) - { - uint8_t bit = 1 << (index % 8); - bits[byte] ^= bit; - } + uint8_t bit = 1 << (index % 8); + _bits[byte] ^= bit; } + bool is_set (T index) const { - uint32_t byte = index / 8; - if(byte < size) + size_type byte = index / 8; + if(byte < _size) { uint8_t bit = 1 << (index % 8); - return bit & bits[byte]; + return bit & _bits[byte]; } else return false; } + /// WARNING: this can truncate long bit arrays - uint32_t as_int () + template + I as_int () const { - if(!bits) + if(!_bits) return 0; - if(size >= 4) - return *(uint32_t *)bits; - uint32_t target = 0; - memcpy (&target, bits,size); + if (_size >= sizeof(I)) + // FIXME (C++23): should be std::start_lifetime_as + return *reinterpret_cast(_bits); + I target = 0; + std::memcpy(&target, _bits, _size); return target; } + /// WARNING: this can be truncated / only overwrite part of the data - bool operator =(uint32_t data) + template + bool operator =(I data) { - if(!bits) + if(!_bits) return false; - if (size >= 4) + if (_size >= sizeof(I)) { - (*(uint32_t *)bits) = data; + *reinterpret_cast(_bits) = data; return true; } - memcpy(bits, &data, size); + std::memcpy(_bits, &data, _size); return true; } + friend std::ostream& operator<< (std::ostream &out, BitArray &ba) { std::stringstream sstr; - for (int i = 0; i < ba.size * 8; i++) + for (int i = 0; i < ba._size * 8; i++) { if(ba.is_set((T)i)) sstr << "1 "; @@ -162,23 +205,44 @@ namespace DFHack out << sstr.str(); return out; } - uint8_t * bits; - uint32_t size; }; template class DfArray { + private: T *m_data; unsigned short m_size; + + void resize(unsigned new_size, const DfArray* replacement) + { + if (new_size == m_size) + return; + + T* old_data = m_data; + + m_data = (T*) new T[new_size]; + + T* copysrc = replacement ? replacement->m_data : m_data; + unsigned short copysize = replacement ? replacement->m_size : m_size; + + if (copysrc) + std::memcpy(m_data, copysrc, sizeof(T) * copysize); + + if (new_size > m_size) + std::memset(m_data + m_size, 0, sizeof(T) * (new_size - m_size)); + + delete[] old_data; + + m_size = new_size; + } public: - DfArray() : m_data(NULL), m_size(0) {} - ~DfArray() { free(m_data); } + DfArray() : m_data(nullptr), m_size(0) {} + ~DfArray() { delete[] m_data; } - DfArray(const DfArray &other) : m_data(NULL), m_size(0) + DfArray(const DfArray &other) : m_data(nullptr), m_size(0) { - resize(other.m_size); - memcpy(m_data, other.m_data,m_size*sizeof(T)); + resize(other.m_size, &other); } typedef T value_type; @@ -195,28 +259,12 @@ namespace DFHack void resize(unsigned new_size) { - if (new_size == m_size) - return; - if(!m_data) - { - m_data = (T*) malloc(sizeof(T)*new_size); - } - else - { - T* mem = (T*) realloc(m_data, sizeof(T)*new_size); - if(!mem && new_size != 0) - throw std::bad_alloc(); - m_data = mem; - } - if (new_size > m_size) - memset(m_data+sizeof(T)*m_size, 0, sizeof(T)*(new_size - m_size)); - m_size = new_size; + resize(new_size, nullptr); } DfArray &operator= (const DfArray &other) { - resize(other.size()); - memcpy(data(), other.data(), sizeof(T)*size()); + resize(other.size(), &other); return *this; } diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 74ffbe251a..9ce2cd4dac 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -948,7 +948,7 @@ namespace DFHack { inline void flagarray_to_string(std::vector *pvec, const BitArray &val) { typedef df::enum_traits traits; int size = traits::last_item_value-traits::first_item_value+1; - flagarrayToString(pvec, val.bits, val.size, + flagarrayToString(pvec, val.bits(), val.size(), (int)traits::first_item_value, size, traits::key_table); } diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 4bd4976250..f8fd3c6fd7 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -523,7 +523,7 @@ namespace df protected: virtual int item_count(void *ptr, CountMode cnt) const { - return cnt == COUNT_LEN ? ((container*)ptr)->size * 8 : -1; + return cnt == COUNT_LEN ? ((container*)ptr)->size() * 8 : -1; } virtual bool get_item(void *ptr, int idx) const { return ((container*)ptr)->is_set(idx); diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h index 38c234c5fb..f041c5d7b8 100644 --- a/library/include/RemoteTools.h +++ b/library/include/RemoteTools.h @@ -74,7 +74,7 @@ namespace DFHack */ template void flagarray_to_ints(RepeatedField *pf, const BitArray &val) { - for (size_t i = 0; i < val.size*8; i++) + for (size_t i = 0; i < val.size()*8; i++) if (val.is_set(T(i))) pf->Add(i); } From 6d21dcc379d1286f02f2858086a39197e841a702 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 19 Nov 2025 19:11:43 -0600 Subject: [PATCH 0609/1082] add missing header --- library/include/BitArray.h | 1 + 1 file changed, 1 insertion(+) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index dae604f659..19b226f7a1 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -29,6 +29,7 @@ distribution. #include #include +#include #include #include From 207e443b82d3161cd4d8e9e4961dd50ae215a5ac Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 20:52:33 -0500 Subject: [PATCH 0610/1082] Refactor hotkey code into a new Hotkey module --- library/CMakeLists.txt | 2 + library/Core.cpp | 333 ++---------------- library/include/Core.h | 39 +- library/include/modules/DFSDL.h | 4 + library/include/modules/Hotkey.h | 57 +++ library/modules/DFSDL.cpp | 18 + library/modules/Hotkey.cpp | 300 ++++++++++++++++ plugins/blueprint.cpp | 3 +- plugins/embark-assistant/embark-assistant.cpp | 3 +- plugins/hotkeys.cpp | 68 ++-- 10 files changed, 442 insertions(+), 385 deletions(-) create mode 100644 library/include/modules/Hotkey.h create mode 100644 library/modules/Hotkey.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index fa5c482bf6..6f91c6c08b 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -156,6 +156,7 @@ set(MODULE_HEADERS include/modules/Graphic.h include/modules/Gui.h include/modules/GuiHooks.h + include/modules/Hotkey.h include/modules/Items.h include/modules/Job.h include/modules/Kitchen.h @@ -186,6 +187,7 @@ set(MODULE_SOURCES modules/Filesystem.cpp modules/Graphic.cpp modules/Gui.cpp + modules/Hotkey.cpp modules/Items.cpp modules/Job.cpp modules/Kitchen.cpp diff --git a/library/Core.cpp b/library/Core.cpp index 33f66d767e..6cc60aaf85 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -47,6 +47,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Textures.h" #include "modules/World.h" #include "modules/Persistence.h" @@ -104,11 +105,9 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = nullptr); static size_t loadScriptFiles(Core* core, color_ostream& out, std::span prefix, const std::filesystem::path& folder); namespace DFHack { - DBG_DECLARE(core, keybinding, DebugCategory::LINFO); DBG_DECLARE(core, script, DebugCategory::LINFO); @@ -274,34 +273,6 @@ struct IODATA PluginManager * plug_mgr; }; -// A thread function... for handling hotkeys. This is needed because -// all the plugin commands are expected to be run from foreign threads. -// Running them from one of the main DF threads will result in deadlock! -static void fHKthread(IODATA * iodata) -{ - Core * core = iodata->core; - PluginManager * plug_mgr = iodata->plug_mgr; - if(!plug_mgr || !core) - { - std::cerr << "Hotkey thread has croaked." << std::endl; - return; - } - bool keep_going = true; - while(keep_going) - { - std::string stuff = core->getHotkeyCmd(keep_going); // waits on mutex! - if(!stuff.empty()) - { - color_ostream_proxy out(core->getConsole()); - - auto rv = core->runCommand(out, stuff); - - if (rv == CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", stuff.c_str()); - } - } -} - static std::string dfhack_version_desc() { std::stringstream s; @@ -990,11 +961,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { std::string keystr = parts[1]; if (parts[0] == "set") - ClearKeyBindings(keystr); + hotkey_mgr->clearKeybind(keystr); // for (int i = parts.size()-1; i >= 2; i--) for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - if (!AddKeyBinding(keystr, part)) { + if (!hotkey_mgr->addKeybind(keystr, part)) { con.printerr("Invalid key spec: %s\n", keystr.c_str()); break; } @@ -1005,7 +976,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s // for (size_t i = 1; i < parts.size(); i++) for (const auto& part : parts | std::views::drop(1)) { - if (!ClearKeyBindings(part)) { + if (!hotkey_mgr->clearKeybind(part)) { con.printerr("Invalid key spec: %s\n", part.c_str()); break; } @@ -1013,7 +984,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (parts.size() == 2 && parts[0] == "list") { - std::vector list = ListKeyBindings(parts[1]); + std::vector list = hotkey_mgr->listKeybinds(parts[1]); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) @@ -1471,8 +1442,7 @@ Core::~Core() Core::Core() : d(std::make_unique()), script_path_mutex{}, - HotkeyMutex{}, - HotkeyCond{}, + armok_mutex{}, alias_mutex{}, started{false}, CoreSuspendMutex{}, @@ -1488,7 +1458,6 @@ Core::Core() : // set up hotkey capture suppress_duplicate_keyboard_events = true; - hotkey_set = NO; last_world_data_ptr = nullptr; last_local_map_ptr = nullptr; last_pause_state = false; @@ -1841,6 +1810,8 @@ bool Core::InitSimulationThread() plug_mgr->init(); std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); + this->hotkey_mgr = new HotkeyManager(); + auto *temp = new IODATA; temp->core = this; temp->plug_mgr = plug_mgr; @@ -1858,8 +1829,6 @@ bool Core::InitSimulationThread() } std::cerr << "Starting DF input capture thread.\n"; - // set up hotkey capture - d->hotkeythread = std::thread(fHKthread, temp); started = true; modstate = 0; @@ -1933,31 +1902,6 @@ bool Core::InitSimulationThread() return true; } -/// sets the current hotkey command -bool Core::setHotkeyCmd( std::string cmd ) -{ - // access command - std::lock_guard lock(HotkeyMutex); - hotkey_set = SET; - hotkey_cmd = std::move(cmd); - HotkeyCond.notify_all(); - return true; -} -/// removes the hotkey command and gives it to the caller thread -std::string Core::getHotkeyCmd( bool &keep_going ) -{ - std::string returner; - std::unique_lock lock(HotkeyMutex); - HotkeyCond.wait(lock, [this]() -> bool {return this->hotkey_set;}); - if (hotkey_set == SHUTDOWN) { - keep_going = false; - return returner; - } - hotkey_set = NO; - returner = hotkey_cmd; - hotkey_cmd.clear(); - return returner; -} void Core::print(const char *format, ...) { @@ -2405,17 +2349,14 @@ int Core::Shutdown ( void ) con.shutdown(); } - if (d->hotkeythread.joinable()) { - std::unique_lock hot_lock(HotkeyMutex); - hotkey_set = SHUTDOWN; - HotkeyCond.notify_one(); - } - ServerMain::block(); - d->hotkeythread.join(); d->iothread.join(); + if (hotkey_mgr) { + delete hotkey_mgr; + } + if(plug_mgr) { delete plug_mgr; @@ -2490,16 +2431,24 @@ void Core::setSuppressDuplicateKeyboardEvents(bool suppress) { } void Core::setMortalMode(bool value) { - std::lock_guard lock(HotkeyMutex); - mortal_mode = value; + mortal_mode.exchange(value); +} + +bool Core::getMortalMode() { + return mortal_mode.load(); } void Core::setArmokTools(const std::vector &tool_names) { - std::lock_guard lock(HotkeyMutex); + std::lock_guard lock(armok_mutex); armok_tools.clear(); armok_tools.insert(tool_names.begin(), tool_names.end()); } +bool Core::isArmokTool(const std::string& tool) { + std::lock_guard lock(armok_mutex); + return armok_tools.contains(tool); +} + // returns true if the event is handled bool Core::DFH_SDL_Event(SDL_Event* ev) { uint32_t start_ms = p->getTickCount(); @@ -2540,7 +2489,7 @@ bool Core::doSdlInputEvent(SDL_Event* ev) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress DEBUG(keybinding).print("key down: sym=%d (%c)\n", sym, sym); - if (SelectHotkey(sym, modstate)) { + if (hotkey_mgr->handleKeybind(sym, modstate)) { hotkey_states[sym] = true; if (modstate & (DFH_MOD_CTRL | DFH_MOD_ALT)) { DEBUG(keybinding).print("modifier key detected; not inhibiting SDL key down event\n"); @@ -2561,8 +2510,8 @@ bool Core::doSdlInputEvent(SDL_Event* ev) DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { - SDL_Keycode sym = SDLK_F13 + but.button - 4; - if (sym <= SDLK_F24 && SelectHotkey(sym, modstate)) + SDL_Keycode sym = -but.button; + if (sym >= -15 && sym <= -1 && hotkey_mgr->handleKeybind(sym, modstate)) return suppress_duplicate_keyboard_events; } } else if (ev->type == SDL_TEXTINPUT) { @@ -2582,236 +2531,6 @@ bool Core::doSdlInputEvent(SDL_Event* ev) return false; } -bool Core::SelectHotkey(int sym, int modifiers) -{ - // Find the topmost viewscreen - if (!df::global::gview || !df::global::plotinfo) - return false; - - df::viewscreen *screen = &df::global::gview->view; - while (screen->child) - screen = screen->child; - - if (sym == SDLK_KP_ENTER) - sym = SDLK_RETURN; - - std::string cmd; - - DEBUG(keybinding).print("checking hotkeys for sym=%d (%c), modifiers=%x\n", sym, sym, modifiers); - - { - std::lock_guard lock(HotkeyMutex); - - // Check the internal keybindings - std::vector &bindings = key_bindings[sym]; - //for (int i = bindings.size()-1; i >= 0; --i) { - for (const auto& binding : bindings | std::views::reverse) { - DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); - - if (binding.modifiers != modifiers) { - DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x%x != 0x%x\n", - binding.modifiers, modifiers); - continue; - } - if (!binding.focus.empty()) { - if (!Gui::matchFocusString(binding.focus)) { - std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' != '%s'\n", - join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); - continue; - } - } - if (!plug_mgr->CanInvokeHotkey(binding.command[0], screen)) { - DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - if (mortal_mode && armok_tools.contains(binding.command[0])) { - DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - - cmd = binding.cmdline; - DEBUG(keybinding).print("matched hotkey\n"); - break; - } - - if (cmd.empty()) { - // Check the hotkey keybindings - int idx = sym - SDLK_F1; - if(idx >= 0 && idx < 8) - { -/* TODO: understand how this changes for v50 - if (modifiers & 1) - idx += 8; - - if (strict_virtual_cast(screen) && - df::global::plotinfo->main.mode != ui_sidebar_mode::Hotkeys && - df::global::plotinfo->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None) - { - cmd = df::global::plotinfo->main.hotkeys[idx].name; - } -*/ - } - } - } - - if (!cmd.empty()) { - setHotkeyCmd(cmd); - return true; - } - - return false; -} - -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus) -{ - *pmod = 0; - - if (pfocus) - { - *pfocus = ""; - - size_t idx = keyspec.find('@'); - if (idx != std::string::npos) - { - *pfocus = keyspec.substr(idx+1); - keyspec = keyspec.substr(0, idx); - } - } - - // ugh, ugly - for (;;) { - if (keyspec.size() > 6 && keyspec.starts_with("Shift-")) { - *pmod |= 1; - keyspec = keyspec.substr(6); - } else if (keyspec.size() > 5 && keyspec.starts_with("Ctrl-")) { - *pmod |= 2; - keyspec = keyspec.substr(5); - } else if (keyspec.size() > 4 && keyspec.starts_with("Alt-")) { - *pmod |= 4; - keyspec = keyspec.substr(4); - } else - break; - } - - if (keyspec.size() == 1 && keyspec[0] >= 'A' && keyspec[0] <= 'Z') { - *psym = SDLK_a + (keyspec[0]-'A'); - return true; - } else if (keyspec.size() == 1 && keyspec[0] == '`') { - *psym = SDLK_BACKQUOTE; - return true; - } else if (keyspec.size() == 1 && keyspec[0] >= '0' && keyspec[0] <= '9') { - *psym = SDLK_0 + (keyspec[0]-'0'); - return true; - } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { - *psym = SDLK_F1 + (keyspec[1]-'1'); - return true; - } else if (keyspec.size() == 3 && keyspec.starts_with("F1") && keyspec[2] >= '0' && keyspec[2] <= '2') { - *psym = SDLK_F10 + (keyspec[2]-'0'); - return true; - } else if (keyspec.size() == 6 && keyspec.starts_with("MOUSE") && keyspec[5] >= '4' && keyspec[5] <= '9') { - *psym = SDLK_F13 + (keyspec[5]-'4'); - return true; - } else if (keyspec.size() == 7 && keyspec.starts_with("MOUSE1") && keyspec[5] >= '0' && keyspec[5] <= '5') { - *psym = SDLK_F19 + (keyspec[5]-'0'); - return true; - } else if (keyspec == "Enter") { - *psym = SDLK_RETURN; - return true; - } else - return false; -} - -bool Core::ClearKeyBindings(std::string keyspec) -{ - int sym, mod; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return false; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == mod && prefix_matches(focus, bindings[i].focus)) - bindings.erase(bindings.begin()+i); - } - - return true; -} - -bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) -{ - size_t at_pos = keyspec.find('@'); - if (at_pos != std::string::npos) - { - std::string raw_spec = keyspec.substr(0, at_pos); - std::string raw_focus = keyspec.substr(at_pos + 1); - if (raw_focus.find('|') != std::string::npos) - { - std::vector focus_strings; - split_string(&focus_strings, raw_focus, "|"); - for (const auto& fs : focus_strings) - { - if (!AddKeyBinding(raw_spec + "@" + fs, cmdline)) - return false; - } - return true; - } - } - int sym; - KeyBinding binding; - if (!parseKeySpec(keyspec, &sym, &binding.modifiers, &binding.focus)) - return false; - - cheap_tokenise(cmdline, binding.command); - if (binding.command.empty()) - return false; - - std::lock_guard lock(HotkeyMutex); - - // Don't add duplicates - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == binding.modifiers && - bindings[i].cmdline == cmdline && - bindings[i].focus == binding.focus) - return true; - } - - binding.cmdline = cmdline; - bindings.push_back(binding); - return true; -} - -std::vector Core::ListKeyBindings(std::string keyspec) -{ - int sym, mod; - std::vector rv; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return rv; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (focus.size() && focus != bindings[i].focus) - continue; - if (bindings[i].modifiers == mod) - { - std::string cmd = bindings[i].cmdline; - if (!bindings[i].focus.empty()) - cmd = "@" + bindings[i].focus + ": " + cmd; - rv.push_back(cmd); - } - } - - return rv; -} - bool Core::AddAlias(const std::string &name, const std::vector &command, bool replace) { std::lock_guard lock(alias_mutex); diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef1..08c7ca3d2e 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -68,6 +68,7 @@ namespace DFHack class Core; class ServerMain; class CoreSuspender; + class HotkeyManager; namespace Lua { namespace Core { DFHACK_EXPORT void Reset(color_ostream &out, const char *where); @@ -164,10 +165,6 @@ namespace DFHack Materials * getMaterials(); /// get the graphic module Graphic * getGraphic(); - /// sets the current hotkey command - bool setHotkeyCmd( std::string cmd ); - /// removes the hotkey command and gives it to the caller thread - std::string getHotkeyCmd( bool &keep_going ); 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); @@ -182,11 +179,10 @@ namespace DFHack bool getSuppressDuplicateKeyboardEvents() const; void setSuppressDuplicateKeyboardEvents(bool suppress); void setMortalMode(bool value); + bool getMortalMode(); void setArmokTools(const std::vector &tool_names); + bool isArmokTool(const std::string& name); - bool ClearKeyBindings(std::string keyspec); - bool AddKeyBinding(std::string keyspec, std::string cmdline); - std::vector ListKeyBindings(std::string keyspec); int8_t getModstate() { return modstate; } bool AddAlias(const std::string &name, const std::vector &command, bool replace = false); @@ -213,6 +209,7 @@ namespace DFHack static void printerr(const char *format, ...) Wformat(printf,1,2); PluginManager *getPluginManager() { return plug_mgr; } + HotkeyManager *getHotkeyManager() { return hotkey_mgr; } static void cheap_tokenise(std::string const& input, std::vector &output); @@ -267,39 +264,25 @@ namespace DFHack Graphic * pGraphic; } s_mods; std::vector> allModules; - DFHack::PluginManager * plug_mgr; + DFHack::PluginManager *plug_mgr; + // Hotkey Manager + DFHack::HotkeyManager *hotkey_mgr; + + // FIXME: remove all this junk (hotkey related) std::vector script_paths[3]; std::mutex script_path_mutex; - // hotkey-related stuff - struct KeyBinding { - int modifiers; - std::vector command; - std::string cmdline; - std::string focus; - }; int8_t modstate; bool suppress_duplicate_keyboard_events; - bool mortal_mode; + std::atomic mortal_mode; std::unordered_set armok_tools; - std::map > key_bindings; - std::string hotkey_cmd; - enum hotkey_set_t { - NO, - SET, - SHUTDOWN, - }; - hotkey_set_t hotkey_set; - std::mutex HotkeyMutex; - std::condition_variable HotkeyCond; + std::mutex armok_mutex; std::map> aliases; std::recursive_mutex alias_mutex; - bool SelectHotkey(int key, int modifiers); - // for state change tracking df::world_data *last_world_data_ptr; // for state change tracking diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index ff8e81ab2c..161cc35171 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -4,12 +4,14 @@ #include "ColorText.h" #include +#include struct SDL_Surface; struct SDL_Rect; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; +typedef int32_t SDL_Keycode; namespace DFHack { @@ -63,6 +65,8 @@ namespace DFHack::DFSDL DFHACK_EXPORT char* DFSDL_GetPrefPath(const char* org, const char* app); DFHACK_EXPORT char* DFSDL_GetBasePath(); + DFHACK_EXPORT SDL_Keycode DFSDL_GetKeyFromName(const char* name); + DFHACK_EXPORT const char* DFSDL_GetKeyName(SDL_Keycode key); } namespace DFHack diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h new file mode 100644 index 0000000000..581ebe43e0 --- /dev/null +++ b/library/include/modules/Hotkey.h @@ -0,0 +1,57 @@ +#pragma once + +#include "Export.h" + +#include +#include +#include +#include +#include + +namespace DFHack { + class DFHACK_EXPORT HotkeyManager { + public: + HotkeyManager(); + ~HotkeyManager(); + + struct KeySpec { + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + }; + + struct KeyBinding { + HotkeyManager::KeySpec spec; + std::string command; + std::string cmdline; + }; + + bool addKeybind(std::string keyspec, std::string cmd); + bool addKeybind(KeySpec spec, std::string cmd); + bool clearKeybind(std::string keyspec, bool any_focus=false); + bool clearKeybind(const KeySpec& spec, bool any_focus=false); + + std::vector listKeybinds(std::string keyspec); + std::vector listKeybinds(const KeySpec& spec); + + std::vector listActiveKeybinds(); + + bool handleKeybind(int sym, int modifiers); + + void setHotkeyCommand(std::string cmd); + + std::optional parseKeySpec(std::string spec); + private: + std::thread hotkey_thread; + std::mutex lock {}; + std::condition_variable cond {}; + + int hotkey_sig = 0; + std::string queued_command = ""; + + std::map> bindings; + + void hotkey_thread_fn(); + }; +} diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 9da2bd8057..028422d75a 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -7,6 +7,7 @@ #include "PluginManager.h" #include +#include #ifdef WIN32 # include @@ -62,6 +63,9 @@ int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; char* (*g_SDL_GetBasePath)() = nullptr; +SDL_Keycode (*g_SDL_GetKeyFromName)(const char* name) = nullptr; +const char* (*g_SDL_GetKeyName)(SDL_Keycode key) = nullptr; + bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { if ((g_sdl_handle = OpenPlugin(lib_str.c_str()))) @@ -106,6 +110,8 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_ShowSimpleMessageBox); bind(g_sdl_handle, SDL_GetPrefPath); bind(g_sdl_handle, SDL_GetBasePath); + bind(g_sdl_handle, SDL_GetKeyFromName); + bind(g_sdl_handle, SDL_GetKeyName); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -196,6 +202,18 @@ int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const c return g_SDL_ShowSimpleMessageBox(flags, title, message, window); } +SDL_Keycode DFSDL::DFSDL_GetKeyFromName(const char* name) { + if (!g_SDL_GetKeyFromName) + return SDLK_UNKNOWN; + return g_SDL_GetKeyFromName(name); +} + +const char* DFSDL::DFSDL_GetKeyName(SDL_Keycode key) { + if (!g_SDL_GetKeyName) + return ""; + return g_SDL_GetKeyName(key); +} + // convert tabs to spaces so they don't get converted to '?' static char * tabs_to_spaces(char *str) { for (char *c = str; *c; ++c) { diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp new file mode 100644 index 0000000000..2cf87129d4 --- /dev/null +++ b/library/modules/Hotkey.cpp @@ -0,0 +1,300 @@ +#include "modules/Hotkey.h" +#include "Core.h" +#include "CoreDefs.h" +#include "ColorText.h" +#include "MiscUtils.h" +#include "modules/DFSDL.h" +#include "modules/Gui.h" +#include "PluginManager.h" +#include "df/global_objects.h" +#include "df/viewscreen.h" +#include "df/interfacest.h" + +#include + +#include "SDL_keycode.h" + +using namespace DFHack; + +enum HotkeySignal { + None = 0, + CmdReady, + Shutdown, +}; + +bool operator==(const HotkeyManager::KeySpec& a, const HotkeyManager::KeySpec& b) { + return a.modifiers == b.modifiers && a.sym == b.sym && + a.focus.size() == b.focus.size() && + std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); +} + +// Equality operator for key bindings +bool operator==(const HotkeyManager::KeyBinding& a, const HotkeyManager::KeyBinding& b) { + return a.spec == b.spec && + a.command == b.command && + a.cmdline == b.cmdline; +} + +// Hotkeys actions are executed from an external thread to avoid deadlocks +// that may occur if running commands from the render or simulation threads. +void HotkeyManager::hotkey_thread_fn() { + auto& core = DFHack::Core::getInstance(); + + std::unique_lock l(lock); + while (true) { + cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); + if (hotkey_sig == HotkeySignal::Shutdown) + return; + if (hotkey_sig != HotkeySignal::CmdReady) + continue; + + // Copy and reset important data, then release the lock + this->hotkey_sig = HotkeySignal::None; + std::string cmd = this->queued_command; + this->queued_command.clear(); + l.unlock(); + + // Attempt execution of command + DFHack::color_ostream_proxy out(core.getConsole()); + auto res = core.runCommand(out, cmd); + if (res == DFHack::CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); + l.lock(); + } +} + +std::optional HotkeyManager::parseKeySpec(std::string spec) { + KeySpec out; + + // Determine focus string, if present + size_t focus_idx = spec.find('@'); + if (focus_idx != std::string::npos) { + split_string(&out.focus, spec.substr(focus_idx + 1), "|"); + spec.erase(focus_idx); + } + + // Determine modifier flags + auto match_modifier = [&out, &spec](std::string_view prefix, int mod) { + bool found = spec.starts_with(prefix); + if (found) { + out.modifiers |= mod; + spec.erase(0, prefix.size()); + } + return found; + }; + while (match_modifier("Shift-", DFH_MOD_SHIFT) || match_modifier("Ctrl-", DFH_MOD_CTRL) || match_modifier("Alt-", DFH_MOD_ALT)) {} + + out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); + if (out.sym != SDLK_UNKNOWN) + return out; + + // Attempt to parse as a mouse binding + if (spec.starts_with("MOUSE")) { + spec.erase(0, 5); + // Read button number, ensuring between 1 and 15 inclusive + try { + int mbutton = std::stoi(spec); + if (mbutton >= 1 && mbutton <= 15) { + out.sym = -mbutton; + return out; + } + } catch (...) { + // If integer parsing fails, it isn't valid + } + } + + // Invalid key binding + return std::nullopt; +} + +bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { + // No point in a hotkey with no action + if (cmd.empty()) + return false; + + KeyBinding binding; + binding.spec = spec; + binding.cmdline = cmd; + + std::lock_guard l(lock); + auto& bindings = this->bindings[binding.spec.sym]; + for (auto& bind : bindings) { + // Don't set a keybind twice, but return true as there isn't an issue + if (bind == binding) + return true; + } + + bindings.emplace_back(binding); + return true; +} + +bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return false; + return this->addKeybind(spec_opt.value(), cmd); +} + +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return false; + auto& binds = bindings[spec.sym]; + + auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec](const auto& v) { + return any_focus + ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers + : v.spec == spec; + }); + if (new_end == binds.end()) + return false; // No bindings removed + + binds.erase(new_end, binds.end()); + return true; +} + +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return false; + return this->clearKeybind(spec_opt.value(), any_focus); +} + +std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return {}; + + std::vector out; + + auto& binds = bindings[spec.sym]; + for (const auto& bind : binds) { + if (bind.spec.modifiers != spec.modifiers) + continue; + + // If no focus is required, it is always active + if (spec.focus.empty() || bind.spec.focus.empty()) { + out.push_back(bind.cmdline); + continue; + } + + // If a focus is required, determine if search spec if the same or more specific + for (const auto& requested : spec.focus) { + for (const auto& to_match : bind.spec.focus) { + if (prefix_matches(to_match, requested)) + out.push_back("@" + to_match + ":" + bind.cmdline); + } + } + } + + return out; +} + +std::vector HotkeyManager::listKeybinds(std::string keyspec) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return {}; + return this->listKeybinds(spec_opt.value()); +} + +std::vector HotkeyManager::listActiveKeybinds() { + std::vector out; + + for(const auto& [_, bind_set] : bindings) { + for (const auto& binding : bind_set) { + if (binding.spec.focus.empty()) { + // Binding always active + out.emplace_back(binding); + continue; + } + for (const auto& focus : binding.spec.focus) { + // Determine if focus string allows this binding + if (Gui::matchFocusString(focus)) { + out.emplace_back(binding); + break; + } + } + } + } + + return out; +} + +bool HotkeyManager::handleKeybind(int sym, int modifiers) { + // Ensure gamestate is ready + if (!df::global::gview || !df::global::plotinfo) + return false; + + // Get bottommost active screen + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + + if (sym == SDLK_KP_ENTER) + sym = SDLK_RETURN; + + std::unique_lock l(lock); + if (!bindings.contains(sym)) + return false; + auto& binds = bindings[sym]; + + auto& core = Core::getInstance(); + bool mortal_mode = core.getMortalMode(); + + for (const auto& bind : binds | std::views::reverse) { + if (bind.spec.modifiers != modifiers) + continue; + + if (!bind.spec.focus.empty()) { + bool matched = false; + for (const auto& focus : bind.spec.focus) { + printf("Focus check for: %s", focus.c_str()); + if (Gui::matchFocusString(focus)) { + printf("Matched\n"); + matched = true; + break; + } + } + if (!matched) + continue; + } + + if (!Core::getInstance().getPluginManager()->CanInvokeHotkey(bind.command, screen)) + continue; + + if (mortal_mode && core.isArmokTool(bind.command)) + continue; + + queued_command = bind.cmdline; + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); + return true; + } + + return false; +} + +void HotkeyManager::setHotkeyCommand(std::string cmd) { + std::unique_lock l(lock); + queued_command = cmd; + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); +} + +HotkeyManager::HotkeyManager() { + this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); +} + +HotkeyManager::~HotkeyManager() { + // Set shutdown signal and notify thread + { + std::lock_guard l(lock); + this->hotkey_sig = HotkeySignal::Shutdown; + } + cond.notify_all(); + + if (this->hotkey_thread.joinable()) + this->hotkey_thread.join(); +} diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 98b4c1c155..330a0938f9 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -19,6 +19,7 @@ #include "modules/Constructions.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Maps.h" #include "modules/World.h" @@ -1668,7 +1669,7 @@ static command_result do_blueprint(color_ostream &out, string command_str = command.str(); out.print("launching %s\n", command_str.c_str()); - Core::getInstance().setHotkeyCmd(command_str); + Core::getInstance().getHotkeyManager()->setHotkeyCommand(command_str); return CR_OK; } diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp index db04308d0b..5b9d11f78a 100644 --- a/plugins/embark-assistant/embark-assistant.cpp +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -5,6 +5,7 @@ #include "PluginManager.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Screen.h" #include "../uicommon.h" @@ -161,7 +162,7 @@ struct start_site_hook : df::viewscreen_choose_start_sitest { { if (!embark_assist::main::state && input->count(interface_key::CUSTOM_A)) { - Core::getInstance().setHotkeyCmd("embark-assistant"); + Core::getInstance().getHotkeyManager()->setHotkeyCommand("embark-assistant"); return; } INTERPOSE_NEXT(feed)(input); diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 13988a21f1..197b0d685d 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -2,8 +2,10 @@ #include #include +#include "modules/DFSDL.h" #include "modules/Gui.h" #include "modules/Screen.h" +#include "modules/Hotkey.h" #include "Debug.h" #include "LuaTools.h" @@ -44,7 +46,7 @@ static int cleanupHotkeys(lua_State *) { std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().ClearKeyBindings(keyspec); + Core::getInstance().getHotkeyManager()->clearKeybind(keyspec); }); valid = false; sorted_keys.clear(); @@ -83,60 +85,30 @@ static void add_binding_if_valid(color_ostream &out, const string &sym, const st string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); - Core::getInstance().AddKeyBinding(keyspec, binding); + Core::getInstance().getHotkeyManager()->addKeybind(keyspec, binding); } static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, bool filtermenu) { - DEBUG(log).print("scanning for active keybindings\n"); if (valid) cleanupHotkeys(NULL); - vector valid_keys; - - for (char c = '0'; c <= '9'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (char c = 'A'; c <= 'Z'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (int i = 1; i <= 12; i++) { - valid_keys.push_back('F' + int_to_string(i)); - } - - valid_keys.push_back("`"); - - for (int shifted = 0; shifted < 2; shifted++) { - for (int alt = 0; alt < 2; alt++) { - for (int ctrl = 0; ctrl < 2; ctrl++) { - for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { - string sym; - if (ctrl) sym += "Ctrl-"; - if (alt) sym += "Alt-"; - if (shifted) sym += "Shift-"; - sym += *it; - - auto list = Core::getInstance().ListKeyBindings(sym); - for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { - string::size_type colon_pos = invoke_cmd->find(":"); - // colons at location 0 are for commands like ":lua" - if (colon_pos == string::npos || colon_pos == 0) { - add_binding_if_valid(out, sym, *invoke_cmd, screen, filtermenu); - } - else { - vector tokens; - split_string(&tokens, *invoke_cmd, ":"); - string focus = tokens[0].substr(1); - if(Gui::matchFocusString(focus)) { - auto cmdline = trim(tokens[1]); - add_binding_if_valid(out, sym, cmdline, screen, filtermenu); - } - } - } - } - } + auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); + for (const auto& bind : active_binds) { + string sym; + if (bind.spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; + if (bind.spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (bind.spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; + + std::string key_name; + if (bind.spec.sym < 0) { + key_name = "MOUSE" + std::to_string(-bind.spec.sym); + } else { + key_name = DFSDL::DFSDL_GetKeyName(bind.spec.sym); } + if (key_name.empty()) continue; + sym += key_name; + + add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } valid = true; From 835966798226fcf69d5fa7142d96853572cfb76b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 19 Nov 2025 19:56:07 -0600 Subject: [PATCH 0611/1082] add explicit conversion --- library/include/RemoteTools.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h index f041c5d7b8..8731ad3474 100644 --- a/library/include/RemoteTools.h +++ b/library/include/RemoteTools.h @@ -74,7 +74,7 @@ namespace DFHack */ template void flagarray_to_ints(RepeatedField *pf, const BitArray &val) { - for (size_t i = 0; i < val.size()*8; i++) + for (size_t i = 0; i < size_t(val.size()*8); i++) if (val.is_set(T(i))) pf->Add(i); } From 1554b55fb8ba91ddd0717c2eec8d9b0d0bd92b89 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 20:59:57 -0500 Subject: [PATCH 0612/1082] Reorder imports --- library/include/modules/Hotkey.h | 2 +- library/modules/Hotkey.cpp | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 581ebe43e0..d6dbf591d8 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -4,9 +4,9 @@ #include #include +#include #include #include -#include namespace DFHack { class DFHACK_EXPORT HotkeyManager { diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 2cf87129d4..329db01fb7 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -1,18 +1,19 @@ #include "modules/Hotkey.h" + #include "Core.h" -#include "CoreDefs.h" #include "ColorText.h" #include "MiscUtils.h" +#include "PluginManager.h" + #include "modules/DFSDL.h" #include "modules/Gui.h" -#include "PluginManager.h" + #include "df/global_objects.h" #include "df/viewscreen.h" #include "df/interfacest.h" #include - -#include "SDL_keycode.h" +#include using namespace DFHack; From ec2b30972541130ddecc664dc5108939b9982b81 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 21:12:03 -0500 Subject: [PATCH 0613/1082] Move keybinding command handling into Hotkey module --- library/Core.cpp | 48 +------------------------------- library/include/modules/Hotkey.h | 3 ++ library/modules/Hotkey.cpp | 43 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 6cc60aaf85..93b5f93da6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -957,53 +957,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "keybinding") { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; - if (parts[0] == "set") - hotkey_mgr->clearKeybind(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!hotkey_mgr->addKeybind(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!hotkey_mgr->clearKeybind(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = hotkey_mgr->listKeybinds(parts[1]); - if (list.empty()) - con << "No bindings." << std::endl; - for (const auto& kb : list) - con << " " << kb << std::endl; - } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; - } + this->hotkey_mgr->handleKeybindingCommand(con, parts); } else if (first == "alias") { diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index d6dbf591d8..6c4eefaf25 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -1,6 +1,7 @@ #pragma once #include "Export.h" +#include "ColorText.h" #include #include @@ -10,6 +11,7 @@ namespace DFHack { class DFHACK_EXPORT HotkeyManager { + friend class Core; public: HotkeyManager(); ~HotkeyManager(); @@ -53,5 +55,6 @@ namespace DFHack { std::map> bindings; void hotkey_thread_fn(); + void handleKeybindingCommand(color_ostream& out, const std::vector& parts); }; } diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 329db01fb7..800bafa674 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -284,6 +284,49 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } + +void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { + std::string keystr = parts[1]; + if (parts[0] == "set") + clearKeybind(keystr); + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { + if (!addKeybind(keystr, part)) { + con.printerr("Invalid key spec: %s\n", keystr.c_str()); + break; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") { + for (const auto& part : parts | std::views::drop(1)) { + if (!clearKeybind(part)) { + con.printerr("Invalid key spec: %s\n", part.c_str()); + break; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") { + std::vector list = listKeybinds(parts[1]); + if (list.empty()) + con << "No bindings." << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; + } + else { + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + } +} + HotkeyManager::HotkeyManager() { this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); } From 3c2dc3cfa04d10e110d26999a7ff9a53391f9989 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 11:39:15 -0500 Subject: [PATCH 0614/1082] Properly populate the command field on keybinds --- library/modules/Hotkey.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 800bafa674..52657ac4f2 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -116,6 +116,8 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { KeyBinding binding; binding.spec = spec; binding.cmdline = cmd; + size_t space_idx = cmd.find(' '); + binding.command = space_idx == std::string::npos ? cmd : cmd.substr(0, space_idx); std::lock_guard l(lock); auto& bindings = this->bindings[binding.spec.sym]; From b5d8899a8bb72d50b2fb0e1480e25f4a294e16db Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 13:32:12 -0500 Subject: [PATCH 0615/1082] Lua api for hotkeys and library support for future keybinding gui --- library/LuaApi.cpp | 73 +++++++++++++++++++++++++++ library/include/modules/Hotkey.h | 54 ++++++++++++-------- library/modules/Hotkey.cpp | 84 ++++++++++++++++++++++++++++---- plugins/hotkeys.cpp | 15 +----- 4 files changed, 182 insertions(+), 44 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1989b9a78b..6ae5c071e8 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -47,6 +47,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/Kitchen.h" @@ -1866,6 +1867,77 @@ static const luaL_Reg dfhack_gui_funcs[] = { { NULL, NULL } }; +/***** Hotkey module *****/ +static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return false; + return hotkey_mgr->addKeybind(spec, cmd); +} + +static bool hotkey_clearKeybind(const std::string spec, bool any_focus, std::string cmd) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return false; + return hotkey_mgr->clearKeybind(spec, any_focus, cmd); +} + +static void hotkey_requestKeybindingInput() { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); +} + +static std::string hotkey_readKeybindInput() { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return ""; + return hotkey_mgr->readKeybindInput(); +} + +void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { + lua_createtable(L, binds.size(), 0); + int i = 1; + for (const auto& bind : binds) { + lua_createtable(L, 0, 2); + + lua_pushstring(L, "spec"); + lua_pushstring(L, Hotkey::keyspec_to_string(bind.spec, true).c_str()); + lua_settable(L, -3); + + lua_pushstring(L, "command"); + lua_pushstring(L, bind.cmdline.c_str()); + lua_settable(L, -3); + lua_rawseti(L, -2, i++); + } +} + +static int hotkey_listActiveKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listActiveKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static int hotkey_listAllKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listAllKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static const luaL_Reg dfhack_hotkey_funcs[] = { + { "listActiveKeybinds", hotkey_listActiveKeybinds }, + { "listAllKeybinds", hotkey_listAllKeybinds }, + { NULL, NULL } +}; + +static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { + WRAPN(addKeybind, hotkey_addKeybind), + WRAPN(clearKeybind, hotkey_clearKeybind), + WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), + WRAPN(readKeybindInput, hotkey_readKeybindInput), + { NULL, NULL } +}; + /***** Job module *****/ static bool jobEqual(const df::job *job1, const df::job *job2) @@ -4304,6 +4376,7 @@ void OpenDFHackApi(lua_State *state) luaL_setfuncs(state, dfhack_funcs, 0); OpenModule(state, "translation", dfhack_translation_module); OpenModule(state, "gui", dfhack_gui_module, dfhack_gui_funcs); + OpenModule(state, "hotkey", dfhack_hotkey_module, dfhack_hotkey_funcs); OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); OpenModule(state, "textures", dfhack_textures_funcs); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 6c4eefaf25..7eed1200be 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -10,49 +10,63 @@ #include namespace DFHack { + namespace Hotkey { + struct KeySpec { + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + }; + + struct KeyBinding { + KeySpec spec; + std::string command; + std::string cmdline; + }; + + DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); + } class DFHACK_EXPORT HotkeyManager { friend class Core; public: HotkeyManager(); ~HotkeyManager(); - struct KeySpec { - int modifiers = 0; - // Negative numbers denote mouse buttons - int sym = 0; - std::vector focus; - }; - - struct KeyBinding { - HotkeyManager::KeySpec spec; - std::string command; - std::string cmdline; - }; bool addKeybind(std::string keyspec, std::string cmd); - bool addKeybind(KeySpec spec, std::string cmd); - bool clearKeybind(std::string keyspec, bool any_focus=false); - bool clearKeybind(const KeySpec& spec, bool any_focus=false); + bool addKeybind(Hotkey::KeySpec spec, std::string cmd); + // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command + bool clearKeybind(std::string keyspec, bool any_focus=false, std::string cmdline=""); + bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string cmdline=""); std::vector listKeybinds(std::string keyspec); - std::vector listKeybinds(const KeySpec& spec); + std::vector listKeybinds(const Hotkey::KeySpec& spec); - std::vector listActiveKeybinds(); + std::vector listActiveKeybinds(); + std::vector listAllKeybinds(); bool handleKeybind(int sym, int modifiers); - void setHotkeyCommand(std::string cmd); - std::optional parseKeySpec(std::string spec); + // Used to request the next keybind input is saved. + // This is to allow for graphical keybinding menus. + void requestKeybindInput(); + // Returns the latest requested keybind input + std::string readKeybindInput(); + + std::optional parseKeySpec(std::string spec); private: std::thread hotkey_thread; std::mutex lock {}; std::condition_variable cond {}; + bool keybind_save_requested = false; + std::string requested_keybind; + int hotkey_sig = 0; std::string queued_command = ""; - std::map> bindings; + std::map> bindings; void hotkey_thread_fn(); void handleKeybindingCommand(color_ostream& out, const std::vector& parts); diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 52657ac4f2..7b57e032bd 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -16,6 +16,8 @@ #include using namespace DFHack; +using Hotkey::KeySpec; +using Hotkey::KeyBinding; enum HotkeySignal { None = 0, @@ -23,19 +25,49 @@ enum HotkeySignal { Shutdown, }; -bool operator==(const HotkeyManager::KeySpec& a, const HotkeyManager::KeySpec& b) { +bool operator==(const KeySpec& a, const KeySpec& b) { return a.modifiers == b.modifiers && a.sym == b.sym && a.focus.size() == b.focus.size() && std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); } // Equality operator for key bindings -bool operator==(const HotkeyManager::KeyBinding& a, const HotkeyManager::KeyBinding& b) { +bool operator==(const KeyBinding& a, const KeyBinding& b) { return a.spec == b.spec && a.command == b.command && a.cmdline == b.cmdline; } +std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { + std::string sym; + if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; + if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; + + std::string key_name; + if (spec.sym < 0) { + key_name = "MOUSE" + std::to_string(-spec.sym); + } else { + key_name = DFSDL::DFSDL_GetKeyName(spec.sym); + } + sym += key_name; + + if (include_focus && !spec.focus.empty()) { + sym += "@"; + bool first = true; + for (const auto& focus : spec.focus) { + if (first) { + first = false; + sym += focus; + } else { + sym += "|" + focus; + } + } + } + + return sym; +} + // Hotkeys actions are executed from an external thread to avoid deadlocks // that may occur if running commands from the render or simulation threads. void HotkeyManager::hotkey_thread_fn() { @@ -64,7 +96,7 @@ void HotkeyManager::hotkey_thread_fn() { } } -std::optional HotkeyManager::parseKeySpec(std::string spec) { +std::optional HotkeyManager::parseKeySpec(std::string spec) { KeySpec out; // Determine focus string, if present @@ -138,16 +170,16 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; auto& binds = bindings[spec.sym]; - auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec](const auto& v) { - return any_focus + auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec, &cmdline](const auto& v) { + return (any_focus ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers - : v.spec == spec; + : v.spec == spec) && (cmdline.empty() || v.cmdline == cmdline); }); if (new_end == binds.end()) return false; // No bindings removed @@ -156,11 +188,11 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus) { +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { std::optional spec_opt = parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; - return this->clearKeybind(spec_opt.value(), any_focus); + return this->clearKeybind(spec_opt.value(), any_focus, cmdline); } std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { @@ -200,7 +232,7 @@ std::vector HotkeyManager::listKeybinds(std::string keyspec) { return this->listKeybinds(spec_opt.value()); } -std::vector HotkeyManager::listActiveKeybinds() { +std::vector HotkeyManager::listActiveKeybinds() { std::vector out; for(const auto& [_, bind_set] : bindings) { @@ -223,6 +255,17 @@ std::vector HotkeyManager::listActiveKeybinds() { return out; } +std::vector HotkeyManager::listAllKeybinds() { + std::vector out; + + for (const auto& [_, bind_set] : bindings) { + for (const auto& bind : bind_set) { + out.emplace_back(bind); + } + } + return out; +} + bool HotkeyManager::handleKeybind(int sym, int modifiers) { // Ensure gamestate is ready if (!df::global::gview || !df::global::plotinfo) @@ -237,6 +280,17 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { sym = SDLK_RETURN; std::unique_lock l(lock); + + // If reading input for a keybinding screen, save the input and exit early + if (keybind_save_requested) { + KeySpec spec; + spec.sym = sym; + spec.modifiers = modifiers; + requested_keybind = Hotkey::keyspec_to_string(spec); + keybind_save_requested = false; + return true; + } + if (!bindings.contains(sym)) return false; auto& binds = bindings[sym]; @@ -286,6 +340,16 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } +void HotkeyManager::requestKeybindInput() { + std::lock_guard l(lock); + keybind_save_requested = true; + requested_keybind = ""; +} + +std::string HotkeyManager::readKeybindInput() { + std::lock_guard l(lock); + return requested_keybind; +} void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 197b0d685d..1e87d7296d 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -94,20 +94,7 @@ static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); for (const auto& bind : active_binds) { - string sym; - if (bind.spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; - if (bind.spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; - if (bind.spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; - - std::string key_name; - if (bind.spec.sym < 0) { - key_name = "MOUSE" + std::to_string(-bind.spec.sym); - } else { - key_name = DFSDL::DFSDL_GetKeyName(bind.spec.sym); - } - if (key_name.empty()) continue; - sym += key_name; - + string sym = Hotkey::keyspec_to_string(bind.spec); add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } From 74737b415d21b2781e3d77b54ebf7623fe408f8b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 16:18:59 -0500 Subject: [PATCH 0616/1082] Prefer using to typedef --- library/include/modules/DFSDL.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 161cc35171..9bba7eb4ab 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -11,7 +11,7 @@ struct SDL_Rect; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; -typedef int32_t SDL_Keycode; +using SDL_Keycode = int32_t; namespace DFHack { From 1fcee14fe8c18d542814edbcb5d967a40247adc8 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 16:44:05 -0500 Subject: [PATCH 0617/1082] Improve keybinding command error messages for invalid keyspecs --- library/include/modules/Hotkey.h | 2 +- library/modules/Hotkey.cpp | 92 +++++++++++++++++++------------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 7eed1200be..75f1af1604 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -24,6 +24,7 @@ namespace DFHack { std::string cmdline; }; + DFHACK_EXPORT std::optional parseKeySpec(std::string spec, std::string* err = nullptr); DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); } class DFHACK_EXPORT HotkeyManager { @@ -54,7 +55,6 @@ namespace DFHack { // Returns the latest requested keybind input std::string readKeybindInput(); - std::optional parseKeySpec(std::string spec); private: std::thread hotkey_thread; std::mutex lock {}; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 7b57e032bd..ab64f2e141 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -68,35 +68,8 @@ std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { return sym; } -// Hotkeys actions are executed from an external thread to avoid deadlocks -// that may occur if running commands from the render or simulation threads. -void HotkeyManager::hotkey_thread_fn() { - auto& core = DFHack::Core::getInstance(); - - std::unique_lock l(lock); - while (true) { - cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); - if (hotkey_sig == HotkeySignal::Shutdown) - return; - if (hotkey_sig != HotkeySignal::CmdReady) - continue; - // Copy and reset important data, then release the lock - this->hotkey_sig = HotkeySignal::None; - std::string cmd = this->queued_command; - this->queued_command.clear(); - l.unlock(); - - // Attempt execution of command - DFHack::color_ostream_proxy out(core.getConsole()); - auto res = core.runCommand(out, cmd); - if (res == DFHack::CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); - l.lock(); - } -} - -std::optional HotkeyManager::parseKeySpec(std::string spec) { +std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) { KeySpec out; // Determine focus string, if present @@ -136,10 +109,42 @@ std::optional HotkeyManager::parseKeySpec(std::string spec) { } } + if (err) + *err = "Unknown key '" + spec + "'"; + // Invalid key binding return std::nullopt; } +// Hotkeys actions are executed from an external thread to avoid deadlocks +// that may occur if running commands from the render or simulation threads. +void HotkeyManager::hotkey_thread_fn() { + auto& core = DFHack::Core::getInstance(); + + std::unique_lock l(lock); + while (true) { + cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); + if (hotkey_sig == HotkeySignal::Shutdown) + return; + if (hotkey_sig != HotkeySignal::CmdReady) + continue; + + // Copy and reset important data, then release the lock + this->hotkey_sig = HotkeySignal::None; + std::string cmd = this->queued_command; + this->queued_command.clear(); + l.unlock(); + + // Attempt execution of command + DFHack::color_ostream_proxy out(core.getConsole()); + auto res = core.runCommand(out, cmd); + if (res == DFHack::CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); + l.lock(); + } +} + + bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { // No point in a hotkey with no action if (cmd.empty()) @@ -164,7 +169,7 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { } bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -189,7 +194,7 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin } bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; return this->clearKeybind(spec_opt.value(), any_focus, cmdline); @@ -226,7 +231,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -352,27 +357,42 @@ std::string HotkeyManager::readKeybindInput() { } void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { + std::string parse_error; if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { std::string keystr = parts[1]; if (parts[0] == "set") clearKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - if (!addKeybind(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); + auto spec = Hotkey::parseKeySpec(keystr, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + break; + } + if (!addKeybind(spec.value(), part)) { + con.printerr("Invalid command: '%s'\n", part.c_str()); break; } } } else if (parts.size() >= 2 && parts[0] == "clear") { for (const auto& part : parts | std::views::drop(1)) { - if (!clearKeybind(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); + auto spec = Hotkey::parseKeySpec(part, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + } + if (!clearKeybind(spec.value())) { + con.printerr("No matching keybinds to remove\n"); break; } } } else if (parts.size() == 2 && parts[0] == "list") { - std::vector list = listKeybinds(parts[1]); + auto spec = Hotkey::parseKeySpec(parts[1], &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + return; + } + std::vector list = listKeybinds(spec.value()); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) From e49d2204cc393b03b7d0f71144f54dbddf89013f Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 17:20:53 -0500 Subject: [PATCH 0618/1082] Make keyspecs case-insensitive (excluding focus strings) --- library/modules/Hotkey.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index ab64f2e141..aa38773865 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -79,6 +79,9 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) spec.erase(focus_idx); } + // Treat remaining keyspec as lowercase for case-insensitivity. + std::transform(spec.begin(), spec.end(), spec.begin(), tolower); + // Determine modifier flags auto match_modifier = [&out, &spec](std::string_view prefix, int mod) { bool found = spec.starts_with(prefix); @@ -88,25 +91,28 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) } return found; }; - while (match_modifier("Shift-", DFH_MOD_SHIFT) || match_modifier("Ctrl-", DFH_MOD_CTRL) || match_modifier("Alt-", DFH_MOD_ALT)) {} + while (match_modifier("shift-", DFH_MOD_SHIFT) || match_modifier("ctrl-", DFH_MOD_CTRL) || match_modifier("alt-", DFH_MOD_ALT)) {} out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); if (out.sym != SDLK_UNKNOWN) return out; // Attempt to parse as a mouse binding - if (spec.starts_with("MOUSE")) { + if (spec.starts_with("mouse")) { spec.erase(0, 5); // Read button number, ensuring between 1 and 15 inclusive try { int mbutton = std::stoi(spec); - if (mbutton >= 1 && mbutton <= 15) { + if (mbutton >= 4 && mbutton <= 15) { out.sym = -mbutton; return out; } } catch (...) { // If integer parsing fails, it isn't valid } + if (err) + *err = "Invalid mouse button '" + spec + "', only 4-15 are valid"; + return std::nullopt; } if (err) From 43ecdad98912e6a20882603033bd9215e2a6c44c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 20:48:52 -0500 Subject: [PATCH 0619/1082] Address code review concerns --- library/Core.cpp | 3 ++- library/LuaApi.cpp | 9 +++++---- library/include/modules/Hotkey.h | 4 ++-- library/modules/Hotkey.cpp | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 93b5f93da6..12ee89ed15 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2464,8 +2464,9 @@ bool Core::doSdlInputEvent(SDL_Event* ev) DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { + // We represent mouse buttons as a negative number, permitting buttons 4-15 SDL_Keycode sym = -but.button; - if (sym >= -15 && sym <= -1 && hotkey_mgr->handleKeybind(sym, modstate)) + if (sym >= -15 && sym <= -4 && hotkey_mgr->handleKeybind(sym, modstate)) return suppress_duplicate_keyboard_events; } } else if (ev->type == SDL_TEXTINPUT) { diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 6ae5c071e8..de6ffd7864 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1897,12 +1897,13 @@ void hotkey_pushBindArray(lua_State *L, const std::vector& b for (const auto& bind : binds) { lua_createtable(L, 0, 2); - lua_pushstring(L, "spec"); - lua_pushstring(L, Hotkey::keyspec_to_string(bind.spec, true).c_str()); + lua_pushlstring(L, "spec", 4); + auto spec_str = Hotkey::keyspec_to_string(bind.spec, true); + lua_pushlstring(L, spec_str.data(), spec_str.size()); lua_settable(L, -3); - lua_pushstring(L, "command"); - lua_pushstring(L, bind.cmdline.c_str()); + lua_pushlstring(L, "command", 7); + lua_pushlstring(L, bind.cmdline.data(), bind.cmdline.size()); lua_settable(L, -3); lua_rawseti(L, -2, i++); } diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 75f1af1604..8f3690b846 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -37,8 +37,8 @@ namespace DFHack { bool addKeybind(std::string keyspec, std::string cmd); bool addKeybind(Hotkey::KeySpec spec, std::string cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command - bool clearKeybind(std::string keyspec, bool any_focus=false, std::string cmdline=""); - bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string cmdline=""); + bool clearKeybind(std::string keyspec, bool any_focus=false, std::string_view cmdline=""); + bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string_view cmdline=""); std::vector listKeybinds(std::string keyspec); std::vector listKeybinds(const Hotkey::KeySpec& spec); diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index aa38773865..e3b301e8f4 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -181,7 +181,7 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string cmdline) { +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string_view cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; @@ -199,7 +199,7 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string_view cmdline) { std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; From 2ee9d7e08afad751808de4af956fe1917237aa0c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 21:31:32 -0500 Subject: [PATCH 0620/1082] Initial documentation pass, and improvements to lua api ergonomics --- docs/builtins/keybinding.rst | 8 ++++---- docs/dev/Lua API.rst | 40 ++++++++++++++++++++++++++++++++++++ library/LuaApi.cpp | 14 +++++++++---- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index 3e6970b6fc..ea27b6bcf3 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -9,9 +9,9 @@ Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in :file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or ` -(the key below the :kbd:`Esc` key on most keyboards). You can also represent -mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``. +Hotkeys can be any combinations of Ctrl/Alt/Shift with any key recognized by SDL. +You can also represent mouse buttons beyond the first three with ``MOUSE4`` +through ``MOUSE15``. Usage ----- @@ -27,7 +27,7 @@ Usage ``keybinding set "" ["" ...]`` Clear, and then add bindings for the specified key. -The ```` parameter above has the following **case-sensitive** syntax:: +The ```` parameter above has the following case-insensitive syntax:: [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index a4a039f409..582e0b7e53 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1430,6 +1430,46 @@ Job module Returns the job's description, as seen in the Units and Jobs screens. +Hotkey module +------------- + +* ``dfhack.hotkey.addKeybind(keyspec, command)`` + + Creates a new keybind with the provided keyspec (see the `keybinding` documentation + for details on format). + Returns false on failure to create keybind. + +* ``dfhack.hotkey.clearKeybind(keyspec, any_focus, command)`` + + Removes keybinds matching the provided keyspec. + If any_focus is true, the focus portion of the keyspec is ignored. + If command is not an empty string, the command is matched against as well. + Returns false if no keybinds were removed. + +* ``dfhack.hotkey.listActiveKeybinds()`` + + Returns a list of keybinds active within the current context. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.listAllKeybinds()`` + + Returns a list of all keybinds currently registered. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.requestKeybindInput()`` + + Requests that the next hotkey-compatible input is saved and not processed, + retrievable with ``dfhack.hotkey.readKeybindInput()``. + +* ``dfhack.hotkey.readKeybindInput()`` + + Reads the latest saved keybind input that was requested. + Returns a keyspec string for the input, or nil if no input is saved. + Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index de6ffd7864..319e1f3375 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1885,10 +1885,16 @@ static void hotkey_requestKeybindingInput() { if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); } -static std::string hotkey_readKeybindInput() { +static int hotkey_readKeybindInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (!hotkey_mgr) return ""; - return hotkey_mgr->readKeybindInput(); + auto input = hotkey_mgr->readKeybindInput(); + + if (input.empty()) { + lua_pushnil(L); + } else { + lua_pushlstring(L, input.data(), input.size()); + } + return 1; } void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { @@ -1928,6 +1934,7 @@ static int hotkey_listAllKeybinds(lua_State *L) { static const luaL_Reg dfhack_hotkey_funcs[] = { { "listActiveKeybinds", hotkey_listActiveKeybinds }, { "listAllKeybinds", hotkey_listAllKeybinds }, + { "readKeybindInput", hotkey_readKeybindInput }, { NULL, NULL } }; @@ -1935,7 +1942,6 @@ static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), WRAPN(clearKeybind, hotkey_clearKeybind), WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), - WRAPN(readKeybindInput, hotkey_readKeybindInput), { NULL, NULL } }; From 37dbb46c05b62e92cd186c91e635c4ca073717c2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 20 Nov 2025 23:51:24 -0600 Subject: [PATCH 0621/1082] fixes don't code when tired, kids, you make mistakes Co-Authored-By: Nicholas McDaniel --- library/include/BitArray.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 19b226f7a1..c258add590 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -215,7 +215,7 @@ namespace DFHack T *m_data; unsigned short m_size; - void resize(unsigned new_size, const DfArray* replacement) + void resize(unsigned short new_size, const DfArray* replacement) { if (new_size == m_size) return; @@ -224,11 +224,11 @@ namespace DFHack m_data = (T*) new T[new_size]; - T* copysrc = replacement ? replacement->m_data : m_data; + T* copysrc = replacement ? replacement->m_data : old_data; unsigned short copysize = replacement ? replacement->m_size : m_size; if (copysrc) - std::memcpy(m_data, copysrc, sizeof(T) * copysize); + std::memcpy(m_data, copysrc, sizeof(T) * std::min(copysize, new_size)); if (new_size > m_size) std::memset(m_data + m_size, 0, sizeof(T) * (new_size - m_size)); From de45ea4d2eb2f159a660f9830747514e1d41512c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Nov 2025 11:02:30 -0600 Subject: [PATCH 0622/1082] move fencepost Co-Authored-By: Nicholas McDaniel --- library/include/BitArray.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index c258add590..360281c6b1 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -79,7 +79,7 @@ namespace DFHack void extend(T index) { - size_type newsize = (index / 8) + 1; + size_type newsize = (index + 7 ) / 8; if (newsize > _size) resize(newsize); } From fec896577ec7477c2db821254478d35aea283a5a Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:09:20 -0500 Subject: [PATCH 0623/1082] The default Filter() constructor fails to initialize some members. Provide safe defaults. --- plugins/debug.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/debug.cpp b/plugins/debug.cpp index eb2fc4c1c7..90e5c783d6 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -254,10 +254,10 @@ struct Filter { private: std::regex category_; std::regex plugin_; - DebugCategory::level level_; - size_t matches_; - bool persistent_; - bool enabled_; + DebugCategory::level level_{DebugCategory::level::LTRACE}; + size_t matches_{0}; + bool persistent_{false}; + bool enabled_{false}; std::string categoryText_; std::string pluginText_; }; From 69e734fe43768d2e10984b3fa2be05157198adce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Nov 2025 20:56:43 -0600 Subject: [PATCH 0624/1082] add missing break --- library/LuaApi.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1989b9a78b..4edf60000a 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2637,6 +2637,7 @@ static int maps_setTileAquifer(lua_State* L) case 1: Lua::CheckDFAssign(L, &p, 1); rv = Maps::setTileAquifer(p); + break; case 2: Lua::CheckDFAssign(L, &p, 1); rv = Maps::setTileAquifer(p, lua_toboolean(L, 2)); From 7ce44a55da0ff9a5dc6c17b6a97842aae31f75fc Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 22 Nov 2025 19:48:17 -0500 Subject: [PATCH 0625/1082] Improve api ergonomics and add missing locks --- docs/dev/Lua API.rst | 17 +++++----- library/LuaApi.cpp | 57 ++++++++++++++++++++++++-------- library/include/modules/Hotkey.h | 12 +++---- library/modules/Hotkey.cpp | 30 +++++++++-------- plugins/hotkeys.cpp | 2 +- 5 files changed, 77 insertions(+), 41 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 582e0b7e53..c7c35c5441 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1439,11 +1439,11 @@ Hotkey module for details on format). Returns false on failure to create keybind. -* ``dfhack.hotkey.clearKeybind(keyspec, any_focus, command)`` +* ``dfhack.hotkey.removeKeybind(keyspec, [match_focus=true, command])`` Removes keybinds matching the provided keyspec. - If any_focus is true, the focus portion of the keyspec is ignored. - If command is not an empty string, the command is matched against as well. + If match_focus is set, the focus portion of the keyspec is matched against. + If command is provided and not an empty string, the command is matched against. Returns false if no keybinds were removed. * ``dfhack.hotkey.listActiveKeybinds()`` @@ -1460,15 +1460,16 @@ Hotkey module :spec: The keyspec for the hotkey :command: The command the hotkey runs when pressed -* ``dfhack.hotkey.requestKeybindInput()`` +* ``dfhack.hotkey.requestKeybindingInput([cancel=false])`` - Requests that the next hotkey-compatible input is saved and not processed, - retrievable with ``dfhack.hotkey.readKeybindInput()``. + Enqueues or cancels a request that the next hotkey-compatible input is saved + and not processed, retrievable with ``dfhack.hotkey.getKeybindingInput()``. + If cancel is true, any current request is cancelled. -* ``dfhack.hotkey.readKeybindInput()`` +* ``dfhack.hotkey.getKeybindingInput()`` Reads the latest saved keybind input that was requested. - Returns a keyspec string for the input, or nil if no input is saved. + Returns a keyspec string for the input, or nil if no input has been saved. Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 319e1f3375..e7b0d1197b 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1874,20 +1874,19 @@ static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { return hotkey_mgr->addKeybind(spec, cmd); } -static bool hotkey_clearKeybind(const std::string spec, bool any_focus, std::string cmd) { +static int hotkey_requestKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (!hotkey_mgr) return false; - return hotkey_mgr->clearKeybind(spec, any_focus, cmd); -} - -static void hotkey_requestKeybindingInput() { - auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); + if (!hotkey_mgr) return 0; + bool cancel = false; + if (lua_gettop(L) == 1) + cancel = lua_toboolean(L, -1); + hotkey_mgr->requestKeybindingInput(cancel); + return 0; } -static int hotkey_readKeybindInput(lua_State *L) { +static int hotkey_getKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - auto input = hotkey_mgr->readKeybindInput(); + auto input = hotkey_mgr->getKeybindingInput(); if (input.empty()) { lua_pushnil(L); @@ -1897,6 +1896,38 @@ static int hotkey_readKeybindInput(lua_State *L) { return 1; } +static int hotkey_removeKeybind(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) { + lua_pushboolean(L, false); + return 1; + } + + bool res = false; + switch (lua_gettop(L)) { + case 1: + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -1)); + break; + case 2: + luaL_checkstring(L, -2); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -2), lua_toboolean(L, -1)); + break; + case 3: + luaL_checkstring(L, -3); + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind( + lua_tostring(L, -3), + lua_toboolean(L, -2), + lua_tostring(L, -1) + ); + break; + } + + lua_pushboolean(L, res); + return 1; +} + void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { lua_createtable(L, binds.size(), 0); int i = 1; @@ -1932,16 +1963,16 @@ static int hotkey_listAllKeybinds(lua_State *L) { } static const luaL_Reg dfhack_hotkey_funcs[] = { + { "removeKeybind", hotkey_removeKeybind }, { "listActiveKeybinds", hotkey_listActiveKeybinds }, { "listAllKeybinds", hotkey_listAllKeybinds }, - { "readKeybindInput", hotkey_readKeybindInput }, + { "requestKeybindingInput", hotkey_requestKeybindingInput }, + { "getKeybindingInput", hotkey_getKeybindingInput }, { NULL, NULL } }; static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), - WRAPN(clearKeybind, hotkey_clearKeybind), - WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), { NULL, NULL } }; diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 8f3690b846..6414ec9204 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -37,8 +37,8 @@ namespace DFHack { bool addKeybind(std::string keyspec, std::string cmd); bool addKeybind(Hotkey::KeySpec spec, std::string cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command - bool clearKeybind(std::string keyspec, bool any_focus=false, std::string_view cmdline=""); - bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string_view cmdline=""); + bool removeKeybind(std::string keyspec, bool match_focus=true, std::string_view cmdline=""); + bool removeKeybind(const Hotkey::KeySpec& spec, bool match_focus=true, std::string_view cmdline=""); std::vector listKeybinds(std::string keyspec); std::vector listKeybinds(const Hotkey::KeySpec& spec); @@ -49,11 +49,11 @@ namespace DFHack { bool handleKeybind(int sym, int modifiers); void setHotkeyCommand(std::string cmd); - // Used to request the next keybind input is saved. + // Used to request the next hotkey-compatible input is saved. // This is to allow for graphical keybinding menus. - void requestKeybindInput(); + void requestKeybindingInput(bool cancel=false); // Returns the latest requested keybind input - std::string readKeybindInput(); + std::string getKeybindingInput(); private: std::thread hotkey_thread; @@ -64,7 +64,7 @@ namespace DFHack { std::string requested_keybind; int hotkey_sig = 0; - std::string queued_command = ""; + std::string queued_command; std::map> bindings; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index e3b301e8f4..761a237672 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -181,16 +181,17 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string_view cmdline) { +bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::string_view cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; auto& binds = bindings[spec.sym]; - auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec, &cmdline](const auto& v) { - return (any_focus - ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers - : v.spec == spec) && (cmdline.empty() || v.cmdline == cmdline); + auto new_end = std::remove_if(binds.begin(), binds.end(), [match_focus, spec, &cmdline](const auto& v) { + return v.spec.sym == spec.sym + && v.spec.modifiers == spec.modifiers + && (!match_focus || v.spec.focus == spec.focus) + && (cmdline.empty() || v.cmdline == cmdline); }); if (new_end == binds.end()) return false; // No bindings removed @@ -199,11 +200,11 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string_view cmdline) { +bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; - return this->clearKeybind(spec_opt.value(), any_focus, cmdline); + return this->removeKeybind(spec_opt.value(), match_focus, cmdline); } std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { @@ -237,6 +238,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { + std::lock_guard l(lock); std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return {}; @@ -244,6 +246,7 @@ std::vector HotkeyManager::listKeybinds(std::string keyspec) { } std::vector HotkeyManager::listActiveKeybinds() { + std::lock_guard l(lock); std::vector out; for(const auto& [_, bind_set] : bindings) { @@ -267,6 +270,7 @@ std::vector HotkeyManager::listActiveKeybinds() { } std::vector HotkeyManager::listAllKeybinds() { + std::lock_guard l(lock); std::vector out; for (const auto& [_, bind_set] : bindings) { @@ -351,13 +355,13 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } -void HotkeyManager::requestKeybindInput() { +void HotkeyManager::requestKeybindingInput(bool cancel) { std::lock_guard l(lock); - keybind_save_requested = true; - requested_keybind = ""; + keybind_save_requested = !cancel; + requested_keybind.clear(); } -std::string HotkeyManager::readKeybindInput() { +std::string HotkeyManager::getKeybindingInput() { std::lock_guard l(lock); return requested_keybind; } @@ -367,7 +371,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { std::string keystr = parts[1]; if (parts[0] == "set") - clearKeybind(keystr); + removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { auto spec = Hotkey::parseKeySpec(keystr, &parse_error); if (!spec.has_value()) { @@ -386,7 +390,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); } - if (!clearKeybind(spec.value())) { + if (!removeKeybind(spec.value())) { con.printerr("No matching keybinds to remove\n"); break; } diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 1e87d7296d..c0a5f4fccd 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -46,7 +46,7 @@ static int cleanupHotkeys(lua_State *) { std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().getHotkeyManager()->clearKeybind(keyspec); + Core::getInstance().getHotkeyManager()->removeKeybind(keyspec); }); valid = false; sorted_keys.clear(); From 68e0d5b59e8ed6cee9c69c6f27f5c8c8eb7abc4e Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 22 Nov 2025 20:34:38 -0500 Subject: [PATCH 0626/1082] First pass at changelog --- docs/changelog.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8551676b26..6cfa16e4bf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,8 +65,15 @@ Template for new versions: ## Documentation ## API +- ``Hotkey``: New module for hotkey functionality ## Lua +- ``dfhack.hotkey.addKeybind``: Creates new keybindings +- ``dfhack.hotkey.removeKeybind``: Removes existing keybindings +- ``dfhack.hotkey.listActiveKeybinds``: Lists all keybinds for the current context +- ``dfhack.hotkey.listAllKeybinds``: Lists all keybinds for all contexts +- ``dfhack.hotkey.requestKeybindingInput``: Requests the next keybind-compatible input is saved +- ``dfhack.hotkey.getKeybindingInput``: Reads the input saved in response to a request. ## Removed From 9c87028411cad69ee1102513485d3563c573d7de Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sun, 23 Nov 2025 17:28:57 -0500 Subject: [PATCH 0627/1082] Allow "Super" as a hotkey modifier --- .gitignore | 2 +- docs/builtins/keybinding.rst | 4 ++-- docs/dev/Lua API.rst | 2 +- library/Core.cpp | 2 ++ library/LuaApi.cpp | 3 +++ library/include/Core.h | 1 + library/modules/Hotkey.cpp | 6 +++++- 7 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 60a0e1b6b2..6142448b7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # linux backup files *~ - +.cache # Kdevelop project files *.kdev4 .kdev4 diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index ea27b6bcf3..f77a081650 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -9,7 +9,7 @@ Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in :file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with any key recognized by SDL. +Hotkeys can be any combinations of Ctrl/Alt/Super/Shift with any key recognized by SDL. You can also represent mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``. @@ -29,7 +29,7 @@ Usage The ```` parameter above has the following case-insensitive syntax:: - [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] + [Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]] where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote optional parts. diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index c7c35c5441..27dcf62558 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3535,7 +3535,7 @@ and are only documented here for completeness: * ``dfhack.internal.getModifiers()`` Returns the state of the keyboard modifier keys in a table of string -> - boolean. The keys are ``ctrl``, ``shift``, and ``alt``. + boolean. The keys are ``ctrl``, ``shift``, ``super``, and ``alt``. * ``dfhack.internal.getSuppressDuplicateKeyboardEvents()`` * ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)`` diff --git a/library/Core.cpp b/library/Core.cpp index 12ee89ed15..e8ddf47270 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2439,6 +2439,8 @@ bool Core::doSdlInputEvent(SDL_Event* ev) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_CTRL : modstate & ~DFH_MOD_CTRL; else if (sym == SDLK_LALT || sym == SDLK_RALT) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_ALT : modstate & ~DFH_MOD_ALT; + else if (sym == SDLK_LGUI || sym == SDLK_RGUI) // Renamed to LMETA/RMETA in SDL3 + modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_SUPER : modstate & ~DFH_MOD_SUPER; else if (ke.state == SDL_PRESSED && !hotkey_states[sym]) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index e7b0d1197b..3021a70b1d 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -4073,6 +4073,9 @@ static int internal_getModifiers(lua_State *L) lua_pushstring(L, "alt"); lua_pushboolean(L, modstate & DFH_MOD_ALT); lua_settable(L, -3); + lua_pushstring(L, "super"); + lua_pushboolean(L, modstate & DFH_MOD_SUPER); + lua_settable(L, -3); return 1; } diff --git a/library/include/Core.h b/library/include/Core.h index 08c7ca3d2e..bf0378b05d 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -58,6 +58,7 @@ namespace DFHack constexpr auto DFH_MOD_SHIFT = 1; constexpr auto DFH_MOD_CTRL = 2; constexpr auto DFH_MOD_ALT = 4; + constexpr auto DFH_MOD_SUPER = 8; class Process; class Module; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 761a237672..11a462dec9 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -42,6 +42,7 @@ std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { std::string sym; if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (spec.modifiers & DFH_MOD_SUPER) sym += "Super-"; if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; std::string key_name; @@ -91,7 +92,10 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) } return found; }; - while (match_modifier("shift-", DFH_MOD_SHIFT) || match_modifier("ctrl-", DFH_MOD_CTRL) || match_modifier("alt-", DFH_MOD_ALT)) {} + while (match_modifier("shift-", DFH_MOD_SHIFT) + || match_modifier("ctrl-", DFH_MOD_CTRL) + || match_modifier("alt-", DFH_MOD_ALT) + || match_modifier("super-", DFH_MOD_SUPER)) {} out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); if (out.sym != SDLK_UNKNOWN) From 05f6440c3aab8a036024d6140c804774b0afb794 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sun, 23 Nov 2025 18:51:38 -0500 Subject: [PATCH 0628/1082] Add heuristic method to catch potentially disruptive keybinds --- docs/dev/Lua API.rst | 6 +++ library/LuaApi.cpp | 10 ++++- library/include/modules/Hotkey.h | 21 ++++++---- library/modules/Hotkey.cpp | 68 ++++++++++++++++++++------------ plugins/hotkeys.cpp | 2 +- 5 files changed, 71 insertions(+), 36 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 27dcf62558..27103b2d9d 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1471,6 +1471,12 @@ Hotkey module Reads the latest saved keybind input that was requested. Returns a keyspec string for the input, or nil if no input has been saved. +* ``dfhack.hotkey.isDisruptiveKeybind(keyspec)`` + + Determines if the provided keyspec could be disruptive to the game experience. + This includes the majority of standard characters and special keys such as escape, + backspace, and return when lacking modifiers other than Shift. + Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 3021a70b1d..6353f5c07f 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1874,6 +1874,13 @@ static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { return hotkey_mgr->addKeybind(spec, cmd); } +static bool hotkey_isDisruptiveKeybind(const std::string spec) { + auto key = Hotkey::KeySpec::parse(spec); + if (!key.has_value()) + return true; + return key.value().isDisruptive(); +} + static int hotkey_requestKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); if (!hotkey_mgr) return 0; @@ -1935,7 +1942,7 @@ void hotkey_pushBindArray(lua_State *L, const std::vector& b lua_createtable(L, 0, 2); lua_pushlstring(L, "spec", 4); - auto spec_str = Hotkey::keyspec_to_string(bind.spec, true); + auto spec_str = bind.spec.toString(true); lua_pushlstring(L, spec_str.data(), spec_str.size()); lua_settable(L, -3); @@ -1973,6 +1980,7 @@ static const luaL_Reg dfhack_hotkey_funcs[] = { static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), + WRAPN(isDisruptiveKeybind, hotkey_isDisruptiveKeybind), { NULL, NULL } }; diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 6414ec9204..2e9a400cd1 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -11,11 +11,19 @@ namespace DFHack { namespace Hotkey { - struct KeySpec { - int modifiers = 0; - // Negative numbers denote mouse buttons - int sym = 0; - std::vector focus; + class DFHACK_EXPORT KeySpec { + public: + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + + static std::optional parse(std::string spec, std::string* err = nullptr); + std::string toString(bool include_focus=true) const; + + // Determines if a keybind could be disruptive to normal gameplay, + // including typing and navigating the UI. + bool isDisruptive() const; }; struct KeyBinding { @@ -23,9 +31,6 @@ namespace DFHack { std::string command; std::string cmdline; }; - - DFHACK_EXPORT std::optional parseKeySpec(std::string spec, std::string* err = nullptr); - DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); } class DFHACK_EXPORT HotkeyManager { friend class Core; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 11a462dec9..eb7b483af0 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -38,39 +38,38 @@ bool operator==(const KeyBinding& a, const KeyBinding& b) { a.cmdline == b.cmdline; } -std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { - std::string sym; - if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; - if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; - if (spec.modifiers & DFH_MOD_SUPER) sym += "Super-"; - if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; +std::string KeySpec::toString(bool include_focus) const { + std::string out; + if (modifiers & DFH_MOD_CTRL) out += "Ctrl-"; + if (modifiers & DFH_MOD_ALT) out += "Alt-"; + if (modifiers & DFH_MOD_SUPER) out += "Super-"; + if (modifiers & DFH_MOD_SHIFT) out += "Shift-"; std::string key_name; - if (spec.sym < 0) { - key_name = "MOUSE" + std::to_string(-spec.sym); + if (this->sym < 0) { + key_name = "MOUSE" + std::to_string(-this->sym); } else { - key_name = DFSDL::DFSDL_GetKeyName(spec.sym); + key_name = DFSDL::DFSDL_GetKeyName(this->sym); } - sym += key_name; + out += key_name; - if (include_focus && !spec.focus.empty()) { - sym += "@"; + if (include_focus && !this->focus.empty()) { + out += "@"; bool first = true; - for (const auto& focus : spec.focus) { + for (const auto& fc : this->focus) { if (first) { first = false; - sym += focus; + out += fc; } else { - sym += "|" + focus; + out += "|" + fc; } } } - return sym; + return out; } - -std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) { +std::optional KeySpec::parse(std::string spec, std::string* err) { KeySpec out; // Determine focus string, if present @@ -126,6 +125,23 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) return std::nullopt; } +bool KeySpec::isDisruptive() const { + // Miscellaneous essential keys + const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./"; + + // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys + bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) + || (this->sym >= SDLK_0 && this->sym <= SDLK_9) + || essential_key_set.find(this->sym) != std::string::npos + || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); + + // Essential keys are safe, so long as they have a modifier that isn't Shift + if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT)) + return true; + + return false; +} + // Hotkeys actions are executed from an external thread to avoid deadlocks // that may occur if running commands from the render or simulation threads. void HotkeyManager::hotkey_thread_fn() { @@ -179,7 +195,7 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { } bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -205,7 +221,7 @@ bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::st } bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return false; return this->removeKeybind(spec_opt.value(), match_focus, cmdline); @@ -243,7 +259,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { std::vector HotkeyManager::listKeybinds(std::string keyspec) { std::lock_guard l(lock); - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -305,7 +321,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { KeySpec spec; spec.sym = sym; spec.modifiers = modifiers; - requested_keybind = Hotkey::keyspec_to_string(spec); + requested_keybind = spec.toString(false); keybind_save_requested = false; return true; } @@ -377,7 +393,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (parts[0] == "set") removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - auto spec = Hotkey::parseKeySpec(keystr, &parse_error); + auto spec = KeySpec::parse(keystr, &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); break; @@ -390,7 +406,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } else if (parts.size() >= 2 && parts[0] == "clear") { for (const auto& part : parts | std::views::drop(1)) { - auto spec = Hotkey::parseKeySpec(part, &parse_error); + auto spec = KeySpec::parse(part, &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); } @@ -401,7 +417,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } } else if (parts.size() == 2 && parts[0] == "list") { - auto spec = Hotkey::parseKeySpec(parts[1], &parse_error); + auto spec = KeySpec::parse(parts[1], &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); return; @@ -419,7 +435,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Supported keys: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl << "Context may be used to limit the scope of the binding, by" << std::endl << "requiring the current context to have a certain prefix." << std::endl << "Current UI context is: " << std::endl diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index c0a5f4fccd..448d2a1c10 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -94,7 +94,7 @@ static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); for (const auto& bind : active_binds) { - string sym = Hotkey::keyspec_to_string(bind.spec); + string sym = bind.spec.toString(false); add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } From 5ee8b3c5f17f1704278f6be9292a0c4ade7a1437 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 19:00:37 -0600 Subject: [PATCH 0629/1082] disable infinite-sky changes in DF make infinite-sky unsafe to use --- docs/changelog.txt | 1 + plugins/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 46f1a718e6..617a5849c8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,7 @@ Template for new versions: ## Lua ## Removed +- `infiniteSky`: Temporarily disabled due to incompatibility with changes made as part of DF's siege update # 53.05-r1 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 1f721ec713..a49fdf7a9f 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -86,7 +86,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(generated-creature-renamer generated-creature-renamer.cpp) dfhack_plugin(getplants getplants.cpp) dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua) - dfhack_plugin(infinite-sky infinite-sky.cpp LINK_LIBRARIES lua) + #dfhack_plugin(infinite-sky infinite-sky.cpp LINK_LIBRARIES lua) #dfhack_plugin(jobutils jobutils.cpp) dfhack_plugin(lair lair.cpp) dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) From 12a311e287719abac8b8690d726a6cba6d635113 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 00:12:43 -0600 Subject: [PATCH 0630/1082] use `constexpr` in `Core` where possible --- library/Core.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 33f66d767e..99919099db 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -201,7 +201,7 @@ uint32_t PerfCounters::getUnpausedFps() { struct CommandDepthCounter { - static const int MAX_DEPTH = 20; + static constexpr int MAX_DEPTH = 20; static thread_local int depth; CommandDepthCounter() { depth++; } ~CommandDepthCounter() { depth--; } @@ -902,8 +902,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "plug") { - const char *header_format = "%30s %10s %4s %8s\n"; - const char *row_format = "%30s %10s %4i %8s\n"; + constexpr auto header_format = "%30s %10s %4s %8s\n"; + constexpr auto row_format = "%30s %10s %4i %8s\n"; con.print(header_format, "Name", "State", "Cmds", "Enabled"); plug_mgr->refresh(); @@ -1601,7 +1601,7 @@ bool Core::InitMainThread() { { if (!Version::git_xml_match()) { - const char *msg = ( + constexpr auto msg = ( "*******************************************************\n" "* BIG, UGLY ERROR MESSAGE *\n" "*******************************************************\n" @@ -2166,7 +2166,7 @@ static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, con } size_t loadScriptFiles(Core* core, color_ostream& out, const std::span prefix, const std::filesystem::path& folder) { - static const std::string suffix = ".init"; + static constexpr std::string suffix = ".init"; std::vector scriptFiles; for ( const auto& p : prefix ) { getFilesWithPrefixAndSuffix(folder, p, ".init", scriptFiles); From 61d8540fc6e041e4f78a99f8544746525457c020 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 00:42:54 -0600 Subject: [PATCH 0631/1082] fix row format, roll back one `constexpr` --- library/Core.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 99919099db..bb631414c6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -903,7 +903,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else if (first == "plug") { constexpr auto header_format = "%30s %10s %4s %8s\n"; - constexpr auto row_format = "%30s %10s %4i %8s\n"; + constexpr auto row_format = "%30s %10s %4zu %8s\n"; con.print(header_format, "Name", "State", "Cmds", "Enabled"); plug_mgr->refresh(); @@ -2166,7 +2166,7 @@ static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, con } size_t loadScriptFiles(Core* core, color_ostream& out, const std::span prefix, const std::filesystem::path& folder) { - static constexpr std::string suffix = ".init"; + static const std::string suffix = ".init"; std::vector scriptFiles; for ( const auto& p : prefix ) { getFilesWithPrefixAndSuffix(folder, p, ".init", scriptFiles); From c6a0181b1d6408cb548c2a229e6aa8a44007e978 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:21:28 +0000 Subject: [PATCH 0632/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 72f90ad582..6a369da932 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 72f90ad5824fa532525ab123eda8fe91851178ff +Subproject commit 6a369da93210d260a66f39fddd5d95c85962d053 From fce63b7a713b955ad094853642f316daef653c43 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 22 Nov 2025 05:12:44 -0600 Subject: [PATCH 0633/1082] info sort overlay: support new "Siege engines" subtab Move the Places tab search widget: - in wide interfaces it was being drawn over the new "Siege engines" subtab; move it down so it is drawn in the blank line between the subtab group and the content area (like the Tasks search widget) - in narrow interfaces it was being drawn too far right to show its text field inside the bounds of DF's Info panel; move it left into the space made available now that DF is wrapping the subtab group due to the addition of the new subtab Register a handler for the new "Siege engine" subtab; generate search keys that match the text that DF displays. --- docs/changelog.txt | 2 + plugins/lua/sort/info.lua | 3 +- plugins/lua/sort/places.lua | 115 +++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 617a5849c8..a18405fd9b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,9 +57,11 @@ Template for new versions: ## New Tools ## New Features +- `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit +- `sort`: Places search widget moved to account for DF's new "Siege engines" subtab ## Misc Improvements diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index feadee99c0..e2c728e0e6 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -299,8 +299,7 @@ function get_panel_offsets() if tabs_in_two_rows then t_offset = shift_right and 0 or 3 end - if info.current_mode == df.info_interface_mode_type.JOBS or - info.current_mode == df.info_interface_mode_type.BUILDINGS then + if info.current_mode == df.info_interface_mode_type.JOBS then t_offset = t_offset - 1 end return l_offset, t_offset diff --git a/plugins/lua/sort/places.lua b/plugins/lua/sort/places.lua index f2588cc0ff..c353d5a948 100644 --- a/plugins/lua/sort/places.lua +++ b/plugins/lua/sort/places.lua @@ -170,6 +170,117 @@ local function get_farmplot_search_key(farmplot) return table.concat(result, ' ') end +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_type(siege_engine) + if siege_engine.type == df.siegeengine_type.BoltThrower then + return 'Bolt Thrower' + end + return df.siegeengine_type[siege_engine.type] +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_status(siege_engine) + -- portions of return value with with underscores are to allow easier + -- word-anchored matching even when the DFHack full-text search mode is + -- enabled; e.g. + -- - "loaded" would match "Loaded" and "Unloaded", + -- - but "_loaded" would only match "_Loaded" + local count = 0 + local count_all = siege_engine.type == df.siegeengine_type.BoltThrower + for _, building_item in ipairs(siege_engine.contained_items) do + if building_item.use_mode == df.building_item_role_type.TEMP then + if not count_all then + return 'Loaded _Loaded' + end + count = count + building_item.item:getStackSize() + end + end + if count_all and count > 0 then + return ('%d bolts _%d_bolts'):format(count, count) + end + return 'Unloaded' +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_job_status(siege_engine) + for _, job in ipairs(siege_engine.jobs) do + if job.job_type == df.job_type.LoadCatapult + or job.job_type == df.job_type.LoadBallista + or job.job_type == df.job_type.LoadBoltThrower + then + if dfhack.job.getWorker(job) ~= nil then + return 'Loading' + else + return 'Inactive load task' + end + end + local firing_bolt_thrower = job.job_type == df.job_type.FireBoltThrower + local firing = job.job_type == df.job_type.FireCatapult + or job.job_type == df.job_type.FireBallista + or firing_bolt_thrower + if firing then + local unit = dfhack.job.getWorker(job) + if unit == nil then + return 'No operator' + else + ---@type integer?, integer?, integer? + local x, y, z = dfhack.units.getPosition(unit) + -- DF shows "present" when the unit is inside the building's + -- footprint (or, for bolt throwers, next to it); the unit does + -- not need to be at the exact firing position tile (which + -- varies based on siege engine type and direction) + if x ~= nil and z == siege_engine.z then + ---@cast y integer + if firing_bolt_thrower then + if siege_engine.x1 - 1 <= x and x <= siege_engine.x2 + 1 + and siege_engine.y1 - 1 <= y and y <= siege_engine.y2 + 1 + then + return 'Operator present' + end + elseif dfhack.buildings.containsTile(siege_engine, x, y) then + return 'Operator present' + end + end + return 'Operator assigned' + end + end + end + return '' +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function get_siege_engine_search_key(siege_engine) + -- DF 53.05 Info window, Places tab, Siege Engines subtab shows this info: + -- name: assigned name or siege engine type name + -- status: "Unloaded", "Loaded", " bolts" + -- job status: + -- - "Inactive load task" (load job unassigned), + -- - "Loading" (load job assigned), + -- - "No operator" (fire job unassigned), + -- - "Operator present" (fire job assigned), + -- - "Operator assigned" (fire job assigned, but not in position), + -- - blank + -- action: (icons) fire-at-will, practice, prepare-to-fire, keep-loaded, not-in-use + -- These have associated text blurbs that are shown in the + -- building info window, but those texts are not discoverable + -- from the Info > Places > Siege engine list view. + local result = {} + + if #siege_engine.name ~= 0 then table.insert(result, siege_engine.name) end + + table.insert(result, siege_engine_type(siege_engine)) + + table.insert(result, siege_engine_status(siege_engine)) + + table.insert(result, siege_engine_job_status(siege_engine)) + + return table.concat(result, ' ') +end + -- ---------------------- -- PlacesOverlay -- @@ -177,7 +288,8 @@ end PlacesOverlay = defclass(PlacesOverlay, sortoverlay.SortOverlay) PlacesOverlay.ATTRS{ desc='Adds search functionality to the places overview screens.', - default_pos={x=71, y=9}, + default_pos={x=52, y=9}, + version=2, viewscreens='dwarfmode/Info', frame={w=40, h=6} } @@ -205,6 +317,7 @@ function PlacesOverlay:init() self:register_handler('STOCKPILES', buildings.list[df.buildings_mode_type.STOCKPILES], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_stockpile_search_key})) self:register_handler('WORKSHOPS', buildings.list[df.buildings_mode_type.WORKSHOPS], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_workshop_search_key})) self:register_handler('FARMPLOTS', buildings.list[df.buildings_mode_type.FARMPLOTS], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_farmplot_search_key})) + self:register_handler('SIEGE_ENGINES', buildings.list[df.buildings_mode_type.SIEGE_ENGINES], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_siege_engine_search_key})) end function PlacesOverlay:get_key() From 6eaf98b1d07c6735d1cd7e9f163ff9fa1fd79c63 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 22 Nov 2025 05:12:44 -0600 Subject: [PATCH 0634/1082] info sort overlay: use interface width in resize_overlay The DF info window is based on the interface area, not the full window area. So the sort info overlays should also work with the interface width instead of the window width. If DF is configured to use an interface percentage smaller than 100, then the interface area will be narrower than the full window once the window is resized beyond the minimum width. Basing the info sort overlay size on the window size can let some UI elements extend outside the footprint of the info window. For example, when the DF interface percentage is set to 90, the search box for the Task tab - is too close to the info window border at window widths 115 and 147 - shares a edge with the info window border at window widths 116 and 146 - extends beyond the info window border for window widths 117-145 --- plugins/lua/sort/info.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index e2c728e0e6..0b261da2d7 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -278,8 +278,8 @@ function InfoOverlay:get_key() end function resize_overlay(self) - local sw = dfhack.screen.getWindowSize() - local overlay_width = math.min(40, sw-(self.frame_rect.x1 + 30)) + local iw = gui.get_interface_rect().width + local overlay_width = math.min(40, iw - (self.frame_rect.x1 + 30)) if overlay_width ~= self.frame.w then self.frame.w = overlay_width return true From 30536a65bb2d59a6d0bcd1e83ea1278ac6818646 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Nov 2025 06:30:09 -0600 Subject: [PATCH 0635/1082] info sort overlay: remove check for unused Labor tab There hasn't been a LABOR tab sort overlay since 50.12. The WORK_DETAIL overlay was removed in 4c928766. That commit was part of 50.12-r1~34. The DF 50.12 release notes say that work detail sorting and searching was added, and the DFHack 50.12-r1 release notes say that search widgets were removed for screens that gained native search. https://store.steampowered.com/news/app/975370/view/4136064865380703752 https://docs.dfhack.org/en/stable/docs/NEWS.html#dfhack-50-12-r1 --- plugins/lua/sort/info.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 0b261da2d7..4f0462dfb7 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -292,8 +292,7 @@ end function get_panel_offsets() local tabs_in_two_rows = is_tabs_in_two_rows() - local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS or - info.current_mode == df.info_interface_mode_type.LABOR + local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS local l_offset = (not tabs_in_two_rows and shift_right) and 4 or 0 local t_offset = 1 if tabs_in_two_rows then From c5c820cb1ca9d809769fd95d159902ffae4133c2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 10:58:26 -0600 Subject: [PATCH 0636/1082] revert permissive matching for itch --- .github/workflows/generate-symbols.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 18a99b9db8..f78da19e74 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -164,7 +164,7 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="dwarf_fortress_${minor}_${patch}_linux*.tar.bz2" + fname="dwarf_fortress_${minor}_${patch}_linux.tar.bz2" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" tar xjf "kitfoxgames/dwarf-fortress/files/${fname}" -C DF_itch tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_itch @@ -272,7 +272,7 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="dwarf_fortress_${minor}_${patch}_windows*.zip" + fname="dwarf_fortress_${minor}_${patch}_windows.zip" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" unzip -d DF_itch "kitfoxgames/dwarf-fortress/files/${fname}" xml/symbols_gen_windows.sh ${{ inputs.version }} ITCH DF_itch From 30ec1420d4ba5ab48d19b50cc9ddcacb37f4e8d1 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:27:37 +0000 Subject: [PATCH 0637/1082] Auto-update structures ref for 53.06 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 2bd33eba05..a451f62ba8 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2bd33eba052f6ea3f65f4a22670029f2311b07b4 +Subproject commit a451f62ba8fe66aaa748ad85a709fe4d4de4f486 From cab99e8017e3221f848394fc7404604a7f01a98d Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:32:31 +0000 Subject: [PATCH 0638/1082] Auto-update structures ref for 53.06 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a451f62ba8..3e60b0bad1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a451f62ba8fe66aaa748ad85a709fe4d4de4f486 +Subproject commit 3e60b0bad1c1d2b72d4ff145ce29056c4de2d247 From 237a424226dd47dd89bec5733cb4a9c4124c646b Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:35:26 +0000 Subject: [PATCH 0639/1082] Auto-update structures ref for 53.06 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 3e60b0bad1..2b631c0e55 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3e60b0bad1c1d2b72d4ff145ce29056c4de2d247 +Subproject commit 2b631c0e55abc5009d95b4f291081d267e8dadb9 From 72c30832ad9b069d520e2614908b720dba0d32f7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 11:44:21 -0600 Subject: [PATCH 0640/1082] update version to 53.06 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a9bb70aa15..e78a562626 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.05") +set(DF_VERSION "53.06") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 31af7ac67242ea0610d3f412435dd37c3a1544e8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 11:56:09 -0600 Subject: [PATCH 0641/1082] 53.06-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ library/xml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 617a5849c8..91c50db23a 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.06-r1 + +## New Tools + +## New Features + ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit diff --git a/library/xml b/library/xml index 2b631c0e55..24322fe4a3 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2b631c0e55abc5009d95b4f291081d267e8dadb9 +Subproject commit 24322fe4a30209399c1c5fe903726828edd9032b From 858b05f83d745ebad8a4d85fce82a973e88ca294 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 12:46:19 -0600 Subject: [PATCH 0642/1082] 53.06-r1 again --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6a369da932..dd60bf218e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6a369da93210d260a66f39fddd5d95c85962d053 +Subproject commit dd60bf218e6f4a9c421327d65094998fb6459995 From 06695c9d8e5585d1efe4298efeec372a1bc1eb48 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 01:59:25 -0600 Subject: [PATCH 0643/1082] refactor Core - part 1 of ? --- library/CMakeLists.txt | 4 + library/Commands.cpp | 49 ++++ library/Core.cpp | 176 +----------- library/LuaApi.cpp | 1 + library/MemoryPatcher.cpp | 84 ++++++ library/Process.cpp | 8 + library/VTableInterpose.cpp | 1 + library/include/Commands.h | 16 ++ library/include/Core.h | 12 +- library/include/MemAccess.h | 485 ++++++++++++++++---------------- library/include/MemoryPatcher.h | 35 +++ 11 files changed, 459 insertions(+), 412 deletions(-) create mode 100644 library/Commands.cpp create mode 100644 library/MemoryPatcher.cpp create mode 100644 library/include/Commands.h create mode 100644 library/include/MemoryPatcher.h diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index fa5c482bf6..d10978e97b 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -48,6 +48,7 @@ set(MAIN_HEADERS include/DFHackVersion.h include/BitArray.h include/ColorText.h + include/Commands.h include/Console.h include/Core.h include/CoreDefs.h @@ -66,6 +67,7 @@ set(MAIN_HEADERS include/MiscUtils.h include/Module.h include/MemAccess.h + include/MemoryPatcher.h include/ModuleFactory.h include/PluginLua.h include/PluginManager.h @@ -88,6 +90,7 @@ set(MAIN_HEADERS_WINDOWS set(MAIN_SOURCES Core.cpp ColorText.cpp + Commands.cpp CompilerWorkAround.cpp DataDefs.cpp DataIdentity.cpp @@ -100,6 +103,7 @@ set(MAIN_SOURCES LuaApi.cpp DataStatics.cpp DataStaticsCtor.cpp + MemoryPatcher.cpp MiscUtils.cpp Types.cpp PluginManager.cpp diff --git a/library/Commands.cpp b/library/Commands.cpp new file mode 100644 index 0000000000..a71c628d02 --- /dev/null +++ b/library/Commands.cpp @@ -0,0 +1,49 @@ + +#include "ColorText.h" +#include "Commands.h" +#include "Core.h" +#include "CoreDefs.h" + +#include +#include + +namespace DFHack +{ + command_result Commands::help(color_ostream& con, const std::string& first, const std::vector& parts) + { + if (!parts.size()) + { + if (con.is_console()) + { + con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" + "Some basic editing capabilities are included (single-line text editing).\n" + "The console also has a command history - you can navigate it with Up and Down keys.\n" + "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" + "by clicking on the program icon in the top bar of the window.\n\n"); + } + con.print("Here are some basic commands to get you started:\n" + " help|?|man - This text.\n" + " help - Usage help for the given plugin, command, or script.\n" + " tags - List the tags that the DFHack tools are grouped by.\n" + " ls|dir [] - List commands, optionally filtered by a tag or substring.\n" + " Optional parameters:\n" + " --notags: skip printing tags for each command.\n" + " --dev: include commands intended for developers and modders.\n" + " cls|clear - Clear the console.\n" + " fpause - Force DF to pause.\n" + " die - Force DF to close immediately, without saving.\n" + " keybinding - Modify bindings of commands to in-game key shortcuts.\n" + "\n" + "See more commands by running 'ls'.\n\n" + ); + + con.print("DFHack version %s\n", dfhack_version_desc().c_str()); + } + else + { + DFHack::help_helper(con, parts[0]); + } + return CR_OK; + } + +} \ No newline at end of file diff --git a/library/Core.cpp b/library/Core.cpp index bb631414c6..5c1abdb41c 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -30,6 +30,7 @@ distribution. #include "DataDefs.h" #include "Debug.h" #include "Console.h" +#include "MemoryPatcher.h" #include "MiscUtils.h" #include "Module.h" #include "VersionInfoFactory.h" @@ -302,7 +303,7 @@ static void fHKthread(IODATA * iodata) } } -static std::string dfhack_version_desc() +std::string DFHack::dfhack_version_desc() { std::stringstream s; s << Version::dfhack_version() << " "; @@ -363,13 +364,13 @@ static command_result enableLuaScript(color_ostream &out, const std::string_view return ok ? CR_OK : CR_FAILURE; } -command_result Core::runCommand(color_ostream &out, const std::string &command) +command_result Core::runCommand(color_ostream& out, const std::string& command) { if (!command.empty()) { std::vector parts; - Core::cheap_tokenise(command,parts); - if(parts.size() == 0) + Core::cheap_tokenise(command, parts); + if (parts.size() == 0) return CR_NOT_IMPLEMENTED; std::string first = parts[0]; @@ -385,20 +386,23 @@ command_result Core::runCommand(color_ostream &out, const std::string &command) return CR_NOT_IMPLEMENTED; } -bool is_builtin(color_ostream &con, const std::string &command) { +bool is_builtin(color_ostream& con, const std::string& command) +{ CoreSuspender suspend; auto L = DFHack::Core::getInstance().getLuaState(); Lua::StackUnwinder top(L); if (!lua_checkstack(L, 1) || - !Lua::PushModulePublic(con, L, "helpdb", "is_builtin")) { + !Lua::PushModulePublic(con, L, "helpdb", "is_builtin")) + { con.printerr("Failed to load helpdb Lua code\n"); return false; } Lua::Push(L, command); - if (!Lua::SafeCall(con, L, 1, 1)) { + if (!Lua::SafeCall(con, L, 1, 1)) + { con.printerr("Failed Lua call to helpdb.is_builtin.\n"); return false; } @@ -625,7 +629,7 @@ static std::string sc_event_name (state_change_event id) { return "SC_UNKNOWN"; } -void help_helper(color_ostream &con, const std::string &entry_name) { +void DFHack::help_helper(color_ostream &con, const std::string &entry_name) { ConditionalCoreSuspender suspend{}; if (!suspend) { @@ -743,38 +747,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s command_result res; if (first == "help" || first == "man" || first == "?") { - if(!parts.size()) - { - if (con.is_console()) - { - con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" - "Some basic editing capabilities are included (single-line text editing).\n" - "The console also has a command history - you can navigate it with Up and Down keys.\n" - "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" - "by clicking on the program icon in the top bar of the window.\n\n"); - } - con.print("Here are some basic commands to get you started:\n" - " help|?|man - This text.\n" - " help - Usage help for the given plugin, command, or script.\n" - " tags - List the tags that the DFHack tools are grouped by.\n" - " ls|dir [] - List commands, optionally filtered by a tag or substring.\n" - " Optional parameters:\n" - " --notags: skip printing tags for each command.\n" - " --dev: include commands intended for developers and modders.\n" - " cls|clear - Clear the console.\n" - " fpause - Force DF to pause.\n" - " die - Force DF to close immediately, without saving.\n" - " keybinding - Modify bindings of commands to in-game key shortcuts.\n" - "\n" - "See more commands by running 'ls'.\n\n" - ); - - con.print("DFHack version %s\n", dfhack_version_desc().c_str()); - } - else - { - help_helper(con, parts[0]); - } + Commands::help(con, first, parts); } else if (first == "tags") { @@ -1278,7 +1251,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s res = plug_mgr->InvokeCommand(con, first, parts); if (res == CR_WRONG_USAGE) { - help_helper(con, first); + DFHack::help_helper(con, first); } else if (res == CR_NOT_IMPLEMENTED) { @@ -2872,127 +2845,6 @@ std::string Core::GetAliasCommand(const std::string &name, bool ignore_params) return join_strings(" ", aliases[name]); } -///////////////// -// ClassNameCheck -///////////////// - -// Since there is no Process.cpp, put ClassNameCheck stuff in Core.cpp - -static std::set known_class_names; -static std::map known_vptrs; - -ClassNameCheck::ClassNameCheck(std::string _name) : name(_name), vptr(0) -{ - known_class_names.insert(name); -} - -ClassNameCheck &ClassNameCheck::operator= (const ClassNameCheck &b) -{ - name = b.name; vptr = b.vptr; return *this; -} - -bool ClassNameCheck::operator() (Process *p, void * ptr) const { - if (vptr == 0 && p->readClassName(ptr) == name) - { - vptr = ptr; - known_vptrs[name] = ptr; - } - return (vptr && vptr == ptr); -} - -void ClassNameCheck::getKnownClassNames(std::vector &names) -{ - for(const auto& kcn : known_class_names) { - names.push_back(kcn); - } -} - -MemoryPatcher::MemoryPatcher(Process *p_) : p(p_) -{ - if (!p) - p = Core::getInstance().p.get(); -} - -MemoryPatcher::~MemoryPatcher() -{ - close(); -} - -bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write) -{ - auto *sptr = (uint8_t*)target; - uint8_t *eptr = sptr + count; - - // Find the valid memory ranges - if (ranges.empty()) - p->getMemRanges(ranges); - - // Find the ranges that this area spans - unsigned start = 0; - while (start < ranges.size() && ranges[start].end <= sptr) - start++; - if (start >= ranges.size() || ranges[start].start > sptr) - return false; - - unsigned end = start+1; - while (end < ranges.size() && ranges[end].start < eptr) - { - if (ranges[end].start != ranges[end-1].end) - return false; - end++; - } - if (ranges[end-1].end < eptr) - return false; - - // Verify current permissions - for (unsigned i = start; i < end; i++) - if (!ranges[i].valid || !(ranges[i].read || ranges[i].execute) || ranges[i].shared) - return false; - - // Apply writable permissions & update - for (unsigned i = start; i < end; i++) - { - auto &perms = ranges[i]; - if ((perms.write || !write) && perms.read) - continue; - - save.push_back(perms); - perms.write = perms.read = true; - if (!p->setPermissions(perms, perms)) - return false; - } - - return true; -} - -bool MemoryPatcher::write(void *target, const void *src, size_t size) -{ - if (!makeWritable(target, size)) - return false; - - memmove(target, src, size); - - p->flushCache(target, size); - return true; -} - -void MemoryPatcher::close() -{ - for (size_t i = 0; i < save.size(); i++) - p->setPermissions(save[i], save[i]); - - save.clear(); - ranges.clear(); -}; - - -bool Process::patchMemory(void *target, const void* src, size_t count) -{ - MemoryPatcher patcher(this); - - return patcher.write(target, src, count); -} - /******************************************************************************* M O D U L E S *******************************************************************************/ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4edf60000a..450d03e95e 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -36,6 +36,7 @@ distribution. #include "LuaTools.h" #include "LuaWrapper.h" #include "md5wrapper.h" +#include "MemoryPatcher.h" #include "MiscUtils.h" #include "PluginManager.h" diff --git a/library/MemoryPatcher.cpp b/library/MemoryPatcher.cpp new file mode 100644 index 0000000000..8fcf1b21c3 --- /dev/null +++ b/library/MemoryPatcher.cpp @@ -0,0 +1,84 @@ +#include "Core.h" +#include "MemoryPatcher.h" +#include "Process.h" + +namespace DFHack +{ + MemoryPatcher::MemoryPatcher(Process* p_) : p(p_) + { + if (!p) + p = Core::getInstance().p.get(); + } + + MemoryPatcher::~MemoryPatcher() + { + close(); + } + + bool MemoryPatcher::verifyAccess(void* target, size_t count, bool write) + { + auto* sptr = (uint8_t*)target; + uint8_t* eptr = sptr + count; + + // Find the valid memory ranges + if (ranges.empty()) + p->getMemRanges(ranges); + + // Find the ranges that this area spans + unsigned start = 0; + while (start < ranges.size() && ranges[start].end <= sptr) + start++; + if (start >= ranges.size() || ranges[start].start > sptr) + return false; + + unsigned end = start + 1; + while (end < ranges.size() && ranges[end].start < eptr) + { + if (ranges[end].start != ranges[end - 1].end) + return false; + end++; + } + if (ranges[end - 1].end < eptr) + return false; + + // Verify current permissions + for (unsigned i = start; i < end; i++) + if (!ranges[i].valid || !(ranges[i].read || ranges[i].execute) || ranges[i].shared) + return false; + + // Apply writable permissions & update + for (unsigned i = start; i < end; i++) + { + auto& perms = ranges[i]; + if ((perms.write || !write) && perms.read) + continue; + + save.push_back(perms); + perms.write = perms.read = true; + if (!p->setPermissions(perms, perms)) + return false; + } + + return true; + } + + bool MemoryPatcher::write(void* target, const void* src, size_t size) + { + if (!makeWritable(target, size)) + return false; + + memmove(target, src, size); + + p->flushCache(target, size); + return true; + } + + void MemoryPatcher::close() + { + for (size_t i = 0; i < save.size(); i++) + p->setPermissions(save[i], save[i]); + + save.clear(); + ranges.clear(); + }; +} \ No newline at end of file diff --git a/library/Process.cpp b/library/Process.cpp index 101eecbc71..beec599ef8 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -57,6 +57,7 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "Memory.h" +#include "MemoryPatcher.h" #include "MiscUtils.h" #include "VersionInfo.h" #include "VersionInfoFactory.h" @@ -796,3 +797,10 @@ int Process::memProtect(void *ptr, const int length, const int prot) return !VirtualProtect(ptr, length, prot_native, &old_prot); #endif /* WIN32 */ } + +bool Process::patchMemory(void* target, const void* src, size_t count) +{ + MemoryPatcher patcher(this); + + return patcher.write(target, src, count); +} diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 3c2a4a50fc..212d6e7dca 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -32,6 +32,7 @@ distribution. #include "Core.h" #include "DataFuncs.h" #include "MemAccess.h" +#include "MemoryPatcher.h" #include "VersionInfo.h" #include "VTableInterpose.h" diff --git a/library/include/Commands.h b/library/include/Commands.h new file mode 100644 index 0000000000..bcd8cd0a41 --- /dev/null +++ b/library/include/Commands.h @@ -0,0 +1,16 @@ +#pragma once + +#include "ColorText.h" +#include "CoreDefs.h" +#include "Core.h" + +#include +#include + +namespace DFHack +{ + namespace Commands + { + command_result help(color_ostream& con, const std::string& first, const std::vector& parts); + } +} diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef1..777d6881d6 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -25,6 +25,7 @@ distribution. #pragma once #include "Console.h" +#include "Commands.h" #include "CoreDefs.h" #include "Export.h" #include "Hooks.h" @@ -170,7 +171,8 @@ namespace DFHack std::string getHotkeyCmd( bool &keep_going ); 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); + command_result runCommand(color_ostream& out, const std::string& command); + bool loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent = false); bool addScriptPath(std::filesystem::path path, bool search_before = false); @@ -502,4 +504,12 @@ namespace DFHack operator bool() const { return owns_lock(); } }; + // unclassified functions related to core + + void help_helper(color_ostream& con, const std::string& entry_name); + + std::string dfhack_version_desc(); + } + + diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 5ef352a99e..19d65468ed 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -50,9 +50,9 @@ namespace DFHack */ struct DFHACK_EXPORT t_memrange { - void * base; - void * start; - void * end; + void* base; + void* start; + void* end; // memory range name (if any) char name[1024]; // permission to read @@ -63,7 +63,7 @@ namespace DFHack bool execute : 1; // is a shared region bool shared : 1; - inline bool isInRange( void * address) + inline bool isInRange(void* address) { if (address >= start && address < end) return true; return false; @@ -77,237 +77,239 @@ namespace DFHack */ class DFHACK_EXPORT Process { - public: - /// this is the single most important destructor ever. ~px - Process(const VersionInfoFactory& known_versions); - ~Process(); - /// read a 8-byte integer - uint64_t readQuad(const void * address) - { - return *(uint64_t *)address; - } - /// read a 8-byte integer - void readQuad(const void * address, uint64_t & value) - { - value = *(uint64_t *)address; - }; - /// write a 8-byte integer - void writeQuad(const void * address, const uint64_t value) - { - (*(uint64_t *)address) = value; - }; - - /// read a 4-byte integer - uint32_t readDWord(const void * address) - { - return *(uint32_t *)address; - } - /// read a 4-byte integer - void readDWord(const void * address, uint32_t & value) - { - value = *(uint32_t *)address; - }; - /// write a 4-byte integer - void writeDWord(const void * address, const uint32_t value) - { - (*(uint32_t *)address) = value; - }; - - /// read a pointer - char * readPtr(const void * address) - { - return *(char **)address; - } - /// read a pointer - void readPtr(const void * address, char * & value) - { - value = *(char **)address; - }; - - /// read a float - float readFloat(const void * address) - { - return *(float*)address; - } - /// write a float - void readFloat(const void * address, float & value) - { - value = *(float*)address; - }; - - /// read a 2-byte integer - uint16_t readWord(const void * address) - { - return *(uint16_t *)address; - } - /// read a 2-byte integer - void readWord(const void * address, uint16_t & value) - { - value = *(uint16_t *)address; - }; - /// write a 2-byte integer - void writeWord(const void * address, const uint16_t value) - { - (*(uint16_t *)address) = value; - }; - - /// read a byte - uint8_t readByte(const void * address) - { - return *(uint8_t *)address; - } - /// read a byte - void readByte(const void * address, uint8_t & value) - { - value = *(uint8_t *)address; - }; - /// write a byte - void writeByte(const void * address, const uint8_t value) - { - (*(uint8_t *)address) = value; - }; - - /// read an arbitrary amount of bytes - void read(void * address, uint32_t length, uint8_t* buffer) - { - memcpy(buffer, (void *) address, length); - }; - /// write an arbitrary amount of bytes - void write(void * address, uint32_t length, uint8_t* buffer) - { - memcpy((void *) address, buffer, length); - }; - - /// read an STL string - const std::string readSTLString (void * offset) - { - std::string * str = (std::string *) offset; - return *str; - }; - /// read an STL string - size_t readSTLString (void * offset, char * buffer, size_t bufcapacity) - { - if(!bufcapacity || bufcapacity == 1) - return 0; - std::string * str = (std::string *) offset; - size_t copied = str->copy(buffer,bufcapacity-1); - buffer[copied] = 0; - return copied; - }; - /** - * write an STL string - * @return length written - */ - size_t writeSTLString(const void * address, const std::string writeString) - { - std::string * str = (std::string *) address; - str->assign(writeString); - return writeString.size(); - }; - /** - * attempt to copy a string from source address to target address. may truncate or leak, depending on platform - * @return length copied - */ - size_t copySTLString(const void * address, const uintptr_t target) - { - std::string * strsrc = (std::string *) address; - std::string * str = (std::string *) target; - str->assign(*strsrc); - return str->size(); - } - - /// get class name of an object with rtti/type info - std::string doReadClassName(void * vptr); - - std::string readClassName(void * vptr) - { - std::map::iterator it = classNameCache.find(vptr); - if (it != classNameCache.end()) - return it->second; - return classNameCache[vptr] = doReadClassName(vptr); - } - - /// read a null-terminated C string - const std::string readCString (void * offset) - { - return std::string((char *) offset); - }; - - /// @return true if the process is suspended - bool isSuspended() - { - return true; - }; - /// @return true if the process is identified -- has a symbol table extension - bool isIdentified() - { - return identified; - }; - - /// get virtual memory ranges of the process (what is mapped where) - static void getMemRanges(std::vector & ranges); - - /// get the symbol table extension of this process - std::shared_ptr getDescriptor() - { - return my_descriptor; - }; - - void ValidateDescriptionOS() { - if (my_descriptor) - my_descriptor->ValidateOS(); - }; - - uintptr_t getBase(); - /// get the DF Process ID - int getPID(); - /// get the DF Process FilePath - std::filesystem::path getPath(); - /// Adjust between in-memory and in-file image offset - int adjustOffset(int offset, bool to_file = false); - - /// millisecond tick count, exactly as DF uses - uint32_t getTickCount(); - - /// modify permisions of memory range - bool setPermissions(const t_memrange & range,const t_memrange &trgrange); - - /// write a possibly read-only memory area - bool patchMemory(void *target, const void* src, size_t count); - - /// flush cache - bool flushCache(const void* target, size_t count); - - /// allocate new memory pages for code or stuff - /// returns -1 on error (0 is a valid address) - void* memAlloc(const int length); - - /// free memory pages from memAlloc - /// should have length = alloced length for portability - /// returns 0 on success - int memDealloc(void *ptr, const int length); - - /// change memory page permissions - /// prot is a bitwise OR of the MemProt enum - /// returns 0 on success - int memProtect(void *ptr, const int length, const int prot); - - enum MemProt { - READ = 1, - WRITE = 2, - EXEC = 4 - }; - - uint32_t getPE() { return my_pe; } - std::string getMD5() { return my_md5; } + public: + /// this is the single most important destructor ever. ~px + Process(const VersionInfoFactory& known_versions); + ~Process(); + /// read a 8-byte integer + uint64_t readQuad(const void* address) + { + return *(uint64_t*)address; + } + /// read a 8-byte integer + void readQuad(const void* address, uint64_t& value) + { + value = *(uint64_t*)address; + }; + /// write a 8-byte integer + void writeQuad(const void* address, const uint64_t value) + { + (*(uint64_t*)address) = value; + }; + + /// read a 4-byte integer + uint32_t readDWord(const void* address) + { + return *(uint32_t*)address; + } + /// read a 4-byte integer + void readDWord(const void* address, uint32_t& value) + { + value = *(uint32_t*)address; + }; + /// write a 4-byte integer + void writeDWord(const void* address, const uint32_t value) + { + (*(uint32_t*)address) = value; + }; + + /// read a pointer + char* readPtr(const void* address) + { + return *(char**)address; + } + /// read a pointer + void readPtr(const void* address, char*& value) + { + value = *(char**)address; + }; + + /// read a float + float readFloat(const void* address) + { + return *(float*)address; + } + /// write a float + void readFloat(const void* address, float& value) + { + value = *(float*)address; + }; + + /// read a 2-byte integer + uint16_t readWord(const void* address) + { + return *(uint16_t*)address; + } + /// read a 2-byte integer + void readWord(const void* address, uint16_t& value) + { + value = *(uint16_t*)address; + }; + /// write a 2-byte integer + void writeWord(const void* address, const uint16_t value) + { + (*(uint16_t*)address) = value; + }; + + /// read a byte + uint8_t readByte(const void* address) + { + return *(uint8_t*)address; + } + /// read a byte + void readByte(const void* address, uint8_t& value) + { + value = *(uint8_t*)address; + }; + /// write a byte + void writeByte(const void* address, const uint8_t value) + { + (*(uint8_t*)address) = value; + }; + + /// read an arbitrary amount of bytes + void read(void* address, uint32_t length, uint8_t* buffer) + { + memcpy(buffer, (void*)address, length); + }; + /// write an arbitrary amount of bytes + void write(void* address, uint32_t length, uint8_t* buffer) + { + memcpy((void*)address, buffer, length); + }; + + /// read an STL string + const std::string readSTLString(void* offset) + { + std::string* str = (std::string*)offset; + return *str; + }; + /// read an STL string + size_t readSTLString(void* offset, char* buffer, size_t bufcapacity) + { + if (!bufcapacity || bufcapacity == 1) + return 0; + std::string* str = (std::string*)offset; + size_t copied = str->copy(buffer, bufcapacity - 1); + buffer[copied] = 0; + return copied; + }; + /** + * write an STL string + * @return length written + */ + size_t writeSTLString(const void* address, const std::string writeString) + { + std::string* str = (std::string*)address; + str->assign(writeString); + return writeString.size(); + }; + /** + * attempt to copy a string from source address to target address. may truncate or leak, depending on platform + * @return length copied + */ + size_t copySTLString(const void* address, const uintptr_t target) + { + std::string* strsrc = (std::string*)address; + std::string* str = (std::string*)target; + str->assign(*strsrc); + return str->size(); + } + + /// get class name of an object with rtti/type info + std::string doReadClassName(void* vptr); + + std::string readClassName(void* vptr) + { + std::map::iterator it = classNameCache.find(vptr); + if (it != classNameCache.end()) + return it->second; + return classNameCache[vptr] = doReadClassName(vptr); + } + + /// read a null-terminated C string + const std::string readCString(void* offset) + { + return std::string((char*)offset); + }; + + /// @return true if the process is suspended + bool isSuspended() + { + return true; + }; + /// @return true if the process is identified -- has a symbol table extension + bool isIdentified() + { + return identified; + }; + + /// get virtual memory ranges of the process (what is mapped where) + static void getMemRanges(std::vector& ranges); + + /// get the symbol table extension of this process + std::shared_ptr getDescriptor() + { + return my_descriptor; + }; + + void ValidateDescriptionOS() + { + if (my_descriptor) + my_descriptor->ValidateOS(); + }; + + uintptr_t getBase(); + /// get the DF Process ID + int getPID(); + /// get the DF Process FilePath + std::filesystem::path getPath(); + /// Adjust between in-memory and in-file image offset + int adjustOffset(int offset, bool to_file = false); + + /// millisecond tick count, exactly as DF uses + uint32_t getTickCount(); + + /// modify permisions of memory range + bool setPermissions(const t_memrange& range, const t_memrange& trgrange); + + /// write a possibly read-only memory area + bool patchMemory(void* target, const void* src, size_t count); + + /// flush cache + bool flushCache(const void* target, size_t count); + + /// allocate new memory pages for code or stuff + /// returns -1 on error (0 is a valid address) + void* memAlloc(const int length); + + /// free memory pages from memAlloc + /// should have length = alloced length for portability + /// returns 0 on success + int memDealloc(void* ptr, const int length); + + /// change memory page permissions + /// prot is a bitwise OR of the MemProt enum + /// returns 0 on success + int memProtect(void* ptr, const int length, const int prot); + + enum MemProt + { + READ = 1, + WRITE = 2, + EXEC = 4 + }; + + uint32_t getPE() { return my_pe; } + std::string getMD5() { return my_md5; } private: std::shared_ptr my_descriptor; - PlatformSpecific *d; + PlatformSpecific* d; bool identified; uint32_t my_pid; uint32_t base; - std::map classNameCache; + std::map classNameCache; uint32_t my_pe; std::string my_md5; }; @@ -315,36 +317,21 @@ namespace DFHack class DFHACK_EXPORT ClassNameCheck { std::string name; - mutable void * vptr; + mutable void* vptr; public: ClassNameCheck() : vptr(0) {} ClassNameCheck(std::string _name); - ClassNameCheck &operator= (const ClassNameCheck &b); + ClassNameCheck& operator= (const ClassNameCheck& b); // Is the class name of the given virtual table pointer the same as the // name for thei ClassNameCheck object? - bool operator() (Process *p, void * ptr) const; + bool operator() (Process* p, void* ptr) const; // Get list of names given to ClassNameCheck constructors. - static void getKnownClassNames(std::vector &names); + static void getKnownClassNames(std::vector& names); }; - class DFHACK_EXPORT MemoryPatcher - { - Process *p; - std::vector ranges, save; - public: - MemoryPatcher(Process *p = NULL); - ~MemoryPatcher(); - - bool verifyAccess(void *target, size_t size, bool write = false); - bool makeWritable(void *target, size_t size) { - return verifyAccess(target, size, true); - } - bool write(void *target, const void *src, size_t size); - - void close(); - }; } + #endif diff --git a/library/include/MemoryPatcher.h b/library/include/MemoryPatcher.h new file mode 100644 index 0000000000..8577a762ef --- /dev/null +++ b/library/include/MemoryPatcher.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include "MemAccess.h" + +namespace DFHack { + + class Process; + + class MemoryPatcher { + public: + explicit MemoryPatcher(Process *p_ = nullptr); + ~MemoryPatcher(); + + // Ensure the target memory region is accessible and optionally writable. + // If `write` is true this will attempt to make the pages writable. + bool verifyAccess(void *target, size_t count, bool write); + + // Write `size` bytes from `src` to `target`. Returns true on success. + bool write(void *target, const void *src, size_t size); + + // Restore any modified permissions and clear internal state. + void close(); + + bool makeWritable(void* target, size_t size) + { + return verifyAccess(target, size, true); + } + private: + Process* p; + std::vector ranges, save; + }; + +} From 2a220df662cb36b1c91149ec5dca062b567238bc Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 02:02:49 -0600 Subject: [PATCH 0644/1082] whitespace --- library/Commands.cpp | 2 +- library/MemoryPatcher.cpp | 2 +- library/include/Core.h | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index a71c628d02..15dd38374a 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -46,4 +46,4 @@ namespace DFHack return CR_OK; } -} \ No newline at end of file +} diff --git a/library/MemoryPatcher.cpp b/library/MemoryPatcher.cpp index 8fcf1b21c3..e05b5b1575 100644 --- a/library/MemoryPatcher.cpp +++ b/library/MemoryPatcher.cpp @@ -81,4 +81,4 @@ namespace DFHack save.clear(); ranges.clear(); }; -} \ No newline at end of file +} diff --git a/library/include/Core.h b/library/include/Core.h index 777d6881d6..2c4d37b783 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -511,5 +511,3 @@ namespace DFHack std::string dfhack_version_desc(); } - - From 8205a60f8b75b0218a064a1806d7805c6b0168c6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 10:24:21 -0600 Subject: [PATCH 0645/1082] remove unneeded include there is a "Process.h" in the winapi, so on windows this includes an entirely unneeded windows API header and on linux it simply fails --- library/MemoryPatcher.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/MemoryPatcher.cpp b/library/MemoryPatcher.cpp index e05b5b1575..44aa865c42 100644 --- a/library/MemoryPatcher.cpp +++ b/library/MemoryPatcher.cpp @@ -1,6 +1,5 @@ #include "Core.h" #include "MemoryPatcher.h" -#include "Process.h" namespace DFHack { From fe85497448fd1ff6937fe9a0be5183a9eace40a8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 12:29:05 -0600 Subject: [PATCH 0646/1082] push more commands into `Commands` and assorted other adjustments appertaining thereto --- library/Commands.cpp | 328 ++++++++++++++++++++++++++++++++++++- library/Core.cpp | 285 ++------------------------------ library/include/Commands.h | 9 +- library/include/Core.h | 12 +- 4 files changed, 353 insertions(+), 281 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 15dd38374a..724c407d8c 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -1,15 +1,22 @@ -#include "ColorText.h" #include "Commands.h" + +#include "ColorText.h" #include "Core.h" #include "CoreDefs.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "Modules/Gui.h" #include #include +#include + namespace DFHack { - command_result Commands::help(color_ostream& con, const std::string& first, const std::vector& parts) + command_result Commands::help(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { if (!parts.size()) { @@ -46,4 +53,321 @@ namespace DFHack return CR_OK; } + command_result Commands::load(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool all = false; + bool load = (first == "load"); + bool unload = (first == "unload"); + bool reload = (first == "reload"); + auto plug_mgr = core.getPluginManager(); + if (parts.size()) + { + for (const auto& p : parts) + { + if (p.size() && p[0] == '-') + { + if (p.find('a') != std::string::npos) + all = true; + } + } + auto ret = CR_OK; + if (all) + { + if (load && !plug_mgr->loadAll()) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unloadAll()) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reloadAll()) + ret = CR_FAILURE; + } + else + { + for (auto& p : parts) + { + if (p.empty() || p[0] == '-') + continue; + if (load && !plug_mgr->load(p)) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unload(p)) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reload(p)) + ret = CR_FAILURE; + } + } + if (ret != CR_OK) + con.printerr("%s failed\n", first.c_str()); + return ret; + } + else + { + con.printerr("%s: no arguments\n", first.c_str()); + return CR_FAILURE; + } + + } + + command_result Commands::enable(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + CoreSuspender suspend; + bool enable = (first == "enable"); + auto plug_mgr = core.getPluginManager(); + + if (parts.size()) + { + command_result res{CR_FAILURE}; + + for (auto& part_ : parts) + { + // have to copy to modify as passed argument is const + std::string part(part_); + + if (has_backslashes(part)) + { + con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); + replace_backslashes_with_forwardslashes(part); + } + + auto alias = core.GetAliasCommand(part, true); + + Plugin* plug = (*plug_mgr)[alias]; + + if (!plug) + { + std::filesystem::path lua = core.findScript(part + ".lua"); + if (!lua.empty()) + { + res = core.enableLuaScript(con, part, enable); + } + else + { + res = CR_NOT_FOUND; + con.printerr("No such plugin or Lua script: %s\n", part.c_str()); + } + } + else if (!plug->can_set_enabled()) + { + res = CR_NOT_IMPLEMENTED; + con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str()); + } + else + { + res = plug->set_enabled(con, enable); + + if (res != CR_OK || plug->is_enabled() != enable) + con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str()); + } + } + + return res; + } + else + { + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + if (!plug->can_be_enabled()) continue; + + con.print( + "%21s %-3s%s\n", + (key + ":").c_str(), + plug->is_enabled() ? "on" : "off", + plug->can_set_enabled() ? "" : " (controlled internally)" + ); + } + + Lua::CallLuaModuleFunction(con, "script-manager", "list"); + + return CR_OK; + } + } + + command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + constexpr auto header_format = "%30s %10s %4s %8s\n"; + constexpr auto row_format = "%30s %10s %4i %8s\n"; + + con.print(header_format, "Name", "State", "Cmds", "Enabled"); + + auto plug_mgr = core.getPluginManager(); + + plug_mgr->refresh(); + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + + if (parts.size() && std::ranges::find(parts, key) == parts.end()) + continue; + + color_value color; + switch (plug->getState()) + { + case Plugin::PS_LOADED: + color = COLOR_RESET; + break; + case Plugin::PS_UNLOADED: + case Plugin::PS_UNLOADING: + color = COLOR_YELLOW; + break; + case Plugin::PS_LOADING: + color = COLOR_LIGHTBLUE; + break; + case Plugin::PS_BROKEN: + color = COLOR_LIGHTRED; + break; + default: + color = COLOR_LIGHTMAGENTA; + break; + } + con.color(color); + con.print(row_format, + plug->getName().c_str(), + Plugin::getStateDescription(plug->getState()), + plug->size(), + (plug->can_be_enabled() + ? (plug->is_enabled() ? "enabled" : "disabled") + : "n/a") + ); + con.color(COLOR_RESET); + } + + return CR_OK; + } + + command_result Commands::type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + auto plug_mgr = core.getPluginManager(); + + if (!parts.size()) + { + con.printerr("type: no argument\n"); + return CR_WRONG_USAGE; + } + con << parts[0]; + bool builtin = is_builtin(con, parts[0]); + std::filesystem::path lua_path = core.findScript(parts[0] + ".lua"); + Plugin* plug = plug_mgr->getPluginByCommand(parts[0]); + if (builtin) + { + con << " is a built-in command"; + con << std::endl; + } + else if (core.IsAlias(parts[0])) + { + con << " is an alias: " << core.GetAliasCommand(parts[0]) << std::endl; + } + else if (plug) + { + con << " is a command implemented by the plugin " << plug->getName() << std::endl; + } + else if (!lua_path.empty()) + { + con << " is a Lua script: " << lua_path << std::endl; + } + else + { + con << " is not a recognized command." << std::endl; + plug = plug_mgr->getPluginByName(parts[0]); + if (plug) + con << "Plugin " << parts[0] << " exists and implements " << plug->size() << " commands." << std::endl; + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) + { + std::string keystr = parts[1]; + if (parts[0] == "set") + core.ClearKeyBindings(keystr); + // for (int i = parts.size()-1; i >= 2; i--) + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) + { + if (!core.AddKeyBinding(keystr, part)) + { + con.printerr("Invalid key spec: %s\n", keystr.c_str()); + return CR_FAILURE; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") + { + // for (size_t i = 1; i < parts.size(); i++) + for (const auto& part : parts | std::views::drop(1)) + { + if (!core.ClearKeyBindings(part)) + { + con.printerr("Invalid key spec: %s\n", part.c_str()); + return CR_FAILURE; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") + { + std::vector list = core.ListKeyBindings(parts[1]); + if (list.empty()) + con << "No bindings." << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; + } + else + { + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + } + + return CR_OK; + } + + command_result Commands::alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) + { + const std::string& name = parts[1]; + std::vector cmd(parts.begin() + 2, parts.end()); + if (!core.AddAlias(name, cmd, parts[0] == "replace")) + { + con.printerr("Could not add alias %s - already exists\n", name.c_str()); + return CR_FAILURE; + } + } + else if (parts.size() >= 2 && (parts[0] == "delete" || parts[0] == "clear")) + { + if (!core.RemoveAlias(parts[1])) + { + con.printerr("Could not remove alias %s\n", parts[1].c_str()); + return CR_FAILURE; + } + } + else if (parts.size() >= 1 && (parts[0] == "list")) + { + auto aliases = core.ListAliases(); + for (auto p : aliases) + { + con << p.first << ": " << join_strings(" ", p.second) << std::endl; + } + } + else + { + con << "Usage: " << std::endl + << " alias add|replace " << std::endl + << " alias delete|clear " << std::endl + << " alias list" << std::endl; + } + + return CR_OK; + } + } diff --git a/library/Core.cpp b/library/Core.cpp index 5c1abdb41c..62b8860609 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -22,11 +22,12 @@ must not be misrepresented as being the original software. distribution. */ +#include "Core.h" + #include "Internal.h" #include "Error.h" #include "MemAccess.h" -#include "Core.h" #include "DataDefs.h" #include "Debug.h" #include "Console.h" @@ -43,6 +44,8 @@ distribution. #include "DFHackVersion.h" #include "md5wrapper.h" +#include "Commands.h" + #include "modules/DFSDL.h" #include "modules/DFSteam.h" #include "modules/EventManager.h" @@ -353,7 +356,7 @@ static bool init_enable_script(color_ostream &out, lua_State *state, const std:: return true; } -static command_result enableLuaScript(color_ostream &out, const std::string_view name, bool enabled) +command_result Core::enableLuaScript(color_ostream &out, const std::string_view name, bool enabled) { auto init_fn = [name, enabled](color_ostream& out, lua_State* state) -> bool { return init_enable_script(out, state, name, enabled); @@ -386,7 +389,7 @@ command_result Core::runCommand(color_ostream& out, const std::string& command) return CR_NOT_IMPLEMENTED; } -bool is_builtin(color_ostream& con, const std::string& command) +bool DFHack::is_builtin(color_ostream& con, const std::string& command) { CoreSuspender suspend; auto L = DFHack::Core::getInstance().getLuaState(); @@ -747,7 +750,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s command_result res; if (first == "help" || first == "man" || first == "?") { - Commands::help(con, first, parts); + return Commands::help(con, *this, first, parts); } else if (first == "tags") { @@ -755,119 +758,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "load" || first == "unload" || first == "reload") { - bool all = false; - bool load = (first == "load"); - bool unload = (first == "unload"); - bool reload = (first == "reload"); - if (parts.size()) - { - for (const auto& p : parts) - { - if (p.size() && p[0] == '-') - { - if (p.find('a') != std::string::npos) - all = true; - } - } - auto ret = CR_OK; - if (all) - { - if (load && !plug_mgr->loadAll()) - ret = CR_FAILURE; - else if (unload && !plug_mgr->unloadAll()) - ret = CR_FAILURE; - else if (reload && !plug_mgr->reloadAll()) - ret = CR_FAILURE; - } - else - { - for (auto& p : parts) - { - if (p.empty() || p[0] == '-') - continue; - if (load && !plug_mgr->load(p)) - ret = CR_FAILURE; - else if (unload && !plug_mgr->unload(p)) - ret = CR_FAILURE; - else if (reload && !plug_mgr->reload(p)) - ret = CR_FAILURE; - } - } - if (ret != CR_OK) - con.printerr("%s failed\n", first.c_str()); - return ret; - } - else { - con.printerr("%s: no arguments\n", first.c_str()); - return CR_FAILURE; - } + return Commands::load(con, *this, first, parts); } else if( first == "enable" || first == "disable" ) { - CoreSuspender suspend; - bool enable = (first == "enable"); - - if(parts.size()) - { - for (auto& part : parts) - { - if (has_backslashes(part)) - { - con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); - replace_backslashes_with_forwardslashes(part); - } - - part = GetAliasCommand(part, true); - - Plugin * plug = (*plug_mgr)[part]; - - if(!plug) - { - std::filesystem::path lua = findScript(part + ".lua"); - if (!lua.empty()) - { - res = enableLuaScript(con, part, enable); - } - else - { - res = CR_NOT_FOUND; - con.printerr("No such plugin or Lua script: %s\n", part.c_str()); - } - } - else if (!plug->can_set_enabled()) - { - res = CR_NOT_IMPLEMENTED; - con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str()); - } - else - { - res = plug->set_enabled(con, enable); - - if (res != CR_OK || plug->is_enabled() != enable) - con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str()); - } - } - - return res; - } - else - { - for (auto& [key, plug] : *plug_mgr) - { - if (!plug) - continue; - if (!plug->can_be_enabled()) continue; - - con.print( - "%21s %-3s%s\n", - (key+":").c_str(), - plug->is_enabled() ? "on" : "off", - plug->can_set_enabled() ? "" : " (controlled internally)" - ); - } - - Lua::CallLuaModuleFunction(con, "script-manager", "list"); - } + return Commands::enable(con, *this, first, parts); } else if (first == "ls" || first == "dir") { @@ -875,173 +770,19 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "plug") { - constexpr auto header_format = "%30s %10s %4s %8s\n"; - constexpr auto row_format = "%30s %10s %4zu %8s\n"; - con.print(header_format, "Name", "State", "Cmds", "Enabled"); - - plug_mgr->refresh(); - for (auto& [key, plug] : *plug_mgr) - { - if (!plug) - continue; - - if (parts.size() && std::ranges::find(parts, key) == parts.end()) - continue; - - color_value color; - switch (plug->getState()) - { - case Plugin::PS_LOADED: - color = COLOR_RESET; - break; - case Plugin::PS_UNLOADED: - case Plugin::PS_UNLOADING: - color = COLOR_YELLOW; - break; - case Plugin::PS_LOADING: - color = COLOR_LIGHTBLUE; - break; - case Plugin::PS_BROKEN: - color = COLOR_LIGHTRED; - break; - default: - color = COLOR_LIGHTMAGENTA; - break; - } - con.color(color); - con.print(row_format, - plug->getName().c_str(), - Plugin::getStateDescription(plug->getState()), - plug->size(), - (plug->can_be_enabled() - ? (plug->is_enabled() ? "enabled" : "disabled") - : "n/a") - ); - con.color(COLOR_RESET); - } + return Commands::plug(con, *this, first, parts); } else if (first == "type") { - if (!parts.size()) - { - con.printerr("type: no argument\n"); - return CR_WRONG_USAGE; - } - con << parts[0]; - bool builtin = is_builtin(con, parts[0]); - std::filesystem::path lua_path = findScript(parts[0] + ".lua"); - Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); - if (builtin) - { - con << " is a built-in command"; - con << std::endl; - } - else if (IsAlias(parts[0])) - { - con << " is an alias: " << GetAliasCommand(parts[0]) << std::endl; - } - else if (plug) - { - con << " is a command implemented by the plugin " << plug->getName() << std::endl; - } - else if (!lua_path.empty()) - { - con << " is a Lua script: " << lua_path << std::endl; - } - else - { - con << " is not a recognized command." << std::endl; - plug = plug_mgr->getPluginByName(parts[0]); - if (plug) - con << "Plugin " << parts[0] << " exists and implements " << plug->size() << " commands." << std::endl; - return CR_FAILURE; - } + return Commands::type(con, *this, first, parts); } else if (first == "keybinding") { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; - if (parts[0] == "set") - ClearKeyBindings(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!AddKeyBinding(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!ClearKeyBindings(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = ListKeyBindings(parts[1]); - if (list.empty()) - con << "No bindings." << std::endl; - for (const auto& kb : list) - con << " " << kb << std::endl; - } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; - } + return Commands::keybinding(con, *this, first, parts); } else if (first == "alias") { - if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) - { - const std::string &name = parts[1]; - std::vector cmd(parts.begin() + 2, parts.end()); - if (!AddAlias(name, cmd, parts[0] == "replace")) - { - con.printerr("Could not add alias %s - already exists\n", name.c_str()); - return CR_FAILURE; - } - } - else if (parts.size() >= 2 && (parts[0] == "delete" || parts[0] == "clear")) - { - if (!RemoveAlias(parts[1])) - { - con.printerr("Could not remove alias %s\n", parts[1].c_str()); - return CR_FAILURE; - } - } - else if (parts.size() >= 1 && (parts[0] == "list")) - { - auto aliases = ListAliases(); - for (auto p : aliases) - { - con << p.first << ": " << join_strings(" ", p.second) << std::endl; - } - } - else - { - con << "Usage: " << std::endl - << " alias add|replace " << std::endl - << " alias delete|clear " << std::endl - << " alias list" << std::endl; - } + return Commands::alias(con, *this, first, parts); } else if (first == "fpause") { diff --git a/library/include/Commands.h b/library/include/Commands.h index bcd8cd0a41..66f2034263 100644 --- a/library/include/Commands.h +++ b/library/include/Commands.h @@ -11,6 +11,13 @@ namespace DFHack { namespace Commands { - command_result help(color_ostream& con, const std::string& first, const std::vector& parts); + command_result help(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result load(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result enable(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + } } diff --git a/library/include/Core.h b/library/include/Core.h index 2c4d37b783..649fc9fea2 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -25,7 +25,6 @@ distribution. #pragma once #include "Console.h" -#include "Commands.h" #include "CoreDefs.h" #include "Export.h" #include "Hooks.h" @@ -214,7 +213,7 @@ namespace DFHack static void print(const char *format, ...) Wformat(printf,1,2); static void printerr(const char *format, ...) Wformat(printf,1,2); - PluginManager *getPluginManager() { return plug_mgr; } + PluginManager* getPluginManager() const { return plug_mgr; } static void cheap_tokenise(std::string const& input, std::vector &output); @@ -226,6 +225,8 @@ namespace DFHack return State; } + static command_result enableLuaScript(color_ostream& out, const std::string_view name, bool enabled); + private: DFHack::Console con; @@ -249,8 +250,8 @@ namespace DFHack void onStateChange(color_ostream &out, state_change_event event); void handleLoadAndUnloadScripts(color_ostream &out, state_change_event event); - Core(Core const&); // Don't Implement - void operator=(Core const&); // Don't implement + Core(Core const&) = delete; + void operator=(Core const&) = delete; // report error to user while failing void fatal (std::string output, const char * title = nullptr); @@ -507,7 +508,6 @@ namespace DFHack // unclassified functions related to core void help_helper(color_ostream& con, const std::string& entry_name); - std::string dfhack_version_desc(); - + bool is_builtin(color_ostream& con, const std::string& command); } From 771c4ae05bf9e7e63b5d4d058c5b4b314e0eb04f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 12:36:13 -0600 Subject: [PATCH 0647/1082] silly case-dependent file systems --- library/Commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 724c407d8c..9b033f853f 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -7,7 +7,7 @@ #include "LuaTools.h" #include "PluginManager.h" -#include "Modules/Gui.h" +#include "modules/Gui.h" #include #include From f447711167b29458e3fce2b4abb5121277ede466 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 13:04:50 -0600 Subject: [PATCH 0648/1082] fix printf --- library/Commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 9b033f853f..e9ee4a8a94 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -185,7 +185,7 @@ namespace DFHack command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { constexpr auto header_format = "%30s %10s %4s %8s\n"; - constexpr auto row_format = "%30s %10s %4i %8s\n"; + constexpr auto row_format = "%30s %10s %4llu %8s\n"; con.print(header_format, "Name", "State", "Cmds", "Enabled"); From 023b78c211134966a8a3de5a55aacfdd865741d3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 13:53:42 -0600 Subject: [PATCH 0649/1082] really fix printf --- library/Commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index e9ee4a8a94..1c67d5ebe4 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -185,7 +185,7 @@ namespace DFHack command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { constexpr auto header_format = "%30s %10s %4s %8s\n"; - constexpr auto row_format = "%30s %10s %4llu %8s\n"; + constexpr auto row_format = "%30s %10s %4zu %8s\n"; con.print(header_format, "Name", "State", "Cmds", "Enabled"); From 89dd6e70fde682f236151fd389e94ffcc2126e2f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 14:57:43 -0600 Subject: [PATCH 0650/1082] move `fpause` to `Commands.cpp` incidentally also reenable using `fpause` during worldgen --- library/Commands.cpp | 22 ++++++++++++++++++++++ library/Core.cpp | 9 +-------- library/include/Commands.h | 2 +- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 1c67d5ebe4..aae3ecc9c5 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -8,6 +8,9 @@ #include "PluginManager.h" #include "modules/Gui.h" +#include "modules/World.h" + +#include "df/viewscreen_new_regionst.h" #include #include @@ -370,4 +373,23 @@ namespace DFHack return CR_OK; } + command_result Commands::fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (auto scr = Gui::getViewscreenByType()) + { + if (scr->doing_mods || scr->doing_simple_params || scr->doing_params) + { + con.printerr("Cannot pause now.\n"); + return CR_FAILURE; + } + scr->abort_world_gen_dialogue = true; + } + else + { + World::SetPauseState(true); + } + con.print("The game was forced to pause!\n"); + return CR_OK; + } + } diff --git a/library/Core.cpp b/library/Core.cpp index 62b8860609..00370ee192 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -786,14 +786,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "fpause") { - World::SetPauseState(true); -/* TODO: understand how this changes for v50 - if (auto scr = Gui::getViewscreenByType()) - { - scr->worldgen_paused = true; - } -*/ - con.print("The game was forced to pause!\n"); + return Commands::fpause(con, *this, first, parts); } else if (first == "cls" || first == "clear") { diff --git a/library/include/Commands.h b/library/include/Commands.h index 66f2034263..984b34a637 100644 --- a/library/include/Commands.h +++ b/library/include/Commands.h @@ -18,6 +18,6 @@ namespace DFHack command_result type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); command_result keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); command_result alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); - + command_result fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); } } From df4af1464ea0f7695ebe3f1b99f2ede569e56077 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 18:03:36 -0600 Subject: [PATCH 0651/1082] move rest of builtin commands to `Commands.cpp` --- library/Commands.cpp | 186 +++++++++++++++++++++++++++++++++ library/Core.cpp | 170 ++---------------------------- library/include/ColorText.h | 3 + library/include/Commands.h | 7 ++ library/include/Console.h | 3 +- library/include/Core.h | 24 +++++ library/include/RemoteServer.h | 1 + 7 files changed, 232 insertions(+), 162 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index aae3ecc9c5..796a1f2701 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -6,6 +6,7 @@ #include "CoreDefs.h" #include "LuaTools.h" #include "PluginManager.h" +#include "RemoteTools.h" #include "modules/Gui.h" #include "modules/World.h" @@ -392,4 +393,189 @@ namespace DFHack return CR_OK; } + command_result Commands::clear(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (con.can_clear()) + { + con.clear(); + return CR_OK; + } + else + { + con.printerr("No console to clear, or this console does not support clearing.\n"); + return CR_NEEDS_CONSOLE; + } + } + + command_result Commands::kill_lua(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool force = std::ranges::any_of(parts, [] (const std::string& part) { return part == "force"; }); + if (!Lua::Interrupt(force)) + { + con.printerr( + "Failed to register hook. This can happen if you have" + " lua profiling or coverage monitoring enabled. Use" + " 'kill-lua force' to force, but this may disable" + " profiling and coverage monitoring.\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + core.loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); + return CR_OK; + } + else + { + con << "Usage:" << std::endl + << " script " << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::show(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().show()) + { + con.printerr("Could not show console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::hide(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().hide()) + { + con.printerr("Could not hide console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::sc_script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.empty() || parts[0] == "help" || parts[0] == "?") + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + con << "Valid event names (SC_ prefix is optional):" << std::endl; + for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) + { + std::string name = sc_event_name((state_change_event)i); + if (name != "SC_UNKNOWN") + con << " " << name << std::endl; + } + return CR_OK; + } + else if (parts[0] == "list") + { + std::string event_name = parts.size() >= 2 ? parts[1] : ""; + if (event_name.size() && sc_event_id(event_name) == SC_UNKNOWN) + { + con << "Unrecognized event name: " << parts[1] << std::endl; + return CR_WRONG_USAGE; + } + for (const auto& state_script : core.getStateChangeScripts()) + { + if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) + { + con.print("%s (%s): %s%s\n", sc_event_name(state_script.event).c_str(), + state_script.save_specific ? "save-specific" : "global", + state_script.save_specific ? "/raw/" : "/", + state_script.path.c_str()); + } + } + return CR_OK; + } + else if (parts[0] == "add") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript script(evt, parts[2], save_specific); + for (const auto& state_script : core.getStateChangeScripts()) + { + if (script == state_script) + { + con << "Script already registered" << std::endl; + return CR_FAILURE; + } + } + core.addStateChangeScript(script); + return CR_OK; + } + else if (parts[0] == "remove") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript tmp(evt, parts[2], save_specific); + if (core.removeStateChangeScript(tmp)) + { + return CR_OK; + } + else + { + con << "Unrecognized script" << std::endl; + return CR_FAILURE; + } + } + else + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::dump_rpc(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + std::ofstream file(parts[0]); + CoreService coreSvc; + coreSvc.dumpMethods(file); + + for (auto& it : *core.getPluginManager()) + { + Plugin* plug = it.second; + if (!plug) + continue; + + std::unique_ptr svc(plug->rpc_connect(con)); + if (!svc) + continue; + + file << "// Plugin: " << plug->getName() << std::endl; + svc->dumpMethods(file); + } + } + else + { + con << "Usage: devel/dump-rpc \"filename\"" << std::endl; + return CR_WRONG_USAGE; + } + return CR_OK; + } } diff --git a/library/Core.cpp b/library/Core.cpp index 00370ee192..298bb1edfa 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -612,7 +612,7 @@ static void sc_event_map_init() { } } -static state_change_event sc_event_id (std::string name) { +state_change_event DFHack::sc_event_id (std::string name) { sc_event_map_init(); auto it = state_change_event_map.find(name); if (it != state_change_event_map.end()) @@ -622,7 +622,7 @@ static state_change_event sc_event_id (std::string name) { return SC_UNKNOWN; } -static std::string sc_event_name (state_change_event id) { +std::string DFHack::sc_event_name (state_change_event id) { sc_event_map_init(); for (auto it = state_change_event_map.begin(); it != state_change_event_map.end(); ++it) { @@ -790,13 +790,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "cls" || first == "clear") { - if (con.is_console()) - ((Console&)con).clear(); - else - { - con.printerr("No console to clear.\n"); - return CR_NEEDS_CONSOLE; - } + return Commands::clear(con, *this, first, parts); } else if (first == "die") { @@ -808,173 +802,27 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "kill-lua") { - bool force = false; - for (const auto& part : parts) - { - if (part == "force") - force = true; - } - if (!Lua::Interrupt(force)) - { - con.printerr( - "Failed to register hook. This can happen if you have" - " lua profiling or coverage monitoring enabled. Use" - " 'kill-lua force' to force, but this may disable" - " profiling and coverage monitoring.\n"); - } + return Commands::kill_lua(con, *this, first, parts); } else if (first == "script") { - if(parts.size() == 1) - { - loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); - } - else - { - con << "Usage:" << std::endl - << " script " << std::endl; - return CR_WRONG_USAGE; - } + return Commands::script(con, *this, first, parts); } else if (first == "hide") { - if (!getConsole().hide()) - { - con.printerr("Could not hide console\n"); - return CR_FAILURE; - } - return CR_OK; + return Commands::hide(con, *this, first, parts); } else if (first == "show") { - if (!getConsole().show()) - { - con.printerr("Could not show console\n"); - return CR_FAILURE; - } - return CR_OK; + return Commands::show(con, *this, first, parts); } else if (first == "sc-script") { - if (parts.empty() || parts[0] == "help" || parts[0] == "?") - { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; - con << "Valid event names (SC_ prefix is optional):" << std::endl; - for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) - { - std::string name = sc_event_name((state_change_event)i); - if (name != "SC_UNKNOWN") - con << " " << name << std::endl; - } - return CR_OK; - } - else if (parts[0] == "list") - { - if(parts.size() < 2) - parts.push_back(""); - if (parts[1].size() && sc_event_id(parts[1]) == SC_UNKNOWN) - { - con << "Unrecognized event name: " << parts[1] << std::endl; - return CR_WRONG_USAGE; - } - for (const auto& state_script : state_change_scripts) - { - if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) - { - con.print("%s (%s): %s%s\n", sc_event_name(state_script.event).c_str(), - state_script.save_specific ? "save-specific" : "global", - state_script.save_specific ? "/raw/" : "/", - state_script.path.c_str()); - } - } - return CR_OK; - } - else if (parts[0] == "add") - { - if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) - { - con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; - return CR_WRONG_USAGE; - } - state_change_event evt = sc_event_id(parts[1]); - if (evt == SC_UNKNOWN) - { - con << "Unrecognized event: " << parts[1] << std::endl; - return CR_FAILURE; - } - bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); - StateChangeScript script(evt, parts[2], save_specific); - for (const auto& state_script : state_change_scripts) - { - if (script == state_script) - { - con << "Script already registered" << std::endl; - return CR_FAILURE; - } - } - state_change_scripts.push_back(script); - return CR_OK; - } - else if (parts[0] == "remove") - { - if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) - { - con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; - return CR_WRONG_USAGE; - } - state_change_event evt = sc_event_id(parts[1]); - if (evt == SC_UNKNOWN) - { - con << "Unrecognized event: " << parts[1] << std::endl; - return CR_FAILURE; - } - bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); - StateChangeScript tmp(evt, parts[2], save_specific); - auto it = std::find(state_change_scripts.begin(), state_change_scripts.end(), tmp); - if (it != state_change_scripts.end()) - { - state_change_scripts.erase(it); - return CR_OK; - } - else - { - con << "Unrecognized script" << std::endl; - return CR_FAILURE; - } - } - else - { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; - return CR_WRONG_USAGE; - } + return Commands::sc_script(con, *this, first, parts); } else if (first == "devel/dump-rpc") { - if (parts.size() == 1) - { - std::ofstream file(parts[0]); - CoreService core; - core.dumpMethods(file); - - for (auto & it : *plug_mgr) - { - Plugin * plug = it.second; - if (!plug) - continue; - - std::unique_ptr svc(plug->rpc_connect(con)); - if (!svc) - continue; - - file << "// Plugin: " << plug->getName() << std::endl; - svc->dumpMethods(file); - } - } - else - { - con << "Usage: devel/dump-rpc \"filename\"" << std::endl; - return CR_WRONG_USAGE; - } + return Commands::dump_rpc(con, *this, first, parts); } else if (RunAlias(con, first, parts, res)) { diff --git a/library/include/ColorText.h b/library/include/ColorText.h index f5768c0ffa..d8cd23c480 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -122,6 +122,9 @@ namespace DFHack virtual bool is_console() { return false; } virtual color_ostream *proxy_target() { return NULL; } + virtual bool can_clear() const { return false; } + virtual void clear() {} + static bool log_errors_to_stderr; }; diff --git a/library/include/Commands.h b/library/include/Commands.h index 984b34a637..163d2e164f 100644 --- a/library/include/Commands.h +++ b/library/include/Commands.h @@ -19,5 +19,12 @@ namespace DFHack command_result keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); command_result alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); command_result fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result clear(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result kill_lua(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result show(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result hide(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result sc_script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result dump_rpc(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); } } diff --git a/library/include/Console.h b/library/include/Console.h index 4397c75954..cbf7d58511 100644 --- a/library/include/Console.h +++ b/library/include/Console.h @@ -141,8 +141,9 @@ namespace DFHack /// shutdown the console. NOT thread-safe bool shutdown( void ); + bool can_clear() const { return true; } /// Clear the console, along with its scrollback - void clear(); + void clear() override; /// Position cursor at x,y. 1,1 = top left corner void gotoxy(int x, int y); /// Enable or disable the caret/cursor diff --git a/library/include/Core.h b/library/include/Core.h index 649fc9fea2..da44fea04a 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -227,6 +227,27 @@ namespace DFHack static command_result enableLuaScript(color_ostream& out, const std::string_view name, bool enabled); + const std::vector getStateChangeScripts() const + { + return state_change_scripts; + } + + void addStateChangeScript(const StateChangeScript& script) + { + state_change_scripts.push_back(script); + } + + bool removeStateChangeScript(const StateChangeScript& script) + { + auto it = std::find(state_change_scripts.begin(), state_change_scripts.end(), script); + if (it != state_change_scripts.end()) + { + state_change_scripts.erase(it); + return true; + } + return false; + } + private: DFHack::Console con; @@ -510,4 +531,7 @@ namespace DFHack void help_helper(color_ostream& con, const std::string& entry_name); std::string dfhack_version_desc(); bool is_builtin(color_ostream& con, const std::string& command); + std::string sc_event_name(state_change_event id); + state_change_event sc_event_id(std::string name); + } diff --git a/library/include/RemoteServer.h b/library/include/RemoteServer.h index 8a5bee86f7..bade25b521 100644 --- a/library/include/RemoteServer.h +++ b/library/include/RemoteServer.h @@ -211,6 +211,7 @@ namespace DFHack functions.push_back(new VoidServerMethod(this, name, flags, fptr)); } + public: void dumpMethods(std::ostream & out) const; }; From b5744f3b0c11f7a2fcb27f3fca12c3e5900c50d2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 18:13:14 -0600 Subject: [PATCH 0652/1082] add missing `` --- library/include/Core.h | 1 + 1 file changed, 1 insertion(+) diff --git a/library/include/Core.h b/library/include/Core.h index da44fea04a..5b85d97252 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -31,6 +31,7 @@ distribution. #include "modules/Graphic.h" +#include #include #include #include From 5f3314bba8e73fd16a1b76b66f09b2eeee50cd10 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 18:22:19 -0600 Subject: [PATCH 0653/1082] add changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a..ba30756d5b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -80,6 +80,7 @@ Template for new versions: - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements +-- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). ## Documentation From 017d569edee4c0f1b019b589f1350caa3f6cc38b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 18:03:45 -0600 Subject: [PATCH 0654/1082] move changelog to correct section --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ba30756d5b..f7d146dc45 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). ## Documentation @@ -80,7 +81,6 @@ Template for new versions: - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements --- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). ## Documentation From b89d747492fcc7eb33970f0605cedac5e58d54cc Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Tue, 25 Nov 2025 11:01:46 -0500 Subject: [PATCH 0655/1082] Cleanup pass --- .gitignore | 2 +- docs/builtins/keybinding.rst | 3 ++- docs/changelog.txt | 1 + library/include/Core.h | 1 - library/modules/Hotkey.cpp | 26 ++++++++++++++------------ 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 6142448b7a..60a0e1b6b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # linux backup files *~ -.cache + # Kdevelop project files *.kdev4 .kdev4 diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index f77a081650..e8f206848d 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -32,7 +32,8 @@ The ```` parameter above has the following case-insensitive syntax:: [Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]] where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote -optional parts. +optional parts. It is important to note that the key is the non-shifted version +of the key. For example ``!`` would be defined as ``Shift-0``. DFHack commands can advertise the contexts in which they can be usefully run. For example, a command that acts on a selected unit can tell `keybinding` that diff --git a/docs/changelog.txt b/docs/changelog.txt index 6cfa16e4bf..05ebf4907b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. ## Documentation diff --git a/library/include/Core.h b/library/include/Core.h index bf0378b05d..c250eef2e2 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -270,7 +270,6 @@ namespace DFHack // Hotkey Manager DFHack::HotkeyManager *hotkey_mgr; - // FIXME: remove all this junk (hotkey related) std::vector script_paths[3]; std::mutex script_path_mutex; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index eb7b483af0..d6c72e60b9 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -1,5 +1,8 @@ #include "modules/Hotkey.h" +#include +#include + #include "Core.h" #include "ColorText.h" #include "MiscUtils.h" @@ -12,8 +15,6 @@ #include "df/viewscreen.h" #include "df/interfacest.h" -#include -#include using namespace DFHack; using Hotkey::KeySpec; @@ -103,7 +104,7 @@ std::optional KeySpec::parse(std::string spec, std::string* err) { // Attempt to parse as a mouse binding if (spec.starts_with("mouse")) { spec.erase(0, 5); - // Read button number, ensuring between 1 and 15 inclusive + // Read button number, ensuring between 4 and 15 inclusive try { int mbutton = std::stoi(spec); if (mbutton >= 4 && mbutton <= 15) { @@ -126,14 +127,15 @@ std::optional KeySpec::parse(std::string spec, std::string* err) { } bool KeySpec::isDisruptive() const { - // Miscellaneous essential keys + // SDLK enum uses the actual characters for a key as its value. + // Escaped values included are Return, Escape, Backspace, and Tab const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./"; // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys - bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) - || (this->sym >= SDLK_0 && this->sym <= SDLK_9) + bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) // A-Z + || (this->sym >= SDLK_0 && this->sym <= SDLK_9) // 0-9 || essential_key_set.find(this->sym) != std::string::npos - || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); + || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); // Arrow keys // Essential keys are safe, so long as they have a modifier that isn't Shift if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT)) @@ -306,11 +308,12 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { if (!df::global::gview || !df::global::plotinfo) return false; - // Get bottommost active screen + // Get topmost active screen df::viewscreen *screen = &df::global::gview->view; while (screen->child) screen = screen->child; + // Map keypad return to return if (sym == SDLK_KP_ENTER) sym = SDLK_RETURN; @@ -333,6 +336,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { auto& core = Core::getInstance(); bool mortal_mode = core.getMortalMode(); + // Iterate in reverse, prioritizing the last added keybinds for (const auto& bind : binds | std::views::reverse) { if (bind.spec.modifiers != modifiers) continue; @@ -340,9 +344,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { if (!bind.spec.focus.empty()) { bool matched = false; for (const auto& focus : bind.spec.focus) { - printf("Focus check for: %s", focus.c_str()); if (Gui::matchFocusString(focus)) { - printf("Matched\n"); matched = true; break; } @@ -351,7 +353,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { continue; } - if (!Core::getInstance().getPluginManager()->CanInvokeHotkey(bind.command, screen)) + if (!core.getPluginManager()->CanInvokeHotkey(bind.command, screen)) continue; if (mortal_mode && core.isArmokTool(bind.command)) @@ -435,7 +437,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl << "Context may be used to limit the scope of the binding, by" << std::endl << "requiring the current context to have a certain prefix." << std::endl << "Current UI context is: " << std::endl From f0e98b5ab51cf5712670f8d995b7b50fb1309c00 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Tue, 25 Nov 2025 12:10:31 -0500 Subject: [PATCH 0656/1082] Address tidy checks --- library/include/modules/Hotkey.h | 10 +++---- library/modules/Hotkey.cpp | 50 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 2e9a400cd1..78978f29f6 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -39,8 +39,8 @@ namespace DFHack { ~HotkeyManager(); - bool addKeybind(std::string keyspec, std::string cmd); - bool addKeybind(Hotkey::KeySpec spec, std::string cmd); + bool addKeybind(std::string keyspec, std::string_view cmd); + bool addKeybind(Hotkey::KeySpec spec, std::string_view cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command bool removeKeybind(std::string keyspec, bool match_focus=true, std::string_view cmdline=""); bool removeKeybind(const Hotkey::KeySpec& spec, bool match_focus=true, std::string_view cmdline=""); @@ -62,13 +62,13 @@ namespace DFHack { private: std::thread hotkey_thread; - std::mutex lock {}; - std::condition_variable cond {}; + std::mutex lock; + std::condition_variable cond; bool keybind_save_requested = false; std::string requested_keybind; - int hotkey_sig = 0; + uint8_t hotkey_sig = 0; std::string queued_command; std::map> bindings; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index d6c72e60b9..d7ef91ed10 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -20,20 +20,20 @@ using namespace DFHack; using Hotkey::KeySpec; using Hotkey::KeyBinding; -enum HotkeySignal { +enum HotkeySignal : uint8_t { None = 0, CmdReady, Shutdown, }; -bool operator==(const KeySpec& a, const KeySpec& b) { +static bool operator==(const KeySpec& a, const KeySpec& b) { return a.modifiers == b.modifiers && a.sym == b.sym && a.focus.size() == b.focus.size() && std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); } // Equality operator for key bindings -bool operator==(const KeyBinding& a, const KeyBinding& b) { +static bool operator==(const KeyBinding& a, const KeyBinding& b) { return a.spec == b.spec && a.command == b.command && a.cmdline == b.cmdline; @@ -134,7 +134,7 @@ bool KeySpec::isDisruptive() const { // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) // A-Z || (this->sym >= SDLK_0 && this->sym <= SDLK_9) // 0-9 - || essential_key_set.find(this->sym) != std::string::npos + || (this->sym < CHAR_MAX && essential_key_set.find((char)this->sym) != std::string::npos) || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); // Arrow keys // Essential keys are safe, so long as they have a modifier that isn't Shift @@ -173,13 +173,13 @@ void HotkeyManager::hotkey_thread_fn() { } -bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { +bool HotkeyManager::addKeybind(KeySpec spec, std::string_view cmd) { // No point in a hotkey with no action if (cmd.empty()) return false; KeyBinding binding; - binding.spec = spec; + binding.spec = std::move(spec); binding.cmdline = cmd; size_t space_idx = cmd.find(' '); binding.command = space_idx == std::string::npos ? cmd : cmd.substr(0, space_idx); @@ -196,8 +196,8 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { return true; } -bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = KeySpec::parse(keyspec); +bool HotkeyManager::addKeybind(std::string keyspec, std::string_view cmd) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -223,7 +223,7 @@ bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::st } bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { - std::optional spec_opt = KeySpec::parse(keyspec); + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return false; return this->removeKeybind(spec_opt.value(), match_focus, cmdline); @@ -261,7 +261,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { std::vector HotkeyManager::listKeybinds(std::string keyspec) { std::lock_guard l(lock); - std::optional spec_opt = KeySpec::parse(keyspec); + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -371,7 +371,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { void HotkeyManager::setHotkeyCommand(std::string cmd) { std::unique_lock l(lock); - queued_command = cmd; + queued_command = std::move(cmd); hotkey_sig = HotkeySignal::CmdReady; l.unlock(); cond.notify_all(); @@ -391,7 +391,7 @@ std::string HotkeyManager::getKeybindingInput() { void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { std::string parse_error; if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { - std::string keystr = parts[1]; + const std::string& keystr = parts[1]; if (parts[0] == "set") removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { @@ -426,22 +426,22 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } std::vector list = listKeybinds(spec.value()); if (list.empty()) - con << "No bindings." << std::endl; + con << "No bindings.\n"; for (const auto& kb : list) - con << " " << kb << std::endl; + con << " " << kb << "\n"; } else { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + con << "Usage:\n" + << " keybinding list \n" + << " keybinding clear [@context]...\n" + << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" + << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" + << "Later adds, and earlier items within one command have priority.\n" + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" + << "Context may be used to limit the scope of the binding, by\n" + << "requiring the current context to have a certain prefix.\n" + << "Current UI context is: \n" + << join_strings("\n", Gui::getCurFocus(true)) << "\n"; } } From d8316fc68a8a237c95e21660edba67a2b2f59be4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 23:30:42 -0600 Subject: [PATCH 0657/1082] add fmtlib adds fmtlib as a dependency adds `format` and `format_err` template functions to `color_ostream` --- CMakeLists.txt | 8 ++++++++ library/CMakeLists.txt | 4 ++-- library/include/ColorText.h | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e78a562626..e8fe082b7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -347,6 +347,14 @@ if(BUILD_LIBRARY) endif() endif() +INCLUDE(FetchContent) +FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG master +) +FetchContent_MakeAvailable(fmt) + if(APPLE) # libstdc++ (GCC 4.8.5 for OS X 10.6) # fixes crash-on-unwind bug in DF's libstdc++ diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index fa5c482bf6..48e2af5199 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -320,12 +320,12 @@ if(UNIX) endif() if(APPLE) - set(PROJECT_LIBS dl dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS dl dfhack-md5 fmt ${DFHACK_TINYXML}) elseif(UNIX) set(PROJECT_LIBS rt dl dfhack-md5 ${DFHACK_TINYXML}) else(WIN32) # FIXME: do we really need psapi? - set(PROJECT_LIBS psapi dbghelp dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS psapi dbghelp dfhack-md5 fmt ${DFHACK_TINYXML}) endif() set(VERSION_SRCS DFHackVersion.cpp) diff --git a/library/include/ColorText.h b/library/include/ColorText.h index f5768c0ffa..05e38363bd 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -33,6 +33,8 @@ distribution. #include #include +#include + namespace dfproto { class CoreTextNotification; @@ -104,6 +106,25 @@ namespace DFHack color_ostream(); virtual ~color_ostream(); + template + void format(const char* format, Args&& ... args) + { + auto str = fmt::format(format, std::forward(args)...); + flush_buffer(false); + add_text(cur_color, str); + } + + template + void format_err(const char* format, Args&& ... args) + { + auto str = fmt::format(format, std::forward(args)...); + if (log_errors_to_stderr) { + std::cerr << str; + } + flush_buffer(false); + add_text(COLOR_LIGHTRED, str); + } + /// Print a formatted string, like printf void print(const char *format, ...) Wformat(printf,2,3); void vprint(const char *format, va_list args) Wformat(printf,2,0); From 4859b8dc7ca7a1464f60c9807152b71a44db6e0d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 23:34:04 -0600 Subject: [PATCH 0658/1082] forgot to add `fmt` to "UNIX" lol --- library/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 48e2af5199..05f6d2166c 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -322,7 +322,7 @@ endif() if(APPLE) set(PROJECT_LIBS dl dfhack-md5 fmt ${DFHACK_TINYXML}) elseif(UNIX) - set(PROJECT_LIBS rt dl dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS rt dl dfhack-md5 fmt ${DFHACK_TINYXML}) else(WIN32) # FIXME: do we really need psapi? set(PROJECT_LIBS psapi dbghelp dfhack-md5 fmt ${DFHACK_TINYXML}) From c6eaf6df724387b6de63f846204994196fc42b62 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 00:13:36 -0600 Subject: [PATCH 0659/1082] add `fmt` to library lists for `dfhack-client` and plugins --- library/CMakeLists.txt | 2 +- plugins/Plugins.cmake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 05f6d2166c..6d1d46141d 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -420,7 +420,7 @@ endif() target_link_libraries(dfhack protobuf-lite clsocket lua jsoncpp_static dfhack-version ${PROJECT_LIBS}) set_target_properties(dfhack PROPERTIES INTERFACE_LINK_LIBRARIES "") -target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static) +target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static fmt) if(WIN32) target_link_libraries(dfhack-client dbghelp) endif() diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 3726b86c7c..107f9a8068 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -124,7 +124,7 @@ macro(dfhack_plugin) target_include_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proto") target_link_libraries(${PLUGIN_NAME} protobuf-lite) endif() - target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version ${PLUGIN_LINK_LIBRARIES}) + target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version fmt ${PLUGIN_LINK_LIBRARIES}) if(UNIX) set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_GCC}") From 3ff667ff9493802ef9a083ed06ab85c03e7ba8d4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 00:19:53 -0600 Subject: [PATCH 0660/1082] missed one? --- library/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 6d1d46141d..471d2f808a 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -391,7 +391,7 @@ 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) + target_link_libraries(dfhooks_dfhack dfhack fmt) endif() # effectively disables debug builds... From bc9224dcbcb263fb1e1c0a8f9cb3b78fad390e43 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 17:40:03 -0600 Subject: [PATCH 0661/1082] convert over to fmtlib This also adds custom formatters for `df::coord` and `df::coord2d` --- library/ColorText.cpp | 48 ---- library/Core.cpp | 146 +++++------ library/DataDefs.cpp | 4 +- library/Error.cpp | 5 +- library/LuaApi.cpp | 4 +- library/LuaTools.cpp | 34 +-- library/LuaTypes.cpp | 2 +- library/LuaWrapper.cpp | 13 +- library/PlugLoad.cpp | 4 +- library/PluginManager.cpp | 72 +++--- library/Process.cpp | 2 +- library/RemoteClient.cpp | 42 ++-- library/RemoteServer.cpp | 18 +- library/RemoteTools.cpp | 8 +- library/TileTypes.cpp | 37 ++- library/VTableInterpose.cpp | 2 +- library/VersionInfoFactory.cpp | 4 +- library/dfhack-run.cpp | 4 +- library/include/ColorText.h | 19 +- library/include/Core.h | 15 +- library/include/DataDefs.h | 26 +- library/modules/DFSteam.cpp | 8 +- library/modules/EventManager.cpp | 72 +++--- library/modules/Gui.cpp | 36 +-- library/modules/Items.cpp | 6 +- library/modules/Kitchen.cpp | 6 +- library/modules/Materials.cpp | 8 +- library/modules/Persistence.cpp | 4 +- library/modules/Screen.cpp | 4 +- library/modules/Textures.cpp | 25 +- library/modules/World.cpp | 8 +- plugins/3dveins.cpp | 78 +++--- plugins/add-spatter.cpp | 4 +- plugins/aquifer.cpp | 47 ++-- plugins/army-controller-sanity.cpp | 18 +- plugins/autobutcher.cpp | 56 ++--- plugins/autochop.cpp | 72 +++--- plugins/autoclothing.cpp | 14 +- plugins/autodump.cpp | 4 +- plugins/autofarm.cpp | 14 +- plugins/autogems.cpp | 6 +- plugins/autolabor/autolabor.cpp | 30 +-- plugins/autonestbox.cpp | 38 +-- plugins/autoslab.cpp | 16 +- plugins/blueprint.cpp | 10 +- plugins/buildingplan/buildingplan.cpp | 136 +++++----- plugins/buildingplan/buildingplan_cycle.cpp | 50 ++-- plugins/buildingplan/buildingtypekey.cpp | 2 +- plugins/buildingplan/buildingtypekey.h | 11 + plugins/buildingplan/defaultitemfilters.cpp | 20 +- plugins/buildingplan/itemfilter.cpp | 10 +- plugins/buildingplan/plannedbuilding.cpp | 8 +- plugins/burrow.cpp | 22 +- plugins/changeitem.cpp | 2 +- plugins/channel-safely/channel-groups.cpp | 20 +- plugins/channel-safely/channel-manager.cpp | 22 +- .../channel-safely/channel-safely-plugin.cpp | 42 ++-- plugins/channel-safely/include/inlines.h | 2 +- plugins/cleanconst.cpp | 4 +- plugins/cleaners.cpp | 8 +- plugins/cleanowned.cpp | 8 +- plugins/createitem.cpp | 10 +- plugins/cursecheck.cpp | 10 +- plugins/cxxrandom.cpp | 2 +- plugins/debug.cpp | 10 +- plugins/deramp.cpp | 4 +- plugins/devel/buildprobe.cpp | 2 +- .../check-structures-sanity/dispatch.cpp | 4 +- .../devel/check-structures-sanity/main.cpp | 2 +- .../devel/check-structures-sanity/types.cpp | 2 +- .../check-structures-sanity/validate.cpp | 4 +- plugins/devel/color-dfhack-text.cpp | 2 +- plugins/devel/counters.cpp | 2 +- plugins/devel/dumpmats.cpp | 94 +++---- plugins/devel/eventExample.cpp | 28 +-- plugins/devel/frozen.cpp | 4 +- plugins/devel/kittens.cpp | 22 +- plugins/devel/memutils.cpp | 4 +- plugins/devel/memview.cpp | 10 +- plugins/devel/onceExample.cpp | 2 +- plugins/devel/stepBetween.cpp | 2 +- plugins/devel/tilesieve.cpp | 2 +- plugins/devel/vectors.cpp | 8 +- plugins/devel/zoom.cpp | 2 +- plugins/dig-now.cpp | 23 +- plugins/dig.cpp | 40 +-- plugins/digFlood.cpp | 2 +- plugins/diggingInvaders/assignJob.cpp | 6 +- plugins/diggingInvaders/diggingInvaders.cpp | 16 +- plugins/dwarfvet.cpp | 14 +- plugins/embark-assistant/finder_ui.cpp | 10 +- plugins/embark-assistant/matcher.cpp | 234 +++++++++--------- .../examples/persistent_per_save_example.cpp | 22 +- plugins/examples/simple_command_example.cpp | 2 +- plugins/examples/skeleton.cpp | 10 +- plugins/examples/ui_addition_example.cpp | 4 +- plugins/fastdwarf.cpp | 28 +-- plugins/filltraffic.cpp | 2 +- plugins/fix-occupancy.cpp | 32 +-- plugins/fixveins.cpp | 4 +- plugins/flows.cpp | 10 +- plugins/follow.cpp | 2 +- plugins/forceequip.cpp | 38 +-- plugins/generated-creature-renamer.cpp | 4 +- plugins/getplants.cpp | 46 ++-- plugins/hotkeys.cpp | 18 +- plugins/infinite-sky.cpp | 19 +- plugins/jobutils.cpp | 30 +-- plugins/logistics.cpp | 46 ++-- plugins/misery.cpp | 18 +- plugins/nestboxes.cpp | 26 +- plugins/orders.cpp | 2 +- plugins/overlay.cpp | 4 +- plugins/pathable.cpp | 18 +- plugins/pet-uncapper.cpp | 22 +- plugins/plant.cpp | 58 ++--- plugins/preserve-rooms.cpp | 82 +++--- plugins/preserve-tombs.cpp | 36 +-- plugins/probe.cpp | 109 ++++---- plugins/prospector.cpp | 6 +- plugins/regrass.cpp | 46 ++-- .../remotefortressreader.cpp | 10 +- plugins/rendermax/renderer_light.cpp | 16 +- plugins/rendermax/rendermax.cpp | 2 +- plugins/seedwatch.cpp | 38 +-- plugins/showmood.cpp | 77 +++--- plugins/siege-engine.cpp | 2 +- plugins/sort.cpp | 8 +- plugins/spectate.cpp | 50 ++-- plugins/steam-engine.cpp | 2 +- plugins/stockflow.cpp | 4 +- plugins/stockpiles/OrganicMatLookup.cpp | 18 +- plugins/stockpiles/StockpileSerializer.cpp | 82 +++--- plugins/stockpiles/stockpiles.cpp | 30 +-- plugins/strangemood.cpp | 10 +- plugins/suspendmanager.cpp | 40 +-- plugins/tailor.cpp | 97 ++++---- plugins/tiletypes.cpp | 32 +-- plugins/timestream.cpp | 26 +- plugins/tubefill.cpp | 2 +- plugins/tweak/tweak.cpp | 12 +- plugins/work-now.cpp | 14 +- plugins/workflow.cpp | 34 +-- 143 files changed, 1686 insertions(+), 1701 deletions(-) diff --git a/library/ColorText.cpp b/library/ColorText.cpp index 8dc5cc5dcf..bdd3e98f44 100644 --- a/library/ColorText.cpp +++ b/library/ColorText.cpp @@ -91,54 +91,6 @@ color_ostream::~color_ostream() delete buf(); } -void color_ostream::print(const char *format, ...) -{ - va_list args; - va_start(args, format); - vprint(format, args); - va_end(args); -} - -void color_ostream::vprint(const char *format, va_list args) -{ - std::string str = stl_vsprintf(format, args); - - if (!str.empty()) { - flush_buffer(false); - add_text(cur_color, str); - if (str[str.size()-1] == '\n') - flush_proxy(); - } -} - -void color_ostream::printerr(const char * format, ...) -{ - va_list args; - va_start(args, format); - vprinterr(format, args); - va_end(args); -} - -void color_ostream::vprinterr(const char *format, va_list args) -{ - color_value save = cur_color; - - if (log_errors_to_stderr) - { - va_list args1; - va_copy(args1, args); - vfprintf(stderr, format, args1); - va_end(args1); - } - - color(COLOR_LIGHTRED); - va_list args2; - va_copy(args2, args); - vprint(format, args2); - va_end(args2); - color(save); -} - void color_ostream::color(color_value c) { if (c == cur_color) diff --git a/library/Core.cpp b/library/Core.cpp index bb631414c6..6960c12eb9 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -87,6 +87,8 @@ distribution. #include #include +#include + #ifdef _WIN32 #define NOMINMAX #include @@ -297,7 +299,7 @@ static void fHKthread(IODATA * iodata) auto rv = core->runCommand(out, stuff); if (rv == CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", stuff.c_str()); + out.printerr("Invalid hotkey command: '{}'\n", stuff); } } } @@ -444,7 +446,7 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: { completed = possible[0]; //fprintf(stderr, "Autocompleted %s to %s\n", , ); - con.printerr("%s is not recognized. Did you mean %s?\n", first.c_str(), completed.c_str()); + con.printerr("{} is not recognized. Did you mean {}?\n", first, completed); return true; } @@ -453,7 +455,7 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: std::string out; for (size_t i = 0; i < possible.size(); i++) out += " " + possible[i]; - con.printerr("%s is not recognized. Possible completions:%s\n", first.c_str(), out.c_str()); + con.printerr("{} is not recognized. Possible completions:{}\n", first, out); return true; } @@ -529,7 +531,7 @@ std::filesystem::path Core::findScript(std::string name) std::filesystem::path path = std::filesystem::weakly_canonical(*it / name, ec); if (ec) { - con.printerr("Error loading '%s' (%s)\n", (*it / name).string().c_str(), ec.message().c_str()); + con.printerr("Error loading '{}' ({})\n", *it / name, ec.message()); continue; } @@ -546,7 +548,7 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) if (!file) { if (!silent) - out.printerr("Could not load %s\n", filename.c_str()); + out.printerr("Could not load {}\n", filename); return false; } std::string raw; @@ -565,10 +567,10 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) if (ch == '+' || ch == '-') { if (!Core::getInstance().addScriptPath(path, ch == '+') && !silent) - out.printerr("%s:%i: Failed to add path: %s\n", filename.c_str(), line, path.c_str()); + out.printerr("{}:{}: Failed to add path: {}\n", filename, line, path); } else if (!silent) - out.printerr("%s:%i: Illegal character: %c\n", filename.c_str(), line, ch); + out.printerr("{}:{}: Illegal character: {}\n", filename, line, ch); } return true; } @@ -583,7 +585,7 @@ static void loadModScriptPaths(color_ostream &out) { DEBUG(script,out).print("final mod script paths:\n"); for (auto& path : mod_script_paths_str) { - DEBUG(script, out).print(" %s\n", path.c_str()); + DEBUG(script, out).print(" {}\n", path); mod_script_paths.push_back(std::filesystem::weakly_canonical(std::filesystem::path{ path })); } Core::getInstance().setModScriptPaths(mod_script_paths); @@ -725,8 +727,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s CommandDepthCounter counter; if (!counter.ok()) { - con.printerr("Cannot invoke \"%s\": maximum command depth exceeded (%i)\n", - first.c_str(), CommandDepthCounter::MAX_DEPTH); + con.printerr("Cannot invoke \"{}\": maximum command depth exceeded ({})\n", + first, CommandDepthCounter::MAX_DEPTH); return CR_FAILURE; } @@ -735,7 +737,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (has_backslashes(first)) { - con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", first.c_str()); + con.printerr("Replacing backslashes with forward slashes in \"{}\"\n", first); replace_backslashes_with_forwardslashes(first); } @@ -769,7 +771,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s "See more commands by running 'ls'.\n\n" ); - con.print("DFHack version %s\n", dfhack_version_desc().c_str()); + con.print("DFHack version {}\n", dfhack_version_desc()); } else { @@ -821,11 +823,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } } if (ret != CR_OK) - con.printerr("%s failed\n", first.c_str()); + con.printerr("{} failed\n", first); return ret; } else { - con.printerr("%s: no arguments\n", first.c_str()); + con.printerr("{}: no arguments\n", first); return CR_FAILURE; } } @@ -840,7 +842,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if (has_backslashes(part)) { - con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); + con.printerr("Replacing backslashes with forward slashes in \"{}\"\n", part); replace_backslashes_with_forwardslashes(part); } @@ -858,20 +860,20 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else { res = CR_NOT_FOUND; - con.printerr("No such plugin or Lua script: %s\n", part.c_str()); + con.printerr("No such plugin or Lua script: {}\n", part); } } else if (!plug->can_set_enabled()) { res = CR_NOT_IMPLEMENTED; - con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str()); + con.printerr("Cannot {} plugin: {}\n", first, part); } else { res = plug->set_enabled(con, enable); if (res != CR_OK || plug->is_enabled() != enable) - con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str()); + con.printerr("Could not {} plugin: {}\n", first, part); } } @@ -886,8 +888,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (!plug->can_be_enabled()) continue; con.print( - "%21s %-3s%s\n", - (key+":").c_str(), + "{:21} {:<3}{}\n", + (key+":"), plug->is_enabled() ? "on" : "off", plug->can_set_enabled() ? "" : " (controlled internally)" ); @@ -995,7 +997,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { if (!AddKeyBinding(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); + con.printerr("Invalid key spec: {}\n", keystr); break; } } @@ -1006,7 +1008,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s for (const auto& part : parts | std::views::drop(1)) { if (!ClearKeyBindings(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); + con.printerr("Invalid key spec: {}\n", part); break; } } @@ -1042,7 +1044,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s std::vector cmd(parts.begin() + 2, parts.end()); if (!AddAlias(name, cmd, parts[0] == "replace")) { - con.printerr("Could not add alias %s - already exists\n", name.c_str()); + con.printerr("Could not add alias {} - already exists\n", name); return CR_FAILURE; } } @@ -1050,7 +1052,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if (!RemoveAlias(parts[1])) { - con.printerr("Could not remove alias %s\n", parts[1].c_str()); + con.printerr("Could not remove alias {}\n", parts[1]); return CR_FAILURE; } } @@ -1174,10 +1176,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) { - con.print("%s (%s): %s%s\n", sc_event_name(state_script.event).c_str(), + con.print("{} ({}): {}{}\n", sc_event_name(state_script.event), state_script.save_specific ? "save-specific" : "global", state_script.save_specific ? "/raw/" : "/", - state_script.path.c_str()); + state_script.path); } } return CR_OK; @@ -1290,26 +1292,26 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else if (!no_autocomplete && try_autocomplete(con, first, completed)) res = CR_NOT_IMPLEMENTED; else - con.printerr("%s is not a recognized command.\n", first.c_str()); + con.printerr("{} is not a recognized command.\n", first); if (res == CR_NOT_IMPLEMENTED) { Plugin *p = plug_mgr->getPluginByName(first); if (p) { - con.printerr("%s is a plugin ", first.c_str()); + con.printerr("{} is a plugin ", first); if (p->getState() == Plugin::PS_UNLOADED) - con.printerr("that is not loaded - try \"load %s\" or check stderr.log\n", - first.c_str()); + con.printerr("that is not loaded - try \"load {}\" or check stderr.log\n", + first); else if (p->size()) - con.printerr("that implements %zi commands - see \"help %s\" for details\n", - p->size(), first.c_str()); + con.printerr("that implements {} commands - see \"help {}\" for details\n", + p->size(), first); else con.printerr("but does not implement any commands\n"); } } } else if (res == CR_NEEDS_CONSOLE) - con.printerr("%s needs an interactive console to work.\n" + con.printerr("{} needs an interactive console to work.\n" "Please run this command from the DFHack console.\n\n" #ifdef WIN32 "You can show the console with the 'show' command." @@ -1317,7 +1319,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s "The console is accessible when you run DF from the commandline\n" "via the './dfhack' script." #endif - "\n", first.c_str()); + "\n", first); return res; } @@ -1334,7 +1336,7 @@ bool Core::loadScriptFile(color_ostream &out, std::filesystem::path fname, bool if ( !script ) { if(!silent) - out.printerr("Error loading script: %s\n", fname.string().c_str()); + out.printerr("Error loading script: {}\n", fname); return false; } std::string command; @@ -1418,9 +1420,9 @@ static void fIOthread(IODATA * iod) run_dfhack_init(con, core); con.print("DFHack is ready. Have a nice day!\n" - "DFHack version %s\n" + "DFHack version {}\n" "Type in '?' or 'help' for general help, 'ls' to see all commands.\n", - dfhack_version_desc().c_str()); + dfhack_version_desc()); int clueless_counter = 0; @@ -1507,11 +1509,11 @@ void Core::fatal (std::string output, const char * title) out << "DFHack will now deactivate.\n"; if(con.isInited()) { - con.printerr("%s", out.str().c_str()); + con.printerr("{}", out.str()); con.reset_color(); con.print("\n"); } - fprintf(stderr, "%s\n", out.str().c_str()); + fmt::print(stderr, "{}\n", out.str()); out << "Check file stderr.log for details.\n"; std::cout << "DFHack fatal error: " << out.str() << std::endl; if (!title) @@ -1771,15 +1773,15 @@ bool Core::InitSimulationThread() // create config directory if it doesn't already exist if (!Filesystem::mkdir_recursive(getConfigPath())) - con.printerr("Failed to create config directory: '%s'\n", getConfigPath().c_str()); + con.printerr("Failed to create config directory: '{}'\n", getConfigPath()); // copy over default config files if necessary std::map config_files; std::map default_config_files; if (Filesystem::listdir_recursive(getConfigPath(), config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", getConfigPath().c_str()); + con.printerr("Failed to list directory: '{}'\n", getConfigPath()); else if (Filesystem::listdir_recursive(getConfigDefaultsPath(), default_config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", getConfigDefaultsPath().c_str()); + con.printerr("Failed to list directory: '{}'\n", getConfigDefaultsPath()); else { // ensure all config file directories exist before we start copying files @@ -1789,7 +1791,7 @@ bool Core::InitSimulationThread() continue; std::filesystem::path dirname = getConfigPath() / entry.first; if (!Filesystem::mkdir_recursive(dirname)) - con.printerr("Failed to create config directory: '%s'\n", dirname.c_str()); + con.printerr("Failed to create config directory: '{}'\n", dirname); } // copy files from the default tree that don't already exist in the config tree @@ -1806,7 +1808,7 @@ bool Core::InitSimulationThread() std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); if (!src.good() || !dest.good()) { - con.printerr("Copy failed: '%s'\n", filename.c_str()); + con.printerr("Copy failed: '{}'\n", filename); continue; } dest << src.rdbuf(); @@ -1959,26 +1961,6 @@ std::string Core::getHotkeyCmd( bool &keep_going ) return returner; } -void Core::print(const char *format, ...) -{ - color_ostream_proxy proxy(getInstance().con); - - va_list args; - va_start(args,format); - proxy.vprint(format,args); - va_end(args); -} - -void Core::printerr(const char *format, ...) -{ - color_ostream_proxy proxy(getInstance().con); - - va_list args; - va_start(args,format); - proxy.vprinterr(format,args); - va_end(args); -} - Core& Core::getInstance() { static Core instance; return instance; @@ -2323,7 +2305,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) if (evtlog.fail()) { if (DFHack::Filesystem::isdir(save_dir)) - out.printerr("Could not append to %s\n", evtlogpath.c_str()); + out.printerr("Could not append to {}\n", evtlogpath); } else { @@ -2484,8 +2466,8 @@ bool Core::getSuppressDuplicateKeyboardEvents() const { } void Core::setSuppressDuplicateKeyboardEvents(bool suppress) { - DEBUG(keybinding).print("setting suppress_duplicate_keyboard_events to %s\n", - suppress ? "true" : "false"); + DEBUG(keybinding).print("setting suppress_duplicate_keyboard_events to {}\n", + suppress); suppress_duplicate_keyboard_events = suppress; } @@ -2539,26 +2521,26 @@ bool Core::doSdlInputEvent(SDL_Event* ev) else if (ke.state == SDL_PRESSED && !hotkey_states[sym]) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress - DEBUG(keybinding).print("key down: sym=%d (%c)\n", sym, sym); + DEBUG(keybinding).print("key down: sym={}, char={}\n", sym, static_cast(sym)); if (SelectHotkey(sym, modstate)) { hotkey_states[sym] = true; if (modstate & (DFH_MOD_CTRL | DFH_MOD_ALT)) { DEBUG(keybinding).print("modifier key detected; not inhibiting SDL key down event\n"); return false; } - DEBUG(keybinding).print("%sinhibiting SDL key down event\n", + DEBUG(keybinding).print("{}inhibiting SDL key down event\n", suppress_duplicate_keyboard_events ? "" : "not "); return suppress_duplicate_keyboard_events; } } else if (ke.state == SDL_RELEASED) { - DEBUG(keybinding).print("key up: sym=%d (%c)\n", sym, sym); + DEBUG(keybinding).print("key up: sym={}, char={}\n", sym, static_cast(sym)); hotkey_states[sym] = false; } } else if (ev->type == SDL_MOUSEBUTTONDOWN) { auto &but = ev->button; - DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); + DEBUG(keybinding).print("mouse button down: button={}\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { SDL_Keycode sym = SDLK_F13 + but.button - 4; @@ -2567,13 +2549,13 @@ bool Core::doSdlInputEvent(SDL_Event* ev) } } else if (ev->type == SDL_TEXTINPUT) { auto &te = ev->text; - DEBUG(keybinding).print("text input: '%s' (modifiers: %s%s%s)\n", + DEBUG(keybinding).print("text input: '{}' (modifiers: {}{}{})\n", te.text, modstate & DFH_MOD_SHIFT ? "Shift" : "", modstate & DFH_MOD_CTRL ? "Ctrl" : "", modstate & DFH_MOD_ALT ? "Alt" : ""); if (strlen(te.text) == 1 && hotkey_states[te.text[0]]) { - DEBUG(keybinding).print("%sinhibiting SDL text event\n", + DEBUG(keybinding).print("{}inhibiting SDL text event\n", suppress_duplicate_keyboard_events ? "" : "not "); return suppress_duplicate_keyboard_events; } @@ -2597,7 +2579,7 @@ bool Core::SelectHotkey(int sym, int modifiers) std::string cmd; - DEBUG(keybinding).print("checking hotkeys for sym=%d (%c), modifiers=%x\n", sym, sym, modifiers); + DEBUG(keybinding).print("checking hotkeys for sym={}, char={}, modifiers={:x}\n", sym, static_cast(sym), modifiers); { std::lock_guard lock(HotkeyMutex); @@ -2606,29 +2588,29 @@ bool Core::SelectHotkey(int sym, int modifiers) std::vector &bindings = key_bindings[sym]; //for (int i = bindings.size()-1; i >= 0; --i) { for (const auto& binding : bindings | std::views::reverse) { - DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); + DEBUG(keybinding).print("examining hotkey with commandline: '{}'\n", binding.cmdline); if (binding.modifiers != modifiers) { - DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x%x != 0x%x\n", + DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x{:x} != 0x{:x}\n", binding.modifiers, modifiers); continue; } if (!binding.focus.empty()) { if (!Gui::matchFocusString(binding.focus)) { std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' != '%s'\n", - join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); + DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '{}' != '{}'\n", + join_strings(", ", focusStrings), binding.focus); continue; } } if (!plug_mgr->CanInvokeHotkey(binding.command[0], screen)) { - DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '%s')\n", - binding.command[0].c_str()); + DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '{}')\n", + binding.command[0]); continue; } if (mortal_mode && armok_tools.contains(binding.command[0])) { - DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '%s')\n", - binding.command[0].c_str()); + DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '{}')\n", + binding.command[0]); continue; } diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 5c187290f7..7a9ae4ae35 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -262,7 +262,7 @@ const std::string bit_container_identity::getFullName(const type_identity *) con const std::string df::buffer_container_identity::getFullName(const type_identity *item) const { return (item ? item->getFullName() : std::string("void")) + - (size > 0 ? stl_sprintf("[%d]", size) : std::string("[]")); + (size > 0 ? std::format("[{}]", size) : std::string("[]")); } union_identity::union_identity(size_t size, const TAllocateFn alloc, @@ -482,7 +482,7 @@ void DFHack::bitfieldToString(std::vector *pvec, const void *p, std::string name = format_key(items[i].name, i); if (items[i].size > 1) - name += stl_sprintf("=%u", value); + name += fmt::format("={}", value); pvec->push_back(name); } diff --git a/library/Error.cpp b/library/Error.cpp index 3cd0eb31d6..fa32daf90e 100644 --- a/library/Error.cpp +++ b/library/Error.cpp @@ -1,6 +1,9 @@ #include "Error.h" #include "MiscUtils.h" +#include +#include + using namespace DFHack::Error; inline std::string safe_str(const char *s) @@ -24,7 +27,7 @@ VTableMissing::VTableMissing(const char *name) {} SymbolsXmlParse::SymbolsXmlParse(const char* desc, int id, int row, int col) - :AllSymbols(stl_sprintf("error %d: %s, at row %d col %d", id, desc, row, col)), + :AllSymbols(fmt::format("error {}: {}, at row {} col {}", id, desc, row, col)), desc(safe_str(desc)), id(id), row(row), col(col) {} diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4edf60000a..36e47f7c1b 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3524,7 +3524,7 @@ static void setPreferredNumberFormat(color_ostream & out, int32_t type_int) { set_preferred_number_format_type(type); break; default: - WARN(luaapi, out).print("invalid number format enum value: %d\n", type_int); + WARN(luaapi, out).print("invalid number format enum value: {}\n", type_int); } } @@ -3608,7 +3608,7 @@ static int internal_setAddress(lua_State *L) // Print via printerr, so that it is definitely logged to stderr.log. uintptr_t iaddr = addr - Core::getInstance().vinfo->getRebaseDelta(); - fprintf(stderr, "Setting global '%s' to %p (%p)\n", name.c_str(), (void*)addr, (void*)iaddr); + fmt::print(stderr, "Setting global '{}' to {} ({})\n", name, (addr), (void*)iaddr); fflush(stderr); return 1; diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 81bcdd50ca..e6083b0a4e 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -244,12 +244,12 @@ static void check_valid_ptr_index(lua_State *state, int val_index) } } -static void signal_typeid_error(color_ostream *out, lua_State *state, - const type_identity *type, const char *msg, +static void signal_typeid_error(color_ostream* out, lua_State* state, + const type_identity* type, std::string_view msg, int val_index, bool perr, bool signal) { std::string typestr = type ? type->getFullName() : "any pointer"; - std::string error = stl_sprintf(msg, typestr.c_str()); + std::string error = fmt::format(fmt::runtime(msg), typestr); if (signal) { @@ -261,7 +261,7 @@ static void signal_typeid_error(color_ostream *out, lua_State *state, else if (perr) { if (out) - out->printerr("%s", error.c_str()); + out->printerr("{}", error); else dfhack_printerr(state, error); } @@ -282,7 +282,7 @@ void *DFHack::Lua::CheckDFObject(lua_State *state, const type_identity *type, in void *rv = get_object_internal(state, type, val_index, exact_type, false); if (!rv) - signal_typeid_error(NULL, state, type, "invalid pointer type; expected: %s", + signal_typeid_error(NULL, state, type, "invalid pointer type; expected: {}", val_index, false, true); return rv; @@ -366,9 +366,9 @@ static int lua_dfhack_print(lua_State *S) { std::string str = lua_print_fmt(S); if (color_ostream *out = Lua::GetOutput(S)) - out->print("%s", str.c_str());//*out << str; + out->print("{}", str); else - Core::print("%s", str.c_str()); + Core::print("{}", str); return 0; } @@ -378,16 +378,16 @@ static int lua_dfhack_println(lua_State *S) if (color_ostream *out = Lua::GetOutput(S)) *out << str << std::endl; else - Core::print("%s\n", str.c_str()); + Core::print("{}\n", str); return 0; } void dfhack_printerr(lua_State *S, const std::string &str) { if (color_ostream *out = Lua::GetOutput(S)) - out->printerr("%s\n", str.c_str()); + out->printerr("{}\n", str); else - Core::printerr("%s\n", str.c_str()); + Core::printerr("{}\n", str); } static int lua_dfhack_printerr(lua_State *S) @@ -563,7 +563,7 @@ static void report_error(lua_State *L, color_ostream *out = NULL, bool pop = fal assert(msg); if (out) - out->printerr("%s\n", msg); + out->printerr("{}\n", msg); else dfhack_printerr(L, msg); @@ -842,7 +842,7 @@ bool DFHack::Lua::CallLuaModuleFunction(color_ostream &out, lua_State *L, if (!lua_checkstack(L, 1 + nargs) || !Lua::PushModulePublic(out, L, module_name, fn_name)) { if (perr) - out.printerr("Failed to load %s Lua code\n", module_name); + out.printerr("Failed to load {} Lua code\n", module_name); return false; } @@ -850,7 +850,7 @@ bool DFHack::Lua::CallLuaModuleFunction(color_ostream &out, lua_State *L, if (!Lua::SafeCall(out, L, nargs, nres, perr)) { if (perr) - out.printerr("Failed Lua call to '%s.%s'\n", module_name, fn_name); + out.printerr("Failed Lua call to '{}.{}'\n", module_name, fn_name); return false; } @@ -1100,7 +1100,7 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, } else if (!lua_isuserdata(state, val_index)) { - signal_typeid_error(out, state, type, "pointer to %s expected", + signal_typeid_error(out, state, type, "pointer to {} expected", val_index, perr, signal); return false; } @@ -1109,13 +1109,13 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, void *in_ptr = Lua::GetDFObject(state, type, val_index, exact); if (!in_ptr) { - signal_typeid_error(out, state, type, "incompatible pointer type: %s expected", + signal_typeid_error(out, state, type, "incompatible pointer type: {} expected", val_index, perr, signal); return false; } if (!type->copy(target, in_ptr)) { - signal_typeid_error(out, state, type, "no copy support for %s", + signal_typeid_error(out, state, type, "no copy support for {}", val_index, perr, signal); return false; } @@ -2134,7 +2134,7 @@ void DFHack::Lua::Core::Reset(color_ostream &out, const char *where) if (top != 0) { - out.printerr("Common lua context stack top left at %d after %s.\n", top, where); + out.printerr("Common lua context stack top left at {} after {}.\n", top, where); lua_settop(State, 0); } diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index cdacd5ebfe..0f990a6b84 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -1253,7 +1253,7 @@ int LuaWrapper::method_wrapper_core(lua_State *state, function_identity_base *id field_error(state, UPVAL_METHOD_NAME, e.what(), "invoke"); } catch (std::exception &e) { - std::string tmp = stl_sprintf("C++ exception: %s", e.what()); + std::string tmp = fmt::format("C++ exception: {}", e.what()); field_error(state, UPVAL_METHOD_NAME, tmp.c_str(), "invoke"); } diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 823d542b79..9f0ecab738 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -1009,7 +1009,8 @@ static int meta_type_tostring(lua_State *state) lua_getfield(state, -1, "__metatable"); const char *cname = lua_tostring(state, -1); - lua_pushstring(state, stl_sprintf("", cname).c_str()); + auto str = fmt::format("", cname); + lua_pushlstring(state, str.data(), str.size()); return 1; } @@ -1033,10 +1034,12 @@ static int meta_ptr_tostring(lua_State *state) lua_getfield(state, UPVAL_METATABLE, "__metatable"); const char *cname = lua_tostring(state, -1); - if (has_length) - lua_pushstring(state, stl_sprintf("<%s[%" PRIu64 "]: %p>", cname, length, (void*)ptr).c_str()); - else - lua_pushstring(state, stl_sprintf("<%s: %p>", cname, (void*)ptr).c_str()); + auto str = has_length ? + fmt::format("<{}[{}]: {}>", cname, length, static_cast(ptr)) : + fmt::format("<{}: {}>", cname, static_cast(ptr)); + + lua_pushlstring(state, str.data(), str.size()); + return 1; } diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index ec72e36fb9..336e7f50e7 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -77,7 +77,7 @@ namespace DFHack DFLibrary* ret = (DFLibrary*)load_library(filename); if (!ret) { auto error = get_error(); - WARN(plugins).print("OpenPlugin on %s failed: %s\n", filename.string().c_str(), error.c_str()); + WARN(plugins).print("OpenPlugin on {} failed: {}\n", filename.string(), error); } return ret; } @@ -91,7 +91,7 @@ namespace DFHack if (res != 0) { auto error = get_error(); - WARN(plugins).print("ClosePlugin failed: %s\n", error.c_str()); + WARN(plugins).print("ClosePlugin failed: {}\n", error); } return (res == 0); diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 575ebdc7f5..b7e0b2c3c4 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -238,7 +238,7 @@ bool Plugin::load(color_ostream &con) else if(state != PS_UNLOADED && state != PS_DELETED) { if (state == PS_BROKEN) - con.printerr("Plugin %s is broken - cannot be loaded\n", name.c_str()); + con.printerr("Plugin {} is broken - cannot be loaded\n", name); return false; } state = PS_LOADING; @@ -246,19 +246,19 @@ bool Plugin::load(color_ostream &con) // enter suspend CoreSuspender suspend; // open the library, etc - fprintf(stderr, "loading plugin %s\n", name.c_str()); + fmt::print(stderr, "loading plugin {}\n", name); DFLibrary * plug = OpenPlugin(path); if(!plug) { RefAutolock lock(access); if (!Filesystem::isfile(path)) { - con.printerr("Plugin %s does not exist on disk\n", name.c_str()); + con.printerr("Plugin {} does not exist on disk\n", name); state = PS_DELETED; return false; } else { - con.printerr("Can't load plugin %s\n", name.c_str()); + con.printerr("Can't load plugin {}\n", name); state = PS_UNLOADED; return false; } @@ -267,7 +267,7 @@ bool Plugin::load(color_ostream &con) #define plugin_check_symbol(sym) \ if (!LookupPlugin(plug, sym)) \ { \ - con.printerr("Plugin %s: missing symbol: %s\n", name.c_str(), sym); \ + con.printerr("Plugin {}: missing symbol: {}\n", name, sym); \ plugin_abort_load; \ return false; \ } @@ -281,7 +281,7 @@ bool Plugin::load(color_ostream &con) const char ** plug_name =(const char ** ) LookupPlugin(plug, "plugin_name"); if (name != *plug_name) { - con.printerr("Plugin %s: name mismatch, claims to be %s\n", name.c_str(), *plug_name); + con.printerr("Plugin {}: name mismatch, claims to be {}\n", name, *plug_name); plugin_abort_load; return false; } @@ -294,15 +294,15 @@ bool Plugin::load(color_ostream &con) const char *plug_git_desc = plug_git_desc_ptr ? *plug_git_desc_ptr : "unknown"; if (*plugin_abi_version != Version::dfhack_abi_version()) { - con.printerr("Plugin %s: ABI version mismatch (Plugin: %i, DFHack: %i)\n", + con.printerr("Plugin {}: ABI version mismatch (Plugin: {}, DFHack: {})\n", *plug_name, *plugin_abi_version, Version::dfhack_abi_version()); plugin_abort_load; return false; } if (strcmp(dfhack_version, *plug_version) != 0) { - con.printerr("Plugin %s was not built for this version of DFHack.\n" - "Plugin: %s, DFHack: %s\n", *plug_name, *plug_version, dfhack_version); + con.printerr("Plugin {} was not built for this version of DFHack.\n" + "Plugin: {}, DFHack: {}\n", *plug_name, *plug_version, dfhack_version); plugin_abort_load; return false; } @@ -310,18 +310,18 @@ bool Plugin::load(color_ostream &con) { if (strcmp(dfhack_git_desc, plug_git_desc) != 0) { - std::string msg = stl_sprintf("Warning: Plugin %s compiled for DFHack %s, running DFHack %s\n", + std::string msg = fmt::format("Warning: Plugin {} compiled for DFHack {}, running DFHack {}\n", *plug_name, plug_git_desc, dfhack_git_desc); con << msg << std::flush; std::cerr << msg << std::flush; } } else - con.printerr("Warning: Plugin %s missing git information\n", *plug_name); + con.printerr("Warning: Plugin {} missing git information\n", *plug_name); bool *plug_dev = (bool*)LookupPlugin(plug, "plugin_dev"); if (plug_dev && *plug_dev && getenv("DFHACK_NO_DEV_PLUGINS")) { - con.print("Skipping dev plugin: %s\n", *plug_name); + con.print("Skipping dev plugin: {}\n", *plug_name); plugin_abort_load; return false; } @@ -338,7 +338,7 @@ bool Plugin::load(color_ostream &con) } if (missing_globals.size()) { - con.printerr("Plugin %s is missing required globals: %s\n", + con.printerr("Plugin {} is missing required globals: {}\n", *plug_name, join_strings(", ", missing_globals).c_str()); plugin_abort_load; return false; @@ -364,18 +364,18 @@ bool Plugin::load(color_ostream &con) state = PS_LOADED; parent->registerCommands(this); if ((plugin_onupdate || plugin_enable) && !plugin_is_enabled) - con.printerr("Plugin %s has no enabled var!\n", name.c_str()); + con.printerr("Plugin {} has no enabled var!\n", name); if (Core::getInstance().isWorldLoaded() && plugin_load_world_data && plugin_load_world_data(con) != CR_OK) - con.printerr("Plugin %s has failed to load saved world data.\n", name.c_str()); + con.printerr("Plugin {} has failed to load saved world data.\n", name); if (Core::getInstance().isMapLoaded() && plugin_load_site_data && World::IsSiteLoaded() && plugin_load_site_data(con) != CR_OK) - con.printerr("Plugin %s has failed to load saved site data.\n", name.c_str()); - fprintf(stderr, "loaded plugin %s; DFHack build %s\n", name.c_str(), plug_git_desc); - fflush(stderr); + con.printerr("Plugin {} has failed to load saved site data.\n", name); + + fmt::print(stderr, "loaded plugin {}; DFHack build {}\n", name, plug_git_desc); return true; } else { - con.printerr("Plugin %s has failed to initialize properly.\n", name.c_str()); + con.printerr("Plugin {} has failed to initialize properly.\n", name); plugin_is_enabled = 0; plugin_onupdate = 0; reset_lua(); @@ -393,7 +393,7 @@ bool Plugin::unload(color_ostream &con) { if (Screen::hasActiveScreens(this)) { - con.printerr("Cannot unload plugin %s: has active viewscreens\n", name.c_str()); + con.printerr("Cannot unload plugin {}: has active viewscreens\n", name); access->unlock(); return false; } @@ -402,7 +402,7 @@ bool Plugin::unload(color_ostream &con) if (plugin_onstatechange && plugin_onstatechange(con, SC_BEGIN_UNLOAD) != CR_OK) { - con.printerr("Plugin %s has refused to be unloaded.\n", name.c_str()); + con.printerr("Plugin {} has refused to be unloaded.\n", name); access->unlock(); return false; } @@ -418,9 +418,9 @@ bool Plugin::unload(color_ostream &con) CoreSuspender suspend; access->lock(); if (Core::getInstance().isMapLoaded() && plugin_save_site_data && World::IsSiteLoaded() && plugin_save_site_data(con) != CR_OK) - con.printerr("Plugin %s has failed to save site data.\n", name.c_str()); + con.printerr("Plugin {} has failed to save site data.\n", name); if (Core::getInstance().isWorldLoaded() && plugin_save_world_data && plugin_save_world_data(con) != CR_OK) - con.printerr("Plugin %s has failed to save world data.\n", name.c_str()); + con.printerr("Plugin {} has failed to save world data.\n", name); // holding the access lock while releasing the CoreSuspender creates a deadlock risk access->unlock(); } @@ -448,7 +448,7 @@ bool Plugin::unload(color_ostream &con) } else { - con.printerr("Plugin %s has failed to shutdown!\n",name.c_str()); + con.printerr("Plugin {} has failed to shutdown!\n",name); state = PS_BROKEN; } access->unlock(); @@ -460,7 +460,7 @@ bool Plugin::unload(color_ostream &con) return true; } else if (state == PS_BROKEN) - con.printerr("Plugin %s is broken - cannot be unloaded\n", name.c_str()); + con.printerr("Plugin {} is broken - cannot be unloaded\n", name); access->unlock(); return false; } @@ -490,7 +490,7 @@ command_result Plugin::invoke(color_ostream &out, const std::string & command, s else if (cmdIt->guard) { CoreSuspender suspend; if (!cmdIt->guard(Core::getInstance().getTopViewscreen())) { - out.printerr("Could not invoke %s: unsuitable UI state.\n", command.c_str()); + out.printerr("Could not invoke {}: unsuitable UI state.\n", command); cr = CR_WRONG_USAGE; } else { @@ -894,13 +894,13 @@ bool PluginManager::addPlugin(string name) { if (all_plugins.find(name) != all_plugins.end()) { - Core::printerr("Plugin already exists: %s\n", name.c_str()); + Core::printerr("Plugin already exists: {}\n", name); return false; } std::filesystem::path path = getPluginPath(name); if (!Filesystem::isfile(path)) { - Core::printerr("Plugin does not exist: %s\n", name.c_str()); + Core::printerr("Plugin does not exist: {}\n", name); return false; } Plugin * p = new Plugin(core, path, name, this); @@ -944,7 +944,7 @@ bool PluginManager::load (const string &name) Plugin *p = (*this)[name]; if (!p) { - Core::printerr("Plugin failed to register: %s\n", name.c_str()); + Core::printerr("Plugin failed to register: {}\n", name); return false; } return p->load(core->getConsole()); @@ -969,7 +969,7 @@ bool PluginManager::unload (const string &name) std::lock_guard lock{*plugin_mutex}; if (!(*this)[name]) { - Core::printerr("Plugin does not exist: %s\n", name.c_str()); + Core::printerr("Plugin does not exist: {}\n", name); return false; } return (*this)[name]->unload(core->getConsole()); @@ -1069,8 +1069,8 @@ void PluginManager::registerCommands( Plugin * p ) std::string name = cmds[i].name; if (command_map.find(name) != command_map.end()) { - core->printerr("Plugin %s re-implements command \"%s\" (from plugin %s)\n", - p->getName().c_str(), name.c_str(), command_map[name]->getName().c_str()); + core->printerr("Plugin {} re-implements command \"{}\" (from plugin {})\n", + p->getName(), name, command_map[name]->getName()); continue; } command_map[name] = p; @@ -1096,12 +1096,12 @@ void PluginManager::doSaveData(color_ostream &out) if (World::IsSiteLoaded()) { cr = it->second->save_site_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to save site data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to save site data.\n", it->first); } cr = it->second->save_world_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to save world data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to save world data.\n", it->first); } } @@ -1112,7 +1112,7 @@ void PluginManager::doLoadWorldData(color_ostream &out) command_result cr = it->second->load_world_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to load saved world data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to load saved world data.\n", it->first); } } @@ -1123,7 +1123,7 @@ void PluginManager::doLoadSiteData(color_ostream &out) command_result cr = it->second->load_site_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to load saved site data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to load saved site data.\n", it->first); } } diff --git a/library/Process.cpp b/library/Process.cpp index 101eecbc71..47d110ba92 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -194,7 +194,7 @@ Process::Process(const VersionInfoFactory& known_versions) : identified(false) cerr << "1KB hexdump follows:" << endl; for(int i = 0; i < 64; i++) { - fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + fmt::print(std::cerr, "{:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}\n", first_kb[i*16], first_kb[i*16+1], first_kb[i*16+2], diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp index 20990428dc..b159843761 100644 --- a/library/RemoteClient.cpp +++ b/library/RemoteClient.cpp @@ -180,7 +180,7 @@ bool RemoteClient::connect(int port) if (!socket->Open("localhost", port)) { - default_output().printerr("Could not connect to localhost:%d\n", port); + default_output().printerr("Could not connect to localhost:{}\n", port); return false; } @@ -335,8 +335,8 @@ bool RemoteFunctionBase::bind(color_ostream &out, RemoteClient *client, if (p_client == client && this->name == name && this->plugin == plugin) return true; - out.printerr("Function already bound to %s::%s\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("Function already bound to {}::{}\n", + this->plugin, this->name); return false; } @@ -372,15 +372,15 @@ command_result RemoteFunctionBase::execute(color_ostream &out, { if (!isValid()) { - out.printerr("Calling an unbound RPC function %s::%s.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("Calling an unbound RPC function {}:{}.\n", + this->plugin, this->name); return CR_NOT_IMPLEMENTED; } if (!p_client->socket->IsSocketValid()) { - out.printerr("In call to %s::%s: invalid socket.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: invalid socket.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -388,15 +388,15 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (send_size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In call to %s::%s: message too large: %d.\n", - this->plugin.c_str(), this->name.c_str(), send_size); + out.printerr("In call to {}:{}: message too large: {}.\n", + this->plugin, this->name, send_size); return CR_LINK_FAILURE; } if (!sendRemoteMessage(p_client->socket, id, input, true)) { - out.printerr("In call to %s::%s: I/O error in send.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: I/O error in send.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -410,8 +410,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (!readFullBuffer(p_client->socket, &header, sizeof(header))) { - out.printerr("In call to %s::%s: I/O error in receive header.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: I/O error in receive header.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -422,8 +422,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In call to %s::%s: invalid received size %d.\n", - this->plugin.c_str(), this->name.c_str(), header.size); + out.printerr("In call to {}:{}: invalid received size {}.\n", + this->plugin, this->name, header.size); return CR_LINK_FAILURE; } @@ -431,8 +431,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (!readFullBuffer(p_client->socket, buf, header.size)) { - out.printerr("In call to %s::%s: I/O error in receive %d bytes of data.\n", - this->plugin.c_str(), this->name.c_str(), header.size); + out.printerr("In call to {}:{}: I/O error in receive {} bytes of data.\n", + this->plugin, this->name, header.size); return CR_LINK_FAILURE; } @@ -440,8 +440,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, case RPC_REPLY_RESULT: if (!output->ParseFromArray(buf, header.size)) { - out.printerr("In call to %s::%s: error parsing received result.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: error parsing received result.\n", + this->plugin, this->name); delete[] buf; return CR_LINK_FAILURE; } @@ -454,8 +454,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (text_data.ParseFromArray(buf, header.size)) text_decoder.decode(&text_data); else - out.printerr("In call to %s::%s: received invalid text data.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: received invalid text data.\n", + this->plugin, this->name); break; default: diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 4e26b06497..9557a15862 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -189,14 +189,14 @@ ServerFunctionBase *ServerConnection::findFunction(color_ostream &out, const std Plugin *plug = Core::getInstance().plug_mgr->getPluginByName(plugin); if (!plug) { - out.printerr("No such plugin: %s\n", plugin.c_str()); + out.printerr("No such plugin: {}\n", plugin); return NULL; } svc = plug->rpc_connect(out); if (!svc) { - out.printerr("Plugin %s doesn't export any RPC methods.\n", plugin.c_str()); + out.printerr("Plugin {} doesn't export any RPC methods.\n", plugin); return NULL; } @@ -299,7 +299,7 @@ void ServerConnection::threadFn() if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In RPC server: invalid received size %d.\n", header.size); + out.printerr("In RPC server: invalid received size {}.\n", header.size); break; } @@ -307,7 +307,7 @@ void ServerConnection::threadFn() if (!readFullBuffer(socket, buf.get(), header.size)) { - out.printerr("In RPC server: I/O error in receive %d bytes of data.\n", header.size); + out.printerr("In RPC server: I/O error in receive {} bytes of data.\n", header.size); break; } @@ -323,17 +323,17 @@ void ServerConnection::threadFn() if (!fn) { - stream.printerr("RPC call of invalid id %d\n", header.id); + stream.printerr("RPC call of invalid id {}\n", header.id); } else { if (((fn->flags & SF_ALLOW_REMOTE) != SF_ALLOW_REMOTE) && strcmp(socket->GetClientAddr(), "127.0.0.1") != 0) { - stream.printerr("In call to %s: forbidden host: %s\n", fn->name, socket->GetClientAddr()); + stream.printerr("In call to {}: forbidden host: {}\n", fn->name, socket->GetClientAddr()); } else if (!fn->in()->ParseFromArray(buf.get(), header.size)) { - stream.printerr("In call to %s: could not decode input args.\n", fn->name); + stream.printerr("In call to {}: could not decode input args.\n", fn->name); } else { @@ -364,7 +364,7 @@ void ServerConnection::threadFn() if (out_size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - stream.printerr("In call to %s: reply too large: %d.\n", + stream.printerr("In call to {}: reply too large: {}.\n", (fn ? fn->name : "UNKNOWN"), out_size); res = CR_LINK_FAILURE; } @@ -489,7 +489,7 @@ void ServerMainImpl::threadFn(std::promise promise, int port) break; case CSimpleSocket::SocketFirewallError: case CSimpleSocket::SocketProtocolError: - WARN(socket).print("Connection failed: %s\n", server.socket.DescribeError()); + WARN(socket).print("Connection failed: {}\n", server.socket.DescribeError()); break; default: break; diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index 56b45eba01..dbef7a9906 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -696,16 +696,16 @@ command_result CoreService::BindMethod(color_ostream &stream, if (!fn) { - stream.printerr("RPC method not found: %s::%s\n", - in->plugin().c_str(), in->method().c_str()); + stream.printerr("RPC method not found: {}::{}\n", + in->plugin(), in->method()); return CR_FAILURE; } if (fn->p_in_template->GetTypeName() != in->input_msg() || fn->p_out_template->GetTypeName() != in->output_msg()) { - stream.printerr("Requested wrong signature for RPC method: %s::%s\n", - in->plugin().c_str(), in->method().c_str()); + stream.printerr("Requested wrong signature for RPC method: {}::{}\n", + in->plugin(), in->method()); return CR_FAILURE; } diff --git a/library/TileTypes.cpp b/library/TileTypes.cpp index 7636d5bab2..9f8a72ade6 100644 --- a/library/TileTypes.cpp +++ b/library/TileTypes.cpp @@ -61,9 +61,9 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No shape %s in %s.\n", - enum_item_key(shape).c_str(), enum_item_key(mat).c_str() + fmt::print( + stderr, "NOTE: No shape {} in {}.\n", + enum_item_key(shape), enum_item_key(mat) ); } @@ -93,10 +93,10 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No special %s in %s:%s.\n", - enum_item_key(special).c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str() + fmt::print( + stderr, "NOTE: No special {} in {}:{}.\n", + enum_item_key(special), enum_item_key(mat), + enum_item_key(shape) ); } @@ -139,10 +139,9 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No direction '%s' in %s:%s:%s.\n", - dir.c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str(), enum_item_key(special).c_str() + fmt::print( + stderr, "NOTE: No direction '{}' in {}:{}:{}.\n", + dir, enum_item_key(mat), enum_item_key(shape), enum_item_key(special) ); } @@ -162,10 +161,10 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No variant '%s' in %s:%s:%s:%s.\n", - enum_item_key(variant).c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str(), enum_item_key(special).c_str(), dir.c_str() + fmt::print( + stderr, "NOTE: No variant '{}' in {}:{}:{}:{}.\n", + enum_item_key(variant), enum_item_key(mat), + enum_item_key(shape), enum_item_key(special), dir ); } @@ -214,8 +213,8 @@ static void init_tables() tile_to_mat[tiletype_material::STONE][tt] = ttm; if (ttm == tiletype::Void) - fprintf(stderr, "No match for tile %s in STONE.\n", - enum_item_key(tt).c_str()); + fmt::print(stderr, "No match for tile {} in STONE.\n", + enum_item_key(tt)); } else { @@ -233,8 +232,8 @@ static void init_tables() tile_to_mat[mat][tt] = ttm; if (ttm == tiletype::Void) - fprintf(stderr, "No match for tile %s in %s.\n", - enum_item_key(tt).c_str(), enum_item_key(mat).c_str()); + fmt::print(stderr, "No match for tile {} in {}.\n", + enum_item_key(tt), enum_item_key(mat)); } } } diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 3c2a4a50fc..1d3635a74a 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -311,7 +311,7 @@ VMethodInterposeLinkBase::VMethodInterposeLinkBase(const virtual_identity *host, * - interpose_method comes from method_pointer_to_addr_ */ - fprintf(stderr, "Bad VMethodInterposeLinkBase arguments: %d %p (%s)\n", + fmt::print(stderr, "Bad VMethodInterposeLinkBase arguments: {} {} ({})\n", vmethod_idx, interpose_method, name_str); fflush(stderr); abort(); diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index 776362855a..6b2c1e62f6 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -209,7 +209,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "md5-hash") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): MD5: %s\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); + fmt::print(stderr, "{} ({}): MD5: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addMD5(cstr_value); @@ -217,7 +217,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "binary-timestamp") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): PE: %s\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); + fmt::print(stderr, "{} ({}): PE: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addPE(strtol(cstr_value, 0, 16)); diff --git a/library/dfhack-run.cpp b/library/dfhack-run.cpp index df10bf1485..171032a39c 100644 --- a/library/dfhack-run.cpp +++ b/library/dfhack-run.cpp @@ -119,8 +119,8 @@ int main (int argc, char *argv[]) if (rv == CR_OK) { for (int i = 0; i < run_call.out()->value_size(); i++) - printf("%s%s", (i>0?"\t":""), run_call.out()->value(i).c_str()); - printf("\n"); + fmt::print("{}{}", (i>0?"\t":""), run_call.out()->value(i)); + fmt::print("\n"); } } else diff --git a/library/include/ColorText.h b/library/include/ColorText.h index 05e38363bd..3fce782ecf 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -34,6 +34,7 @@ distribution. #include #include +#include namespace dfproto { @@ -106,16 +107,16 @@ namespace DFHack color_ostream(); virtual ~color_ostream(); - template - void format(const char* format, Args&& ... args) + template + void print(fmt::format_string format, Args&& ... args) { auto str = fmt::format(format, std::forward(args)...); flush_buffer(false); add_text(cur_color, str); } - template - void format_err(const char* format, Args&& ... args) + template + void printerr(fmt::format_string format, Args&& ... args) { auto str = fmt::format(format, std::forward(args)...); if (log_errors_to_stderr) { @@ -125,14 +126,6 @@ namespace DFHack add_text(COLOR_LIGHTRED, str); } - /// Print a formatted string, like printf - void print(const char *format, ...) Wformat(printf,2,3); - void vprint(const char *format, va_list args) Wformat(printf,2,0); - - /// Print a formatted string, like printf, in red - void printerr(const char *format, ...) Wformat(printf,2,3); - void vprinterr(const char *format, va_list args) Wformat(printf,2,0); - /// Get color color_value color() { return cur_color; } /// Set color (ANSI color number) @@ -196,4 +189,6 @@ namespace DFHack void decode(dfproto::CoreTextNotification *data); }; + } + diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef1..e762703c5a 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -209,8 +209,19 @@ namespace DFHack std::unique_ptr p; std::shared_ptr vinfo; - static void print(const char *format, ...) Wformat(printf,1,2); - static void printerr(const char *format, ...) Wformat(printf,1,2); + template + static void print(fmt::format_string format, Args&& ... args) + { + color_ostream_proxy proxy(getInstance().con); + proxy.print(format, std::forward(args)...); + } + + template + static void printerr(fmt::format_string format, Args&& ... args) + { + color_ostream_proxy proxy(getInstance().con); + proxy.printerr(format, std::forward(args)...); + } PluginManager *getPluginManager() { return plug_mgr; } diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 9ce2cd4dac..182148d635 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -36,6 +36,8 @@ distribution. #include "BitArray.h" #include "Export.h" +#include + struct lua_State; /* @@ -981,7 +983,7 @@ namespace DFHack { #define ENUM_KEY_STR(enum,val) (DFHack::enum_item_key(val)) #define ENUM_FIRST_ITEM(enum) (df::enum_traits::first_item) #define ENUM_LAST_ITEM(enum) (df::enum_traits::last_item) - +#define ENUM_AS_STR(val) (DFHack::enum_item_key(val)) #define ENUM_NEXT_ITEM(enum,val) \ (DFHack::next_enum_item(val)) #define FOR_ENUM_ITEMS(enum,iter) \ @@ -1009,3 +1011,25 @@ namespace std { } }; } + +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const df::coord& c, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {}, {})", c.x, c.y, c.z), ctx); + } +}; + +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const df::coord2d& c, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {})", c.x, c.y), ctx); + } +}; diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index b5892bbb16..10074eef29 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -179,8 +179,8 @@ static bool findProcess(color_ostream& out, std::string name, pid_t &pid) { command += name; FILE *cmd_pipe = popen(command.c_str(), "r"); if (!cmd_pipe) { - WARN(dfsteam, out).print("failed to exec '%s' (error: %d)\n", - command.c_str(), errno); + WARN(dfsteam, out).print("failed to exec '{}' (error: {})\n", + command, errno); return false; } @@ -204,7 +204,7 @@ static bool launchDFHack(color_ostream& out) { pid = fork(); if (pid == -1) { - WARN(dfsteam, out).print("failed to fork (error: %d)\n", errno); + WARN(dfsteam, out).print("failed to fork (error: {})\n", errno); return false; } else if (pid == 0) { // child process @@ -248,5 +248,5 @@ void DFSteam::launchSteamDFHackIfNecessary(color_ostream& out) { } bool ret = launchDFHack(out); - DEBUG(dfsteam, out).print("launching DFHack via Steam: %s\n", ret ? "successful" : "unsuccessful"); + DEBUG(dfsteam, out).print("launching DFHack via Steam: {}\n", ret ? "successful" : "unsuccessful"); } diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index d504e74c3d..4a4d47e765 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -71,7 +71,10 @@ static int32_t eventLastTick[EventType::EVENT_MAX]; static const int32_t ticksPerYear = 403200; void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler) { - DEBUG(log).print("registering handler %p from plugin %s for event %d\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str(), e); + DEBUG(log).print("registering handler {} from plugin {} for event {}\n", + static_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : "", + static_cast(e)); handlers[e].insert(pair(handler.plugin, handler)); } @@ -87,7 +90,9 @@ int32_t DFHack::EventManager::registerTick(EventHandler handler, int32_t when, b } handler.freq = when; tickQueue.insert(pair(handler.freq, handler)); - DEBUG(log).print("registering handler %p from plugin %s for event TICK\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str()); + DEBUG(log).print("registering handler {} from plugin {} for event TICK\n", + static_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : ""); handlers[EventType::TICK].insert(pair(handler.plugin,handler)); return when; } @@ -113,7 +118,10 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl i++; continue; } - DEBUG(log).print("unregistering handler %p from plugin %s for event %d\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str(), e); + DEBUG(log).print("unregistering handler {} from plugin {} for event {}\n", + static_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : "", + static_cast(e)); i = handlers[e].erase(i); if ( e == EventType::TICK ) removeFromTickQueue(handler); @@ -121,7 +129,8 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl } void DFHack::EventManager::unregisterAll(Plugin* plugin) { - DEBUG(log).print("unregistering all handlers for plugin %s\n", !plugin ? "" : plugin->getName().c_str()); + DEBUG(log).print("unregistering all handlers for plugin {}\n", + plugin ? plugin->getName() : ""); for ( auto i = handlers[EventType::TICK].find(plugin); i != handlers[EventType::TICK].end(); i++ ) { if ( (*i).first != plugin ) break; @@ -408,7 +417,7 @@ void DFHack::EventManager::manageEvents(color_ostream& out) { CoreSuspender suspender; int32_t tick = df::global::world->frame_counter; - TRACE(log,out).print("processing events at tick %d\n", tick); + TRACE(log,out).print("processing events at tick {}\n", tick); auto &core = Core::getInstance(); auto &counters = core.perf_counters; @@ -599,13 +608,13 @@ static void manageJobCompletedEvent(color_ostream& out) { df::job& job1 = *(*j).second; out.print("new job\n" - " location : 0x%X\n" - " id : %d\n" - " type : %d %s\n" - " working : %d\n" - " completion_timer : %d\n" - " workerID : %d\n" - " time : %d -> %d\n" + " location : {:#X}\n" + " id : {}\n" + " type : {} {}\n" + " working : {}\n" + " completion_timer : {}\n" + " workerID : {}\n" + " time : {} -> {}\n" "\n", job1.list_link->item, job1.id, job1.job_type, ENUM_ATTR(job_type, caption, job1.job_type), job1.flags.bits.working, job1.completion_timer, getWorkerID(&job1), tick0, tick1); } for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { @@ -613,13 +622,13 @@ static void manageJobCompletedEvent(color_ostream& out) { auto j = nowJobs.find((*i).first); if ( j == nowJobs.end() ) { out.print("job deallocated\n" - " location : 0x%X\n" - " id : %d\n" - " type : %d %s\n" - " working : %d\n" - " completion_timer : %d\n" - " workerID : %d\n" - " time : %d -> %d\n" + " location : {:#X}\n" + " id : {}\n" + " type : {} {}\n" + " working : {}\n" + " completion_timer : {}\n" + " workerID : {}\n" + " time : {} -> {}\n" ,job0.list_link == NULL ? 0 : job0.list_link->item, job0.id, job0.job_type, ENUM_ATTR(job_type, caption, job0.job_type), job0.flags.bits.working, job0.completion_timer, getWorkerID(&job0), tick0, tick1); continue; } @@ -631,14 +640,14 @@ static void manageJobCompletedEvent(color_ostream& out) { continue; out.print("job change\n" - " location : 0x%X -> 0x%X\n" - " id : %d -> %d\n" - " type : %d -> %d\n" - " type : %s -> %s\n" - " working : %d -> %d\n" - " completion timer : %d -> %d\n" - " workerID : %d -> %d\n" - " time : %d -> %d\n" + " location : {:#X} -> {:#X}\n" + " id : {} -> {}\n" + " type : {} -> {}\n" + " type : {} -> {}\n" + " working : {} -> {}\n" + " completion timer : {} -> {}\n" + " workerID : {} -> {}\n" + " time : {} -> {}\n" "\n", job0.list_link->item, job1.list_link->item, job0.id, job1.id, @@ -1229,7 +1238,7 @@ static void manageUnitAttackEvent(color_ostream& out) { if ( reportStr.find("severed part") ) continue; if ( Once::doOnce("EventManager neither wound") ) { - out.print("%s, %d: neither wound: %s\n", __FILE__, __LINE__, reportStr.c_str()); + out.print("{}, {}: neither wound: {}\n", __FILE__, __LINE__, reportStr.c_str()); } } } @@ -1352,9 +1361,8 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, //if trying attack-defend pair and it fails to find attacker, try defend only InteractionData result = /*(InteractionData)*/ { std::string(), std::string(), -1, -1, -1, -1 }; if ( attackers.size() > 1 ) { -//out.print("%s,%d\n",__FILE__,__LINE__); if ( Once::doOnce("EventManager interaction ambiguous attacker") ) { - out.print("%s,%d: ambiguous attacker on report\n \'%s\'\n '%s'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text.c_str() : "", defendEvent ? defendEvent->text.c_str() : ""); + out.print("{},{}: ambiguous attacker on report\n \'{}\'\n \'{}\'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text : "", defendEvent ? defendEvent->text : ""); } } else if ( attackers.empty() ) { //out.print("%s,%d\n",__FILE__,__LINE__); @@ -1368,7 +1376,7 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, result.defender = defenders[0]->id; if ( defenders.size() > 1 ) { if ( Once::doOnce("EventManager interaction ambiguous defender") ) { - out.print("%s,%d: ambiguous defender: shouldn't happen. On report\n \'%s\'\n '%s'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text.c_str() : "", defendEvent ? defendEvent->text.c_str() : ""); + out.print("{}, {}: ambiguous defender: shouldn't happen. On report\n \'{}\'\n \'{}\'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text : "", defendEvent ? defendEvent->text : ""); } } result.attackVerb = attackVerb; @@ -1395,7 +1403,7 @@ static vector gatherRelevantUnits(color_ostream& out, df::report* r1, vector& units = reportToRelevantUnits[report->id]; if ( units.size() > 2 ) { if ( Once::doOnce("EventManager interaction too many relevant units") ) { - out.print("%s,%d: too many relevant units. On report\n \'%s\'\n", __FILE__, __LINE__, report->text.c_str()); + out.print("{},{}: too many relevant units. On report\n \'{}\'\n", __FILE__, __LINE__, report->text); } } for (int & unit_id : units) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index fae55ce9d7..68d63e68de 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1842,7 +1842,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce return -1; else if (message.empty()) { - Core::printerr("Empty announcement %u\n", type); // DF would print this to errorlog.txt + Core::printerr("Empty announcement {}\n", ENUM_AS_STR(type)); // DF would print this to errorlog.txt return -1; } @@ -2070,17 +2070,17 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { // Based on reverse-engineering of "make_announcement" FUN_1400574e0 (v50.11 win64 Steam) if (!world->allow_announcements) { - DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n{}\n", message); return false; } else if (!is_valid_enum_item(info.type) || info.type == df::announcement_type::NONE) { - WARN(gui).print("Invalid announcement type:\n%s\n", message.c_str()); + WARN(gui).print("Invalid announcement type:\n{}\n", message); return false; } else if (message.empty()) { - Core::printerr("Empty announcement %u\n", info.type); // DF would print this to errorlog.txt + Core::printerr("Empty announcement {}\n", ENUM_AS_STR(info.type)); // DF would print this to errorlog.txt return false; } @@ -2091,7 +2091,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!a_flags.bits.A_DISPLAY && !a_flags.bits.DO_MEGA) { - DEBUG(gui).print("Skipped announcement not enabled at all for adventure mode:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement not enabled at all for adventure mode:\n{}\n", message); return false; } @@ -2105,7 +2105,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { // Adventure mode reuses a dwarf mode digging designation bit to determine current visibility if (!Maps::isValidTilePos(info.pos) || (Maps::getTileDesignation(info.pos)->whole & 0x10) == 0x0) { - DEBUG(gui).print("Adventure mode announcement not detected:\n%s\n", message.c_str()); + DEBUG(gui).print("Adventure mode announcement not detected:\n{}\n", message); return false; } } @@ -2115,7 +2115,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if ((info.unit_a || info.unit_d) && (!info.unit_a || Units::isHidden(info.unit_a)) && (!info.unit_d || Units::isHidden(info.unit_d))) { - DEBUG(gui).print("Dwarf mode announcement not detected:\n%s\n", message.c_str()); + DEBUG(gui).print("Dwarf mode announcement not detected:\n{}\n", message); return false; } @@ -2125,7 +2125,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!info.unit_a && !info.unit_d) { - DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT because it has no units:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT because it has no units:\n{}\n", message); return false; } } @@ -2133,12 +2133,12 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) { - DEBUG(gui).print("Skipped announcement not enabled at all for dwarf mode:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement not enabled at all for dwarf mode:\n{}\n", message); return false; } else if (!recent_report_any(info.unit_a) && !recent_report_any(info.unit_d)) { - DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT_ALL_ACTIVE because there's no active report:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT_ALL_ACTIVE because there's no active report:\n{}\n", message); return false; } } @@ -2171,7 +2171,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) if (samp_index >= 0) { - DEBUG(gui).print("Playing sound #%d for announcement.\n", samp_index); + DEBUG(gui).print("Playing sound #{} for announcement.\n", samp_index); //play_sound(musicsound_info, samp_index, 255, true); // g_src/music_and_sound_g.h // TODO: implement sounds } } @@ -2198,7 +2198,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) if (a_flags.bits.D_DISPLAY) world->status.display_timer = info.display_timer; - DEBUG(gui).print("Announcement succeeded as repeat:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement succeeded as repeat:\n{}\n", message); return true; } } @@ -2284,10 +2284,10 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) (*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || // Did adventure announcement (a_flags.bits.DO_MEGA && !adv_unconscious)) // Did popup { - DEBUG(gui).print("Announcement succeeded and displayed:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement succeeded and displayed:\n{}\n", message); } else - DEBUG(gui).print("Announcement added internally and to gamelog.txt but didn't qualify to be displayed anywhere:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement added internally and to gamelog.txt but didn't qualify to be displayed anywhere:\n{}\n", message); return true; } @@ -2493,8 +2493,8 @@ void Gui::MTB_parse(df::markup_text_boxst *mtb, string parse_text) if (buff1 == "VAR") // Color from dipscript var { - DEBUG(gui).print("MTB_parse received:\n[C:VAR:%s:%s]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: %s\n", - buff2.c_str(), buff3.c_str(), mtb->environment ? "Active" : "NULL"); + DEBUG(gui).print("MTB_parse received:\n[C:VAR:{}:{}]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: {}\n", + buff2, buff3, mtb->environment ? "Active" : "NULL"); //MTB_set_color_on_var(mtb, buff2, buff3); } else @@ -2548,8 +2548,8 @@ void Gui::MTB_parse(df::markup_text_boxst *mtb, string parse_text) string buff_var_name = grab_token_string_pos(parse_text, i, ':'); i += buff_var_name.size(); - DEBUG(gui).print("MTB_parse received:\n[VAR:%s:%s:%s]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: %s\n", - buff_format.c_str(), buff_var_type.c_str(), buff_var_name.c_str(), mtb->environment ? "Active" : "NULL"); + DEBUG(gui).print("MTB_parse received:\n[VAR:{}:{}:{}]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: {}\n", + buff_format, buff_var_type, buff_var_name, mtb->environment ? "Active" : "NULL"); //MTB_append_variable(mtb, str, buff_format, buff_var_type, buff_var_name); } else if (token_buffer == "R" || token_buffer == "B" || token_buffer == "P") diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index b3d327881e..6acd3b9401 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -155,7 +155,7 @@ string ItemTypeInfo::getToken() { if (custom) rv += ":" + custom->id; else if (subtype != -1 && type != item_type::PLANT_GROWTH) - rv += stl_sprintf(":%d", subtype); + rv += fmt::format(":{}", subtype); return rv; } @@ -859,7 +859,7 @@ static bool detachItem(df::item *item) if (item->flags.bits.on_ground) { if (!removeItemOnGround(item)) - Core::printerr("Item was marked on_ground, but not in block: %d (%d,%d,%d)\n", + Core::printerr("Item was marked on_ground, but not in block: {} ({},{},{})\n", item->id, item->pos.x, item->pos.y, item->pos.z); item->flags.bits.on_ground = false; return true; @@ -1797,7 +1797,7 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t World::isFortressMode() ? df::world_site::find(World::GetCurrentSiteId()) : NULL, NULL); delete prod; - DEBUG(items).print("produced %zd items\n", out_items.size()); + DEBUG(items).print("produced {} items\n", out_items.size()); for (auto out_item : out_items) { // Plant growths need a valid "growth print", otherwise they behave oddly diff --git a/library/modules/Kitchen.cpp b/library/modules/Kitchen.cpp index d6c2b657f5..be1415d90d 100644 --- a/library/modules/Kitchen.cpp +++ b/library/modules/Kitchen.cpp @@ -33,14 +33,14 @@ void Kitchen::debug_print(color_ostream &out) out.print("Kitchen Exclusions\n"); for(std::size_t i = 0; i < size(); ++i) { - out.print("%2zu: IT:%2i IS:%i MT:%3i MI:%2i ET:%i %s\n", + out.print("{:2}: IT:{:2} IS:{:} MT:{:3} MI:{:2} ET:{:} {}\n", i, - plotinfo->kitchen.item_types[i], + ENUM_KEY_STR(item_type,plotinfo->kitchen.item_types[i]), plotinfo->kitchen.item_subtypes[i], plotinfo->kitchen.mat_types[i], plotinfo->kitchen.mat_indices[i], plotinfo->kitchen.exc_types[i].whole, - (plotinfo->kitchen.mat_types[i] >= 419 && plotinfo->kitchen.mat_types[i] <= 618) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id.c_str() : "n/a" + (plotinfo->kitchen.mat_types[i] >= 419 && plotinfo->kitchen.mat_types[i] <= 618) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id : "n/a" ); } out.print("\n"); diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 18dd73b09a..d5358f6139 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -309,7 +309,7 @@ std::string MaterialInfo::getToken() const return "NONE"; if (!material) - return stl_sprintf("INVALID:%d:%d", type, index); + return fmt::format("INVALID:{}:{}", type, index); switch (mode) { case Builtin: @@ -327,7 +327,7 @@ std::string MaterialInfo::getToken() const case Plant: return "PLANT:" + plant->id + ":" + material->id; default: - return stl_sprintf("INVALID_MODE:%d:%d", type, index); + return fmt::format("INVALID_MODE:{}:{}", type, index); } } @@ -337,7 +337,7 @@ std::string MaterialInfo::toString(uint16_t temp, bool named) const return "any"; if (!material) - return stl_sprintf("INVALID:%d:%d", type, index); + return fmt::format("INVALID:{}:{}", type, index); df::matter_state state = matter_state::Solid; if (temp >= material->heat.melting_point) @@ -350,7 +350,7 @@ std::string MaterialInfo::toString(uint16_t temp, bool named) const name = material->prefix + " " + name; if (named && figure) - name += stl_sprintf(" of HF %d", index); + name += fmt::format(" of HF {}", index); return name; } diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index e6a0d50126..b9c3df577b 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -303,7 +303,7 @@ void Persistence::Internal::load(color_ostream& out) { std::filesystem::path save_path = getSavePath(world_name); std::vector files; if (0 != Filesystem::listdir(save_path, files)) { - DEBUG(persistence,out).print("not loading state; save directory doesn't exist: '%s'\n", save_path.c_str()); + DEBUG(persistence,out).print("not loading state; save directory doesn't exist: '{}'\n", save_path); return; } @@ -316,7 +316,7 @@ void Persistence::Internal::load(color_ostream& out) { found = true; std::filesystem::path path = save_path / fname; if (!load_file(path, entity_id)) - out.printerr("Cannot load data from: '%s'\n", path.c_str()); + out.printerr("Cannot load data from: '{}'\n", path); } if (found) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 1a98837739..d629c4b8cf 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -621,7 +621,7 @@ std::set Screen::normalize_text_keys(const std::setlast_text_input[0]) { char c = df::global::enabler->last_text_input[0]; df::interface_key key = charToKey(c); - DEBUG(screen).print("adding character %c as interface key %ld\n", c, key); + DEBUG(screen).print("adding character {} as interface key {}\n", c, ENUM_AS_STR(key)); combined_keys.emplace(key); } return combined_keys; @@ -780,7 +780,7 @@ void dfhack_viewscreen::logic() bool is_df_screen = !is_instance(p); auto *next_p = p->parent; if (is_df_screen && Screen::isDismissed(p)) { - DEBUG(screen).print("raising dismissed DF viewscreen %p\n", p); + DEBUG(screen).print("raising dismissed DF viewscreen {}\n", static_cast(p)); Screen::raise(p); } if (is_df_screen) diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index 8b485a3a62..21642a9bbe 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -204,14 +204,14 @@ std::vector Textures::loadTileset(const std::string& file, int til SDL_Surface* surface = DFIMG_Load(file.c_str()); if (!surface) { - ERR(textures).printerr("unable to load textures from '%s'\n", file.c_str()); + ERR(textures).printerr("unable to load textures from '{}'\n", file); return std::vector{}; } surface = canonicalize_format(surface); auto handles = slice_tileset(surface, tile_px_w, tile_px_h, reserved); - DEBUG(textures).print("loaded %zd textures from '%s'\n", handles.size(), file.c_str()); + DEBUG(textures).print("loaded {} textures from '{}'\n", handles.size(), file); g_tileset_to_handles[file] = handles; return handles; @@ -308,7 +308,7 @@ static void reset_surface() { } static void register_delayed_handles() { - DEBUG(textures).print("register delayed handles, size %zd\n", g_delayed_regs.size()); + DEBUG(textures).print("register delayed handles, size {}\n", g_delayed_regs.size()); for (auto& handle : g_delayed_regs) { auto texpos = add_texture(g_handle_to_surface[handle]); g_handle_to_texpos.emplace(handle, texpos); @@ -322,8 +322,9 @@ struct tracking_stage_new_region : df::viewscreen_new_regionst { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_raw_load_stage != this->raw_load_stage) { - TRACE(textures).print("raw_load_stage %d -> %d\n", this->m_raw_load_stage, - this->raw_load_stage); + TRACE(textures).print("raw_load_stage {} -> {}\n", + this->m_raw_load_stage, + static_cast(this->raw_load_stage)); bool tmp_state = loading_state; loading_state = this->raw_load_stage >= 0 && this->raw_load_stage < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -346,7 +347,9 @@ struct tracking_stage_adopt_region : df::viewscreen_adopt_regionst { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -369,7 +372,9 @@ struct tracking_stage_load_region : df::viewscreen_loadgamest { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -392,7 +397,9 @@ struct tracking_stage_new_arena : df::viewscreen_new_arenast { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -446,7 +453,7 @@ void Textures::init(color_ostream& out) { reserve_static_range(); install_reset_point(); DEBUG(textures, out) - .print("dynamic texture loading ready, reserved range %d-%d\n", reserved_range.start, + .print("dynamic texture loading ready, reserved range {}-{}\n", reserved_range.start, reserved_range.end); } diff --git a/library/modules/World.cpp b/library/modules/World.cpp index b41c625f17..d19f61fcee 100644 --- a/library/modules/World.cpp +++ b/library/modules/World.cpp @@ -223,16 +223,16 @@ int32_t World::GetCurrentSiteId() { DEBUG(world).print("searching for adventure site\n"); auto & world_map = world->map; auto adv_pos = Units::getPosition(adv); - DEBUG(world).print("adv_pos: (%d, %d, %d)\n", adv_pos.x, adv_pos.y, adv_pos.z); + DEBUG(world).print("adv_pos: ({}, {}, {})\n", adv_pos.x, adv_pos.y, adv_pos.z); df::coord2d rgn_pos(world_map.region_x + adv_pos.x/48, world_map.region_y + adv_pos.y/48); for (auto site : world->world_data->sites) { - DEBUG(world).print("scanning site %d: %s\n", site->id, Translation::translateName(&site->name, true).c_str()); - DEBUG(world).print(" rgn_pos: (%d, %d); site bounds: (%d, %d), (%d, %d) \n", + DEBUG(world).print("scanning site {}: {}\n", site->id, Translation::translateName(&site->name, true)); + DEBUG(world).print(" rgn_pos: ({}, {}); site bounds: ({}, {}), ({}, {}) \n", rgn_pos.x, rgn_pos.y, site->global_min_x, site->global_min_y, site->global_max_x, site->global_max_y); if (rgn_pos.x >= site->global_min_x && rgn_pos.x <= site->global_max_x && rgn_pos.y >= site->global_min_y && rgn_pos.y <= site->global_max_y) { - DEBUG(world).print("found site: %d\n", site->id); + DEBUG(world).print("found site: {}\n", site->id); return site->id; } } diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp index 8f75e0549e..07c1ca795d 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -433,13 +433,13 @@ struct GeoLayer void print_mineral_stats(color_ostream &out) { for (auto it = mineral_count.begin(); it != mineral_count.end(); ++it) - INFO(process, out).print("3dveins: %s %s: %d (%f)\n", - MaterialInfo(0, it->first.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, it->first.second).c_str(), + INFO(process, out).print("3dveins: {} {}: {} ({})\n", + MaterialInfo(0, it->first.first).getToken(), + ENUM_KEY_STR(inclusion_type, it->first.second), it->second, (float(it->second) / unmined_tiles)); - INFO(process, out).print ("3dveins: Total tiles: %d (%d unmined)\n", tiles, unmined_tiles); + INFO(process, out).print ("3dveins: Total tiles: {} ({} unmined)\n", tiles, unmined_tiles); } bool form_veins(color_ostream &out); @@ -471,12 +471,12 @@ struct GeoBiome void print_mineral_stats(color_ostream &out) { - INFO(process,out).print("3dveins: Geological biome %d:\n", info.geo_index); + INFO(process,out).print("3dveins: Geological biome {}:\n", info.geo_index); for (size_t i = 0; i < layers.size(); i++) if (layers[i]) { - INFO(process, out).print("3dveins: Layer %ld\n", i); + INFO(process, out).print("3dveins: Layer {}\n", i); layers[i]->print_mineral_stats(out); } } @@ -590,7 +590,7 @@ bool VeinGenerator::init_biomes() if (info.geo_index < 0 || !info.geobiome) { - WARN(process, out).print("Biome %zd is not defined.\n", i); + WARN(process, out).print("Biome {} is not defined.\n", i); return false; } @@ -801,7 +801,7 @@ bool VeinGenerator::scan_layer_depth(Block *b, df::coord2d column, int z) { if (z != min_level[idx]-1 && min_level[idx] <= top_solid) { - WARN(process, out).print("Discontinuous layer %d at (%d,%d,%d).\n", + WARN(process, out).print("Discontinuous layer {} at ({} {} {}).\n", layer->index, x+column.x*16, y+column.y*16, z ); return false; @@ -852,7 +852,7 @@ bool VeinGenerator::adjust_layer_depth(df::coord2d column) if (max_level[i+1] != min_level[i]-1) { WARN(process, out).print( - "Gap or overlap with next layer %d at (%d,%d,%d-%d).\n", + "Gap or overlap with next layer {} at ({} {} {}-{}).\n", i+1, x+column.x*16, y+column.y*16, max_level[i+1], min_level[i] ); return false; @@ -895,7 +895,7 @@ bool VeinGenerator::adjust_layer_depth(df::coord2d column) } WARN(process, out).print( - "Layer height change in layer %d at (%d,%d,%d): %d instead of %d.\n", + "Layer height change in layer {} at ({} {} {}): {} instead of {}.\n", i, x+column.x*16, y+column.y*16, max_level[i], size, biome->layers[i]->thickness ); @@ -936,7 +936,7 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) if (unsigned(key.first) >= materials.size() || unsigned(key.second) >= NUM_INCLUSIONS) { - WARN(process, out).print("Invalid vein code: %d %d - aborting.\n",key.first,key.second); + WARN(process, out).print("Invalid vein code: {} {} - aborting.\n",key.first,ENUM_AS_STR(key.second)); return false; } @@ -946,9 +946,9 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) { // Report first occurence of unreasonable vein spec WARN(process, out).print( - "Unexpected vein %s %s - ", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str() + "Unexpected vein {} {} - ", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second) ); status = materials[key.first].default_type; @@ -956,8 +956,8 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) WARN(process, out).print("will be left in place.\n"); else WARN(process, out).print( - "correcting to %s.\n", - ENUM_KEY_STR(inclusion_type, df::inclusion_type(status)).c_str() + "correcting to {}.\n", + ENUM_KEY_STR(inclusion_type, df::inclusion_type(status)) ); } @@ -1095,7 +1095,7 @@ void VeinGenerator::write_block_tiles(Block *b, df::coord2d column, int z) if (!ok) { WARN(process, out).print( - "Couldn't write %d vein at (%d,%d,%d)\n", + "Couldn't write {} vein at ({} {} {})\n", mat, x+column.x*16, y+column.y*16, z ); } @@ -1285,7 +1285,7 @@ bool GeoLayer::form_veins(color_ostream &out) if (parent_id >= (int)refs.size()) { - WARN(process, out).print("Forward vein reference in biome %d.\n", biome->info.geo_index); + WARN(process, out).print("Forward vein reference in biome {}.\n", biome->info.geo_index); return false; } @@ -1306,10 +1306,10 @@ bool GeoLayer::form_veins(color_ostream &out) ctx = "only be in "+MaterialInfo(0,vptr->parent_mat()).getToken(); WARN(process, out).print( - "Duplicate vein %s %s in biome %d layer %d - will %s.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str(), - biome->info.geo_index, index, ctx.c_str() + "Duplicate vein {} {} in biome {} layer {} - will {}.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second), + biome->info.geo_index, index, ctx ); } @@ -1362,9 +1362,9 @@ bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from) if (best.empty()) { WARN(process,out).print( - "Could not place orphaned vein %s %s anywhere.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str() + "Could not place orphaned vein {} {} anywhere.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second) ); return true; @@ -1396,9 +1396,9 @@ bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from) if (size > 0) { WARN(process, out).print( - "Could not place all of orphaned vein %s %s: %d left.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str(), + "Could not place all of orphaned vein {} {}: {} left.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second), size ); } @@ -1546,8 +1546,8 @@ bool VeinGenerator::place_veins(bool verbose) if (!isStoneInorganic(key.first)) { WARN(process, out).print( - "Invalid vein material: %s\n", - MaterialInfo(0, key.first).getToken().c_str() + "Invalid vein material: {}\n", + MaterialInfo(0, key.first).getToken() ); return false; @@ -1555,7 +1555,7 @@ bool VeinGenerator::place_veins(bool verbose) if (!is_valid_enum_item(key.second)) { - WARN(process, out).print("Invalid vein type: %d\n", key.second); + WARN(process, out).print("Invalid vein type: {}\n", ENUM_AS_STR(key.second)); return false; } @@ -1568,16 +1568,16 @@ bool VeinGenerator::place_veins(bool verbose) sort(queue.begin(), queue.end(), vein_cmp); // Place tiles - TRACE(process,out).print("Processing... (%zu)", queue.size()); + TRACE(process,out).print("Processing... ({})", queue.size()); for (size_t j = 0; j < queue.size(); j++) { if (queue[j]->parent && !queue[j]->parent->placed) { WARN(process, out).print( - "\nParent vein not placed for %s %s.\n", - MaterialInfo(0,queue[j]->vein.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, queue[j]->vein.second).c_str() + "\nParent vein not placed for {} {}.\n", + MaterialInfo(0,queue[j]->vein.first).getToken(), + ENUM_KEY_STR(inclusion_type, queue[j]->vein.second) ); return false; @@ -1591,16 +1591,16 @@ bool VeinGenerator::place_veins(bool verbose) } TRACE(process, out).print( - "\nVein layer %zu of %zu: %s %s (%.2f%%)... ", + "\nVein layer {} of {}: {} {} ({:.2})... ", j+1, queue.size(), - MaterialInfo(0,queue[j]->vein.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, queue[j]->vein.second).c_str(), + MaterialInfo(0,queue[j]->vein.first).getToken(), + ENUM_KEY_STR(inclusion_type, queue[j]->vein.second), queue[j]->density() * 100 ); } else { - TRACE(process, out).print("\rVein layer %zu of %zu... ", j+1, queue.size()); + TRACE(process, out).print("\rVein layer {} of {}... ", j+1, queue.size()); } queue[j]->place_tiles(); diff --git a/plugins/add-spatter.cpp b/plugins/add-spatter.cpp index 0ece73d982..a2685983fb 100644 --- a/plugins/add-spatter.cpp +++ b/plugins/add-spatter.cpp @@ -83,7 +83,7 @@ static void find_material(int *type, int *index, df::item *input, MaterialSource if (!info.findProduct(info, mat.product_name)) { color_ostream_proxy out(Core::getInstance().getConsole()); - out.printerr("Cannot find product '%s'\n", mat.product_name.c_str()); + out.printerr("Cannot find product '{}'\n", mat.product_name); } } @@ -293,7 +293,7 @@ static void find_reagent( return; } - out.printerr("Invalid reagent name '%s' in '%s'\n", name.c_str(), react->code.c_str()); + out.printerr("Invalid reagent name '{}' in '{}'\n", name, react->code); } static void parse_product( diff --git a/plugins/aquifer.cpp b/plugins/aquifer.cpp index 124fa29838..afb8bbe54f 100644 --- a/plugins/aquifer.cpp +++ b/plugins/aquifer.cpp @@ -35,7 +35,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded()) { - out.printerr("Cannot run %s without a loaded map.\n", plugin_name); + out.printerr("Cannot run {} without a loaded map.\n", plugin_name); return CR_FAILURE; } @@ -142,7 +142,7 @@ static int get_max_aq_z(color_ostream &out, const df::coord &pos1, const df::coo static void get_z_range(color_ostream &out, int & minz, int & maxz, const df::coord & pos1, const df::coord & pos2, int levels, bool top_is_aq = false, int skip_top = 0) { - DEBUG(log,out).print("get_z_range: top_is_aq=%d, skip_top=%d\n", top_is_aq, skip_top); + DEBUG(log,out).print("get_z_range: top_is_aq={}, skip_top={}\n", top_is_aq, skip_top); if (!top_is_aq) maxz = get_max_ground_z(out, pos1, pos2) - skip_top; @@ -151,12 +151,12 @@ static void get_z_range(color_ostream &out, int & minz, int & maxz, const df::co minz = std::max((int)pos1.z, maxz - levels + 1); - DEBUG(log,out).print("calculated z range: minz=%d, maxz=%d\n", minz, maxz); + DEBUG(log,out).print("calculated z range: minz={}, maxz={}\n", minz, maxz); } static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_list: pos1=%d,%d,%d, pos2=%d,%d,%d, levels=%d, leaky=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, levels, leaky); + DEBUG(log,out).print("entering aquifer_list: pos1={}, pos2={}, levels={}, leaky={}\n", + pos1, pos2, levels, leaky); std::map> light_tiles, heavy_tiles; @@ -173,14 +173,14 @@ static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int }); if (light_tiles.empty() && heavy_tiles.empty()) { - out.print("No %saquifer tiles in the specified range.\n", leaky ? "leaking " : ""); + out.print("No {}aquifer tiles in the specified range.\n", leaky ? "leaking " : ""); } else { int elev_off = world->map.region_z - 100; for (int z = maxz; z >= minz; --z) { int lcount = light_tiles.contains(z) ? light_tiles[z] : 0; int hcount = heavy_tiles.contains(z) ? heavy_tiles[z] : 0; if (lcount || hcount) - out.print("z-level %3d (elevation %4d) has %6d %slight aquifer tile(s) and %6d %sheavy aquifer tile(s)\n", + out.print("z-level {} (elevation {}) has {} {}light aquifer tile(s) and {} {}heavy aquifer tile(s)\n", z, z+elev_off, lcount, leaky ? "leaking " : "", hcount, leaky ? "leaking " : ""); } } @@ -189,22 +189,21 @@ static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int static int aquifer_drain(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_drain: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_drain: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); const bool all = aq_type == "all"; const bool heavy_state = aq_type == "heavy"; int modified = Maps::removeAreaAquifer(pos1, pos2, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return Maps::isTileAquifer(pos) && (all || Maps::isTileHeavyAquifer(pos) == heavy_state) && (!leaky || is_leaky(pos)); }); - DEBUG(log, out).print("drained aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, all=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, all, modified); + DEBUG(log, out).print("drained aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, all={}, count={}\n", + pos1, pos2, heavy_state, all, modified); return modified; } @@ -212,21 +211,20 @@ static int aquifer_drain(color_ostream &out, string aq_type, static int aquifer_convert(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_convert: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_convert: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); const bool heavy_state = aq_type == "heavy"; int modified = Maps::setAreaAquifer(pos1, pos2, heavy_state, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return Maps::isTileAquifer(pos) && Maps::isTileHeavyAquifer(pos) != heavy_state && (!leaky || is_leaky(pos)); }); - DEBUG(log, out).print("converted aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, modified); + DEBUG(log, out).print("converted aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, count={}\n", + pos1, pos2, heavy_state, modified); return modified; } @@ -234,20 +232,19 @@ static int aquifer_convert(color_ostream &out, string aq_type, static int aquifer_add(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_add: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_add: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); const bool heavy_state = aq_type == "heavy"; int modified = Maps::setAreaAquifer(pos1, pos2, heavy_state, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return (leaky || !is_leaky(pos)) && (!Maps::isTileAquifer(pos) || Maps::isTileHeavyAquifer(pos) != heavy_state); }); - DEBUG(log, out).print("added aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, modified); + DEBUG(log, out).print("added aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, count={}\n", + pos1, pos2, heavy_state, modified); return modified; } diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 089b626b5f..1924cfc9e9 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -54,12 +54,12 @@ namespace { for (auto ac : ent->army_controllers) { if (ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %p found in entity id %d\n", ac, ent->id); + WARN(log).print("acValidationError: Bad controller {} found in entity id {}\n", static_cast(ac), ent->id); ok = false; } if (ac_set.count(ac) != 0 && ac->entity_id != ent->id) { - WARN(log).print("acValidationError: Army controller %d has entity id %d but is linked from entity with id %d\n", ac->id, ac->entity_id, ent->id); + WARN(log).print("acValidationError: Army controller {} has entity id {} but is linked from entity with id {}\n", ac->id, ac->entity_id, ent->id); } } } @@ -68,17 +68,17 @@ namespace { { auto ac = ar->controller; if (ac && ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %p found in army id %d\n", ac, ar->id); + WARN(log).print("acValidationError: Bad controller {} found in army id {}\n", static_cast(ac), ar->id); ok = false; } else if (ac && ac->id != ar->controller_id) { - WARN(log).print("acValidationError: controller %p id mismatch (%d != %d) in army %d\n", ac, ar->controller_id, ac->id, ar->id); + WARN(log).print("acValidationError: controller {} id mismatch ({} != {}) in army {}\n", static_cast(ac), ar->controller_id, ac->id, ar->id); ok = false; } else if (!ac && ar->controller_id != -1) { - WARN(log).print("acValidationError: army %d has nonzero controller %d but controller pointer is null\n", ar->id, ar->controller_id); + WARN(log).print("acValidationError: army {} has nonzero controller {} but controller pointer is null\n", ar->id, ar->controller_id); ok = false; } } @@ -87,17 +87,17 @@ namespace { { auto ac = un->enemy.army_controller; if (ac && ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %p found in unit id %d\n", ac, un->id); + WARN(log).print("acValidationError: Bad controller {} found in unit id {}\n", static_cast(ac), un->id); ok = false; } else if (ac && ac->id != un->enemy.army_controller_id) { - WARN(log).print("acValidationError: controller %p id mismatch (%d != %d) in unit %d\n", ac, un->enemy.army_controller_id, ac->id, un->id); + WARN(log).print("acValidationError: controller {} id mismatch ({} != {}) in unit {}\n", static_cast(ac), un->enemy.army_controller_id, ac->id, un->id); ok = false; } else if (!ac && un->enemy.army_controller_id != -1) { - WARN(log).print("acValidationError: unit %d has has nonzero controller %d but controller pointer is null\n", un->id, un->enemy.army_controller_id); + WARN(log).print("acValidationError: unit {} has has nonzero controller {} but controller pointer is null\n", un->id, un->enemy.army_controller_id); ok = false; } } @@ -105,7 +105,7 @@ namespace { last_army_controller_next_id = *army_controller_next_id; last_ac_vec_size = world->army_controllers.all.size(); - INFO(log).print("acValidation: controller count = %ld, next id = %d, season tick count = %d\n", + INFO(log).print("acValidation: controller count = {}, next id = {}, season tick count = {}\n", last_ac_vec_size, last_army_controller_next_id, *cur_year_tick); return ok; diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 8fde1cc347..8f7bbb16fa 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -82,13 +82,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector fortress_age > 0) autobutcher_cycle(out); } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -104,7 +104,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); cleanup_autobutcher(out); return CR_OK; } @@ -128,7 +128,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { // all the other state we can directly read/modify from the persistent // data structure. is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); // load the persisted watchlist @@ -140,7 +140,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -209,8 +209,8 @@ static bool isHighPriority(df::unit *unit) { } static void doMarkForSlaughter(df::unit *unit) { - DEBUG(cycle).print("marking unit %d for slaughter: %s, %s, high priority: %s, age: %.2f\n", - unit->id, Units::getReadableName(unit).c_str(), + DEBUG(cycle).print("marking unit {} for slaughter: {}, {}, high priority: {}, age: {:.2f}\n", + unit->id, Units::getReadableName(unit), Units::isFemale(unit) ? "female" : "male", isHighPriority(unit) ? "yes" : "no", Units::getAge(unit)); @@ -266,7 +266,7 @@ struct WatchedRace { fk_prot(0), fa_prot(0), mk_prot(0), ma_prot(0), fk_units(compareKids), mk_units(compareKids), fa_units(compareAdults), ma_units(compareAdults) { - TRACE(control,out).print("creating new WatchedRace: id=%d, watched=%s, fk=%u, mk=%u, fa=%u, ma=%u\n", + TRACE(control,out).print("creating new WatchedRace: id={}, watched={}, fk={}, mk={}, fa={}, ma={}\n", id, watch ? "true" : "false", fk, mk, fa, ma); } @@ -293,8 +293,8 @@ struct WatchedRace { rconfig.ival(5) = ma; } else { - ERR(control,out).print("could not create persistent key for race: %s", - Units::getRaceNameById(raceId).c_str()); + ERR(control,out).print("could not create persistent key for race: {}", + Units::getRaceNameById(raceId)); } } @@ -375,14 +375,14 @@ static void init_autobutcher(color_ostream &out) { vector watchlist; World::GetPersistentSiteData(&watchlist, WATCHLIST_CONFIG_KEY_PREFIX, true); for (auto & p : watchlist) { - DEBUG(control,out).print("Reading from save: %s\n", p.key().c_str()); + DEBUG(control,out).print("Reading from save: {}\n", p.key()); WatchedRace *w = new WatchedRace(out, p); watched_races.emplace(w->raceId, w); } } static void cleanup_autobutcher(color_ostream &out) { - DEBUG(control,out).print("cleaning %s state\n", plugin_name); + DEBUG(control,out).print("cleaning {} state\n", plugin_name); race_to_id.clear(); for (auto w : watched_races) delete w.second; @@ -396,7 +396,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o static command_result df_autobutcher(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -508,7 +508,7 @@ static void autobutcher_control(color_ostream &out) { static void autobutcher_target(color_ostream &out, const autobutcher_options &opts) { if (opts.races_new) { - DEBUG(control,out).print("setting targets for new races to fk=%u, mk=%u, fa=%u, ma=%u\n", + DEBUG(control,out).print("setting targets for new races to fk={}, mk={}, fa={}, ma={}\n", opts.fk, opts.mk, opts.fa, opts.ma); config.set_int(CONFIG_DEFAULT_FK, opts.fk); config.set_int(CONFIG_DEFAULT_MK, opts.mk); @@ -517,7 +517,7 @@ static void autobutcher_target(color_ostream &out, const autobutcher_options &op } if (opts.races_all) { - DEBUG(control,out).print("setting targets for all races on watchlist to fk=%u, mk=%u, fa=%u, ma=%u\n", + DEBUG(control,out).print("setting targets for all races on watchlist to fk={}, mk={}, fa={}, ma={}\n", opts.fk, opts.mk, opts.fa, opts.ma); for (auto w : watched_races) { w.second->fk = opts.fk; @@ -530,7 +530,7 @@ static void autobutcher_target(color_ostream &out, const autobutcher_options &op for (auto race : opts.races) { if (!race_to_id.count(*race)) { - out.printerr("race not found: '%s'", race->c_str()); + out.printerr("race not found: '{}'", race->c_str()); continue; } int id = race_to_id[*race]; @@ -559,7 +559,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o for (auto race : opts.races) { if (!race_to_id.count(*race)) { - out.printerr("race not found: '%s'", race->c_str()); + out.printerr("race not found: '{}'", race->c_str()); continue; } ids.emplace(race_to_id[*race]); @@ -576,7 +576,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o config.get_int(CONFIG_DEFAULT_MA))); } else if (!watched_races[id]->isWatched) { - DEBUG(control,out).print("watching: %s\n", opts.command.c_str()); + DEBUG(control,out).print("watching: {}\n", opts.command); watched_races[id]->isWatched = true; } } @@ -590,13 +590,13 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o config.get_int(CONFIG_DEFAULT_MA))); } else if (watched_races[id]->isWatched) { - DEBUG(control,out).print("unwatching: %s\n", opts.command.c_str()); + DEBUG(control,out).print("unwatching: {}\n", opts.command); watched_races[id]->isWatched = false; } } else if (opts.command == "forget") { if (watched_races.count(id)) { - DEBUG(control,out).print("forgetting: %s\n", opts.command.c_str()); + DEBUG(control,out).print("forgetting: {}\n", opts.command); watched_races[id]->RemoveConfig(out); delete watched_races[id]; watched_races.erase(id); @@ -680,7 +680,7 @@ static void autobutcher_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // check if there is anything to watch before walking through units vector if (!config.get_bool(CONFIG_AUTOWATCH)) { @@ -720,8 +720,8 @@ static void autobutcher_cycle(color_ostream &out) { w->UpdateConfig(out); watched_races.emplace(unit->race, w); - INFO(cycle,out).print("New race added to autobutcher watchlist: %s\n", - Units::getRaceNamePluralById(unit->race).c_str()); + INFO(cycle,out).print("New race added to autobutcher watchlist: {}\n", + Units::getRaceNamePluralById(unit->race)); } if (w->isWatched) { @@ -740,8 +740,8 @@ static void autobutcher_cycle(color_ostream &out) { if (slaughter_count) { std::stringstream ss; ss << slaughter_count; - INFO(cycle,out).print("%s marked for slaughter: %s\n", - Units::getRaceNamePluralById(w.first).c_str(), ss.str().c_str()); + INFO(cycle,out).print("{} marked for slaughter: {}\n", + Units::getRaceNamePluralById(w.first), ss.str()); } } } @@ -817,7 +817,7 @@ static bool autowatch_isEnabled() { } static void autowatch_setEnabled(color_ostream &out, bool enable) { - DEBUG(control,out).print("auto-adding to watchlist %s\n", enable ? "started" : "stopped"); + DEBUG(control,out).print("auto-adding to watchlist {}\n", enable ? "started" : "stopped"); config.set_bool(CONFIG_AUTOWATCH, enable); if (config.get_bool(CONFIG_IS_ENABLED)) autobutcher_cycle(out); @@ -843,7 +843,7 @@ static void autobutcher_setWatchListRace(color_ostream &out, unsigned id, unsign WatchedRace * w = new WatchedRace(out, id, watched, fk, mk, fa, ma); w->UpdateConfig(out); watched_races.emplace(id, w); - INFO(control,out).print("New race added to autobutcher watchlist: %s\n", + INFO(control,out).print("New race added to autobutcher watchlist: {}\n", Units::getRaceNamePluralById(id).c_str()); } diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index 7c2cfc9a5e..6b98d0347a 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -79,7 +79,7 @@ static PersistentDataItem & ensure_burrow_config(color_ostream &out, int id) { if (watched_burrows_indices.count(id)) return watched_burrows[watched_burrows_indices[id]]; string keyname = BURROW_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for burrow %d\n", id); + DEBUG(control,out).print("creating new persistent key for burrow {}\n", id); watched_burrows.emplace_back(World::GetPersistentSiteData(keyname, true)); size_t idx = watched_burrows.size()-1; watched_burrows_indices.emplace(id, idx); @@ -88,7 +88,7 @@ static PersistentDataItem & ensure_burrow_config(color_ostream &out, int id) { static void remove_burrow_config(color_ostream &out, int id) { if (!watched_burrows_indices.count(id)) return; - DEBUG(control,out).print("removing persistent key for burrow %d\n", id); + DEBUG(control,out).print("removing persistent key for burrow {}\n", id); size_t idx = watched_burrows_indices[id]; World::DeletePersistentData(watched_burrows[idx]); watched_burrows.erase(watched_burrows.begin()+idx); @@ -110,7 +110,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static int32_t do_cycle(color_ostream &out, bool force_designate = false); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -123,19 +123,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector frame_counter - cycle_timestamp >= CYCLE_TICKS) { int32_t designated = do_cycle(out); if (0 < designated) - out.print("autochop: designated %d tree(s) for chopping\n", designated); + out.print("autochop: designated {} tree(s) for chopping\n", designated); } return CR_OK; } static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -371,7 +371,7 @@ static int32_t scan_tree(color_ostream & out, df::plant *plant, int32_t *expecte map *designated_tree_counts, map &clearcut_burrows, map &chop_burrows) { - TRACE(cycle,out).print(" scanning tree at %d,%d,%d\n", + TRACE(cycle,out).print(" scanning tree at {},{},{}\n", plant->pos.x, plant->pos.y, plant->pos.z); if (!is_valid_tree(plant)) @@ -518,7 +518,7 @@ static void scan_logs(color_ostream &out, int32_t *usable_logs, *inaccessible_logs = 0; for (auto &item : world->items.other[items_other_id::IN_PLAY]) { - TRACE(cycle,out).print(" scanning log %d\n", item->id); + TRACE(cycle,out).print(" scanning log {}\n", item->id); if (item->flags.whole & bad_flags.whole) continue; @@ -538,7 +538,7 @@ static void scan_logs(color_ostream &out, int32_t *usable_logs, } static int32_t do_cycle(color_ostream &out, bool force_designate) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -580,7 +580,7 @@ static int32_t do_cycle(color_ostream &out, bool force_designate) { // of accessible trees int32_t needed = config.get_int(CONFIG_MAX_LOGS) - (usable_logs + expected_yield); - DEBUG(cycle,out).print("needed logs for this cycle: %d\n", needed); + DEBUG(cycle,out).print("needed logs for this cycle: {}\n", needed); for (auto & entry : designatable_trees_by_size) { if (!Designations::markPlant(entry.second)) continue; @@ -590,7 +590,7 @@ static int32_t do_cycle(color_ostream &out, bool force_designate) { return newly_marked; } } - out.print("autochop: insufficient accessible trees to reach log target! Still need %d logs!\n", + out.print("autochop: insufficient accessible trees to reach log target! Still need {} logs!\n", needed); return newly_marked; } @@ -623,8 +623,8 @@ static const char * get_protect_str(bool protect_brewable, bool protect_edible, static void autochop_printStatus(color_ostream &out) { DEBUG(control,out).print("entering autochop_printStatus\n"); validate_burrow_configs(out); - out.print("autochop is %s\n\n", is_enabled ? "enabled" : "disabled"); - out.print(" keeping log counts between %d and %d\n", + out.print("autochop is {}\n\n", is_enabled ? "enabled" : "disabled"); + out.print(" keeping log counts between {} and {}\n", config.get_int(CONFIG_MIN_LOGS), config.get_int(CONFIG_MAX_LOGS)); if (config.get_bool(CONFIG_WAITING_FOR_MIN)) out.print(" currently waiting for min threshold to be crossed before designating more trees\n"); @@ -643,19 +643,19 @@ static void autochop_printStatus(color_ostream &out) { &designated_trees, &accessible_yield, &tree_counts, &designated_tree_counts); out.print("summary:\n"); - out.print(" accessible logs (usable stock): %d\n", usable_logs); - out.print(" inaccessible logs: %d\n", inaccessible_logs); - out.print(" total visible logs: %d\n", usable_logs + inaccessible_logs); + out.print(" accessible logs (usable stock): {}\n", usable_logs); + out.print(" inaccessible logs: {}\n", inaccessible_logs); + out.print(" total visible logs: {}\n", usable_logs + inaccessible_logs); out.print("\n"); - out.print(" accessible trees: %d\n", accessible_trees); - out.print(" inaccessible trees: %d\n", inaccessible_trees); - out.print(" total visible trees: %d\n", accessible_trees + inaccessible_trees); + out.print(" accessible trees: {}\n", accessible_trees); + out.print(" inaccessible trees: {}\n", inaccessible_trees); + out.print(" total visible trees: {}\n", accessible_trees + inaccessible_trees); out.print("\n"); - out.print(" designated trees: %d\n", designated_trees); - out.print(" expected logs from designated trees: %d\n", expected_yield); - out.print(" expected logs from all accessible trees: %d\n", accessible_yield); + out.print(" designated trees: {}\n", designated_trees); + out.print(" expected logs from designated trees: {}\n", expected_yield); + out.print(" expected logs from all accessible trees: {}\n", accessible_yield); out.print("\n"); - out.print(" total trees harvested: %d\n", plotinfo->trees_removed); + out.print(" total trees harvested: {}\n", plotinfo->trees_removed); out.print("\n"); if (!plotinfo->burrows.list.size()) { @@ -671,9 +671,9 @@ static void autochop_printStatus(color_ostream &out) { } name_width = -name_width; // left justify - const char *fmt = "%*s %4s %4s %8s %5s %6s %7s\n"; - out.print(fmt, name_width, "burrow name", " id ", "chop", "clearcut", "trees", "marked", "protect"); - out.print(fmt, name_width, "-----------", "----", "----", "--------", "-----", "------", "-------"); + constexpr auto fmt = "{:{}} {:4} {:4} {:8} {:5} {:6} {:7}\n"; + out.print(fmt, "burrow name", name_width, " id ", "chop", "clearcut", "trees", "marked", "protect"); + out.print(fmt, "-----------", name_width, "----", "----", "--------", "-----", "------", "-------"); for (auto &burrow : plotinfo->burrows.list) { bool chop = false; @@ -689,17 +689,17 @@ static void autochop_printStatus(color_ostream &out) { protect_edible = c.get_bool(BURROW_CONFIG_PROTECT_EDIBLE); protect_cookable = c.get_bool(BURROW_CONFIG_PROTECT_COOKABLE); } - out.print(fmt, name_width, burrow->name.c_str(), int_to_string(burrow->id).c_str(), + out.print(fmt, burrow->name, name_width, burrow->id, chop ? "[x]" : "[ ]", clearcut ? "[x]" : "[ ]", - int_to_string(tree_counts[burrow->id]).c_str(), - int_to_string(designated_tree_counts[burrow->id]).c_str(), + tree_counts[burrow->id], + designated_tree_counts[burrow->id], get_protect_str(protect_brewable, protect_edible, protect_cookable)); } } static void autochop_designate(color_ostream &out) { DEBUG(control,out).print("entering autochop_designate\n"); - out.print("designated %d tree(s) for chopping\n", do_cycle(out, true)); + out.print("designated {} tree(s) for chopping\n", do_cycle(out, true)); } static void autochop_undesignate(color_ostream &out) { @@ -709,7 +709,7 @@ static void autochop_undesignate(color_ostream &out) { if (is_valid_tree(plant) && Designations::unmarkPlant(plant)) ++count; } - out.print("undesignated %d tree(s)\n", count); + out.print("undesignated {} tree(s)\n", count); } static void autochop_setTargets(color_ostream &out, int32_t max_logs, int32_t min_logs) { diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index a4516f4dce..89a45ee15f 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -237,7 +237,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { } is_enabled = enabled.get_bool(CONFIG_IS_ENABLED); - DEBUG(control, out).print("loading persisted enabled state: %s\n", + DEBUG(control, out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); // Parse constraints @@ -282,21 +282,21 @@ DFhackCExport command_result plugin_save_site_data(color_ostream &out) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { auto enabled = World::GetPersistentSiteData(CONFIG_KEY); is_enabled = enable; - DEBUG(control, out).print("%s from the API; persisting\n", + DEBUG(control, out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); enabled.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) do_autoclothing(); } else { - DEBUG(control, out).print("%s from the API, but already %s; no action\n", + DEBUG(control, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -410,7 +410,7 @@ static bool validateMaterialCategory(ClothingRequirement *requirement) { command_result autoclothing(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -527,7 +527,7 @@ static void find_needed_clothing_items() { { auto item = Items::findItemByID(ownedItem); if (!item) { - DEBUG(cycle).print("autoclothing: Invalid inventory item ID: %d\n", ownedItem); + DEBUG(cycle).print("autoclothing: Invalid inventory item ID: {}\n", ownedItem); continue; } @@ -683,7 +683,7 @@ static void generate_control(color_ostream &out) { { auto item = Items::findItemByID(itemId); if (!item) { - DEBUG(cycle, out).print("autoclothing: Invalid inventory item ID: %d\n", itemId); + DEBUG(cycle, out).print("autoclothing: Invalid inventory item ID: {}\n", itemId); continue; } else if (item->getWear() >= 1) diff --git a/plugins/autodump.cpp b/plugins/autodump.cpp index fbf3ff8a82..4376a888d0 100644 --- a/plugins/autodump.cpp +++ b/plugins/autodump.cpp @@ -170,7 +170,7 @@ static command_result autodump_main(color_ostream &out, vector ¶mete } } else - out.print("Could not move item: %s\n", Items::getDescription(itm, 0, true).c_str()); + out.print("Could not move item: {}\n", Items::getDescription(itm, 0, true)); } } else { // Destroy @@ -184,7 +184,7 @@ static command_result autodump_main(color_ostream &out, vector ¶mete dumped_total++; } - out.print("Done. %d items %s.\n", dumped_total, destroy ? "marked for destruction" : "quickdumped"); + out.print("Done. {} items {}.\n", dumped_total, destroy ? "marked for destruction" : "quickdumped"); return CR_OK; } diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index f961bc13a7..65a08123d9 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -200,9 +200,9 @@ class AutoFarm { if (old_plant_id != new_plant_id) { farm->plant_id[season] = new_plant_id; - INFO(cycle, out).print("autofarm: changing farm #%d from %s to %s\n", farm->id, - get_plant_name(old_plant_id).c_str(), - get_plant_name(new_plant_id).c_str()); + INFO(cycle, out).print("autofarm: changing farm #{} from {} to {}\n", farm->id, + get_plant_name(old_plant_id), + get_plant_name(new_plant_id)); } } @@ -390,11 +390,11 @@ class AutoFarm { if (plant != std::end(allPlants)) { setThreshold((*plant)->index, val); - INFO(control, out).print("threshold of %d for plant %s in saved configuration loaded\n", val, id.c_str()); + INFO(control, out).print("threshold of {} for plant {} in saved configuration loaded\n", val, id); } else { - WARN(control, out).print("threshold for unknown plant %s in saved configuration ignored\n", id.c_str()); + WARN(control, out).print("threshold for unknown plant {} in saved configuration ignored\n", id); } } } @@ -454,7 +454,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream& out) DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -513,7 +513,7 @@ static command_result setThresholds(color_ostream& out, std::vector static command_result autofarm(color_ostream& out, std::vector& parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } diff --git a/plugins/autogems.cpp b/plugins/autogems.cpp index 593e64670a..249552a006 100644 --- a/plugins/autogems.cpp +++ b/plugins/autogems.cpp @@ -314,7 +314,7 @@ bool read_config(color_ostream &out) { } } catch (Json::Exception &e) { - out.printerr("autogems: failed to read autogems.json: %s\n", e.what()); + out.printerr("autogems: failed to read autogems.json: {}\n", e.what()); return false; } @@ -326,7 +326,7 @@ bool read_config(color_ostream &out) { blacklist.insert(mat_index(item.asInt())); } else { - out.printerr("autogems: illegal item at position %i in blacklist\n", i); + out.printerr("autogems: illegal item at position {} in blacklist\n", i); } } } @@ -355,7 +355,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (enable != enabled) { if (!INTERPOSE_HOOK(autogem_hook, feed).apply(enable) || !INTERPOSE_HOOK(autogem_hook, render).apply(enable)) { - out.printerr("Could not %s autogem hooks!\n", enable? "insert": "remove"); + out.printerr("Could not {} autogem hooks!\n", enable? "insert": "remove"); return CR_FAILURE; } diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index d755202de6..d0cec796fb 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -697,8 +697,8 @@ static void assign_labor(unit_labor::unit_labor labor, dwarfs[dwarf]->uniform.pickup_flags.bits.update = 1; } - TRACE(cycle, out).print("Dwarf % i \"%s\" assigned %s: value %i %s %s\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, labor).c_str(), values[dwarf], + TRACE(cycle, out).print("Dwarf {} \"{}\" assigned {}: value {} {} {}\n", + dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor), values[dwarf], dwarf_info[dwarf].trader ? "(trader)" : "", dwarf_info[dwarf].diplomacy ? "(diplomacy)" : ""); @@ -766,7 +766,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { df::building_tradedepotst* depot = (df::building_tradedepotst*) build; trader_requested = trader_requested || depot->trade_flags.bits.trader_requested; - TRACE(cycle,out).print(trader_requested + TRACE(cycle,out).print("{}", trader_requested ? "Trade depot found and trader requested, trader will be excluded from all labors.\n" : "Trade depot found but trader is not requested.\n" ); @@ -846,8 +846,8 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) if (p1 || p2) { dwarf_info[dwarf].diplomacy = true; - DEBUG(cycle, out).print("Dwarf %i \"%s\" has a meeting, will be cleared of all labors\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str()); + DEBUG(cycle, out).print("Dwarf {} \"{}\" has a meeting, will be cleared of all labors\n", + dwarf, dwarfs[dwarf]->name.first_name); break; } } @@ -919,15 +919,15 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) dwarf_info[dwarf].state = dwarf_states[job]; else { - WARN(cycle, out).print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job); + WARN(cycle, out).print("Dwarf {} \"{}\" has unknown job {}\n", dwarf, dwarfs[dwarf]->name.first_name, job); dwarf_info[dwarf].state = OTHER; } } state_count[dwarf_info[dwarf].state]++; - TRACE(cycle, out).print("Dwarf %i \"%s\": penalty %i, state %s\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), dwarf_info[dwarf].mastery_penalty, state_names[dwarf_info[dwarf].state]); + TRACE(cycle, out).print("Dwarf {} \"{}\": penalty {}, state {}\n", + dwarf, dwarfs[dwarf]->name.first_name, dwarf_info[dwarf].mastery_penalty, state_names[dwarf_info[dwarf].state]); } std::vector labors; @@ -1034,8 +1034,8 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) labor_infos[labor].active_dwarfs++; - TRACE(cycle, out).print("Dwarf %i \"%s\" assigned %s: hauler\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, labor).c_str()); + TRACE(cycle, out).print("Dwarf {} \"{}\" assigned {}: hauler\n", + dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor)); } for (size_t i = num_haulers; i < hauler_ids.size(); i++) @@ -1076,7 +1076,7 @@ void print_labor (df::unit_labor labor, color_ostream &out) DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable ) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -1095,7 +1095,7 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable ) command_result autolabor (color_ostream &out, std::vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -1137,7 +1137,7 @@ command_result autolabor (color_ostream &out, std::vector & parame if (labor == unit_labor::NONE) { - out.printerr("Could not find labor %s.\n", parameters[0].c_str()); + out.printerr("Could not find labor {}.\n", parameters[0]); return CR_WRONG_USAGE; } @@ -1171,7 +1171,7 @@ command_result autolabor (color_ostream &out, std::vector & parame if (maximum < minimum || maximum < 0 || minimum < 0) { - out.printerr("Syntax: autolabor [] [], %d > %d\n", maximum, minimum); + out.printerr("Syntax: autolabor [] [], {} > {}\n", maximum, minimum); return CR_WRONG_USAGE; } @@ -1235,7 +1235,7 @@ command_result autolabor (color_ostream &out, std::vector & parame { out.print("Automatically assigns labors to dwarves.\n" "Activate with 'enable autolabor', deactivate with 'disable autolabor'.\n" - "Current state: %d.\n", enable_autolabor); + "Current state: {}.\n", enable_autolabor); return CR_OK; } diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index f7f1bdd84b..cc45507b2b 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -63,17 +63,17 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -235,9 +235,9 @@ static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building_ci unit->general_refs.push_back(ref); zone->assigned_units.push_back(unit->id); - INFO(cycle,out).print("Unit %d (%s) assigned to nestbox zone %d (%s)\n", - unit->id, Units::getRaceName(unit).c_str(), - zone->id, zone->name.c_str()); + INFO(cycle,out).print("Unit {} ({}) assigned to nestbox zone {} ({})\n", + unit->id, Units::getRaceName(unit), + zone->id, zone->name); return true; } @@ -247,13 +247,13 @@ static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building_ci static size_t getFreeNestboxZones(color_ostream &out, vector &free_zones) { size_t assigned = 0; for (auto zone : world->buildings.other.ZONE_PEN) { - TRACE(cycle,out).print("scanning pasture %d (%s)\n", zone->id, zone->name.c_str()); + TRACE(cycle,out).print("scanning pasture {} ({})\n", zone->id, zone->name); if (!Buildings::isActive(zone)) { - TRACE(cycle,out).print("pasture %d is inactive\n", zone->id); + TRACE(cycle,out).print("pasture {} is inactive\n", zone->id); continue; } if (!isEmptyPasture(zone)) { - TRACE(cycle,out).print("pasture %d is not empty\n", zone->id); + TRACE(cycle,out).print("pasture {} is not empty\n", zone->id); continue; } @@ -262,23 +262,23 @@ static size_t getFreeNestboxZones(color_ostream &out, vectorx1, zone->y1, zone->z); auto bld = Buildings::findAtTile(pos); if (!bld || bld->getType() != df::building_type::NestBox) { - TRACE(cycle,out).print("pasture %d does not have nestbox in upper left corner\n", zone->id); + TRACE(cycle,out).print("pasture {} does not have nestbox in upper left corner\n", zone->id); continue; } - TRACE(cycle,out).print("found nestbox %d in pasture %d\n", bld->id, zone->id); + TRACE(cycle,out).print("found nestbox {} in pasture {}\n", bld->id, zone->id); df::building_nest_boxst *nestbox = virtual_cast(bld); if (!nestbox) { - TRACE(cycle,out).print("nestbox %d is somehow not a nestbox\n", bld->id); + TRACE(cycle,out).print("nestbox {} is somehow not a nestbox\n", bld->id); continue; } if (nestbox->claimed_by >= 0) { if (auto unit = df::unit::find(nestbox->claimed_by)) { - TRACE(cycle,out).print("nestbox %d is claimed by unit %d (%s)\n", bld->id, - nestbox->claimed_by, Units::getReadableName(unit).c_str()); + TRACE(cycle,out).print("nestbox {} is claimed by unit {} ({})\n", bld->id, + nestbox->claimed_by, Units::getReadableName(unit)); if (!isFreeEgglayer(unit)) { - DEBUG(cycle,out).print("cannot assign unit %d to nestbox %d: not a free egg layer\n", unit->id, bld->id); + DEBUG(cycle,out).print("cannot assign unit {} to nestbox {}: not a free egg layer\n", unit->id, bld->id); } else { // if the nestbox is claimed by a free egg layer, attempt to assign that unit to the zone if (assignUnitToZone(out, unit, zone)) @@ -327,7 +327,7 @@ static size_t assign_nestboxes(color_ostream &out) { DEBUG(cycle,out).print("Failed to assign unit to building.\n"); return assigned; } - DEBUG(cycle,out).print("assigned unit %d to zone %d\n", + DEBUG(cycle,out).print("assigned unit {} to zone {}\n", free_units[idx]->id, free_zones[idx]->id); ++assigned; } diff --git a/plugins/autoslab.cpp b/plugins/autoslab.cpp index f8f93ac747..08c4c837d1 100644 --- a/plugins/autoslab.cpp +++ b/plugins/autoslab.cpp @@ -53,7 +53,7 @@ static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control, out).print("initializing %s\n", plugin_name); + DEBUG(control, out).print("initializing {}\n", plugin_name); return CR_OK; } @@ -62,28 +62,28 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control, out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) do_cycle(out); } else { - DEBUG(control, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control, out).print("shutting down %s\n", plugin_name); + DEBUG(control, out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -104,7 +104,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) // all the other state we can directly read/modify from the persistent // data structure. is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control, out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); + DEBUG(control, out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; } @@ -114,7 +114,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan { if (is_enabled) { - DEBUG(control, out).print("world unloaded; disabling %s\n", plugin_name); + DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } } @@ -174,7 +174,7 @@ static void checkslabs(color_ostream &out) { createSlabJob(ghost); auto fullName = Units::getReadableName(ghost); - out.print("Added slab order for %s\n", DF2CONSOLE(fullName).c_str()); + out.print("Added slab order for {}\n", DF2CONSOLE(fullName)); } } } diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 98b4c1c155..e3a728e2d8 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -1375,8 +1375,8 @@ static bool create_output_dir(color_ostream &out, // create output directory if it doesn't already exist if (!Filesystem::mkdir_recursive(parent_path)) { - out.printerr("could not create output directory: '%s'\n", - parent_path.c_str()); + out.printerr("could not create output directory: '{}'\n", + parent_path); return false; } return true; @@ -1666,7 +1666,7 @@ static command_result do_blueprint(color_ostream &out, command << " " << parameters[i]; } string command_str = command.str(); - out.print("launching %s\n", command_str.c_str()); + out.print("launching {}\n", command_str); Core::getInstance().setHotkeyCmd(command_str); return CR_OK; @@ -1694,7 +1694,7 @@ static command_result do_blueprint(color_ostream &out, } } if (!Maps::isValidTilePos(start)) { - out.printerr("Invalid start position: %d,%d,%d\n", + out.printerr("Invalid start position: {},{},{},\n", start.x, start.y, start.z); return CR_FAILURE; } @@ -1757,7 +1757,7 @@ command_result blueprint(color_ostream &out, vector ¶meters) { else { out.print("Generated blueprint file(s):\n"); for (string &fname : files) - out.print(" %s\n", fname.c_str()); + out.print(" {}\n", fname); } } return cr; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 64ddf89e34..6f5a13a8c7 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -64,7 +64,7 @@ static Tasks tasks; // planned_buildings, then it has either been built or desroyed. therefore there is // no chance of duplicate tasks getting added to the tasks queues. void PlannedBuilding::remove(color_ostream &out) { - DEBUG(control,out).print("removing persistent data for building %d\n", id); + DEBUG(control,out).print("removing persistent data for building {}\n", id); World::DeletePersistentData(bld_config); planned_buildings.erase(id); } @@ -97,8 +97,8 @@ static const vector & get_job_items(color_ostream &out, Bu std::make_tuple(std::get<0>(key), std::get<1>(key), std::get<2>(key), index+1), 1, [&](lua_State *L) { df::job_item *jitem = Lua::GetDFObject(L, -1); - DEBUG(control,out).print("retrieving job_item for (%d, %d, %d) index=%d: 0x%p\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem); + DEBUG(control,out).print("retrieving job_item for {} index={}: {}\n", + key, index, static_cast(jitem)); if (!jitem) failed = true; else @@ -125,35 +125,35 @@ static void cache_matched(int16_t type, int32_t index) { MaterialInfo mi; mi.decode(type, index); if (mi.matches(stone_cat)) { - DEBUG(control).print("cached stone material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached stone material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "stone")); } else if (mi.matches(wood_cat)) { - DEBUG(control).print("cached wood material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached wood material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "wood")); } else if (mi.matches(metal_cat)) { - DEBUG(control).print("cached metal material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached metal material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "metal")); } else if (mi.matches(glass_cat)) { - DEBUG(control).print("cached glass material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached glass material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "glass")); } else if (mi.matches(gem_cat)) { - DEBUG(control).print("cached gem material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached gem material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "gem")); } else if (mi.matches(clay_cat)) { - DEBUG(control).print("cached clay material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached clay material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "clay")); } else if (mi.matches(cloth_cat)) { - DEBUG(control).print("cached cloth material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached cloth material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "cloth")); } else if (mi.matches(silk_cat)) { - DEBUG(control).print("cached silk material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached silk material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "silk")); } else if (mi.matches(yarn_cat)) { - DEBUG(control).print("cached yarn material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached yarn material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "yarn")); } else - TRACE(control).print("not matched: %s\n", mi.toString().c_str()); + TRACE(control).print("not matched: {}\n", mi.toString()); } static void load_organic_material_cache(df::organic_mat_category cat) { @@ -201,7 +201,7 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bool unsuspend_on_finalize); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -214,12 +214,12 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector getType(), get_subtype(bld), bld->getCustomType()); if (pb.item_filters.size() != get_item_filters(out, key).getItemFilters().size()) { - WARN(control).print("loaded state for building %d doesn't match world\n", pb.id); + WARN(control).print("loaded state for building {} doesn't match world\n", pb.id); pb.remove(out); continue; } @@ -354,7 +354,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot configure %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot configure {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -428,8 +428,8 @@ vector getVectorIds(color_ostream &out, const df::job_it // if the filter already has the vector_id set to something specific, use it if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) { - DEBUG(control,out).print("using vector_id from job_item: %s\n", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); + DEBUG(control,out).print("using vector_id from job_item: {}\n", + ENUM_KEY_STR(job_item_vector_id, job_item->vector_id)); ret.push_back(job_item->vector_id); return ret; } @@ -460,7 +460,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bo return false; if (bld->jobs.size() != 1) { - DEBUG(control,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); + DEBUG(control,out).print("unexpected number of jobs: want 1, got {}\n", bld->jobs.size()); return false; } @@ -489,10 +489,10 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bo for (auto vector_id : pb.vector_ids[job_item_idx]) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { tasks[vector_id][bucket].emplace_back(id, rev_jitem_index); - DEBUG(control,out).print("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, rev_jitem_index, tasks.size(), + DEBUG(control,out).print("added task: {}/{}/{}, {}; " + "{} vector(s), {} filter bucket(s), {} task(s) in bucket\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket, id, rev_jitem_index, tasks.size(), tasks[vector_id].size(), tasks[vector_id][bucket].size()); } } @@ -520,16 +520,16 @@ static string get_desc_string(color_ostream &out, df::job_item *jitem, static void printStatus(color_ostream &out) { DEBUG(control,out).print("entering buildingplan_printStatus\n"); - out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); + out.print("buildingplan is {}\n\n", is_enabled ? "enabled" : "disabled"); out.print("Current settings:\n"); - out.print(" use blocks: %s\n", config.get_bool(CONFIG_BLOCKS) ? "yes" : "no"); - out.print(" use boulders: %s\n", config.get_bool(CONFIG_BOULDERS) ? "yes" : "no"); - out.print(" use logs: %s\n", config.get_bool(CONFIG_LOGS) ? "yes" : "no"); - out.print(" use bars: %s\n", config.get_bool(CONFIG_BARS) ? "yes" : "no"); - out.print(" plan constructions on tiles with existing constructed floors/ramps when using box select: %s\n", + out.print(" use blocks: {}\n", config.get_bool(CONFIG_BLOCKS) ? "yes" : "no"); + out.print(" use boulders: {}\n", config.get_bool(CONFIG_BOULDERS) ? "yes" : "no"); + out.print(" use logs: {}\n", config.get_bool(CONFIG_LOGS) ? "yes" : "no"); + out.print(" use bars: {}\n", config.get_bool(CONFIG_BARS) ? "yes" : "no"); + out.print(" plan constructions on tiles with existing constructed floors/ramps when using box select: {}\n", config.get_bool(CONFIG_RECONSTRUCT) ? "yes" : "no"); auto burrow = df::burrow::find(config.get_int(CONFIG_BURROW)); - out.print(" ignore building materials in burrow: %s\n", burrow ? burrow->name.c_str() : "none"); + out.print(" ignore building materials in burrow: {}\n", burrow ? burrow->name.c_str() : "none"); out.print("\n"); size_t bld_count = 0; @@ -560,10 +560,10 @@ static void printStatus(color_ostream &out) { } if (bld_count) { - out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", + out.print("Waiting for {} item(s) to be produced for {} building(s):\n", total, bld_count); for (auto &count : counts) - out.print(" %3d %s%s\n", count.second, count.first.c_str(), count.second == 1 ? "" : "s"); + out.print(" {:3} {}{}\n", count.second, count.first, count.second == 1 ? "" : "s"); } else { out.print("Currently no planned buildings\n"); } @@ -571,7 +571,7 @@ static void printStatus(color_ostream &out) { } static bool setSetting(color_ostream &out, string name, bool value) { - DEBUG(control,out).print("entering setSetting (%s -> %s)\n", name.c_str(), value ? "true" : "false"); + DEBUG(control,out).print("entering setSetting ({} -> {})\n", name, value ? "true" : "false"); if (name == "blocks") config.set_bool(CONFIG_BLOCKS, value); else if (name == "boulders") @@ -583,7 +583,7 @@ static bool setSetting(color_ostream &out, string name, bool value) { else if (name == "reconstruct") config.set_bool(CONFIG_RECONSTRUCT, value); else { - out.printerr("unrecognized setting: '%s'\n", name.c_str()); + out.printerr("unrecognized setting: '{}'\n", name); return false; } @@ -654,8 +654,8 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ vector *item_ids = NULL, map *counts = NULL) { DEBUG(control,out).print( - "entering scanAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering scanAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = heat_override ? *heat_override : get_heat_safety_filter(key); auto &job_items = get_job_items(out, key); @@ -700,7 +700,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ } } - DEBUG(control,out).print("found matches %d\n", count); + DEBUG(control,out).print("found matches {}\n", count); return count; } @@ -713,8 +713,8 @@ static int getAvailableItems(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); vector item_ids; scanAvailableItems(*out, type, subtype, custom, index, true, false, NULL, &item_ids); Lua::PushVector(L, item_ids); @@ -731,8 +731,8 @@ static int getAvailableItemsByHeat(lua_State *L) { int index = luaL_checkint(L, 4); HeatSafety heat = (HeatSafety)luaL_checkint(L, 5); DEBUG(control,*out).print( - "entering getAvailableItemsByHeat building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getAvailableItemsByHeat building_type={} subtype={} custom={} index={} heat={}\n", + ENUM_AS_STR(type), subtype, custom, index, static_cast(heat)); vector item_ids; scanAvailableItems(*out, type, subtype, custom, index, true, true, &heat, &item_ids); Lua::PushVector(L, item_ids); @@ -756,8 +756,8 @@ static int getGlobalSettings(lua_State *L) { static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(control,out).print( - "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering countAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); int count = scanAvailableItems(out, type, subtype, custom, index, false, false); if (count) return count; @@ -816,8 +816,8 @@ static int setMaterialMaskFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering setMaterialMaskFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering setMaterialMaskFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -846,8 +846,8 @@ static int setMaterialMaskFilter(lua_State *L) { mask |= yarn_cat.whole; } DEBUG(control,*out).print( - "setting material mask filter for building_type=%d subtype=%d custom=%d index=%d to %x\n", - type, subtype, custom, index, mask); + "setting material mask filter for building_type={} subtype={} custom={} index={} to {:x}\n", + ENUM_AS_STR(type), subtype, custom, index, mask); ItemFilter filter = filters[index]; filter.setMaterialMask(mask); set new_mats; @@ -875,8 +875,8 @@ static int getMaterialMaskFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key); if (index < 0 || filters.getItemFilters().size() <= (size_t)index) @@ -906,8 +906,8 @@ static int setMaterialFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering setMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering setMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -920,8 +920,8 @@ static int setMaterialFilter(lua_State *L) { mats.emplace(mat_cache.at(mat).first); } DEBUG(control,*out).print( - "setting material filter for building_type=%d subtype=%d custom=%d index=%d to %zd materials\n", - type, subtype, custom, index, mats.size()); + "setting material filter for building_type={} subtype={} custom={} index={} to {} materials\n", + ENUM_AS_STR(type), subtype, custom, index, mats.size()); ItemFilter filter = filters[index]; filter.setMaterials(mats); // ensure relevant masks are explicitly enabled @@ -963,8 +963,8 @@ static int getMaterialFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -1004,8 +1004,8 @@ static int getMaterialFilter(lua_State *L) { static void setChooseItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int choose) { DEBUG(control,out).print( - "entering setChooseItems building_type=%d subtype=%d custom=%d choose=%d\n", - type, subtype, custom, choose); + "entering setChooseItems building_type={} subtype={} custom={} choose={}\n", + ENUM_AS_STR(type), subtype, custom, choose); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key); filters.setChooseItems(choose); @@ -1020,8 +1020,8 @@ static int getChooseItems(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getChooseItems building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getChooseItems building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); Lua::Push(L, get_item_filters(*out, key).getChooseItems()); return 1; @@ -1045,8 +1045,8 @@ static int getHeatSafetyFilter(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getHeatSafetyFilter building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getHeatSafetyFilter building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = get_heat_safety_filter(key); Lua::Push(L, heat); @@ -1069,8 +1069,8 @@ static int getSpecials(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getSpecials building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getSpecials building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); Lua::Push(L, get_item_filters(*out, key).getSpecials()); return 1; @@ -1100,8 +1100,8 @@ static int getQualityFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getQualityFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getQualityFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 1a97a801de..7e8187dfce 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -77,7 +77,7 @@ static bool isAccessible(color_ostream& out, df::item* item) { df::coord item_pos = Items::getPosition(item); uint16_t walkability_group = Maps::getWalkableGroup(item_pos); bool is_walkable = accessible_walkability_groups.contains(walkability_group); - TRACE(cycle, out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", + TRACE(cycle, out).print("item {} in walkability_group {} at ({},{},{}) is {}accessible from job site\n", item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "(probably) " : "not "); return is_walkable; } @@ -192,7 +192,7 @@ bool isJobReady(color_ostream &out, const std::vector &jitems) { int needed_items = 0; for (auto job_item : jitems) { needed_items += job_item->quantity; } if (needed_items) { - DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); + DEBUG(cycle,out).print("building needs {} more item(s)\n", needed_items); return false; } return true; @@ -208,7 +208,7 @@ static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { // remove them to keep the "finalize with buildingplan active" path as similar // as possible to the "finalize with buildingplan disabled" path. void finalizeBuilding(color_ostream &out, df::building *bld, bool unsuspend_on_finalize) { - DEBUG(cycle,out).print("finalizing building %d\n", bld->id); + DEBUG(cycle,out).print("finalizing building {}\n", bld->id); auto job = bld->jobs[0]; // sort the items so they get added to the structure in the correct order @@ -255,7 +255,7 @@ static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, if (bld && bld->jobs[0]->job_items.elements[task.second]->quantity) return bld; } - DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); + DEBUG(cycle,out).print("discarding invalid task: bld={}, job_item_idx={}\n", id, task.second); task_queue.pop_front(); } return NULL; @@ -273,9 +273,9 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); const auto item_vector = df::global::world->items.other[other_id]; - DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", + DEBUG(cycle,out).print("matching {} item(s) in vector {} against {} filter bucket(s)\n", item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + ENUM_KEY_STR(job_item_vector_id, vector_id), buckets.size()); // items we might want to attach (and their positions) @@ -287,12 +287,12 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, std::vector> matching; size_t num_matching = 0; - DEBUG(cycle,out).print("%zu items available for assignment\n", available.size()); + DEBUG(cycle,out).print("{} items available for assignment\n", available.size()); for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { - TRACE(cycle,out).print("scanning bucket: %s/%s\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), bucket_it->first.c_str()); + TRACE(cycle,out).print("scanning bucket: {}/{}\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), bucket_it->first); auto & task_queue = bucket_it->second; bool first_task = true; @@ -322,7 +322,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, num_matching = matching.size(); first_task = false; - TRACE(cycle,out).print("first task in bucket: found %zu matching items\n", + TRACE(cycle,out).print("first task in bucket: found {} matching items\n", num_matching); } // every task: find and attach closest matching item (if any) @@ -342,15 +342,15 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, material.decode(item); ItemTypeInfo item_type; item_type.decode(item); - DEBUG(cycle,out).print("attached %s %s (distance %d) to filter %d for %s(%d): %s/%s\n", - material.toString().c_str(), - item_type.toString().c_str(), + DEBUG(cycle,out).print("attached {} {} (distance {}) to filter {} for {}({}): {}/{}\n", + material.toString(), + item_type.toString(), distance(closest->first, jpos), filter_idx, - ENUM_KEY_STR(building_type, bld->getType()).c_str(), + ENUM_KEY_STR(building_type, bld->getType()), id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket_it->first); // clean up fulfilled task task_queue.pop_front(); // keep quantity aligned with the actual number of remaining @@ -373,10 +373,10 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, if (task_queue.empty()) { DEBUG(cycle,out).print( - "removing empty item bucket: %s/%s; %zu left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); + "removing empty item bucket: {}/{}; {} left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket_it->first, + buckets.size() - 1); bucket_it = buckets.erase(bucket_it); } else { ++bucket_it; @@ -420,8 +420,8 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, auto & buckets = it->second; doVector(out, vector_id, buckets, planned_buildings, unsuspend_on_finalize); if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + DEBUG(cycle,out).print("removing empty vector: {}; {} vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), tasks.size() - 1); it = tasks.erase(it); } @@ -434,12 +434,12 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, auto & buckets = tasks[vector_id]; doVector(out, vector_id, buckets, planned_buildings, unsuspend_on_finalize); if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + DEBUG(cycle,out).print("removing empty vector: {}; {} vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), tasks.size() - 1); tasks.erase(vector_id); } } - DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", + DEBUG(cycle,out).print("cycle done; {} registered building(s) left\n", planned_buildings.size()); } diff --git a/plugins/buildingplan/buildingtypekey.cpp b/plugins/buildingplan/buildingtypekey.cpp index 710878d643..e8136b8896 100644 --- a/plugins/buildingplan/buildingtypekey.cpp +++ b/plugins/buildingplan/buildingtypekey.cpp @@ -21,7 +21,7 @@ static BuildingTypeKey deserialize(color_ostream &out, const std::string &serial vector key_parts; split_string(&key_parts, serialized, ","); if (key_parts.size() != 3) { - WARN(control,out).print("invalid key_str: '%s'\n", serialized.c_str()); + WARN(control,out).print("invalid key_str: '{}'\n", serialized); return BuildingTypeKey(df::building_type::NONE, -1, -1); } return BuildingTypeKey((df::building_type)string_to_int(key_parts[0]), diff --git a/plugins/buildingplan/buildingtypekey.h b/plugins/buildingplan/buildingtypekey.h index 81bb043c55..5b21b6cc2c 100644 --- a/plugins/buildingplan/buildingtypekey.h +++ b/plugins/buildingplan/buildingtypekey.h @@ -17,6 +17,17 @@ struct BuildingTypeKey : public std::tuple std::string serialize() const; }; +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const BuildingTypeKey& key, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {}, {})", ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key)), ctx); + } +}; + struct BuildingTypeKeyHash { std::size_t operator() (const BuildingTypeKey & key) const; }; diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp index eaa11335d0..0e1cab8548 100644 --- a/plugins/buildingplan/defaultitemfilters.cpp +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -40,8 +40,8 @@ static string serialize(const std::vector &item_filters, const std:: DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, const std::vector &jitems) : key(key), choose_items(ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER) { - DEBUG(control,out).print("creating persistent data for filter key %d,%d,%d\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key)); + DEBUG(control,out).print("creating persistent data for filter key {},{},{}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key)); filter_config = World::AddPersistentSiteData(FILTER_CONFIG_KEY); filter_config.set_int(FILTER_CONFIG_TYPE, std::get<0>(key)); filter_config.set_int(FILTER_CONFIG_SUBTYPE, std::get<1>(key)); @@ -61,16 +61,16 @@ DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &f choose_items > ItemSelectionChoice::ITEM_SELECTION_CHOICE_AUTOMATERIAL) choose_items = ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER; auto &serialized = filter_config.get_str(); - DEBUG(control,out).print("deserializing default item filters for key %d,%d,%d: %s\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + DEBUG(control,out).print("deserializing default item filters for key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), serialized); if (!jitems.size()) return; std::vector elems; split_string(&elems, serialized, "|"); std::vector filters = deserialize_item_filters(out, elems[0]); if (filters.size() != jitems.size()) { - WARN(control,out).print("ignoring invalid filters_str for key %d,%d,%d: '%s'\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + WARN(control,out).print("ignoring invalid filters_str for key {},{},{}: '{}'\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), serialized); item_filters.resize(jitems.size()); } else item_filters = filters; @@ -99,13 +99,13 @@ void DefaultItemFilters::setSpecial(const std::string &special, bool val) { void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { if (index < 0 || item_filters.size() <= (size_t)index) { - WARN(control,out).print("invalid index for filter key %d,%d,%d: %d\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), index); + WARN(control,out).print("invalid index for filter key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), index); return; } item_filters[index] = filter; filter_config.set_str( serialize(item_filters, specials)); - DEBUG(control,out).print("updated item filter and persisted for key %d,%d,%d: %s\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), filter_config.get_str().c_str()); + DEBUG(control,out).print("updated item filter and persisted for key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), filter_config.get_str()); } diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index 0e4569f156..09a4a15c8e 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -41,7 +41,7 @@ static bool deserializeMaterialMask(const string& ser, df::dfhack_material_categ return true; if (!parseJobMaterialCategory(&mat_mask, ser)) { - DEBUG(control).print("invalid job material category serialization: '%s'", ser.c_str()); + DEBUG(control).print("invalid job material category serialization: '{}'", ser); return false; } return true; @@ -56,7 +56,7 @@ static bool deserializeMaterials(const string& ser, set &m for (auto m = mat_names.begin(); m != mat_names.end(); m++) { DFHack::MaterialInfo material; if (!material.find(*m) || !material.isValid()) { - DEBUG(control).print("invalid material name serialization: '%s'", ser.c_str()); + DEBUG(control).print("invalid material name serialization: '{}'", ser); return false; } materials.emplace(material); @@ -68,7 +68,7 @@ ItemFilter::ItemFilter(color_ostream &out, const string& serialized) : ItemFilte vector tokens; split_string(&tokens, serialized, "/"); if (tokens.size() < 5) { - DEBUG(control,out).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + DEBUG(control,out).print("invalid ItemFilter serialization: '{}'", serialized); return; } @@ -155,8 +155,8 @@ bool ItemFilter::matches(DFHack::MaterialInfo &material) const { bool ItemFilter::matches(df::item *item) const { int16_t quality = (item->flags.bits.artifact ? df::item_quality::Artifact : item->getQuality()); if (quality < min_quality || quality > max_quality) { - TRACE(cycle).print("item outside of quality range (%d not between %d and %d)\n", - quality, min_quality, max_quality); + TRACE(cycle).print("item outside of quality range ({} not between {} and {})\n", + ENUM_AS_STR(static_cast(quality)), ENUM_AS_STR(min_quality), ENUM_AS_STR(max_quality)); return false; } diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index 5888d169e7..e0f5a8984e 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -41,8 +41,8 @@ static vector> deserialize_vector_ids(color_ostre split_string(&rawstrs, bld_config.get_str(), "|"); const string &serialized = rawstrs[0]; - DEBUG(control,out).print("deserializing vector ids for building %d: %s\n", - bld_config.get_int(BLD_CONFIG_ID), serialized.c_str()); + DEBUG(control,out).print("deserializing vector ids for building {}: {}\n", + bld_config.get_int(BLD_CONFIG_ID), serialized); vector joined; split_string(&joined, serialized, ";"); @@ -100,12 +100,12 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafe : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat), item_filters(item_filters.getItemFilters()), specials(item_filters.getSpecials()) { - DEBUG(control,out).print("creating persistent data for building %d\n", id); + DEBUG(control,out).print("creating persistent data for building {}\n", id); bld_config = World::AddPersistentSiteData(BLD_CONFIG_KEY); bld_config.set_int(BLD_CONFIG_ID, id); bld_config.set_int(BLD_CONFIG_HEAT, heat_safety); bld_config.set_str(serialize(vector_ids, item_filters)); - DEBUG(control,out).print("serialized state for building %d: %s\n", id, bld_config.get_str().c_str()); + DEBUG(control,out).print("serialized state for building {}: {}\n", id, bld_config.get_str()); } PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp index b016014d0c..172a7d37d2 100644 --- a/plugins/burrow.cpp +++ b/plugins/burrow.cpp @@ -47,7 +47,7 @@ static void jobStartedHandler(color_ostream& out, void* ptr); static void jobCompletedHandler(color_ostream& out, void* ptr); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(status, out).print("initializing %s\n", plugin_name); + DEBUG(status, out).print("initializing {}\n", plugin_name); commands.push_back( PluginCommand("burrow", "Quickly adjust burrow tiles and units.", @@ -62,7 +62,7 @@ static void reset() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(status, out).print("%s from the API\n", is_enabled ? "enabled" : "disabled"); + DEBUG(status, out).print("{} from the API\n", is_enabled ? "enabled" : "disabled"); reset(); if (enable) { init_diggers(out); @@ -73,13 +73,13 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } } else { - DEBUG(status, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); + DEBUG(status, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(status, out).print("shutting down %s\n", plugin_name); + DEBUG(status, out).print("shutting down {}\n", plugin_name); reset(); return CR_OK; } @@ -92,7 +92,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -136,8 +136,8 @@ static void jobStartedHandler(color_ostream& out, void* ptr) { return; const df::coord &pos = job->pos; - DEBUG(event, out).print("dig job started: id=%d, pos=(%d,%d,%d), type=%s\n", - job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type).c_str()); + DEBUG(event, out).print("dig job started: id={}, pos=({}, {}, {}), type={}\n", + job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type)); df::tiletype *tt = Maps::getTileType(pos); if (tt) active_dig_jobs[pos] = *tt; @@ -199,8 +199,8 @@ static void jobCompletedHandler(color_ostream& out, void* ptr) { return; const df::coord &pos = job->pos; - DEBUG(event, out).print("dig job completed: id=%d, pos=(%d,%d,%d), type=%s\n", - job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type).c_str()); + DEBUG(event, out).print("dig job completed: id={}, pos=({}, {}, {}), type={}\n", + job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type)); df::tiletype prev_tt = active_dig_jobs[pos]; df::tiletype *tt = Maps::getTileType(pos); @@ -544,7 +544,7 @@ static void flood_fill(lua_State *L, bool enable) { bool start_outside = is_outside(start_pos, start_des); bool start_hidden = start_des->bits.hidden; uint16_t start_walk = Maps::getWalkableGroup(start_pos); - DEBUG(status).print("starting pos: (%d,%d,%d); outside: %d; hidden: %d\n", + DEBUG(status).print("starting pos: ({},{},{}); outside: {}; hidden: {}\n", start_pos.x, start_pos.y, start_pos.z, start_outside, start_hidden); std::stack flood; @@ -554,7 +554,7 @@ static void flood_fill(lua_State *L, bool enable) { const df::coord pos = flood.top(); flood.pop(); - TRACE(status).print("pos: (%d,%d,%d)\n", pos.x, pos.y, pos.z); + TRACE(status).print("pos: ({},{},{})\n", pos.x, pos.y, pos.z); df::tile_designation *des = Maps::getTileDesignation(pos); if (!des || diff --git a/plugins/changeitem.cpp b/plugins/changeitem.cpp index 33387aa141..e07db44570 100644 --- a/plugins/changeitem.cpp +++ b/plugins/changeitem.cpp @@ -236,7 +236,7 @@ command_result df_changeitem(color_ostream &out, vector & parameters) changeitem_execute(out, item, info, force, change_material, new_material, change_quality, new_quality, change_subtype, new_subtype); processed_total++; } - out.print("Done. %d items processed.\n", processed_total); + out.print("Done. {} items processed.\n", processed_total); } else { diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index c1a5b5953f..71cfdddb8d 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -111,7 +111,7 @@ void ChannelGroups::add(const df::coord &map_pos) { // we already have group "prime" if you will, so we're going to merge the new find into prime Group &group2 = groups.at(index2); // merge - TRACE(groups).print(" -> merging two groups. group 1 size: %zu. group 2 size: %zu\n", group->size(), + TRACE(groups).print(" -> merging two groups. group 1 size: {}. group 2 size: {}\n", group->size(), group2.size()); for (auto pos2: group2) { group->emplace(pos2); @@ -119,7 +119,7 @@ void ChannelGroups::add(const df::coord &map_pos) { } group2.clear(); free_spots.emplace(index2); - TRACE(groups).print(" merged size: %zu\n", group->size()); + TRACE(groups).print(" merged size: {}\n", group->size()); } } } @@ -143,13 +143,13 @@ void ChannelGroups::add(const df::coord &map_pos) { } // puts the "add" in "ChannelGroups::add" group->emplace(map_pos); - DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size()); + DEBUG(groups).print(" = group[{}] of (" COORD ") is size: {}\n", group_index, COORDARGS(map_pos), group->size()); // we may have performed a merge, so we update all the `coord -> group index` mappings for (auto &wpos: *group) { groups_map[wpos] = group_index; } - DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size()); + DEBUG(groups).print(" <- add() exits, there are {} mappings\n", groups_map.size()); } // scans a single tile for channel designations @@ -192,7 +192,7 @@ void ChannelGroups::scan(bool full_scan) { std::set gone_jobs; set_difference(last_jobs, jobs, gone_jobs); set_difference(jobs, last_jobs, new_jobs); - INFO(groups).print("gone jobs: %zd\nnew jobs: %zd\n",gone_jobs.size(), new_jobs.size()); + INFO(groups).print("gone jobs: {}\nnew jobs: {}\n",gone_jobs.size(), new_jobs.size()); for (auto &pos : new_jobs) { add(pos); } @@ -236,7 +236,7 @@ void ChannelGroups::scan(bool full_scan) { for (df::block_square_event* event: block->block_events) { if (auto evT = virtual_cast(event)) { // we want to let the user keep some designations free of being managed - TRACE(groups).print(" tile designation priority: %d\n", evT->priority[lx][ly]); + TRACE(groups).print(" tile designation priority: {}\n", evT->priority[lx][ly]); if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { if (empty_group) { group_blocks.emplace(block); @@ -338,9 +338,9 @@ void ChannelGroups::debug_groups() { int idx = 0; DEBUG(groups).print(" debugging group data\n"); for (auto &group: groups) { - DEBUG(groups).print(" group %d (size: %zu)\n", idx, group.size()); + DEBUG(groups).print(" group {} (size: {})\n", idx, group.size()); for (auto &pos: group) { - DEBUG(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(groups).print(" ({},{},{})\n", pos.x, pos.y, pos.z); } idx++; } @@ -350,9 +350,9 @@ void ChannelGroups::debug_groups() { // prints debug info group mappings void ChannelGroups::debug_map() { if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) { - INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); + INFO(groups).print("Group Mappings: {}\n", groups_map.size()); for (auto &pair: groups_map) { - TRACE(groups).print(" map[" COORD "] = %d\n", COORDARGS(pair.first), pair.second); + TRACE(groups).print(" map[" COORD "] = {}\n", COORDARGS(pair.first), pair.second); } } } diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 67f8742b38..6c85fda951 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -89,7 +89,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // the tile below is not solid earth // we're counting accessibility then dealing with 0 access DEBUG(manager).print("analysis: cave-in condition found\n"); - INFO(manager).print("(%d) checking accessibility of (" COORD ") from (" COORD ")\n", cavein_possible,COORDARGS(pos),COORDARGS(miner_pos)); + INFO(manager).print("({}) checking accessibility of ({}) from ({})\n", cavein_possible,pos,miner_pos); auto access = count_accessibility(miner_pos, pos); if (access == 0) { TRACE(groups).print(" has 0 access\n"); @@ -100,7 +100,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // todo: engage dig management, swap channel designations for dig } } else { - WARN(manager).print(" has %d access\n", access); + WARN(manager).print(" has {} access\n", access); cavein_possible = config.riskaverse; cavein_candidates.emplace(pos, access); least_access = std::min(access, least_access); @@ -110,13 +110,13 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool dig_now(DFHack::Core::getInstance().getConsole(), pos); } } - DEBUG(manager).print("cavein possible(%d)\n" - "%zu candidates\n" - "least access %d\n", cavein_possible, cavein_candidates.size(), least_access); + DEBUG(manager).print("cavein possible({})\n" + "{} candidates\n" + "least access {}\n", cavein_possible, cavein_candidates.size(), least_access); } // managing designations if (!group.empty()) { - DEBUG(manager).print("managing group #%d\n", groups.debugGIndex(*group.begin())); + DEBUG(manager).print("managing group #{}\n", groups.debugGIndex(*group.begin())); } for (auto &pos: group) { // if no cave-in is possible [or we don't check for], we'll just execute normally and move on @@ -132,7 +132,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool if (CSP::dignow_queue.count(pos) || (cavein_candidates.count(pos) && least_access < MAX && cavein_candidates[pos] <= least_access+OFFSET)) { - TRACE(manager).print("cave-in evaluated true and either of dignow or (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET); + TRACE(manager).print("cave-in evaluated true and either of dignow or ({} <= {})\n", cavein_candidates[pos], least_access+OFFSET); df::coord local(pos); local.x %= 16; local.y %= 16; @@ -143,7 +143,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // we want to let the user keep some designations free of being managed auto b = std::max(0, cavein_candidates[pos] - least_access); auto v = 1000 + (b * 1700); - DEBUG(manager).print("(" COORD ") 1000+1000(%d) -> %d {least-access: %d}\n",COORDARGS(pos), b, v, least_access); + DEBUG(manager).print("({}) 1000+1000({}) -> {} {least-access: {}}\n", pos, b, v, least_access); evT->priority[Coord(local)] = v; } } @@ -152,7 +152,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool } // cavein possible, but we failed to meet the criteria for activation if (cavein_candidates.count(pos)) { - DEBUG(manager).print("cave-in evaluated true and the cavein candidate's accessibility check was made as (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET); + DEBUG(manager).print("cave-in evaluated true and the cavein candidate's accessibility check was made as ({} <= {})\n", cavein_candidates[pos], least_access+OFFSET); } else { DEBUG(manager).print("cave-in evaluated true and the position was not a candidate, nor was it set for dignow\n"); } @@ -163,7 +163,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool bool ChannelManager::manage_one(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) const { if (Maps::isValidTilePos(map_pos)) { - TRACE(manager).print("manage_one((" COORD "), %d, %d)\n", COORDARGS(map_pos), set_marker_mode, marker_mode); + TRACE(manager).print("manage_one(({}), {}, {})\n", map_pos, set_marker_mode, marker_mode); df::map_block* block = Maps::getTileBlock(map_pos); // we calculate the position inside the block* df::coord local(map_pos); @@ -188,7 +188,7 @@ bool ChannelManager::manage_one(const df::coord &map_pos, bool set_marker_mode, block->flags.bits.designated = true; } tile_occupancy.bits.dig_marked = marker_mode; - TRACE(manager).print("marker mode %s\n", marker_mode ? "ENABLED" : "DISABLED"); + TRACE(manager).print("marker mode {}\n", marker_mode ? "ENABLED" : "DISABLED"); } else { // if we are though, it should be totally safe to dig tile_occupancy.bits.dig_marked = false; diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 4aa1648453..ac69ffdc8e 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -182,7 +182,7 @@ namespace CSP { psetting.ival(IGNORE_THRESH) = config.ignore_threshold; psetting.ival(FALL_THRESH) = config.fall_threshold; } catch (std::exception &e) { - ERR(plugin).print("%s\n", e.what()); + ERR(plugin).print("{}\n", e.what()); } } } @@ -208,7 +208,7 @@ namespace CSP { config.refresh_freq = psetting.ival(REFRESH_RATE); config.monitor_freq = psetting.ival(MONITOR_RATE); } catch (std::exception &e) { - ERR(plugin).print("%s\n", e.what()); + ERR(plugin).print("{}\n", e.what()); } } active_workers.clear(); @@ -320,14 +320,14 @@ namespace CSP { dignow_queue.emplace(report->pos); } - DEBUG(plugin).print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), - COORDARGS(report->pos2), report->text.c_str()); + DEBUG(plugin).print("{}, pos: {} , pos2: {}\n{}\n", report_id, report->pos, + report->pos2, report->text.c_str()); } break; case announcement_type::CAVE_COLLAPSE: if (config.resurrect) { - DEBUG(plugin).print("CAVE IN\n%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), - COORDARGS(report->pos2), report->text.c_str()); + DEBUG(plugin).print("CAVE IN\n{}, pos: {} , pos2: {}\n{}\n", report_id, report->pos, + report->pos2, report->text.c_str()); df::coord below = report->pos; below.z -= 1; @@ -344,12 +344,12 @@ namespace CSP { Units::getUnitsInBox(units, COORDARGS(areaMin), COORDARGS(areaMax)); for (auto unit: units) { endangered_units[unit] = tick; - DEBUG(plugin).print(" [id %d] was near a cave in.\n", unit->id); + DEBUG(plugin).print(" [id {}] was near a cave in.\n", unit->id); } for (auto unit : world->units.all) { if (last_safe.count(unit->id)) { endangered_units[unit] = tick; - DEBUG(plugin).print(" [id %d] is/was a worker, we'll track them too.\n", unit->id); + DEBUG(plugin).print(" [id {}] is/was a worker, we'll track them too.\n", unit->id); } } } @@ -472,7 +472,7 @@ namespace CSP { // clean up any "endangered" workers that have been tracked 100 ticks or more for (auto iter = endangered_units.begin(); iter != endangered_units.end();) { if (tick - iter->second >= 1200) { //keep watch 1 day - DEBUG(plugin).print("It has been one day since [id %d]'s last incident.\n", iter->first->id); + DEBUG(plugin).print("It has been one day since [id {}]'s last incident.\n", iter->first->id); iter = endangered_units.erase(iter); continue; } @@ -525,7 +525,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + outs without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -582,7 +582,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_ev command_result channel_safely(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + outs without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -650,23 +650,23 @@ command_result channel_safely(color_ostream &out, std::vector ¶ return DFHack::CR_WRONG_USAGE; } } catch (const std::exception &e) { - out.printerr("%s\n", e.what()); + out.printerr("{}\n", e.what()); return DFHack::CR_FAILURE; } } } else { - out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED."); + out.print("Channel-Safely is {}\n", enabled ? "ENABLED." : "DISABLED."); out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "risk-averse: ", config.riskaverse ? "on." : "off."); - out.print(" %-20s\t%s\n", "monitoring: ", config.monitoring ? "on." : "off."); - out.print(" %-20s\t%s\n", "require-vision: ", config.require_vision ? "on." : "off."); + out.print(" {:<20}\t{}\n", "risk-averse: ", config.riskaverse ? "on." : "off."); + out.print(" {:<20}\t{}\n", "monitoring: ", config.monitoring ? "on." : "off."); + out.print(" {:<20}\t{}\n", "require-vision: ", config.require_vision ? "on." : "off."); //out.print(" %-20s\t%s\n", "insta-dig: ", config.insta_dig ? "on." : "off."); - out.print(" %-20s\t%s\n", "resurrect: ", config.resurrect ? "on." : "off."); + out.print(" {:<20}\t{}\n", "resurrect: ", config.resurrect ? "on." : "off."); out.print(" SETTINGS:\n"); - out.print(" %-20s\t%" PRIi32 "\n", "refresh-freq: ", config.refresh_freq); - out.print(" %-20s\t%" PRIi32 "\n", "monitor-freq: ", config.monitor_freq); - out.print(" %-20s\t%" PRIu8 "\n", "ignore-threshold: ", config.ignore_threshold); - out.print(" %-20s\t%" PRIu8 "\n", "fall-threshold: ", config.fall_threshold); + out.print(" {:<20}\t{}\n", "refresh-freq: ", config.refresh_freq); + out.print(" {:<20}\t{}\n", "monitor-freq: ", config.monitor_freq); + out.print(" {:<20}\t{}\n", "ignore-threshold: ", config.ignore_threshold); + out.print(" {:<20}\t{}\n", "fall-threshold: ", config.fall_threshold); } CSP::SaveSettings(); return DFHack::CR_OK; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index 362fd927a3..16db1ff7d2 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -311,7 +311,7 @@ inline bool dig_now(color_ostream &out, const df::coord &map_pos) { // fully heals the unit specified, resurrecting if need be inline void resurrect(color_ostream &out, const int32_t &unit) { out.color(DFHack::COLOR_RED); - out.print("channel-safely: resurrecting [id: %d]\n", unit); + out.print("channel-safely: resurrecting [id: {}]\n", unit); std::vector params{"-r", "--unit", std::to_string(unit)}; Core::getInstance().runCommand(out,"full-heal", params); } diff --git a/plugins/cleanconst.cpp b/plugins/cleanconst.cpp index 895f182e76..9e05ecae46 100644 --- a/plugins/cleanconst.cpp +++ b/plugins/cleanconst.cpp @@ -38,7 +38,7 @@ command_result df_cleanconst(color_ostream &out, vector & parameters) df::construction *cons = df::construction::find(pos); if (!cons) { - out.printerr("Item at %i,%i,%i marked as construction but no construction is present!\n", pos.x, pos.y, pos.z); + out.printerr("Item at {} marked as construction but no construction is present!\n", pos); continue; } // if the construction is already labeled as "no build item", then leave it alone @@ -62,7 +62,7 @@ command_result df_cleanconst(color_ostream &out, vector & parameters) cleaned_total++; } - out.print("Done. %d construction items cleaned up.\n", cleaned_total); + out.print("Done. {} construction items cleaned up.\n", cleaned_total); return CR_OK; } diff --git a/plugins/cleaners.cpp b/plugins/cleaners.cpp index 6706bc92d8..74bc40e6ff 100644 --- a/plugins/cleaners.cpp +++ b/plugins/cleaners.cpp @@ -95,7 +95,7 @@ command_result cleanmap (color_ostream &out, bool snow, bool mud, bool item_spat } if(num_blocks) - out.print("Cleaned %d of %zd map blocks.\n", num_blocks, world->map.map_blocks.size()); + out.print("Cleaned {} of {} map blocks.\n", num_blocks, world->map.map_blocks.size()); return CR_OK; } @@ -123,7 +123,7 @@ command_result cleanitems (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d items.\n", cleaned_total, cleaned_items); + out.print("Removed {} contaminants from {} items.\n", cleaned_total, cleaned_items); return CR_OK; } @@ -143,7 +143,7 @@ command_result cleanunits (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d creatures.\n", cleaned_total, cleaned_units); + out.print("Removed {} contaminants from {} creatures.\n", cleaned_total, cleaned_units); return CR_OK; } @@ -163,7 +163,7 @@ command_result cleanplants (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d plants.\n", cleaned_total, cleaned_plants); + out.print("Removed {} contaminants from {} plants.\n", cleaned_total, cleaned_plants); return CR_OK; } diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp index 9e6ddb9849..c8d67d92ad 100644 --- a/plugins/cleanowned.cpp +++ b/plugins/cleanowned.cpp @@ -47,7 +47,7 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) command_result df_cleanowned (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -155,16 +155,16 @@ command_result df_cleanowned (color_ostream &out, vector & parameters) std::string description; item->getItemDescription(&description, 0); out.print( - "[%d] %s (wear level %d)", + "[{}] {} (wear level {})", item->id, - DF2CONSOLE(description).c_str(), + DF2CONSOLE(description), item->getWear() ); df::unit *owner = Items::getOwner(item); if (owner) - out.print(", owner %s", DF2CONSOLE(Units::getReadableName(owner)).c_str()); + out.print(", owner {}", DF2CONSOLE(Units::getReadableName(owner))); if (!dry_run) { diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index fba821b270..36ecdacf4a 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -144,7 +144,7 @@ static inline bool select_caste_mat(color_ostream &out, vector &tokens, out.printerr("You must also specify a caste.\n"); else out.printerr("The creature you specified has no such caste!\n"); - out.printerr("Valid castes:%s\n", castes.c_str()); + out.printerr("Valid castes:{}\n", castes); return false; } } @@ -198,7 +198,7 @@ static inline bool select_plant_growth(color_ostream &out, vector &token out.printerr("You must also specify a growth ID.\n"); else out.printerr("The plant you specified has no such growth!\n"); - out.printerr("Valid growths:%s\n", growths.c_str()); + out.printerr("Valid growths:{}\n", growths); return false; } } @@ -226,7 +226,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { ItemTypeInfo iinfo(item->getType(), item->getSubtype()); MaterialInfo minfo(item->getMaterial(), item->getMaterialIndex()); - out.print("%s %s\n", iinfo.getToken().c_str(), minfo.getToken().c_str()); + out.print("{} {}\n", iinfo.getToken(), minfo.getToken()); return CR_OK; } else if (parameters[0] == "floor") { @@ -268,7 +268,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { dest_container = item->id; string name; item->getItemDescription(&name, 0); - out.print("Items created will be placed inside %s.\n", name.c_str()); + out.print("Items created will be placed inside {}.\n", name); return CR_OK; } else if (parameters[0] == "building") { @@ -308,7 +308,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { dest_building = building->id; string name; building->getName(&name); - out.print("Items created will be placed inside %s.\n", name.c_str()); + out.print("Items created will be placed inside {}.\n", name); return CR_OK; } else diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 0762ad9c36..f0651889a6 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -182,10 +182,10 @@ command_result cursecheck(color_ostream& out, vector & parameters) out << DF2CONSOLE(Units::getReadableName(unit)); auto death = df::incident::find(unit->counters.death_id); - out.print(", born in %d, cursed in %d to be a %s. (%s%s)\n", + out.print(", born in {}, cursed in {} to be a {}. ({}{})\n", unit->birth_year, unit->curse_year, - curse_names[cursetype].c_str(), + curse_names[cursetype], // technically most cursed creatures are undead, // therefore output 'active' if they are not completely dead unit->flags2.bits.killed ? "deceased" : "active", @@ -203,15 +203,15 @@ command_result cursecheck(color_ostream& out, vector & parameters) if (giveUnitID) { - out.print("Creature %d, race %d (%x)\n", unit->id, unit->race, unit->race); + out.print("Creature {}, race {} (0x{:x})\n", unit->id, unit->race, unit->race); } } } if (selected_unit && !giveDetails) - out.print("Selected unit is %scursed\n", cursecount == 0 ? "not " : ""); + out.print("Selected unit is {}cursed\n", cursecount == 0 ? "not " : ""); else if (!selected_unit) - out.print("%zd cursed creatures on map\n", cursecount); + out.print("{} cursed creatures on map\n", cursecount); return CR_OK; } diff --git a/plugins/cxxrandom.cpp b/plugins/cxxrandom.cpp index 3f6a405598..a471cda6be 100644 --- a/plugins/cxxrandom.cpp +++ b/plugins/cxxrandom.cpp @@ -140,7 +140,7 @@ class NumberSequence } void Print() { for( auto v : m_numbers ) { - cout->print( "%" PRId64 " ", v ); + cout->print("{} ", v); } } }; diff --git a/plugins/debug.cpp b/plugins/debug.cpp index 90e5c783d6..51e4a8300b 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -804,9 +804,9 @@ static command_result setFilter(color_ostream& out, return v.match(level); }); if (iter == levelNames.end()) { - ERR(command,out).print("level ('%s') parameter must be one of " + ERR(command,out).print("level ('{}') parameter must be one of " "trace, debug, info, warning, error.\n", - parameters[pos].c_str()); + parameters[pos]); return CR_WRONG_USAGE; } @@ -1062,8 +1062,8 @@ CommandDispatch::dispatch_t CommandDispatch::dispatch { static command_result commandDebugFilter(color_ostream& out, std::vector& parameters) { - DEBUG(command,out).print("debugfilter %s, parameter count %zu\n", - parameters.size() > 0 ? parameters[0].c_str() : "", + DEBUG(command,out).print("debugfilter {}, parameter count {}\n", + parameters.size() > 0 ? parameters[0] : "", parameters.size()); auto iter = CommandDispatch::dispatch.end(); if (0u < parameters.size()) @@ -1096,7 +1096,7 @@ DFhackCExport DFHack::command_result plugin_init(DFHack::color_ostream& out, filter.apply(*cat); } } - INFO(init,out).print("plugin_init with %zu commands, %zu filters and %zu categories\n", + INFO(init,out).print("plugin_init with {}, {} filters and {} categories\n", commands.size(), filMan.size(), catMan.size()); filMan.connectTo(catMan.categorySignal); return rv; diff --git a/plugins/deramp.cpp b/plugins/deramp.cpp index 726693b416..6b41701225 100644 --- a/plugins/deramp.cpp +++ b/plugins/deramp.cpp @@ -98,9 +98,9 @@ command_result df_deramp (color_ostream &out, vector & parameters) } } if (count) - out.print("Found and changed %d tiles.\n", count); + out.print("Found and changed {} tiles.\n", count); if (countbad) - out.print("Fixed %d bad down ramps.\n", countbad); + out.print("Fixed {} bad down ramps.\n", countbad); return CR_OK; } diff --git a/plugins/devel/buildprobe.cpp b/plugins/devel/buildprobe.cpp index 333f1d3bc4..3b4df00e28 100644 --- a/plugins/devel/buildprobe.cpp +++ b/plugins/devel/buildprobe.cpp @@ -55,7 +55,7 @@ command_result readFlag (color_ostream &out, vector & parameters) MapExtras::MapCache * MCache = new MapExtras::MapCache(); t_occupancy oc = MCache->occupancyAt(cursor); - out.print("Current Value: %d\n", oc.bits.building); + out.print("Current Value: {}\n", ENUM_AS_STR(oc.bits.building)); return CR_OK; } diff --git a/plugins/devel/check-structures-sanity/dispatch.cpp b/plugins/devel/check-structures-sanity/dispatch.cpp index 0c0b3453b4..162ea7bbd4 100644 --- a/plugins/devel/check-structures-sanity/dispatch.cpp +++ b/plugins/devel/check-structures-sanity/dispatch.cpp @@ -118,7 +118,7 @@ void Checker::queue_globals() // offset is the position of the DFHack pointer to this global. auto ptr = *reinterpret_cast(field->offset); - QueueItem item(stl_sprintf("df.global.%s", field->name), ptr); + QueueItem item(fmt::format("df.global.{}", field->name), ptr); CheckedStructure cs(field); if (!ptr) @@ -886,7 +886,7 @@ void Checker::check_stl_string(const QueueItem & item) else if (is_gcc && length > 0 && !is_valid_dereference(QueueItem(item, "?start?", reinterpret_cast(string->start)), 1)) { // nullptr is NOT okay here - FAIL("invalid string pointer " << stl_sprintf("0x%" PRIxPTR, string->start)); + FAIL("invalid string pointer " << fmt::format("{:#x}", string->start)); return; } else if (is_local && length >= 16) diff --git a/plugins/devel/check-structures-sanity/main.cpp b/plugins/devel/check-structures-sanity/main.cpp index 5bc7c09d6d..6110afe1c0 100644 --- a/plugins/devel/check-structures-sanity/main.cpp +++ b/plugins/devel/check-structures-sanity/main.cpp @@ -77,7 +77,7 @@ static command_result command(color_ostream & out, std::vector & pa } \ catch (std::exception & ex) \ { \ - out.printerr("check-structures-sanity: argument to -%s: %s\n", #name, ex.what()); \ + out.printerr("check-structures-sanity: argument to -{}: {}\n", #name, ex.what()); \ return CR_WRONG_USAGE; \ } \ } diff --git a/plugins/devel/check-structures-sanity/types.cpp b/plugins/devel/check-structures-sanity/types.cpp index 647fa6141e..4c8a8f6ae2 100644 --- a/plugins/devel/check-structures-sanity/types.cpp +++ b/plugins/devel/check-structures-sanity/types.cpp @@ -10,7 +10,7 @@ QueueItem::QueueItem(const QueueItem & parent, const std::string & member, const { } QueueItem::QueueItem(const QueueItem & parent, size_t index, const void *ptr) : - QueueItem(parent.path + stl_sprintf("[%zu]", index), ptr) + QueueItem(parent.path + fmt::format("[{}]", index), ptr) { } diff --git a/plugins/devel/check-structures-sanity/validate.cpp b/plugins/devel/check-structures-sanity/validate.cpp index 24d5327f92..d6137eff1f 100644 --- a/plugins/devel/check-structures-sanity/validate.cpp +++ b/plugins/devel/check-structures-sanity/validate.cpp @@ -35,10 +35,10 @@ bool Checker::is_valid_dereference(const QueueItem & item, const CheckedStructur // assumes MALLOC_PERTURB_=45 #ifdef DFHACK64 #define UNINIT_PTR 0xd2d2d2d2d2d2d2d2 -#define FAIL_PTR(message) FAIL(stl_sprintf("0x%016zx: ", reinterpret_cast(base)) << message) +#define FAIL_PTR(message) FAIL(fmt::format("{:#016x} ", reinterpret_cast(base)) << message) #else #define UNINIT_PTR 0xd2d2d2d2 -#define FAIL_PTR(message) FAIL(stl_sprintf("0x%08zx: ", reinterpret_cast(base)) << message) +#define FAIL_PTR(message) FAIL(fmt::format("{:#016x} ", reinterpret_cast(base)) << message) #endif if (uintptr_t(base) == UNINIT_PTR) { diff --git a/plugins/devel/color-dfhack-text.cpp b/plugins/devel/color-dfhack-text.cpp index a19a475bb3..a17ad9d072 100644 --- a/plugins/devel/color-dfhack-text.cpp +++ b/plugins/devel/color-dfhack-text.cpp @@ -114,7 +114,7 @@ command_result color(color_ostream &out, std::vector ¶ms) } else if (p != "enable") { - out.printerr("Unrecognized option: %s\n", p.c_str()); + out.printerr("Unrecognized option: {}\n", p); return CR_WRONG_USAGE; } } diff --git a/plugins/devel/counters.cpp b/plugins/devel/counters.cpp index d3672d8d48..05c2024acc 100644 --- a/plugins/devel/counters.cpp +++ b/plugins/devel/counters.cpp @@ -22,7 +22,7 @@ command_result df_counters (color_ostream &out, vector & parameters) for (size_t i = 0; i < counters.size(); i++) { auto counter = counters[i]; - out.print("%i (%s): %i\n", (int)counter->id, ENUM_KEY_STR(misc_trait_type, counter->id).c_str(), counter->value); + out.print("{} ({}) : {}\n", (int)counter->id, ENUM_KEY_STR(misc_trait_type, counter->id), counter->value); } return CR_OK; diff --git a/plugins/devel/dumpmats.cpp b/plugins/devel/dumpmats.cpp index b910b6546b..91b70a2676 100644 --- a/plugins/devel/dumpmats.cpp +++ b/plugins/devel/dumpmats.cpp @@ -32,7 +32,7 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) df::material *mat = world->raws.mat_table.builtin[mat_num]; if (!mat) continue; - out.print("\n[MATERIAL:%s] - reconstructed from data extracted from memory\n", mat->id.c_str()); + out.print("\n[MATERIAL:{}] - reconstructed from data extracted from memory\n", mat->id); int32_t def_color[6] = {-1,-1,-1,-1,-1,-1}; string def_name[6]; @@ -52,10 +52,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_color[matter_state::Liquid] = solid_color; def_color[matter_state::Gas] = solid_color; - out.print("\t[STATE_COLOR:ALL:%s]\n", world->raws.descriptors.colors[solid_color]->id.c_str()); + out.print("\t[STATE_COLOR:ALL:{}]\n", world->raws.descriptors.colors[solid_color]->id); } else - out.print("\t[STATE_COLOR:ALL_SOLID:%s]\n", world->raws.descriptors.colors[solid_color]->id.c_str()); + out.print("\t[STATE_COLOR:ALL_SOLID:{}]\n", world->raws.descriptors.colors[solid_color]->id); } string solid_name = mat->state_name[matter_state::Solid]; @@ -82,10 +82,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) def_name[matter_state::Gas] = solid_name; def_adj[matter_state::Liquid] = solid_name; def_adj[matter_state::Gas] = solid_name; - out.print("\t[STATE_NAME_ADJ:ALL:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME_ADJ:ALL:{}]\n", solid_name); } else - out.print("\t[STATE_NAME_ADJ:ALL_SOLID:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME_ADJ:ALL_SOLID:{}]\n", solid_name); } } else @@ -104,10 +104,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_name[matter_state::Liquid] = solid_name; def_name[matter_state::Gas] = solid_name; - out.print("\t[STATE_NAME:ALL:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME:ALL:{}]\n", solid_name); } else - out.print("\t[STATE_NAME:ALL_SOLID:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME:ALL_SOLID:{}]\n", solid_name); } if (solid_adj == mat->state_adj[matter_state::Powder] || solid_adj == mat->state_adj[matter_state::Paste] || @@ -123,10 +123,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_adj[matter_state::Liquid] = solid_adj; def_adj[matter_state::Gas] = solid_adj; - out.print("\t[STATE_ADJ:ALL:%s]\n", solid_adj.c_str()); + out.print("\t[STATE_ADJ:ALL:{}]\n", solid_adj); } else - out.print("\t[STATE_ADJ:ALL_SOLID:%s]\n", solid_adj.c_str()); + out.print("\t[STATE_ADJ:ALL_SOLID:{}]\n", solid_adj); } } const char *state_names[6] = {"SOLID", "LIQUID", "GAS", "SOLID_POWDER", "SOLID_PASTE", "SOLID_PRESSED"}; @@ -134,110 +134,110 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) FOR_ENUM_ITEMS(matter_state, state) { if (mat->state_color[state] != -1 && mat->state_color[state] != def_color[state]) - out.print("\t[STATE_COLOR:%s:%s]\n", state_names[state], world->raws.descriptors.colors[mat->state_color[state]]->id.c_str()); + out.print("\t[STATE_COLOR:{}:{}]\n", state_names[state], world->raws.descriptors.colors[mat->state_color[state]]->id); if (mat->state_name[state] == mat->state_adj[state]) { if ((mat->state_name[state].size() && mat->state_name[state] != def_name[state]) || (mat->state_adj[state].size() && mat->state_adj[state] != def_adj[state])) - out.print("\t[STATE_NAME_ADJ:%s:%s]\n", state_names[state], mat->state_name[state].c_str()); + out.print("\t[STATE_NAME_ADJ:{}:{}]\n", state_names[state], mat->state_name[state]); } else { if (mat->state_name[state].size() && mat->state_name[state] != def_name[state]) - out.print("\t[STATE_NAME:%s:%s]\n", state_names[state], mat->state_name[state].c_str()); + out.print("\t[STATE_NAME:{}:{}]\n", state_names[state], mat->state_name[state]); if (mat->state_adj[state].size() && mat->state_adj[state] != def_adj[state]) - out.print("\t[STATE_ADJ:%s:%s]\n", state_names[state], mat->state_adj[state].c_str()); + out.print("\t[STATE_ADJ:{}:{}]\n", state_names[state], mat->state_adj[state]); } } if (mat->basic_color[0] != 7 || mat->basic_color[1] != 0) - out.print("\t[BASIC_COLOR:%i:%i]\n", mat->basic_color[0], mat->basic_color[1]); + out.print("\t[BASIC_COLOR:{}:{}]\n", mat->basic_color[0], mat->basic_color[1]); if (mat->build_color[0] != 7 || mat->build_color[1] != 7 || mat->build_color[2] != 0) - out.print("\t[BUILD_COLOR:%i:%i:%i]\n", mat->build_color[0], mat->build_color[1], mat->build_color[2]); + out.print("\t[BUILD_COLOR:{}:{}:{}]\n", mat->build_color[0], mat->build_color[1], mat->build_color[2]); if (mat->tile_color[0] != 7 || mat->tile_color[1] != 7 || mat->tile_color[2] != 0) - out.print("\t[TILE_COLOR:%i:%i:%i]\n", mat->tile_color[0], mat->tile_color[1], mat->tile_color[2]); + out.print("\t[TILE_COLOR:{}:{}:{}]\n", mat->tile_color[0], mat->tile_color[1], mat->tile_color[2]); if (mat->tile != 0xdb) - out.print("\t[TILE:%i]\n", mat->tile); + out.print("\t[TILE:{}]\n", mat->tile); if (mat->item_symbol != 0x07) - out.print("\t[ITEM_SYMBOL:%i]\n", mat->item_symbol); + out.print("\t[ITEM_SYMBOL:{}]\n", mat->item_symbol); if (mat->material_value != 1) - out.print("\t[MATERIAL_VALUE:%i]\n", mat->material_value); + out.print("\t[MATERIAL_VALUE:{}]\n", mat->material_value); if (mat->gem_name1.size()) - out.print("\t[IS_GEM:%s:%s]\n", mat->gem_name1.c_str(), mat->gem_name2.c_str()); + out.print("\t[IS_GEM:{}:{}]\n", mat->gem_name1.c_str(), mat->gem_name2.c_str()); if (mat->stone_name.size()) - out.print("\t[STONE_NAME:%s]\n", mat->stone_name.c_str()); + out.print("\t[STONE_NAME:{}]\n", mat->stone_name.c_str()); if (mat->heat.spec_heat != 60001) - out.print("\t[SPEC_HEAT:%i]\n", mat->heat.spec_heat); + out.print("\t[SPEC_HEAT:{}]\n", mat->heat.spec_heat); if (mat->heat.heatdam_point != 60001) - out.print("\t[HEATDAM_POINT:%i]\n", mat->heat.heatdam_point); + out.print("\t[HEATDAM_POINT:{}]\n", mat->heat.heatdam_point); if (mat->heat.colddam_point != 60001) - out.print("\t[COLDDAM_POINT:%i]\n", mat->heat.colddam_point); + out.print("\t[COLDDAM_POINT:{}]\n", mat->heat.colddam_point); if (mat->heat.ignite_point != 60001) - out.print("\t[IGNITE_POINT:%i]\n", mat->heat.ignite_point); + out.print("\t[IGNITE_POINT:{}]\n", mat->heat.ignite_point); if (mat->heat.melting_point != 60001) - out.print("\t[MELTING_POINT:%i]\n", mat->heat.melting_point); + out.print("\t[MELTING_POINT:{}]\n", mat->heat.melting_point); if (mat->heat.boiling_point != 60001) - out.print("\t[BOILING_POINT:%i]\n", mat->heat.boiling_point); + out.print("\t[BOILING_POINT:{}]\n", mat->heat.boiling_point); if (mat->heat.mat_fixed_temp != 60001) - out.print("\t[MAT_FIXED_TEMP:%i]\n", mat->heat.mat_fixed_temp); + out.print("\t[MAT_FIXED_TEMP:{}]\n", mat->heat.mat_fixed_temp); if (uint32_t(mat->solid_density) != 0xFBBC7818) - out.print("\t[SOLID_DENSITY:%i]\n", mat->solid_density); + out.print("\t[SOLID_DENSITY:{}]\n", mat->solid_density); if (uint32_t(mat->liquid_density) != 0xFBBC7818) - out.print("\t[LIQUID_DENSITY:%i]\n", mat->liquid_density); + out.print("\t[LIQUID_DENSITY:{}]\n", mat->liquid_density); if (uint32_t(mat->molar_mass) != 0xFBBC7818) - out.print("\t[MOLAR_MASS:%i]\n", mat->molar_mass); + out.print("\t[MOLAR_MASS:{}]\n", mat->molar_mass); FOR_ENUM_ITEMS(strain_type, strain) { auto name = ENUM_KEY_STR(strain_type,strain); if (mat->strength.yield[strain] != 10000) - out.print("\t[%s_YIELD:%i]\n", name.c_str(), mat->strength.yield[strain]); + out.print("\t[{}_YIELD:{}]\n", name, mat->strength.yield[strain]); if (mat->strength.fracture[strain] != 10000) - out.print("\t[%s_FRACTURE:%i]\n", name.c_str(), mat->strength.fracture[strain]); + out.print("\t[{}_FRACTURE:{}]\n", name, mat->strength.fracture[strain]); if (mat->strength.strain_at_yield[strain] != 0) - out.print("\t[%s_STRAIN_AT_YIELD:%i]\n", name.c_str(), mat->strength.strain_at_yield[strain]); + out.print("\t[{}_STRAIN_AT_YIELD:{}]\n", name, mat->strength.strain_at_yield[strain]); } if (mat->strength.max_edge != 0) - out.print("\t[MAX_EDGE:%i]\n", mat->strength.max_edge); + out.print("\t[MAX_EDGE:{}]\n", mat->strength.max_edge); if (mat->strength.absorption != 0) - out.print("\t[ABSORPTION:%i]\n", mat->strength.absorption); + out.print("\t[ABSORPTION:{}]\n", mat->strength.absorption); FOR_ENUM_ITEMS(material_flags, i) { if (mat->flags.is_set(i)) - out.print("\t[%s]\n", ENUM_KEY_STR(material_flags, i).c_str()); + out.print("\t[{}]\n", ENUM_KEY_STR(material_flags, i)); } if (mat->extract_storage != item_type::BARREL) - out.print("\t[EXTRACT_STORAGE:%s]\n", ENUM_KEY_STR(item_type, mat->extract_storage).c_str()); + out.print("\t[EXTRACT_STORAGE:{}]\n", ENUM_KEY_STR(item_type, mat->extract_storage)); if (mat->butcher_special_type != item_type::NONE || mat->butcher_special_subtype != -1) - out.print("\t[BUTCHER_SPECIAL:%s:%s]\n", ENUM_KEY_STR(item_type, mat->butcher_special_type).c_str(), (mat->butcher_special_subtype == -1) ? "NONE" : "?"); + out.print("\t[BUTCHER_SPECIAL:{}:{}]\n", ENUM_KEY_STR(item_type, mat->butcher_special_type), (mat->butcher_special_subtype == -1) ? "NONE" : "?"); if (mat->meat_name[0].size() || mat->meat_name[1].size() || mat->meat_name[2].size()) - out.print("\t[MEAT_NAME:%s:%s:%s]\n", mat->meat_name[0].c_str(), mat->meat_name[1].c_str(), mat->meat_name[2].c_str()); + out.print("\t[MEAT_NAME:{}:{}:{}]\n", mat->meat_name[0], mat->meat_name[1], mat->meat_name[2]); if (mat->block_name[0].size() || mat->block_name[1].size()) - out.print("\t[BLOCK_NAME:%s:%s]\n", mat->block_name[0].c_str(), mat->block_name[1].c_str()); + out.print("\t[BLOCK_NAME:{}:{}]\n", mat->block_name[0], mat->block_name[1]); for (std::string *s : mat->reaction_class) - out.print("\t[REACTION_CLASS:%s]\n", s->c_str()); + out.print("\t[REACTION_CLASS:{}]\n", *s); for (size_t i = 0; i < mat->reaction_product.id.size(); i++) { if ((*mat->reaction_product.str[0][i] == "NONE") && (*mat->reaction_product.str[1][i] == "NONE")) - out.print("\t[MATERIAL_REACTION_PRODUCT:%s:%s:%s%s%s]\n", mat->reaction_product.id[i]->c_str(), mat->reaction_product.str[2][i]->c_str(), mat->reaction_product.str[3][i]->c_str(), mat->reaction_product.str[4][i]->size() ? ":" : "", mat->reaction_product.str[4][i]->c_str()); + out.print("\t[MATERIAL_REACTION_PRODUCT:{}:{}:{}{}{}]\n", *mat->reaction_product.id[i], *mat->reaction_product.str[2][i], *mat->reaction_product.str[3][i], mat->reaction_product.str[4][i]->size() ? ":" : "", *mat->reaction_product.str[4][i]); else - out.print("\t[ITEM_REACTION_PRODUCT:%s:%s:%s:%s:%s%s%s]\n", mat->reaction_product.id[i]->c_str(), mat->reaction_product.str[0][i]->c_str(), mat->reaction_product.str[1][i]->c_str(), mat->reaction_product.str[2][i]->c_str(), mat->reaction_product.str[3][i]->c_str(), mat->reaction_product.str[4][i]->size() ? ":" : "", mat->reaction_product.str[4][i]->c_str()); + out.print("\t[ITEM_REACTION_PRODUCT:{}:{}:{}:{}:{}{}]\n", *mat->reaction_product.id[i], *mat->reaction_product.str[0][i], *mat->reaction_product.str[1][i], *mat->reaction_product.str[2][i], *mat->reaction_product.str[3][i], *mat->reaction_product.str[4][i]); } if (mat->hardens_with_water.mat_type != -1) - out.print("\t[HARDENS_WITH_WATER:%s:%s%s%s]\n", mat->hardens_with_water.str[0].c_str(), mat->hardens_with_water.str[1].c_str(), mat->hardens_with_water.str[2].size() ? ":" : "", mat->hardens_with_water.str[2].c_str()); + out.print("\t[HARDENS_WITH_WATER:{}:{}{}{}]\n", mat->hardens_with_water.str[0], mat->hardens_with_water.str[1], mat->hardens_with_water.str[2].size() ? ":" : "", mat->hardens_with_water.str[2]); if (mat->powder_dye != -1) - out.print("\t[POWDER_DYE:%s]\n", world->raws.descriptors.colors[mat->powder_dye]->id.c_str()); + out.print("\t[POWDER_DYE:{}]\n", world->raws.descriptors.colors[mat->powder_dye]->id); if (mat->soap_level != -0) - out.print("\t[SOAP_LEVEL:%o]\n", mat->soap_level); + out.print("\t[SOAP_LEVEL:{}]\n", mat->soap_level); for (size_t i = 0; i < mat->syndrome.syndrome.size(); i++) out.print("\t[SYNDROME] ...\n"); diff --git a/plugins/devel/eventExample.cpp b/plugins/devel/eventExample.cpp index c0944249f1..3b0bd860d4 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -109,7 +109,7 @@ command_result eventExample(color_ostream& out, vector& parameters) { //static int timerCount=0; //static int timerDenom=0; void jobInitiated(color_ostream& out, void* job_) { - out.print("Job initiated! %p\n", job_); + out.print("Job initiated! {}\n", static_cast(job_)); /* df::job* job = (df::job*)job_; out.print(" completion_timer = %d\n", job->completion_timer); @@ -120,37 +120,37 @@ void jobInitiated(color_ostream& out, void* job_) { } void jobCompleted(color_ostream& out, void* job) { - out.print("Job completed! %p\n", job); + out.print("Job completed! {}\n", static_cast(job)); } void timePassed(color_ostream& out, void* ptr) { - out.print("Time: %zi\n", (intptr_t)(ptr)); + out.print("Time: {}\n", (intptr_t)(ptr)); } void unitDeath(color_ostream& out, void* ptr) { - out.print("Death: %zi\n", (intptr_t)(ptr)); + out.print("Death: {}\n", (intptr_t)(ptr)); } void itemCreate(color_ostream& out, void* ptr) { int32_t item_index = df::item::binsearch_index(df::global::world->items.all, (intptr_t)ptr); if ( item_index == -1 ) { - out.print("%s, %d: Error.\n", __FILE__, __LINE__); + out.print("{}: Error.\n", __FILE__, __LINE__); } df::item* item = df::global::world->items.all[item_index]; df::item_type type = item->getType(); df::coord pos = item->pos; - out.print("Item created: %zi, %s, at (%d,%d,%d)\n", (intptr_t)(ptr), ENUM_KEY_STR(item_type, type).c_str(), pos.x, pos.y, pos.z); + out.print("Item created: {}, {}, at ({},{},{})\n", (intptr_t)(ptr), ENUM_KEY_STR(item_type, type).c_str(), pos.x, pos.y, pos.z); } void building(color_ostream& out, void* ptr) { - out.print("Building created/destroyed: %zi\n", (intptr_t)ptr); + out.print("Building created/destroyed: {}\n", (intptr_t)ptr); } void construction(color_ostream& out, void* ptr) { - out.print("Construction created/destroyed: %p\n", ptr); + out.print("Construction created/destroyed: {}\n", ptr); df::construction* constr = (df::construction*)ptr; df::coord pos = constr->pos; - out.print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + out.print(" ({},{},{})\n", pos.x, pos.y, pos.z); if ( df::construction::find(pos) == NULL ) out.print(" construction destroyed\n"); else @@ -160,19 +160,19 @@ void construction(color_ostream& out, void* ptr) { void syndrome(color_ostream& out, void* ptr) { EventManager::SyndromeData* data = (EventManager::SyndromeData*)ptr; - out.print("Syndrome started: unit %d, syndrome %d.\n", data->unitId, data->syndromeIndex); + out.print("Syndrome started: unit {}, syndrome {}.\n", data->unitId, data->syndromeIndex); } void invasion(color_ostream& out, void* ptr) { - out.print("New invasion! %zi\n", (intptr_t)ptr); + out.print("New invasion! {}\n", (intptr_t)ptr); } void unitAttack(color_ostream& out, void* ptr) { EventManager::UnitAttackData* data = (EventManager::UnitAttackData*)ptr; - out.print("unit %d attacks unit %d\n", data->attacker, data->defender); + out.print("unit {} attacks unit {}\n", data->attacker, data->defender); df::unit* defender = df::unit::find(data->defender); if (!defender) { - out.printerr("defender %d does not exist\n", data->defender); + out.printerr("defender {} does not exist\n", data->defender); return; } int32_t woundIndex = df::unit_wound::binsearch_index(defender->body.wounds, data->wound); @@ -187,6 +187,6 @@ void unitAttack(color_ostream& out, void* ptr) { for ( auto a = parts.begin(); a != parts.end(); a++ ) { int32_t body_part_id = (*a); df::body_part_raw* part = defender->body.body_plan->body_parts[body_part_id]; - out.print(" %s\n", part->name_singular[0]->c_str()); + out.print(" {}\n", *part->name_singular[0]); } } diff --git a/plugins/devel/frozen.cpp b/plugins/devel/frozen.cpp index 6c4264783e..a6d862a5a5 100644 --- a/plugins/devel/frozen.cpp +++ b/plugins/devel/frozen.cpp @@ -55,7 +55,7 @@ command_result df_frozenlava (color_ostream &out, vector & parameters) int tiles = changeLiquid(tile_liquid::Magma); if (tiles) - out.print("Changed %i tiles of ice into frozen lava.\n", tiles); + out.print("Changed {} tiles of ice into frozen lava.\n", tiles); return CR_OK; } @@ -72,7 +72,7 @@ command_result df_frozenwater (color_ostream &out, vector & parameters) int tiles = changeLiquid(tile_liquid::Water); if (tiles) - out.print("Changed %i tiles of ice into frozen water.\n", tiles); + out.print("Changed {} tiles of ice into frozen water.\n", tiles); return CR_OK; } diff --git a/plugins/devel/kittens.cpp b/plugins/devel/kittens.cpp index 69430e69da..d9dbf27015 100644 --- a/plugins/devel/kittens.cpp +++ b/plugins/devel/kittens.cpp @@ -115,14 +115,14 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) uint64_t time2 = GetTimeMs64(); uint64_t delta = time2-timeLast; timeLast = time2; - out.print("Time delta = %d ms\n", int(delta)); + out.print("Time delta = {} ms\n", int(delta)); } if(trackmenu_flg) { if (last_menu != plotinfo->main.mode) { last_menu = plotinfo->main.mode; - out.print("Menu: %d\n",last_menu); + out.print("Menu: {}\n", ENUM_AS_STR(last_menu)); } } if(trackpos_flg) @@ -134,14 +134,14 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) last_designation[0] = desig_x; last_designation[1] = desig_y; last_designation[2] = desig_z; - out.print("Designation: %d %d %d\n",desig_x, desig_y, desig_z); + out.print("Designation: {} {} {}\n", desig_x, desig_y, desig_z); } df::coord mousePos = Gui::getMousePos(); if(mousePos.x != last_mouse[0] || mousePos.y != last_mouse[1]) { last_mouse[0] = mousePos.x; last_mouse[1] = mousePos.y; - out.print("Mouse: %d %d\n",mousePos.x, mousePos.y); + out.print("Mouse: {} {}\n", mousePos.x, mousePos.y); } } return CR_OK; @@ -158,7 +158,7 @@ command_result trackmenu (color_ostream &out, vector & parameters) { is_enabled = true; last_menu = plotinfo->main.mode; - out.print("Menu: %d\n",last_menu); + out.print("Menu: {}\n", ENUM_AS_STR(last_menu)); trackmenu_flg = true; return CR_OK; } @@ -182,10 +182,10 @@ command_result colormods (color_ostream &out, vector & parameters) for(df::creature_raw* rawlion : vec) { df::caste_raw * caste = rawlion->caste[0]; - out.print("%s\nCaste addr %p\n",rawlion->creature_id.c_str(), &caste->color_modifiers); + out.print("{}\nCaste addr {}\n",rawlion->creature_id, static_cast(&caste->color_modifiers)); for(size_t j = 0; j < caste->color_modifiers.size();j++) { - out.print("mod %zd: %p\n", j, caste->color_modifiers[j]); + out.print("mod {}: {}\n", j, static_cast(caste->color_modifiers[j])); } } return CR_OK; @@ -203,7 +203,7 @@ command_result ktimer (color_ostream &out, vector & parameters) uint64_t timeend = GetTimeMs64(); timeLast = timeend; timering = true; - out.print("Time to suspend = %d ms\n", int(timeend - timestart)); + out.print("Time to suspend = {} ms\n", int(timeend - timestart)); } is_enabled = true; return CR_OK; @@ -300,9 +300,9 @@ struct Connected : public ClearMem { return this; } ~Connected() { - INFO(command,*out).print("Connected %d had %d count. " - "It was caller %d times. " - "It was callee %d times.\n", + INFO(command,*out).print("Connected {} had {} count. " + "It was caller {} times. " + "It was callee {} times.\n", id, count, caller, callee.load()); } }; diff --git a/plugins/devel/memutils.cpp b/plugins/devel/memutils.cpp index c2a7387fdd..b7ce98ea73 100644 --- a/plugins/devel/memutils.cpp +++ b/plugins/devel/memutils.cpp @@ -52,7 +52,7 @@ namespace memutils { lua_pushstring(state, expr); if (!Lua::SafeCall(*out, state, 1, 1)) { - out->printerr("Failed to evaluate %s\n", expr); + out->printerr("Failed to evaluate {}\n", expr); return NULL; } @@ -60,7 +60,7 @@ namespace memutils { lua_swap(state); if (!Lua::SafeCall(*out, state, 1, 1) || !lua_isinteger(state, -1)) { - out->printerr("Failed to get address: %s\n", expr); + out->printerr("Failed to get address: {}\n", expr); return NULL; } diff --git a/plugins/devel/memview.cpp b/plugins/devel/memview.cpp index a9fba897e1..01397ef7d2 100644 --- a/plugins/devel/memview.cpp +++ b/plugins/devel/memview.cpp @@ -75,7 +75,7 @@ void outputHex(uint8_t *buf,uint8_t *lbuf,size_t len,size_t start,color_ostream for(size_t i=0;i8X} ",i+start); for(size_t j=0;(j2X}",static_cast(buf[j+i])); //if modfied show a star else - con.print(" %02X",buf[j+i]); + con.print(" {:0>2X}",static_cast(buf[j+i])); } con.reset_color(); con.print(" | "); for(size_t j=0;(j31)&&(buf[j+i]<128)) //only printable ascii - con.print("%c",buf[j+i]); + con.print("{}",static_cast(buf[j+i])); else con.print("."); con.print("\n"); @@ -187,7 +187,7 @@ command_result memview (color_ostream &out, vector & parameters) isValid=true; if(!isValid) { - out.printerr("Invalid address: %p\n",memdata.addr); + out.printerr("Invalid address: {}\n",memdata.addr); mymutex->unlock(); return CR_OK; } diff --git a/plugins/devel/onceExample.cpp b/plugins/devel/onceExample.cpp index ebc03e32b7..73361c53e7 100644 --- a/plugins/devel/onceExample.cpp +++ b/plugins/devel/onceExample.cpp @@ -25,7 +25,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector & parameters) { - out.print("Already done = %d.\n", DFHack::Once::alreadyDone("onceExample_1")); + out.print("Already done = {}.\n", DFHack::Once::alreadyDone("onceExample_1")); if ( DFHack::Once::doOnce("onceExample_1") ) { out.print("Printing this message once!\n"); } diff --git a/plugins/devel/stepBetween.cpp b/plugins/devel/stepBetween.cpp index 0eca4ea8c9..9acbce904b 100644 --- a/plugins/devel/stepBetween.cpp +++ b/plugins/devel/stepBetween.cpp @@ -79,7 +79,7 @@ df::coord prev; command_result stepBetween (color_ostream &out, std::vector & parameters) { df::coord bob = Gui::getCursorPos(); - out.print("(%d,%d,%d), (%d,%d,%d): canStepBetween = %d, canWalkBetween = %d\n", prev.x, prev.y, prev.z, bob.x, bob.y, bob.z, Maps::canStepBetween(prev, bob), Maps::canWalkBetween(prev,bob)); + out.print("({},{},{}), ({},{},{}): canStepBetween = {}, canWalkBetween = {}\n", prev.x, prev.y, prev.z, bob.x, bob.y, bob.z, Maps::canStepBetween(prev, bob), Maps::canWalkBetween(prev,bob)); prev = bob; return CR_OK; diff --git a/plugins/devel/tilesieve.cpp b/plugins/devel/tilesieve.cpp index 6b36b8dd0d..9f21d98368 100644 --- a/plugins/devel/tilesieve.cpp +++ b/plugins/devel/tilesieve.cpp @@ -73,7 +73,7 @@ command_result tilesieve(color_ostream &out, std::vector & params) if(seen.count(tt)) continue; seen.insert(tt); - out.print("Found tile %x @ %d %d %d\n", tt, block->map_pos.x + x, block->map_pos.y + y, block->map_pos.z); + out.print("Found tile {} @ {} {} {}\n", ENUM_AS_STR(tt), block->map_pos.x + x, block->map_pos.y + y, block->map_pos.z); } } return CR_OK; diff --git a/plugins/devel/vectors.cpp b/plugins/devel/vectors.cpp index 279fe8a0d5..ac72de3074 100644 --- a/plugins/devel/vectors.cpp +++ b/plugins/devel/vectors.cpp @@ -134,7 +134,7 @@ static void printVec(color_ostream &con, const char* msg, t_vecTriplet *vec, uintptr_t length = (intptr_t)vec->end - (intptr_t)vec->start; uintptr_t offset = pos - start; - con.print("%8s offset 0x%06zx, addr 0x%01zx, start 0x%01zx, length %zi", + con.print("{:8} offset 0x{:06x}, addr 0x{:01x}, start 0x{:01x}, length {:}", msg, offset, pos, intptr_t(vec->start), length); if (length >= 4 && length % 4 == 0) { @@ -146,7 +146,7 @@ static void printVec(color_ostream &con, const char* msg, t_vecTriplet *vec, } std::string classname; if (Core::getInstance().vinfo->getVTableName(ptr, classname)) - con.print(", 1st item: %s", classname.c_str()); + con.print(", 1st item: {}", classname); } con.print("\n"); } @@ -197,10 +197,10 @@ command_result df_vectors (color_ostream &con, vector & parameters) // Found the range containing the start if (!range.isInRange((void *)end)) { - con.print("Scanning %zi bytes would read past end of memory " + con.print("Scanning {} bytes would read past end of memory " "range.\n", bytes); size_t diff = end - (intptr_t)range.end; - con.print("Cutting bytes down by %zi.\n", diff); + con.print("Cutting bytes down by {}.\n", diff); end = (uintptr_t) range.end; } diff --git a/plugins/devel/zoom.cpp b/plugins/devel/zoom.cpp index 18e8d62e32..d37ed3418a 100644 --- a/plugins/devel/zoom.cpp +++ b/plugins/devel/zoom.cpp @@ -42,7 +42,7 @@ command_result df_zoom (color_ostream &out, std::vector & paramete return CR_WRONG_USAGE; if (zcmap.find(parameters[0]) == zcmap.end()) { - out.printerr("Unrecognized zoom command: %s\n", parameters[0].c_str()); + out.printerr("Unrecognized zoom command: {}\n", parameters[0]); out.print("Valid commands:"); for (auto it = zcmap.begin(); it != zcmap.end(); ++it) { diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index 48984e1b9f..259978f9bf 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -48,9 +48,6 @@ namespace DFHack { DBG_DECLARE(dignow, channels, DebugCategory::LINFO); } -#define COORD "%" PRIi16 " %" PRIi16 " %" PRIi16 -#define COORDARGS(id) id.x, id.y, id.z - using namespace DFHack; struct designation{ @@ -486,7 +483,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, DFCoord pos_below(pos.x, pos.y, pos.z-1); if (can_dig_channel(tt) && map.ensureBlockAt(pos_below) && is_diggable(map, pos_below, map.tiletypeAt(pos_below))) { - TRACE(channels).print("dig_tile: channeling at (" COORD ") [can_dig_channel: true]\n",COORDARGS(pos_below)); + TRACE(channels).print("dig_tile: channeling at ({}) [can_dig_channel: true]\n", pos_below); target_type = df::tiletype::OpenSpace; DFCoord pos_above(pos.x, pos.y, pos.z+1); if (map.ensureBlockAt(pos_above)) { @@ -503,7 +500,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, return true; } } else { - DEBUG(channels).print("dig_tile: failed to channel at (" COORD ") [can_dig_channel: false]\n", COORDARGS(pos_below)); + DEBUG(channels).print("dig_tile: failed to channel at ({}) [can_dig_channel: false]\n", pos_below); } break; } @@ -550,8 +547,8 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, case df::tile_dig_designation::No: default: out.printerr( - "unhandled dig designation for tile (%d, %d, %d): %d\n", - pos.x, pos.y, pos.z, designation); + "unhandled dig designation for tile ({}, {}, {}): {}\n", + pos.x, pos.y, pos.z, ENUM_AS_STR(designation)); } // fail if unhandled or no change to tile @@ -559,7 +556,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, return false; dug_tiles.emplace_back(map, pos); - TRACE(general).print("dig_tile: digging the designation tile at (" COORD ")\n",COORDARGS(pos)); + TRACE(general).print("dig_tile: digging the designation tile at ({})\n",pos); dig_type(map, pos, target_type); clean_ramps(map, pos); @@ -898,10 +895,8 @@ static void create_boulders(color_ostream &out, if (num_items != coords.size()) { MaterialInfo material; material.decode(prod->mat_type, prod->mat_index); - out.printerr("unexpected number of %s %s produced: expected %zd," - " got %zd.\n", - material.toString().c_str(), - ENUM_KEY_STR(item_type, prod->item_type).c_str(), + out.printerr("unexpected number of {} {} produced: expected {}, got {}.\n", + material.toString(), ENUM_KEY_STR(item_type, prod->item_type), coords.size(), num_items); num_items = std::min(num_items, entry.second.size()); } @@ -911,7 +906,7 @@ static void create_boulders(color_ostream &out, dump_pos : simulate_fall(coords[i]); if (!Maps::ensureTileBlock(pos)) { out.printerr( - "unable to place boulder generated at (%d, %d, %d)\n", + "unable to place boulder generated at ({}, {}, {})\n", coords[i].x, coords[i].y, coords[i].z); continue; } @@ -956,7 +951,7 @@ static void post_process_dug_tiles(color_ostream &out, continue; if (!Maps::ensureTileBlock(resting_pos)) { - out.printerr("No valid tile beneath (%d, %d, %d); can't move" + out.printerr("No valid tile beneath ({},{},{}) can't move" " units and items to floor", pos.x, pos.y, pos.z); continue; diff --git a/plugins/dig.cpp b/plugins/dig.cpp index 8bdcca59d6..1be2a9a116 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -108,7 +108,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector getassignment(pos), is_warm(pos)); if (warm_mask->getassignment(pos) && is_warm(pos)) { - DEBUG(log,out).print("revealing warm dig tile at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(log,out).print("revealing warm dig tile at ({},{},{})\n", pos.x, pos.y, pos.z); block->designation[pos.x&15][pos.y&15].bits.hidden = false; } } if (auto damp_mask = World::getPersistentTilemask(damp_config, block)) { - TRACE(log,out).print("testing tile at (%d,%d,%d); mask:%d, damp:%d\n", pos.x, pos.y, pos.z, + TRACE(log,out).print("testing tile at ({},{},{}); mask:{}, damp:{}\n", pos.x, pos.y, pos.z, damp_mask->getassignment(pos), is_damp(pos)); if (damp_mask->getassignment(pos) && is_damp(pos)) { - DEBUG(log,out).print("revealing damp dig tile at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(log,out).print("revealing damp dig tile at ({},{},{})\n", pos.x, pos.y, pos.z); block->designation[pos.x&15][pos.y&15].bits.hidden = false; } } @@ -418,7 +418,7 @@ static void unhide_surrounding_tagged_tiles(color_ostream& out, void* job_ptr) { return; const auto & pos = job->pos; - TRACE(log,out).print("handing dig job at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + TRACE(log,out).print("handing dig job at ({},{},{})\n", pos.x, pos.y, pos.z); process_taken_dig_job(out, pos); @@ -626,7 +626,7 @@ int32_t parse_priority(color_ostream &out, vector ¶meters) } else { - out.printerr("invalid priority specified; reverting to %i\n", default_priority); + out.printerr("invalid priority specified; reverting to {}\n", default_priority); break; } } @@ -1470,7 +1470,7 @@ command_result digv (color_ostream &out, vector & parameters) con.printerr("This tile is not a vein.\n"); return CR_FAILURE; } - con.print("%d/%d/%d tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, des.whole); + con.print("{} tiletype: {}, veinmat: {}, designation: 0x{:x} ... DIGGING!\n", xy, ENUM_AS_STR(tt), veinmat, des.whole); stack flood; flood.push(xy); @@ -1654,7 +1654,7 @@ command_result digl (color_ostream &out, vector & parameters) con.printerr("This is a vein. Use digv instead!\n"); return CR_FAILURE; } - con.print("%d/%d/%d tiletype: %d, basemat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, basemat, des.whole); + con.print("{}/{}/{}/ tiletype: {}, basemat: {}, designation: 0x{:x} ... DIGGING!\n", cx,cy,cz, ENUM_AS_STR(tt), basemat, des.whole); stack flood; flood.push(xy); @@ -1841,7 +1841,7 @@ command_result digtype (color_ostream &out, vector & parameters) automine = false; else { - out.printerr("Invalid parameter: '%s'.\n", parameter.c_str()); + out.printerr("Invalid parameter: '{}'.\n", parameter); return CR_FAILURE; } } @@ -1873,7 +1873,7 @@ command_result digtype (color_ostream &out, vector & parameters) out.printerr("This tile is not a vein.\n"); return CR_FAILURE; } - out.print("(%d,%d,%d) tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, baseDes.whole); + out.print("({},{},{}) tiletype: {}, veinmat: {}, designation: 0x{:x} ... DIGGING!\n", cx,cy,cz, ENUM_AS_STR(tt), veinmat, baseDes.whole); if ( targetDigType != -1 ) { @@ -1910,7 +1910,7 @@ command_result digtype (color_ostream &out, vector & parameters) //designate it for digging if ( !mCache->testCoord(current) ) { - out.printerr("testCoord failed at (%d,%d,%d)\n", x, y, z); + out.printerr("testCoord failed at ({},{},{})\n", x, y, z); return CR_FAILURE; } @@ -2158,7 +2158,7 @@ static bool blink(int delay) { } static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = false) { - TRACE(log).print("entering paintScreenDampWarm aquifer_mode=%d, show_damp=%d\n", aquifer_mode, show_damp); + TRACE(log).print("entering paintScreenDampWarm aquifer_mode={}, show_damp={}\n", aquifer_mode, show_damp); static Screen::Pen empty_pen; @@ -2220,7 +2220,7 @@ static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = fals bump_layers(*pen, x, y); } } else { - TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", + TRACE(log).print("scanning map tile at ({},{},{}) screen offset ({},{})\n", pos.x, pos.y, pos.z, x, y); auto des = Maps::getTileDesignation(pos); @@ -2231,7 +2231,7 @@ static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = fals Screen::Pen cur_tile = Screen::readTile(x, y, true); if (!cur_tile.valid()) { - DEBUG(log).print("cannot read tile at offset %d, %d\n", x, y); + DEBUG(log).print("cannot read tile at offset {}, {}\n", x, y); continue; } @@ -2465,7 +2465,7 @@ static void paintScreenDesignated() { if (!Maps::isValidTilePos(map_pos)) continue; - TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", + TRACE(log).print("scanning map tile at ({},{},{}) screen offset ({},{})\n", map_pos.x, map_pos.y, map_pos.z, x, y); Screen::Pen cur_tile; diff --git a/plugins/digFlood.cpp b/plugins/digFlood.cpp index 045e6c61d6..c1d0eccbc5 100644 --- a/plugins/digFlood.cpp +++ b/plugins/digFlood.cpp @@ -181,7 +181,7 @@ command_result digFlood (color_ostream &out, std::vector & paramet } } - out.print("Could not find material \"%s\".\n", parameters[a].c_str()); + out.print("Could not find material \"{}\".\n", parameters[a]); return CR_WRONG_USAGE; loop: continue; diff --git a/plugins/diggingInvaders/assignJob.cpp b/plugins/diggingInvaders/assignJob.cpp index eb74433b86..1bd5c97e4d 100644 --- a/plugins/diggingInvaders/assignJob.cpp +++ b/plugins/diggingInvaders/assignJob.cpp @@ -231,13 +231,13 @@ int32_t assignJob(color_ostream& out, Edge firstImportantEdge, unordered_mapmat_type = material.type; @@ -255,7 +255,7 @@ int32_t assignJob(color_ostream& out, Edge firstImportantEdge, unordered_mapsite_id), NULL); if ( out_items.size() != 1 ) { - out.print("%s, %d: wrong size: %zu.\n", __FILE__, __LINE__, out_items.size()); + out.print("{}, {}: wrong size: {}.\n", __FILE__, __LINE__, out_items.size()); return -1; } out_items[0]->moveToGround(firstInvader->pos.x, firstInvader->pos.y, firstInvader->pos.z); diff --git a/plugins/diggingInvaders/diggingInvaders.cpp b/plugins/diggingInvaders/diggingInvaders.cpp index d883ef4dba..fff7f033ea 100644 --- a/plugins/diggingInvaders/diggingInvaders.cpp +++ b/plugins/diggingInvaders/diggingInvaders.cpp @@ -276,13 +276,13 @@ command_result diggingInvadersCommand(color_ostream& out, std::vectorjob.current_job && lastDigger->job.current_job->id == lastInvasionJob ) { return; } - //out.print("%s,%d: lastDigger = %d, last job = %d, last digger's job = %d\n", __FILE__, __LINE__, lastInvasionDigger, lastInvasionJob, !lastDigger ? -1 : (!lastDigger->job.current_job ? -1 : lastDigger->job.current_job->id)); + //out.print("{},{}: lastDigger = {}, last job = {}, last digger's job = {}\n", __FILE__, __LINE__, lastInvasionDigger, lastInvasionJob, !lastDigger ? -1 : (!lastDigger->job.current_job ? -1 : lastDigger->job.current_job->id)); lastInvasionDigger = lastInvasionJob = -1; clearDijkstra(); @@ -382,7 +382,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { } else if ( unit->flags1.bits.active_invader ) { df::creature_raw* raw = df::creature_raw::find(unit->race); if ( raw == NULL ) { - out.print("%s,%d: WTF? Couldn't find creature raw.\n", __FILE__, __LINE__); + out.print("{},{}: WTF? Couldn't find creature raw.\n", __FILE__, __LINE__); continue; } /* @@ -464,7 +464,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { fringe.erase(fringe.begin()); //out.print("line %d: fringe size = %d, localPtsFound = %d / %d, closedSetSize = %d, pt = %d,%d,%d\n", __LINE__, fringe.size(), localPtsFound, localPts.size(), closedSet.size(), pt.x,pt.y,pt.z); if ( closedSet.find(pt) != closedSet.end() ) { - out.print("%s, line %d: Double closure! Bad!\n", __FILE__, __LINE__); + out.print("{},{}: Double closure! Bad!\n", __FILE__, __LINE__); break; } closedSet.insert(pt); @@ -511,7 +511,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { delete myEdges; } // clock_t time = clock() - t0; - //out.print("tickTime = %d, time = %d, totalEdgeTime = %d, total points = %d, total edges = %d, time per point = %.3f, time per edge = %.3f, clocks/sec = %d\n", (int32_t)tickTime, time, totalEdgeTime, closedSet.size(), edgeCount, (float)time / closedSet.size(), (float)time / edgeCount, CLOCKS_PER_SEC); + //out.print("tickTime = {}, time = {}, totalEdgeTime = {}, total points = {}, total edges = {}, time per point = {:.3f}, time per edge = {:.3f}, clocks/sec = {}\n", (int32_t)tickTime, time, totalEdgeTime, closedSet.size(), edgeCount, (float)time / closedSet.size(), (float)time / edgeCount, CLOCKS_PER_SEC); fringe.clear(); if ( !foundTarget ) @@ -590,7 +590,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { //cancel it job->flags.bits.item_lost = 1; - out.print("%s,%d: cancelling job %d.\n", __FILE__,__LINE__, job->id); + out.print("{},{}: cancelling job {}.\n", __FILE__,__LINE__, job->id); //invaderJobs.remove(job->id); } invaderJobs.erase(lastInvasionJob); diff --git a/plugins/dwarfvet.cpp b/plugins/dwarfvet.cpp index 6f3e32d00f..7847dfa8e8 100644 --- a/plugins/dwarfvet.cpp +++ b/plugins/dwarfvet.cpp @@ -69,17 +69,17 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -140,7 +140,7 @@ static void dwarfvet_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); Lua::CallLuaModuleFunction(out, "plugins.dwarfvet", "checkup"); } diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp index bb90435343..8e695bd748 100644 --- a/plugins/embark-assistant/finder_ui.cpp +++ b/plugins/embark-assistant/finder_ui.cpp @@ -198,7 +198,7 @@ namespace embark_assist { size_t civ = 0; if (!infile) { - out.printerr("No profile file found at %s\n", profile_file_name); + out.printerr("No profile file found at {}\n", profile_file_name); return; } @@ -209,7 +209,7 @@ namespace embark_assist { while (true) { if (!fgets(line, count, infile) || line[0] != '[') { - out.printerr("Failed to find token start '[' at line %i\n", static_cast(i)); + out.printerr("Failed to find token start '[' at line {}\n", static_cast(i)); fclose(infile); return; } @@ -218,7 +218,7 @@ namespace embark_assist { if (line[k] == ':') { for (int l = 1; l < k; l++) { if (state->finder_list[static_cast(i) + civ].text.c_str()[l - 1] != line[l]) { - out.printerr("Token mismatch of %s vs %s\n", line, state->finder_list[static_cast(i) + civ].text.c_str()); + out.printerr("Token mismatch of {} vs {}\n", line, state->finder_list[static_cast(i) + civ].text.c_str()); fclose(infile); return; } @@ -242,7 +242,7 @@ namespace embark_assist { } if (!found) { - out.printerr("Value extraction failure from %s\n", line); + out.printerr("Value extraction failure from {}\n", line); fclose(infile); return; } @@ -252,7 +252,7 @@ namespace embark_assist { } if (!found) { - out.printerr("Value delimiter not found in %s\n", line); + out.printerr("Value delimiter not found in {}\n", line); fclose(infile); return; } diff --git a/plugins/embark-assistant/matcher.cpp b/plugins/embark-assistant/matcher.cpp index 9f3900f0c9..61da64959b 100644 --- a/plugins/embark-assistant/matcher.cpp +++ b/plugins/embark-assistant/matcher.cpp @@ -1450,21 +1450,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->savagery_count[i] < embark_size) { - if (trace) out.print("matcher::world_tile_match: Savagery All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->savagery_count[i] == 0 && !tile->neighboring_savagery[i]) { - if (trace) out.print("matcher::world_tile_match: Savagery Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->savagery_count[i] > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Savagery Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery Absent ({}, {})\n", x, y); return false; } break; @@ -1480,21 +1480,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->evilness_count[i] < embark_size) { - if (trace) out.print("matcher::world_tile_match: Evil All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->evilness_count[i] == 0 && !tile->neighboring_evilness[i]) { - if (trace) out.print("matcher::world_tile_match: Evil Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->evilness_count[i] > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Evil Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil Absent ({}, {})\n", x, y); return false; } break; @@ -1511,14 +1511,14 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::At_Most_Light: if (tile->aquifer == embark_assist::defs::Heavy_Aquifer_Bit) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light ({}, {})\n", x, y); return false; } break; @@ -1526,7 +1526,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_Light: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Light ({}, {})\n", x, y); return false; } break; @@ -1534,21 +1534,21 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_At_Least_Light: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || (tile->aquifer == embark_assist::defs::None_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_At_Least_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_At_Least_Light ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::Light: if (!(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Light ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::At_Least_Light: if (tile->aquifer == embark_assist::defs::None_Aquifer_Bit) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Least_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Least_Light ({}, {})\n", x, y); return false; } break; @@ -1556,7 +1556,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_Heavy: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1564,7 +1564,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::At_Most_Light_Plus_Heavy: if (tile->aquifer == embark_assist::defs::Heavy_Aquifer_Bit || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1572,7 +1572,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::Light_Plus_Heavy: if (!(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Light_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Light_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1580,14 +1580,14 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Light_Heavy: if (tile->aquifer != (embark_assist::defs::None_Aquifer_Bit | embark_assist::defs::Light_Aquifer_Bit | embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Light_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Light_Heavy ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::Heavy: if (!(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Heavy ({}, {})\n", x, y); return false; } break; @@ -1597,42 +1597,42 @@ namespace embark_assist { switch (tile->max_river_size) { case embark_assist::defs::river_sizes::None: if (finder->min_river > embark_assist::defs::river_ranges::None) { - if (trace) out.print("matcher::world_tile_match: River_Size None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Brook: if (finder->min_river > embark_assist::defs::river_ranges::Brook) { - if (trace) out.print("matcher::world_tile_match: River_Size Brook (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Brook ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Stream: if (finder->min_river > embark_assist::defs::river_ranges::Stream) { - if (trace) out.print("matcher::world_tile_match: River_Size Stream (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Stream ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Minor: if (finder->min_river > embark_assist::defs::river_ranges::Minor) { - if (trace) out.print("matcher::world_tile_match: River_Size Mino (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Mino ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Medium: if (finder->min_river > embark_assist::defs::river_ranges::Medium) { - if (trace) out.print("matcher::world_tile_match: River_Size Medium (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Medium ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Major: if (finder->max_river != embark_assist::defs::river_ranges::NA) { - if (trace) out.print("matcher::world_tile_match: River_Size Major (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Major ({}, {})\n", x, y); return false; } break; @@ -1640,13 +1640,13 @@ namespace embark_assist { // Waterfall if (finder->min_waterfall > tile->max_waterfall) { // N/A = -1 is always smaller - if (trace) out.print("matcher::world_tile_match: Waterfall (%i, %i), finder: %i, tile: %i\n", x, y, finder->min_waterfall, tile->max_waterfall); + if (trace) out.print("matcher::world_tile_match: Waterfall ({}, {}), finder: {}, tile: {}\n", x, y, finder->min_waterfall, tile->max_waterfall); return false; } if (finder->min_waterfall == 0 && // Absent embark_size == 256 && tile->max_waterfall > 0) { - if (trace) out.print("matcher::world_tile_match: Waterfall 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Waterfall 2 ({}, {})\n", x, y); return false; } @@ -1660,14 +1660,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->clay_count == 0 && !tile->neighboring_clay) { - if (trace) out.print("matcher::world_tile_match: Clay Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Clay Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->clay_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Clay Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Clay Absent ({}, {})\n", x, y); return false; } break; @@ -1681,14 +1681,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->sand_count == 0 && !tile->neighboring_sand) { - if (trace) out.print("matcher::world_tile_match: Sand Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Sand Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->sand_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Sand Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Sand Absent ({}, {})\n", x, y); return false; } break; @@ -1701,14 +1701,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->flux_count == 0) { - if (trace) out.print("matcher::world_tile_match: Flux Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Flux Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->flux_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Flux Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Flux Absent ({}, {})\n", x, y); return false; } break; @@ -1721,14 +1721,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->coal_count == 0) { - if (trace) out.print("matcher::world_tile_match: Coal Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Coal Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->coal_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Coal Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Coal Absent ({}, {})\n", x, y); return false; } break; @@ -1742,28 +1742,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->max_region_soil < 1) { - if (trace) out.print("matcher::world_tile_match: Soil Min Very Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Very Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->max_region_soil < 2) { - if (trace) out.print("matcher::world_tile_match: Soil Min Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->max_region_soil < 3) { - if (trace) out.print("matcher::world_tile_match: Soil Min Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Deep ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Deep: if (tile->max_region_soil < 4) { - if (trace) out.print("matcher::world_tile_match: Soil Min Very Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Very Deep ({}, {})\n", x, y); return false; } break; @@ -1779,28 +1779,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::None: if (tile->min_region_soil > 0) { - if (trace) out.print("matcher::world_tile_match: Soil_Max None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->min_region_soil > 1) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Very_Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Very_Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->min_region_soil > 2) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->min_region_soil > 3) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Deep ({}, {})\n", x, y); return false; } break; @@ -1840,14 +1840,14 @@ namespace embark_assist { case embark_assist::defs::freezing_ranges::Permanent: if (min_max_temperature > 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::At_Least_Partial: if (min_min_temperature > 0) { - if (trace) out.print("matcher::world_tile_match: Freezing At_Lest_Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing At_Lest_Partial ({}, {})\n", x, y); return false; } break; @@ -1855,21 +1855,21 @@ namespace embark_assist { case embark_assist::defs::freezing_ranges::Partial: if (min_min_temperature > 0 || max_max_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Partial ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::At_Most_Partial: if (max_max_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing At Most_Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing At Most_Partial ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::Never: if (max_min_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Never (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Never ({}, {})\n", x, y); return false; } break; @@ -1884,28 +1884,28 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::Very_Scarce: if (tile->max_tree_level < embark_assist::defs::tree_levels::Very_Scarce) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Very_Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Very_Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Scarce: if (tile->max_tree_level < embark_assist::defs::tree_levels::Scarce) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Woodland: if (tile->max_tree_level < embark_assist::defs::tree_levels::Woodland) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Woodland (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Woodland ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Heavily_Forested: if (tile->max_tree_level < embark_assist::defs::tree_levels::Heavily_Forested) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Heavily_Forested (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Heavily_Forested ({}, {})\n", x, y); return false; } break; @@ -1918,7 +1918,7 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::None: if (tile->min_tree_level > embark_assist::defs::tree_levels::None) { - if (trace) out.print("matcher::world_tile_match: Max_Trees None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees None ({}, {})\n", x, y); return false; } break; @@ -1926,21 +1926,21 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::Very_Scarce: if (tile->min_tree_level > embark_assist::defs::tree_levels::Very_Scarce) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Very_Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Very_Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Scarce: if (tile->min_tree_level > embark_assist::defs::tree_levels::Scarce) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Woodland: if (tile->min_tree_level > embark_assist::defs::tree_levels::Woodland) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Woodland (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Woodland ({}, {})\n", x, y); return false; } break; @@ -1953,14 +1953,14 @@ namespace embark_assist { case embark_assist::defs::yes_no_ranges::Yes: if (!tile->blood_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Blood_Rain Yes (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Blood_Rain Yes ({}, {})\n", x, y); return false; } break; case embark_assist::defs::yes_no_ranges::No: if (tile->blood_rain_full) { - if (trace) out.print("matcher::world_tile_match: Blood_Rain No (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Blood_Rain No ({}, {})\n", x, y); return false; } break; @@ -1973,35 +1973,35 @@ namespace embark_assist { case embark_assist::defs::syndrome_rain_ranges::Any: if (!tile->permanent_syndrome_rain_possible && !tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Permanent: if (!tile->permanent_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Temporary: if (!tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Temporary (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Temporary ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Not_Permanent: if (tile->permanent_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Not_Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Not_Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::None: if (tile->permanent_syndrome_rain_full || tile->temporary_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain None ({}, {})\n", x, y); return false; } break; @@ -2014,42 +2014,42 @@ namespace embark_assist { case embark_assist::defs::reanimation_ranges::Both: if (!tile->reanimating_possible || !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Both (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Both ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Any: if (!tile->reanimating_possible && !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Thralling: if (!tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Reanimation: if (!tile->reanimating_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Reanimation (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Reanimation ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Not_Thralling: if (tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: Reanimation Not_Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Not_Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::None: if (tile->reanimating_full || tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: Reanimation None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation None ({}, {})\n", x, y); return false; } break; @@ -2063,7 +2063,7 @@ namespace embark_assist { // Region Type 1 if (finder->region_type_1 != -1) { if (!tile->neighboring_region_types[finder->region_type_1]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_1 ({}, {})\n", x, y); return false; } } @@ -2071,7 +2071,7 @@ namespace embark_assist { // Region Type 2 if (finder->region_type_2 != -1) { if (!tile->neighboring_region_types[finder->region_type_2]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_2 ({}, {})\n", x, y); return false; } } @@ -2079,7 +2079,7 @@ namespace embark_assist { // Region Type 3 if (finder->region_type_3 != -1) { if (!tile->neighboring_region_types[finder->region_type_3]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_3 ({}, {})\n", x, y); return false; } } @@ -2087,7 +2087,7 @@ namespace embark_assist { // Biome 1 if (finder->biome_1 != -1) { if (!tile->neighboring_biomes[finder->biome_1]) { - if (trace) out.print("matcher::world_tile_match: Biome_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_1 ({}, {})\n", x, y); return false; } } @@ -2095,7 +2095,7 @@ namespace embark_assist { // Biome 2 if (finder->biome_2 != -1) { if (!tile->neighboring_biomes[finder->biome_2]) { - if (trace) out.print("matcher::world_tile_match: Biome_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_2 ({}, {})\n", x, y); return false; } } @@ -2103,7 +2103,7 @@ namespace embark_assist { // Biome 3 if (finder->biome_3 != -1) { if (!tile->neighboring_biomes[finder->biome_3]) { - if (trace) out.print("matcher::world_tile_match: Biome_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_3 ({}, {})\n", x, y); return false; } } @@ -2146,30 +2146,30 @@ namespace embark_assist { !mineral_1 || !mineral_2 || !mineral_3) { - if (trace) out.print("matcher::world_tile_match: Metal/Economic/Mineral (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Metal/Economic/Mineral ({}, {})\n", x, y); return false; } } // Necro Neighbors if (finder->min_necro_neighbors > tile->necro_neighbors) { - if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 1 ({}, {})\n", x, y); return false; } if (finder->max_necro_neighbors < tile->necro_neighbors && finder->max_necro_neighbors != -1) { - if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 2 ({}, {})\n", x, y); return false; } // Civ Neighbors if (finder->min_civ_neighbors > (int16_t)tile->neighbors.size()) { - if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 1 (%i, %i), %i, %i\n", x, y, finder->min_civ_neighbors, (int)tile->neighbors.size()); + if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 1 ({}, {}), {}, {}\n", x, y, finder->min_civ_neighbors, (int)tile->neighbors.size()); return false; } if (finder->max_civ_neighbors < (int8_t)tile->neighbors.size() && finder->max_civ_neighbors != -1) { - if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 2 ({}, {})\n", x, y); return false; } @@ -2191,7 +2191,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: Specific Neighbors Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Specific Neighbors Present ({}, {})\n", x, y); return false; } @@ -2201,7 +2201,7 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Absent: for (uint16_t k = 0; k < tile->neighbors.size(); k++) { if (finder->neighbors[i].entity_raw == tile->neighbors[k]) { - if (trace) out.print("matcher::world_tile_match: Specific Neighbors Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Specific Neighbors Absent ({}, {})\n", x, y); return false; } } @@ -2223,21 +2223,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->savagery_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Savagery All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->savagery_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Savagery Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->savagery_count[i] == 256) { - if (trace) out.print("matcher::world_tile_match: NS Savagery Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery Absent ({}, {})\n", x, y); return false; } break; @@ -2253,21 +2253,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->evilness_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Evil All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->evilness_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Evil Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->evilness_count[i] == 256) { - if (trace) out.print("matcher::world_tile_match: NS Evil Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil Absent ({}, {})\n", x, y); return false; } break; @@ -2295,14 +2295,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->flux_count == 0) { - if (trace) out.print("matcher::world_tile_match: NS Flux Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Flux Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->flux_count == 256) { - if (trace) out.print("matcher::world_tile_match: NS Flux Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Flux Absent ({}, {})\n", x, y); return false; } break; @@ -2315,14 +2315,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->coal_count == 0) { - if (trace) out.print("matcher::world_tile_match: NS Coal Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Coal Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->coal_count == 256) { - if (trace) out.print("matcher::world_tile_match: NS Coal Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Coal Absent ({}, {})\n", x, y); return false; } break; @@ -2336,28 +2336,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->max_region_soil < 1) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->max_region_soil < 2) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->max_region_soil < 3) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Deep ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Deep: if (tile->max_region_soil < 4) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Deep ({}, {})\n", x, y); return false; } break; @@ -2375,14 +2375,14 @@ namespace embark_assist { case embark_assist::defs::yes_no_ranges::Yes: if (!tile->blood_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Blood_Rain Yes (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Blood_Rain Yes ({}, {})\n", x, y); return false; } break; case embark_assist::defs::yes_no_ranges::No: if (tile->blood_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Blood_Rain No (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Blood_Rain No ({}, {})\n", x, y); return false; } break; @@ -2397,35 +2397,35 @@ namespace embark_assist { case embark_assist::defs::syndrome_rain_ranges::Any: if (!tile->permanent_syndrome_rain_possible && !tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Permanent: if (!tile->permanent_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Temporary: if (!tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Temporary (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Temporary ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Not_Permanent: if (tile->permanent_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Not_Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Not_Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::None: if (tile->permanent_syndrome_rain_full || tile->temporary_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain None ({}, {})\n", x, y); return false; } break; @@ -2438,42 +2438,42 @@ namespace embark_assist { case embark_assist::defs::reanimation_ranges::Both: if (!tile->reanimating_possible || !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Both (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Both ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Any: if (!tile->reanimating_possible && !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Thralling: if (!tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Reanimation: if (!tile->reanimating_possible) { - if (trace) out.print("matcher::world_tile_match:NS Reanimating Reanimating (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match:NS Reanimating Reanimating ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Not_Thralling: if (tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Not_Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Not_Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::None: if (tile->reanimating_full || tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating None ({}, {})\n", x, y); return false; } break; @@ -2483,7 +2483,7 @@ namespace embark_assist { // Magma Min/Max // Biome Count Min (Can't do anything with Max at this level) if (finder->biome_count_min > tile->biome_count) { - if (trace) out.print("matcher::world_tile_match: NS Biome_Count (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_Count ({}, {})\n", x, y); return false; } @@ -2503,7 +2503,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_1 ({}, {})\n", x, y); return false; } } @@ -2524,7 +2524,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_2 ({}, {})\n", x, y); return false; } } @@ -2545,7 +2545,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_3 ({}, {})\n", x, y); return false; } } @@ -2562,7 +2562,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_1 ({}, {})\n", x, y); return false; } } @@ -2579,7 +2579,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_2 ({}, {})\n", x, y); return false; } } @@ -2596,7 +2596,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_3 ({}, {})\n", x, y); return false; } } @@ -2639,7 +2639,7 @@ namespace embark_assist { !mineral_1 || !mineral_2 || !mineral_3) { - if (trace) out.print("matcher::world_tile_match: NS Metal/Economic/Mineral (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Metal/Economic/Mineral ({}, {})\n", x, y); return false; } } @@ -2948,11 +2948,11 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter preliminary_matches = preliminary_world_match(survey_results, &iterator->finder, match_results); if (preliminary_matches == 0) { - out.printerr("matcher::find: Preliminarily matching World Tiles: %i\n", preliminary_matches); + out.printerr("matcher::find: Preliminarily matching World Tiles: {}\n", preliminary_matches); return 0; } else { - out.print("matcher::find: Preliminarily matching World Tiles: %i\n", preliminary_matches); + out.print("matcher::find: Preliminarily matching World Tiles: {}\n", preliminary_matches); } while (screen->location.region_pos.x != 0 || screen->location.region_pos.y != 0) { diff --git a/plugins/examples/persistent_per_save_example.cpp b/plugins/examples/persistent_per_save_example.cpp index 3ca26fc1d3..4067e1d161 100644 --- a/plugins/examples/persistent_per_save_example.cpp +++ b/plugins/examples/persistent_per_save_example.cpp @@ -56,14 +56,14 @@ static PersistentDataItem & ensure_elem_config(color_ostream &out, int id) { if (elems.count(id)) return elems[id]; string keyname = ELEM_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for elem id %d\n", id); + DEBUG(control,out).print("creating new persistent key for elem id {}\n", id); elems.emplace(id, World::GetPersistentSiteData(keyname, true)); return elems[id]; } static void remove_elem_config(color_ostream &out, int id) { if (!elems.count(id)) return; - DEBUG(control,out).print("removing persistent key for elem id %d\n", id); + DEBUG(control,out).print("removing persistent key for elem id {}\n", id); World::DeletePersistentData(elems[id]); elems.erase(id); } @@ -82,7 +82,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -95,19 +95,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -190,7 +190,7 @@ static void do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // TODO: logic that runs every CYCLE_TICKS ticks } diff --git a/plugins/examples/simple_command_example.cpp b/plugins/examples/simple_command_example.cpp index ea45b18d47..d708bc222f 100644 --- a/plugins/examples/simple_command_example.cpp +++ b/plugins/examples/simple_command_example.cpp @@ -21,7 +21,7 @@ namespace DFHack { static command_result do_command(color_ostream &out, vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(log,out).print("initializing %s\n", plugin_name); + DEBUG(log,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, diff --git a/plugins/examples/skeleton.cpp b/plugins/examples/skeleton.cpp index fd77ab691a..a6b9e234a3 100644 --- a/plugins/examples/skeleton.cpp +++ b/plugins/examples/skeleton.cpp @@ -66,7 +66,7 @@ static command_result command_callback1(color_ostream &out, vector ¶ // run when the plugin is loaded DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(status,out).print("initializing %s\n", plugin_name); + DEBUG(status,out).print("initializing {}\n", plugin_name); // For in-tree plugins, don't use the "usage" parameter of PluginCommand. // Instead, add an .rst file with the same name as the plugin to the @@ -80,7 +80,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { - DEBUG(command,out).print("%s command called with %zu parameters\n", + DEBUG(command,out).print("{} command called with {} parameters\n", plugin_name, parameters.size()); // Return CR_WRONG_USAGE to print out your help text. The help text is diff --git a/plugins/examples/ui_addition_example.cpp b/plugins/examples/ui_addition_example.cpp index bbd3af3deb..95bcd16455 100644 --- a/plugins/examples/ui_addition_example.cpp +++ b/plugins/examples/ui_addition_example.cpp @@ -39,14 +39,14 @@ struct title_version_hook : df::viewscreen_titlest { IMPLEMENT_VMETHOD_INTERPOSE(title_version_hook, render); DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(log,out).print("shutting down %s\n", plugin_name); + DEBUG(log,out).print("shutting down {}\n", plugin_name); INTERPOSE_HOOK(title_version_hook, render).remove(); return CR_OK; } DFhackCExport command_result plugin_enable (color_ostream &out, bool enable) { if (enable != is_enabled) { - DEBUG(log,out).print("%s %s\n", plugin_name, + DEBUG(log,out).print("{} {}\n", plugin_name, is_enabled ? "enabled" : "disabled"); if (!INTERPOSE_HOOK(title_version_hook, render).apply(enable)) return CR_FAILURE; diff --git a/plugins/fastdwarf.cpp b/plugins/fastdwarf.cpp index 0d4808db8c..d1a6750a5d 100644 --- a/plugins/fastdwarf.cpp +++ b/plugins/fastdwarf.cpp @@ -37,7 +37,7 @@ enum ConfigValues { static command_result do_command(color_ostream &out, vector & parameters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector & commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( "fastdwarf", @@ -48,7 +48,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vectorpath.dest)) return; - DEBUG(cycle,out).print("teleporting unit %d\n", unit->id); + DEBUG(cycle,out).print("teleporting unit {}\n", unit->id); if (!Units::teleport(unit, unit->path.dest)) return; @@ -150,7 +150,7 @@ static void do_tele(color_ostream &out, df::unit * unit) { } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // fast mode 2 is handled by DF itself bool is_fast = config.get_int(CONFIG_FAST) == 1; @@ -161,7 +161,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { do_tele(out, unit); if (is_fast) { - DEBUG(cycle,out).print("fastifying unit %d\n", unit->id); + DEBUG(cycle,out).print("fastifying unit {}\n", unit->id); Units::setGroupActionTimers(out, unit, 1, df::unit_action_type_group::All); } }); @@ -176,7 +176,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -186,7 +186,7 @@ static command_result do_command(color_ostream &out, vector & parameter return CR_WRONG_USAGE; if (num_params == 0 || parameters[0] == "status") { - out.print("Current state: fast = %d, teleport = %d.\n", + out.print("Current state: fast = {}, teleport = {}.\n", config.get_int(CONFIG_FAST), config.get_int(CONFIG_TELE)); return CR_OK; @@ -196,11 +196,11 @@ static command_result do_command(color_ostream &out, vector & parameter int tele = num_params >= 2 ? string_to_int(parameters[1]) : 0; if (fast < 0 || fast > 2) { - out.printerr("Invalid value for fast: '%s'", parameters[0].c_str()); + out.printerr("Invalid value for fast: '{}'", parameters[0]); return CR_WRONG_USAGE; } if (tele < 0 || tele > 1) { - out.printerr("Invalid value for tele: '%s'", parameters[1].c_str()); + out.printerr("Invalid value for tele: '{}'", parameters[1]); return CR_WRONG_USAGE; } diff --git a/plugins/filltraffic.cpp b/plugins/filltraffic.cpp index f16e8bdd83..23952d8ac2 100644 --- a/plugins/filltraffic.cpp +++ b/plugins/filltraffic.cpp @@ -158,7 +158,7 @@ command_result filltraffic(color_ostream &out, std::vector & params return CR_FAILURE; } - out.print("%d/%d/%d ... FILLING!\n", cx,cy,cz); + out.print("{}/{}/{} ... FILLING!\n", cx,cy,cz); //Naive four-way or six-way flood fill with possible tiles on a stack. stack flood; diff --git a/plugins/fix-occupancy.cpp b/plugins/fix-occupancy.cpp index 7b5a416dbb..5ed962e4d4 100644 --- a/plugins/fix-occupancy.cpp +++ b/plugins/fix-occupancy.cpp @@ -107,8 +107,8 @@ static void scan_building(color_ostream &out, df::building * bld, Expected & exp auto expected_bld = expected.bld(x, y, bld->z); if (bld->isSettingOccupancy() && expected_bld) { if (*expected_bld) { - WARN(log,out).print("Buildings overlap at (%d, %d, %d); please manually remove overlapping building." - " Run ':lua dfhack.gui.revealInDwarfmodeMap(%d, %d, %d, true, true)' to zoom to the tile.\n", + WARN(log,out).print("Buildings overlap at ({}, {}, {}); please manually remove overlapping building." + " Run ':lua dfhack.gui.revealInDwarfmodeMap({}, {}, {}, true, true)' to zoom to the tile.\n", x, y, bld->z, x, y, bld->z); } *expected_bld = bld; @@ -163,7 +163,7 @@ static void normalize_item_vector(color_ostream &out, df::map_block *block, bool prev_id = item_id; } if (needs_sorting) { - INFO(log,out).print("%s item list for map block at (%d, %d, %d)\n", + INFO(log,out).print("{} item list for map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) std::sort(block->items.begin(), block->items.end()); @@ -175,7 +175,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: { // clear building occupancy if there is no building there if (expected_occ.bits.building == df::tile_building_occ::None && block_occ.bits.building != df::tile_building_occ::None) { - INFO(log,out).print("%s building occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} building occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.building = df::tile_building_occ::None; @@ -189,7 +189,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: if (block_occ.bits.building == df::tile_building_occ::Dynamic) block_occ.bits.building = prev_occ; else if (prev_occ != block_occ.bits.building) { - INFO(log,out).print("%s building occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} building occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (dry_run) block_occ.bits.building = prev_occ; @@ -198,13 +198,13 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: // clear unit occupancy if there are no units there if (!expected_occ.bits.unit && block_occ.bits.unit) { - INFO(log,out).print("%s standing unit occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} standing unit occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.unit = false; } if (!expected_occ.bits.unit_grounded && block_occ.bits.unit_grounded) { - INFO(log,out).print("%s grounded unit occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} grounded unit occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.unit_grounded = false; @@ -212,7 +212,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: // clear item occupancy if there are no items there if (!expected_occ.bits.item && block_occ.bits.item) { - INFO(log,out).print("%s item occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} item occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.item = false; @@ -223,7 +223,7 @@ static void fix_tile(color_ostream &out, df::coord pos, bool dry_run) { auto occ = Maps::getTileOccupancy(pos); auto block = Maps::getTileBlock(pos); if (!occ || !block) { - WARN(log,out).print("invalid tile: (%d, %d, %d)\n", pos.x, pos.y, pos.z); + WARN(log,out).print("invalid tile: ({}, {}, {})\n", pos.x, pos.y, pos.z); return; } @@ -264,7 +264,7 @@ static void fix_tile(color_ostream &out, df::coord pos, bool dry_run) { if (expected_bld && expected_occ) reconcile_map_tile(out, *expected_bld, *expected_occ, block->occupancy[pos.x&15][pos.y&15], dry_run, pos.x, pos.y, pos.z); - INFO(log,out).print("verified %zd building(s), %zd unit(s), %zd item(s), 1 map block(s), and 1 map tile(s)\n", + INFO(log,out).print("verified {} building(s), {} unit(s), {} item(s), 1 map block(s), and 1 map tile(s)\n", num_buildings, num_units, num_items); } @@ -273,7 +273,7 @@ static void reconcile_block_items(color_ostream &out, std::set * expect if (!expected_items) { if (block_items.size()) { - INFO(log,out).print("%s stale item references in map block at (%d, %d, %d)\n", + INFO(log,out).print("{} stale item references in map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) block_items.resize(0); @@ -282,7 +282,7 @@ static void reconcile_block_items(color_ostream &out, std::set * expect } if (!std::equal(expected_items->begin(), expected_items->end(), block_items.begin(), block_items.end())) { - INFO(log,out).print("%s stale item references in map block at (%d, %d, %d)\n", + INFO(log,out).print("{} stale item references in map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) { block_items.resize(expected_items->size()); @@ -324,20 +324,20 @@ static void fix_map(color_ostream &out, bool dry_run) { auto expected_occ = expected.occ(x, y, z); auto expected_bld = expected.bld(x, y, z); if (!expected_occ || !expected_bld) { - TRACE(log,out).print("pos out of bounds (%d, %d, %d)\n", x, y, z); + TRACE(log,out).print("pos out of bounds ({}, {}, {})\n", x, y, z); continue; } df::tile_occupancy &block_occ = block->occupancy[xoff][yoff]; if (*expected_bld || (expected_occ->whole & occ_mask) != (block_occ.whole & occ_mask)) { - DEBUG(log,out).print("reconciling occupancy at (%d, %d, %d) (bld=%p, 0x%x ?= 0x%x)\n", - x, y, z, *expected_bld, expected_occ->whole & occ_mask, block_occ.whole & occ_mask); + DEBUG(log,out).print("reconciling occupancy at ({}, {}, {}) (bld={}, 0x{:x} ?= 0x{:x})\n", + x, y, z, static_cast(*expected_bld), expected_occ->whole & occ_mask, block_occ.whole & occ_mask); reconcile_map_tile(out, *expected_bld, *expected_occ, block_occ, dry_run, x, y, z); } } } } - INFO(log,out).print("verified %zd buildings, %zd units, %zd items, %zd map blocks, and %zd map tiles\n", + INFO(log,out).print("verified {} buildings, {} units, {} items, {} map blocks, and {} map tiles\n", world->buildings.all.size(), world->units.active.size(), world->items.other.IN_PLAY.size(), world->map.map_blocks.size(), expected.get_size()); } diff --git a/plugins/fixveins.cpp b/plugins/fixveins.cpp index f87701e0bc..1fc003972e 100644 --- a/plugins/fixveins.cpp +++ b/plugins/fixveins.cpp @@ -92,9 +92,9 @@ command_result df_fixveins (color_ostream &out, vector & parameters) } } if (mineral_removed || feature_removed) - out.print("Removed invalid references from %i mineral inclusion and %i map feature tiles.\n", mineral_removed, feature_removed); + out.print("Removed invalid references from {} mineral inclusion and {} map feature tiles.\n", mineral_removed, feature_removed); if (mineral_added || feature_added) - out.print("Restored missing references to %i mineral inclusion and %i map feature tiles.\n", mineral_added, feature_added); + out.print("Restored missing references to {} mineral inclusion and {} map feature tiles.\n", mineral_added, feature_added); return CR_OK; } diff --git a/plugins/flows.cpp b/plugins/flows.cpp index 8ebf968a0b..c13e355b6c 100644 --- a/plugins/flows.cpp +++ b/plugins/flows.cpp @@ -46,11 +46,11 @@ command_result df_flows (color_ostream &out, vector & parameters) } } - out.print("Blocks with liquid_1=true: %d\n", flow1); - out.print("Blocks with liquid_2=true: %d\n", flow2); - out.print("Blocks with both: %d\n", flowboth); - out.print("Water tiles: %d\n", water); - out.print("Magma tiles: %d\n", magma); + out.print("Blocks with liquid_1=true: {}\n", flow1); + out.print("Blocks with liquid_2=true: {}\n", flow2); + out.print("Blocks with both: {}\n", flowboth); + out.print("Water tiles: {}\n", water); + out.print("Magma tiles: {}\n", magma); return CR_OK; } diff --git a/plugins/follow.cpp b/plugins/follow.cpp index 5649d6bdfb..109c37823b 100644 --- a/plugins/follow.cpp +++ b/plugins/follow.cpp @@ -150,7 +150,7 @@ command_result follow (color_ostream &out, std::vector & parameter ss << "Unpause to begin following " << world->raws.creatures.all[followedUnit->race]->name[0]; if (followedUnit->name.has_name) ss << " " << followedUnit->name.first_name; ss << ". Simply manually move the view to break the following.\n"; - out.print("%s", ss.str().c_str()); + out.print("{}", ss.str()); } else followedUnit = 0; is_enabled = (followedUnit != NULL); diff --git a/plugins/forceequip.cpp b/plugins/forceequip.cpp index fa8cb8ef12..290772e87b 100644 --- a/plugins/forceequip.cpp +++ b/plugins/forceequip.cpp @@ -80,7 +80,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * } else if(!item->isClothing() && !item->isArmorNotClothing()) { - if (verbose) { WARN(log).print("Item %d is not clothing or armor; it cannot be equipped. Please choose a different item (or use the Ignore option if you really want to equip an inappropriate item).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is not clothing or armor; it cannot be equipped. Please choose a different item (or use the Ignore option if you really want to equip an inappropriate item).\n", item->id); } return false; } else if (item->getType() != df::enums::item_type::GLOVES && @@ -90,22 +90,22 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * item->getType() != df::enums::item_type::SHOES && !targetBodyPart) { - if (verbose) { WARN(log).print("Item %d is of an unrecognized type; it cannot be equipped (because the module wouldn't know where to put it).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is of an unrecognized type; it cannot be equipped (because the module wouldn't know where to put it).\n", item->id); } return false; } else if (itemOwner && itemOwner->id != unit->id) { - if (verbose) { WARN(log).print("Item %d is owned by someone else. Equipping it on this unit is not recommended. Please use DFHack's Confiscate plugin, choose a different item, or use the Ignore option to proceed in spite of this warning.\n", item->id); } + if (verbose) { WARN(log).print("Item {} is owned by someone else. Equipping it on this unit is not recommended. Please use DFHack's Confiscate plugin, choose a different item, or use the Ignore option to proceed in spite of this warning.\n", item->id); } return false; } else if (item->flags.bits.in_inventory) { - if (verbose) { WARN(log).print("Item %d is already in a unit's inventory. Direct inventory transfers are not recommended; please move the item to the ground first (or use the Ignore option).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is already in a unit's inventory. Direct inventory transfers are not recommended; please move the item to the ground first (or use the Ignore option).\n", item->id); } return false; } else if (item->flags.bits.in_job) { - if (verbose) { WARN(log).print("Item %d is reserved for use in a queued job. Equipping it is not recommended, as this might interfere with the completion of vital jobs. Use the Ignore option to ignore this warning.\n", item->id); } + if (verbose) { WARN(log).print("Item {} is reserved for use in a queued job. Equipping it is not recommended, as this might interfere with the completion of vital jobs. Use the Ignore option to ignore this warning.\n", item->id); } return false; } @@ -132,45 +132,45 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * else if (bpIndex < unit->body.body_plan->body_parts.size()) { // The current body part is not the one that was specified in the function call, but we can keep searching - if (verbose) { WARN(log).print("Found bodypart %s; not a match; continuing search.\n", currPart->token.c_str()); } + if (verbose) { WARN(log).print("Found bodypart {}; not a match; continuing search.\n", currPart->token); } continue; } else { // The specified body part has not been found, and we've reached the end of the list. Report failure. - if (verbose) { WARN(log).print("The specified body part (%s) does not belong to the chosen unit. Please double-check to ensure that your spelling is correct, and that you have not chosen a dismembered bodypart.\n",targetBodyPart->token.c_str()); } + if (verbose) { WARN(log).print("The specified body part ({}) does not belong to the chosen unit. Please double-check to ensure that your spelling is correct, and that you have not chosen a dismembered bodypart.\n",targetBodyPart->token); } return false; } - if (verbose) { DEBUG(log).print("Inspecting bodypart %s.\n", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Inspecting bodypart {}.\n", currPart->token); } // Inspect the current bodypart if (item->getType() == df::enums::item_type::GLOVES && currPart->flags.is_set(df::body_part_raw_flags::GRASP) && ((item->getGloveHandedness() == const_GloveLeftHandedness && currPart->flags.is_set(df::body_part_raw_flags::LEFT)) || (item->getGloveHandedness() == const_GloveRightHandedness && currPart->flags.is_set(df::body_part_raw_flags::RIGHT)))) { - if (verbose) { DEBUG(log).print("Hand found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Hand found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::HELM && currPart->flags.is_set(df::body_part_raw_flags::HEAD)) { - if (verbose) { DEBUG(log).print("Head found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Head found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::ARMOR && currPart->flags.is_set(df::body_part_raw_flags::UPPERBODY)) { - if (verbose) { DEBUG(log).print("Upper body found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Upper body found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::PANTS && currPart->flags.is_set(df::body_part_raw_flags::LOWERBODY)) { - if (verbose) { DEBUG(log).print("Lower body found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Lower body found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::SHOES && currPart->flags.is_set(df::body_part_raw_flags::STANCE)) { - if (verbose) { DEBUG(log).print("Foot found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Foot found ({}).", currPart->token); } } else if (targetBodyPart && ignoreRestrictions) { // The BP in question would normally be considered ineligible for equipment. But since it was deliberately specified by the user, we'll proceed anyways. - if (verbose) { DEBUG(log).print("Non-standard bodypart found (%s)...", targetBodyPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Non-standard bodypart found ({}).", targetBodyPart->token); } } else if (targetBodyPart) { @@ -205,7 +205,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * // Collision detected; have we reached the limit? if (++collisions >= multiEquipLimit) { - if (verbose) { WARN(log).print(" but it already carries %d piece(s) of equipment. Either remove the existing equipment or use the Multi option.\n", multiEquipLimit); } + if (verbose) { WARN(log).print(" but it already carries {} piece(s) of equipment. Either remove the existing equipment or use the Multi option.\n", multiEquipLimit); } confirmedBodyPart = NULL; break; } @@ -382,13 +382,13 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (targetBodyPart->token.compare(targetBodyPartCode) == 0) { // It is indeed a match; exit the loop (while leaving the variable populated) - if (verbose) { INFO(log).print("Matching bodypart (%s) found.\n", targetBodyPart->token.c_str()); } + if (verbose) { INFO(log).print("Matching bodypart ({}) found.\n", targetBodyPart->token); } break; } else { // Not a match; nullify the variable (it will get re-populated on the next pass through the loop) - if (verbose) { WARN(log).print("Bodypart \"%s\" does not match \"%s\".\n", targetBodyPart->token.c_str(), targetBodyPartCode.c_str()); } + if (verbose) { WARN(log).print("Bodypart ({}) does not match ({}).\n", targetBodyPart->token, targetBodyPartCode); } targetBodyPart = NULL; } } @@ -396,7 +396,7 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (!targetBodyPart) { // Loop iteration is complete but no match was found. - WARN(log).print("The unit does not possess a bodypart of type \"%s\". Please check the spelling or choose a different unit.\n", targetBodyPartCode.c_str()); + WARN(log).print("The unit does not possess a bodypart of type ({}). Please check the spelling or choose a different unit.\n", targetBodyPartCode); return CR_FAILURE; } } @@ -466,7 +466,7 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (itemsEquipped == 0 && !verbose) { WARN(log).print("Some items were found but no equipment changes could be made. Use the /verbose switch to display the reasons for failure.\n"); } - if (itemsEquipped > 0) { INFO(log).print("%d items equipped.\n", itemsEquipped); } + if (itemsEquipped > 0) { INFO(log).print("{} items equipped.\n", itemsEquipped); } // Note: we might expect to recalculate the unit's weight at this point, in order to account for the // added items. In fact, this recalculation occurs automatically during each dwarf's "turn". diff --git a/plugins/generated-creature-renamer.cpp b/plugins/generated-creature-renamer.cpp index 0ea3227b1b..7c330e0056 100644 --- a/plugins/generated-creature-renamer.cpp +++ b/plugins/generated-creature-renamer.cpp @@ -190,10 +190,10 @@ command_result list_creatures(color_ostream &out, std::vector & pa auto creatureRaw = world->raws.creatures.all[i]; if (!creatureRaw->flags.is_set(df::enums::creature_raw_flags::GENERATED)) continue; - out.print("%s",creatureRaw->creature_id.c_str()); + out.print("{}",creatureRaw->creature_id); if (detailed) { - out.print("\t%s",creatureRaw->caste[0]->description.c_str()); + out.print("\t{}",creatureRaw->caste[0]->description); } out.print("\n"); } diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 18570fda95..69eba4b1d1 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -79,13 +79,13 @@ enum class selectability { // result in the plants not being usable for farming or even collectable at all). selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bool farming) { - TRACE(log, out).print("analyzing %s\n", plant->id.c_str()); + TRACE(log, out).print("analyzing {}\n", plant->id); const DFHack::MaterialInfo basic_mat = DFHack::MaterialInfo(plant->material_defs.type[plant_material_def::basic_mat], plant->material_defs.idx[plant_material_def::basic_mat]); bool outOfSeason = false; selectability result = selectability::Nonselectable; if (plant->flags.is_set(plant_raw_flags::TREE)) { - DEBUG(log, out).print("%s is a selectable tree\n", plant->id.c_str()); + DEBUG(log, out).print("{} is a selectable tree\n", plant->id); if (farming) { return selectability::Nonselectable; } @@ -94,7 +94,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo } } else if (plant->flags.is_set(plant_raw_flags::GRASS)) { - DEBUG(log, out).print("%s is a non selectable Grass\n", plant->id.c_str()); + DEBUG(log, out).print("{} is a non selectable Grass\n", plant->id); return selectability::Grass; } @@ -106,7 +106,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED))) { - DEBUG(log, out).print("%s is edible\n", plant->id.c_str()); + DEBUG(log, out).print("{} is edible\n", plant->id); if (farming) { if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW)) { result = selectability::Selectable; @@ -122,7 +122,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo plant->flags.is_set(plant_raw_flags::EXTRACT_VIAL) || plant->flags.is_set(plant_raw_flags::EXTRACT_BARREL) || plant->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL)) { - DEBUG(log, out).print("%s is thread/mill/extract\n", plant->id.c_str()); + DEBUG(log, out).print("{} is thread/mill/extract\n", plant->id); if (farming) { result = selectability::Selectable; } @@ -135,7 +135,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo (basic_mat.material->reaction_product.id.size() > 0 || basic_mat.material->reaction_class.size() > 0)) { - DEBUG(log, out).print("%s has a reaction\n", plant->id.c_str()); + DEBUG(log, out).print("{} has a reaction\n", plant->id); if (farming) { result = selectability::Selectable; } @@ -170,7 +170,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo if (*cur_year_tick >= plant->growths[i]->timing_1 && (plant->growths[i]->timing_2 == -1 || *cur_year_tick <= plant->growths[i]->timing_2)) { - DEBUG(log, out).print("%s has an edible seed or a stockpile growth\n", plant->id.c_str()); + DEBUG(log, out).print("{} has an edible seed or a stockpile growth\n", plant->id); if (!farming || seedSource) { return selectability::Selectable; } @@ -204,11 +204,11 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo } if (outOfSeason) { - DEBUG(log, out).print("%s has an out of season growth\n", plant->id.c_str()); + DEBUG(log, out).print("{} has an out of season growth\n", plant->id); return selectability::OutOfSeason; } else { - DEBUG(log, out).print("%s cannot be gathered\n", plant->id.c_str()); + DEBUG(log, out).print("{} cannot be gathered\n", plant->id); return result; } } @@ -247,7 +247,7 @@ bool picked(const df::plant* plant, int32_t growth_subtype, int32_t growth_densi } bool designate(color_ostream& out, const df::plant* plant, bool farming) { - TRACE(log, out).print("Attempting to designate %s at (%i, %i, %i)\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z); + TRACE(log, out).print("Attempting to designate {} at ({}, {}, {})\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z); if (!farming) { bool istree = (tileMaterial(Maps::getTileBlock(plant->pos)->tiletype[plant->pos.x % 16][plant->pos.y % 16]) == tiletype_material::TREE); @@ -281,14 +281,14 @@ bool designate(color_ostream& out, const df::plant* plant, bool farming) { } for (size_t i = 0; i < plant_raw->growths.size(); i++) { - TRACE(log, out).print("growth item type=%d\n", plant_raw->growths[i]->item_type); + TRACE(log, out).print("growth item type={}\n", ENUM_AS_STR(plant_raw->growths[i]->item_type)); // Only trees have seed growths in vanilla, but raws can be modded... if (plant_raw->growths[i]->item_type != df::item_type::SEEDS && plant_raw->growths[i]->item_type != df::item_type::PLANT_GROWTH) continue; const DFHack::MaterialInfo growth_mat = DFHack::MaterialInfo(plant_raw->growths[i]->mat_type, plant_raw->growths[i]->mat_index); - TRACE(log, out).print("edible_cooked=%d edible_raw=%d leaf_mat=%d\n", + TRACE(log, out).print("edible_cooked={} edible_raw={} leaf_mat={}\n", growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED), growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW), growth_mat.material->flags.is_set(material_flags::STOCKPILE_PLANT_GROWTH)); @@ -402,20 +402,20 @@ command_result df_getplants(color_ostream& out, vector & parameters) { plantSelections[i] = selectablePlant(out, plant, farming); switch (plantSelections[i]) { case selectability::Grass: - out.printerr("%s is a grass and cannot be gathered\n", plant->id.c_str()); + out.printerr("{} is a grass and cannot be gathered\n", plant->id); break; case selectability::Nonselectable: if (farming) { - out.printerr("%s does not have any parts that can be gathered for seeds for farming\n", plant->id.c_str()); + out.printerr("{} does not have any parts that can be gathered for seeds for farming\n", plant->id); } else { - out.printerr("%s does not have any parts that can be gathered\n", plant->id.c_str()); + out.printerr("{} does not have any parts that can be gathered\n", plant->id); } break; case selectability::OutOfSeason: - out.printerr("%s is out of season, with nothing that can be gathered now\n", plant->id.c_str()); + out.printerr("{} is out of season, with nothing that can be gathered now\n", plant->id); break; case selectability::Selectable: @@ -429,7 +429,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { if (plantNames.size() > 0) { out.printerr("Invalid plant ID(s):"); for (set::const_iterator it = plantNames.begin(); it != plantNames.end(); it++) - out.printerr(" %s", it->c_str()); + out.printerr(" {}", *it); out.printerr("\n"); return CR_FAILURE; } @@ -454,7 +454,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { case selectability::OutOfSeason: { if (!treesonly) { - out.print("* (shrub) %s - %s is out of season\n", plant->id.c_str(), plant->name.c_str()); + out.print("* (shrub) {} - {} is out of season\n", plant->id, plant->name); } break; } @@ -465,7 +465,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { (shrubsonly && !plant->flags.is_set(plant_raw_flags::TREE)) || (!treesonly && !shrubsonly)) // 'farming' weeds out trees when determining selectability, so no need to test that explicitly { - out.print("* (%s) %s - %s\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id.c_str(), plant->name.c_str()); + out.print("* ({}) {} - {}\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id, plant->name); } break; } @@ -482,7 +482,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { const df::plant* plant = world->plants.all[i]; df::map_block* cur = Maps::getTileBlock(plant->pos); - TRACE(log, out).print("Examining %s at (%i, %i, %i) [index=%d]\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); + TRACE(log, out).print("Examining {} at ({}, {}, {}) [index={}]\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z, (int)i); int x = plant->pos.x % 16; int y = plant->pos.y % 16; @@ -511,7 +511,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { ++count; } if (!deselect && designate(out, plant, farming)) { - DEBUG(log, out).print("Designated %s at (%i, %i, %i), %d\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); + DEBUG(log, out).print("Designated {} at ({}, {}, {}), {}\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z, (int)i); collectionCount[plant->material]++; ++count; } @@ -519,11 +519,11 @@ command_result df_getplants(color_ostream& out, vector & parameters) { if (count && verbose) { for (size_t i = 0; i < plantSelections.size(); i++) { if (collectionCount[i] > 0) - out.print("Updated %d %s designations.\n", (int)collectionCount[i], world->raws.plants.all[i]->id.c_str()); + out.print("Updated {} {} designations.\n", collectionCount[i], world->raws.plants.all[i]->id); } out.print("\n"); } - out.print("Updated %d plant designations.\n", (int)count); + out.print("Updated {} plant designations.\n", count); return CR_OK; } diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 13988a21f1..5879431152 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -40,10 +40,10 @@ static bool can_invoke(const string &cmdline, df::viewscreen *screen) { } static int cleanupHotkeys(lua_State *) { - DEBUG(log).print("cleaning up old stub keybindings for: %s\n", join_strings(", ", Gui::getCurFocus(true)).c_str()); + DEBUG(log).print("cleaning up old stub keybindings for: {}\n", join_strings(", ", Gui::getCurFocus(true))); std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; - DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); + DEBUG(log).print("clearing keybinding: {}\n", keyspec); Core::getInstance().ClearKeyBindings(keyspec); }); valid = false; @@ -82,7 +82,7 @@ static void add_binding_if_valid(color_ostream &out, const string &sym, const st sorted_keys.push_back(sym); string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); - DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); + DEBUG(log).print("adding keybinding: {} -> {}\n", keyspec, binding); Core::getInstance().AddKeyBinding(keyspec, binding); } @@ -164,10 +164,10 @@ static void list(color_ostream &out) { if (!valid) find_active_keybindings(out, Gui::getCurViewscreen(true), false); - out.print("Valid keybindings for the current focus:\n %s\n", - join_strings("\n", Gui::getCurFocus(true)).c_str()); + out.print("Valid keybindings for the current focus:\n {}\n", + join_strings("\n", Gui::getCurFocus(true))); std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) { - out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str()); + out.print("{}: {}\n", sym, current_bindings[sym]); }); if (!was_valid) @@ -181,7 +181,7 @@ static bool invoke_command(color_ostream &out, const size_t index) { return false; auto cmd = current_bindings[sorted_keys[index]]; - DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); + DEBUG(log).print("invoking command: '{}'\n", cmd); { Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP); @@ -194,11 +194,11 @@ static bool invoke_command(color_ostream &out, const size_t index) { static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { if (!parameters.size()) { - DEBUG(log).print("invoking command: '%s'\n", INVOKE_MENU_DEFAULT_COMMAND.c_str()); + DEBUG(log).print("invoking command: '{}'\n", INVOKE_MENU_DEFAULT_COMMAND); return Core::getInstance().runCommand(out, INVOKE_MENU_DEFAULT_COMMAND); } else if (parameters.size() == 2 && parameters[0] == "menu") { string cmd = INVOKE_MENU_BASE_COMMAND + parameters[1]; - DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); + DEBUG(log).print("invoking command: '{}'\n", cmd); return Core::getInstance().runCommand(out, cmd); } diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index ffa62ed830..158dea2a42 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -62,13 +62,13 @@ void cleanup() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; DEBUG(control, out) - .print("%s from the API; persisting\n", + .print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); @@ -80,7 +80,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } } else { DEBUG(control, out) - .print("%s from the API, but already %s; no action\n", + .print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -103,7 +103,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { plugin_enable(out, true); } DEBUG(control, out) - .print("loading persisted enabled state: %s\n", + .print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; } @@ -113,7 +113,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { DEBUG(control, out) - .print("world unloaded; disabling %s\n", plugin_name); + .print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; cleanup(); } @@ -187,8 +187,7 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { world->map.column_index[bpos.x][bpos.y]; if (!column) { DEBUG(cycle, out) - .print("%s, line %d: column is null (%d, %d).\n", __FILE__, - __LINE__, bpos.x, bpos.y); + .print("{}, line {}: column is null ({}).\n", __FILE__, __LINE__, bpos); continue; } df::block_column_print_infost *glyphs = new df::block_column_print_infost; @@ -242,7 +241,7 @@ struct_identity infinitesky_options::_identity{sizeof(infinitesky_options), &df: command_result infiniteSky(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -254,11 +253,11 @@ command_result infiniteSky(color_ostream &out, return CR_WRONG_USAGE; if (opts.n > 0) { - out.print("Infinite-sky: creating %d new z-level%s of sky.\n", opts.n, + out.print("Infinite-sky: creating {} new z-level{} of sky.\n", opts.n, opts.n == 1 ? "" : "s"); doInfiniteSky(out, opts.n); } else { - out.print("Construction monitoring is %s.\n", + out.print("Construction monitoring is {}.\n", is_enabled ? "enabled" : "disabled"); } return CR_OK; diff --git a/plugins/jobutils.cpp b/plugins/jobutils.cpp index 37912d8429..c32b9adf22 100644 --- a/plugins/jobutils.cpp +++ b/plugins/jobutils.cpp @@ -100,8 +100,8 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (!new_mat.isValid() || new_mat.type != 0) { - out.printerr("New job material isn't inorganic: %s\n", - new_mat.toString().c_str()); + out.printerr("New job material isn't inorganic: {}\n", + new_mat.toString()); return CR_FAILURE; } @@ -109,22 +109,22 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (!cur_mat.isValid() || cur_mat.type != 0) { - out.printerr("Current job material isn't inorganic: %s\n", - cur_mat.toString().c_str()); + out.printerr("Current job material isn't inorganic: {}\n", + cur_mat.toString()); return CR_FAILURE; } df::craft_material_class old_class = cur_mat.getCraftClass(); if (old_class == craft_material_class::None) { - out.printerr("Unexpected current material type: %s\n", - cur_mat.toString().c_str()); + out.printerr("Unexpected current material type: {}\n", + cur_mat.toString()); return CR_FAILURE; } if (new_mat.getCraftClass() != old_class) { - out.printerr("New material %s does not satisfy requirement: %s\n", - new_mat.toString().c_str(), ENUM_KEY_STR(craft_material_class, old_class).c_str()); + out.printerr("New material {} does not satisfy requirement: {}\n", + new_mat.toString(), ENUM_KEY_STR(craft_material_class, old_class)); return CR_FAILURE; } @@ -135,15 +135,15 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (item_mat != cur_mat) { - out.printerr("Job item %zu has different material: %s\n", - i, item_mat.toString().c_str()); + out.printerr("Job item {} has different material: {}\n", + i, item_mat.toString()); return CR_FAILURE; } if (!new_mat.matches(*item)) { - out.printerr("Job item %zu requirements not satisfied by %s.\n", - i, new_mat.toString().c_str()); + out.printerr("Job item {} requirements not satisfied by {}.\n", + i, new_mat.toString()); return CR_FAILURE; } } @@ -212,7 +212,7 @@ static command_result job_material_in_build(color_ostream &out, MaterialInfo &ne } } - out.printerr("Could not find material in list: %s\n", new_mat.toString().c_str()); + out.printerr("Could not find material in list: {}\n", new_mat.toString()); return CR_FAILURE; } @@ -222,7 +222,7 @@ static command_result job_material(color_ostream &out, vector & paramet if (parameters.size() == 1) { if (!new_mat.find(parameters[0])) { - out.printerr("Could not find material: %s\n", parameters[0].c_str()); + out.printerr("Could not find material: {}\n", parameters[0]); return CR_WRONG_USAGE; } } @@ -253,7 +253,7 @@ static command_result job_duplicate(color_ostream &out, vector & parame job->job_type != job_type::CollectSand && job->job_type != job_type::CollectClay)) { - out.printerr("Cannot duplicate job %s\n", ENUM_KEY_STR(job_type,job->job_type).c_str()); + out.printerr("Cannot duplicate job {}\n", ENUM_KEY_STR(job_type,job->job_type)); return CR_FAILURE; } diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index 90b31b3509..2358535621 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -66,14 +66,14 @@ enum StockpileConfigForbidValues { }; static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stockpile_number) { - TRACE(control, out).print("ensuring stockpile config stockpile_number=%d\n", stockpile_number); + TRACE(control, out).print("ensuring stockpile config stockpile_number={}\n", stockpile_number); if (watched_stockpiles.count(stockpile_number)) { TRACE(control, out).print("stockpile exists in watched_stockpiles\n"); return watched_stockpiles[stockpile_number]; } string keyname = CONFIG_KEY_PREFIX + int_to_string(stockpile_number); - DEBUG(control, out).print("creating new persistent key for stockpile %d\n", stockpile_number); + DEBUG(control, out).print("creating new persistent key for stockpile {}\n", stockpile_number); watched_stockpiles.emplace(stockpile_number, World::GetPersistentSiteData(keyname, true)); PersistentDataItem& c = watched_stockpiles[stockpile_number]; c.set_int(STOCKPILE_CONFIG_STOCKPILE_NUMBER, stockpile_number); @@ -89,7 +89,7 @@ static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stock static void remove_stockpile_config(color_ostream& out, int stockpile_number) { if (!watched_stockpiles.count(stockpile_number)) return; - DEBUG(control, out).print("removing persistent key for stockpile %d\n", stockpile_number); + DEBUG(control, out).print("removing persistent key for stockpile {}\n", stockpile_number); World::DeletePersistentData(watched_stockpiles[stockpile_number]); watched_stockpiles.erase(stockpile_number); } @@ -105,7 +105,7 @@ static void do_cycle(color_ostream& out, static void logistics_cycle(color_ostream &out, bool quiet); DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { - DEBUG(control, out).print("initializing %s\n", plugin_name); + DEBUG(control, out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -117,7 +117,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -445,12 +445,12 @@ static const struct BadFlags { } bad_flags; static void scan_item(color_ostream &out, df::item *item, StockProcessor &processor) { - DEBUG(cycle,out).print("scan_item [%s] item_id=%d\n", processor.name.c_str(), item->id); + DEBUG(cycle,out).print("scan_item [{}] item_id={}\n", processor.name, item->id); if (DBG_NAME(cycle).isEnabled(DebugCategory::LTRACE)) { string name = ""; item->getItemDescription(&name, 0); - TRACE(cycle,out).print("item: %s\n", name.c_str()); + TRACE(cycle,out).print("item: {}\n", name); } if (processor.is_designated(out, item)) { @@ -539,7 +539,7 @@ static void train_partials(color_ostream& out, int32_t& train_count) { continue; if (Units::assignTrainer(unit)) { - DEBUG(cycle,out).print("assigned trainer to unit %d\n", unit->id); + DEBUG(cycle,out).print("assigned trainer to unit {}\n", unit->id); ++train_count; } } @@ -549,7 +549,7 @@ static void do_cycle(color_ostream& out, int32_t& melt_count, int32_t& trade_count, int32_t& dump_count, int32_t& train_count, int32_t& forbid_count, int32_t& claim_count) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); cycle_timestamp = world->frame_counter; ProcessorStats melt_stats, trade_stats, dump_stats, train_stats, forbid_stats, claim_stats; @@ -593,7 +593,7 @@ static void do_cycle(color_ostream& out, train_partials(out, train_count); } - TRACE(cycle,out).print("exit %s do_cycle\n", plugin_name); + TRACE(cycle,out).print("exit {} do_cycle\n", plugin_name); } ///////////////////////////////////////////////////// @@ -673,21 +673,21 @@ static int logistics_getStockpileData(lua_State *L) { } static void logistics_cycle(color_ostream &out, bool quiet = false) { - DEBUG(control, out).print("entering logistics_cycle%s\n", quiet ? " (quiet)" : ""); + DEBUG(control, out).print("entering logistics_cycle{}\n", quiet ? " (quiet)" : ""); int32_t melt_count = 0, trade_count = 0, dump_count = 0, train_count = 0, forbid_count = 0, claim_count = 0; do_cycle(out, melt_count, trade_count, dump_count, train_count, forbid_count, claim_count); if (0 < melt_count || !quiet) - out.print("logistics: designated %d item%s for melting\n", melt_count, (melt_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for melting\n", melt_count, (melt_count == 1) ? "" : "s"); if (0 < trade_count || !quiet) - out.print("logistics: designated %d item%s for trading\n", trade_count, (trade_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for trading\n", trade_count, (trade_count == 1) ? "" : "s"); if (0 < dump_count || !quiet) - out.print("logistics: designated %d item%s for dumping\n", dump_count, (dump_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for dumping\n", dump_count, (dump_count == 1) ? "" : "s"); if (0 < train_count || !quiet) - out.print("logistics: designated %d animal%s for training\n", train_count, (train_count == 1) ? "" : "s"); + out.print("logistics: designated {} animal{} for training\n", train_count, (train_count == 1) ? "" : "s"); if (0 < forbid_count || !quiet) - out.print("logistics: designated %d item%s forbidden\n", forbid_count, (forbid_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} forbidden\n", forbid_count, (forbid_count == 1) ? "" : "s"); if (0 < claim_count || !quiet) - out.print("logistics: claimed %d forbidden item%s\n", claim_count, (claim_count == 1) ? "" : "s"); + out.print("logistics: claimed {} forbidden item{} \n", claim_count, (claim_count == 1) ? "" : "s"); } static void find_stockpiles(lua_State *L, int idx, @@ -756,11 +756,11 @@ static void logistics_setStockpileConfig(color_ostream& out, int stockpile_numbe bool dump, bool train, int forbid, bool melt_masterworks) { DEBUG(control, out).print("entering logistics_setStockpileConfig\n"); - DEBUG(control, out).print("stockpile_number=%d, melt=%d, trade=%d, dump=%d, train=%d, forbid=%d, melt_masterworks=%d\n", + DEBUG(control, out).print("stockpile_number={}, melt={}, trade={}, dump={}, train={}, forbid={}, melt_masterworks={}\n", stockpile_number, melt, trade, dump, train, forbid, melt_masterworks); if (!find_stockpile(stockpile_number)) { - out.printerr("invalid stockpile number: %d\n", stockpile_number); + out.printerr("invalid stockpile number: {}\n", stockpile_number); return; } @@ -843,8 +843,8 @@ static int logistics_getGlobalCounts(lua_State *L) { } static bool logistics_setFeature(color_ostream &out, bool enabled, string feature) { - DEBUG(control, out).print("entering logistics_setFeature (enabled=%d, feature=%s)\n", - enabled, feature.c_str()); + DEBUG(control, out).print("entering logistics_setFeature (enabled={}, feature={})\n", + enabled, feature); if (feature != "autoretrain") return false; config.set_bool(CONFIG_TRAIN_PARTIAL, enabled); @@ -854,7 +854,7 @@ static bool logistics_setFeature(color_ostream &out, bool enabled, string featur } static bool logistics_getFeature(color_ostream &out, string feature) { - DEBUG(control, out).print("entering logistics_getFeature (feature=%s)\n", feature.c_str()); + DEBUG(control, out).print("entering logistics_getFeature (feature={})\n", feature); if (feature != "autoretrain") return false; return config.get_bool(CONFIG_TRAIN_PARTIAL); diff --git a/plugins/misery.cpp b/plugins/misery.cpp index b81a4f1daa..7ecdb0d344 100644 --- a/plugins/misery.cpp +++ b/plugins/misery.cpp @@ -48,7 +48,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -61,19 +61,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -179,7 +179,7 @@ static void do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); int strength = STRENGTH_MULTIPLIER * config.get_int(CONFIG_FACTOR); diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp index bce36f9103..e471ac20d3 100644 --- a/plugins/nestboxes.cpp +++ b/plugins/nestboxes.cpp @@ -46,7 +46,7 @@ static void do_cycle(color_ostream &out); static command_result do_command(color_ostream &out, std::vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -59,19 +59,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector name.c_str()); + out.print("only protecting eggs inside burrow: {}\n", burrow->name); } else { @@ -159,7 +159,7 @@ static void printStatus(color_ostream &out){ static command_result do_command(color_ostream &out, std::vector ¶meters){ if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -182,7 +182,7 @@ static command_result do_command(color_ostream &out, std::vector ¶me // static void do_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -209,7 +209,7 @@ static void do_cycle(color_ostream &out) { if (sref && sref->data.job) Job::removeJob(sref->data.job); } - out.print("nestboxes: %d eggs %s\n", item->getStackSize(), fertile ? "forbidden" : "unforbidden"); + out.print("nestboxes: {} eggs {}\n", item->getStackSize(), fertile ? "forbidden" : "unforbidden"); } } } diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 5891f924a3..4bdff21fe2 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -93,7 +93,7 @@ static command_result orders_command(color_ostream & out, std::vector & continue; } - DEBUG(log).print("scanning map tile at offset %d, %d\n", x, y); + DEBUG(log).print("scanning map tile at offset {}, {}\n", x, y); Screen::Pen cur_tile = Screen::readTile(x, y, true); - DEBUG(log).print("tile data: ch=%d, fg=%d, bg=%d, bold=%s\n", + DEBUG(log).print("tile data: ch={}, fg={}, bg={}, bold={}\n", cur_tile.ch, cur_tile.fg, cur_tile.bg, cur_tile.bold ? "true" : "false"); - DEBUG(log).print("tile data: tile=%d, tile_mode=%d, tile_fg=%d, tile_bg=%d\n", - cur_tile.tile, cur_tile.tile_mode, cur_tile.tile_fg, cur_tile.tile_bg); + DEBUG(log).print("tile data: tile={}, tile_mode={}, tile_fg={}, tile_bg={}\n", + cur_tile.tile, static_cast(cur_tile.tile_mode), cur_tile.tile_fg, cur_tile.tile_bg); if (!cur_tile.valid()) { - DEBUG(log).print("cannot read tile at offset %d, %d\n", x, y); + DEBUG(log).print("cannot read tile at offset {}, {}\n", x, y); continue; } bool can_walk = get_can_walk(map_pos); - DEBUG(log).print("tile is %swalkable at offset %d, %d\n", + DEBUG(log).print("tile is {}walkable at offset {}, {}\n", can_walk ? "" : "not ", x, y); if (ctx.use_graphics) { @@ -148,7 +148,7 @@ static bool get_depot_coords(color_ostream &out, unordered_set * depo depot_coords->clear(); for (auto bld : world->buildings.other.TRADE_DEPOT){ - DEBUG(log,out).print("found depot at (%d, %d, %d)\n", bld->centerx, bld->centery, bld->z); + DEBUG(log,out).print("found depot at ({}, {}, {})\n", bld->centerx, bld->centery, bld->z); depot_coords->emplace(bld->centerx, bld->centery, bld->z); } @@ -163,7 +163,7 @@ static bool get_pathability_groups(color_ostream &out, unordered_set * for (auto pos : depot_coords) { auto wgroup = Maps::getWalkableGroup(pos); if (wgroup) { - DEBUG(log,out).print("walkability group at (%d, %d, %d) is %d\n", pos.x, pos.y, pos.z, wgroup); + DEBUG(log,out).print("walkability group at ({}, {}, {}) is {}\n", pos.x, pos.y, pos.z, wgroup); depot_pathability_groups->emplace(wgroup); } } @@ -386,7 +386,7 @@ static bool wagon_flood(color_ostream &out, unordered_set * wagon_pat df::coord pos = ctx.search_edge.top(); ctx.search_edge.pop(); - TRACE(log,out).print("checking tile: (%d, %d, %d); pathability group: %d\n", pos.x, pos.y, pos.z, + TRACE(log,out).print("checking tile: ({}, {}, {}); pathability group: {}\n", pos.x, pos.y, pos.z, Maps::getWalkableGroup(pos)); if (entry_tiles.contains(pos)) { diff --git a/plugins/pet-uncapper.cpp b/plugins/pet-uncapper.cpp index 168c5f0bfd..0370015dcb 100644 --- a/plugins/pet-uncapper.cpp +++ b/plugins/pet-uncapper.cpp @@ -119,22 +119,22 @@ void impregnateMany(color_ostream &out, bool verbose = false) { } if (pregnancies || verbose) { - INFO(cycle, out).print("%d pet pregnanc%s initiated\n", + INFO(cycle, out).print("{} pet pregnanc{} initiated\n", pregnancies, pregnancies == 1 ? "y" : "ies"); } } command_result do_command(color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.size() == 0 || parameters[0] == "status") { - out.print("%s is %s\n\n", plugin_name, is_enabled ? "enabled" : "not enabled"); - out.print("population cap per species: %d\n", config.get_int(CONFIG_POP_CAP)); - out.print("updating pregnancies every %d ticks\n", config.get_int(CONFIG_FREQ)); - out.print("pregancies last %d ticks\n", config.get_int(CONFIG_PREG_TIME)); + out.print("{} is {}\n\n", plugin_name, is_enabled ? "enabled" : "not enabled"); + out.print("population cap per species: {}\n", config.get_int(CONFIG_POP_CAP)); + out.print("updating pregnancies every {} ticks\n", config.get_int(CONFIG_FREQ)); + out.print("pregancies last {} ticks\n", config.get_int(CONFIG_PREG_TIME)); } else if (parameters[0] == "now") { impregnateMany(out, true); } else { @@ -165,19 +165,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, vectorflags.is_set(plant_raw_flags::WET)) || (!is_watery && !p_raw->flags.is_set(plant_raw_flags::DRY))) { - out.printerr("Can't create plant: Plant type can't grow this %s water feature!\n" + out.printerr("Can't create plant: Plant type can't grow this {} water feature!\n" "Override with --force\n", is_watery ? "close to" : "far from"); return CR_FAILURE; } @@ -297,7 +297,7 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt { if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } @@ -332,9 +332,9 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt auto tt = Maps::getTileType(plant->pos); if (!tt || tileShape(*tt) != tiletype_shape::SAPLING) { - out.printerr("Invalid sapling tiletype at (%d, %d, %d): %s!\n", + out.printerr("Invalid sapling tiletype at ({}, {}, {}): {}\n", plant->pos.x, plant->pos.y, plant->pos.z, - tt ? ENUM_KEY_STR(tiletype, *tt).c_str() : "No map block!"); + tt ? ENUM_KEY_STR(tiletype, *tt) : "No map block!"); continue; // Bad tiletype } else if (*tt == tiletype::SaplingDead) @@ -349,9 +349,9 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt } if (do_trees) - out.print("%d saplings and %d trees%s set to grow.\n", grown, grown_trees, options.dry_run ? " would be" : ""); + out.print("{} saplings and {} trees{} set to grow.\n", grown, grown_trees, options.dry_run ? " would be" : ""); else - out.print("%d saplings%s set to grow.\n", grown, options.dry_run ? " would be" : ""); + out.print("{} saplings{} set to grow.\n", grown, options.dry_run ? " would be" : ""); return CR_OK; } @@ -426,7 +426,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } @@ -473,9 +473,9 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (tileShape(*tt) != tiletype_shape::SHRUB) { - out.printerr("Bad shrub tiletype at (%d, %d, %d): %s\n", + out.printerr("Bad shrub tiletype at ({}, {}, {}): {}\n", plant.pos.x, plant.pos.y, plant.pos.z, - ENUM_KEY_STR(tiletype, *tt).c_str()); + ENUM_KEY_STR(tiletype, *tt)); bad_tt = true; } } @@ -483,9 +483,9 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (tileShape(*tt) != tiletype_shape::SAPLING) { - out.printerr("Bad sapling tiletype at (%d, %d, %d): %s\n", + out.printerr("Bad sapling tiletype at ({}, {}, {}): {}\n", plant.pos.x, plant.pos.y, plant.pos.z, - ENUM_KEY_STR(tiletype, *tt).c_str()); + ENUM_KEY_STR(tiletype, *tt)); bad_tt = true; } } @@ -493,7 +493,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl } else { - out.printerr("Bad plant tiletype at (%d, %d, %d): No map block!\n", + out.printerr("Bad plant tiletype at ({}, {}, {}): No map block!\n", plant.pos.x, plant.pos.y, plant.pos.z); bad_tt = true; } @@ -508,7 +508,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl if (!options.dry_run) { if (!uncat_plant(&plant)) - out.printerr("Remove plant: No block column at (%d, %d)!\n", plant.pos.x, plant.pos.y); + out.printerr("Remove plant: No block column at ({}, {})!\n", plant.pos.x, plant.pos.y); if (!bad_tt) // TODO: trees set_tt(plant.pos); @@ -518,7 +518,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl } } - out.print("Plants%s removed: %d (%d bad)\n", options.dry_run ? " that would be" : "", count, count_bad); + out.print("Plants{} removed: {} ({} bad)\n", options.dry_run ? " that would be" : "", count, count_bad); return CR_OK; } @@ -538,11 +538,11 @@ command_result df_plant(color_ostream &out, vector ¶meters) { // Print all non-grass raw IDs ("plant list") out.print("--- Shrubs ---\n"); for (auto p_raw : world->raws.plants.bushes) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); out.print("\n--- Saplings ---\n"); for (auto p_raw : world->raws.plants.trees) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); return CR_OK; } @@ -574,7 +574,7 @@ command_result df_plant(color_ostream &out, vector ¶meters) return CR_FAILURE; } - DEBUG(log, out).print("pos_1 = (%d, %d, %d)\npos_2 = (%d, %d, %d)\n", + DEBUG(log, out).print("pos_1 = ({}, {}, {})\npos_2 = ({}, {}, {})\n", pos_1.x, pos_1.y, pos_1.z, pos_2.x, pos_2.y, pos_2.z); if (!Core::getInstance().isMapLoaded()) @@ -599,7 +599,7 @@ command_result df_plant(color_ostream &out, vector ¶meters) if (!pos_1.isValid()) { // Attempt to use cursor for pos if active Gui::getCursorCoords(pos_1); - DEBUG(log, out).print("Try to use cursor (%d, %d, %d) for pos_1.\n", + DEBUG(log, out).print("Try to use cursor ({}, {}, {}) for pos_1.\n", pos_1.x, pos_1.y, pos_1.z); if (!pos_1.isValid()) @@ -609,20 +609,20 @@ command_result df_plant(color_ostream &out, vector ¶meters) } } - DEBUG(log, out).print("plant_idx = %d\n", options.plant_idx); + DEBUG(log, out).print("plant_idx = {}\n", options.plant_idx); auto p_raw = vector_get(world->raws.plants.all, options.plant_idx); if (p_raw) { - DEBUG(log, out).print("Plant raw: %s\n", p_raw->id.c_str()); + DEBUG(log, out).print("Plant raw: {}\n", p_raw->id); if (p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Plant raw was grass: %d (%s)\n", options.plant_idx, p_raw->id.c_str()); + out.printerr("Plant raw was grass: {} ({})\n", options.plant_idx, p_raw->id); return CR_FAILURE; } } else { - out.printerr("Plant raw not found for create: %d\n", options.plant_idx); + out.printerr("Plant raw not found for create: {}\n", options.plant_idx); return CR_FAILURE; } } @@ -638,24 +638,24 @@ command_result df_plant(color_ostream &out, vector ¶meters) for (auto idx : filter) { - DEBUG(log, out).print("Filter/exclude test idx: %d\n", idx); + DEBUG(log, out).print("Filter/exclude test idx: {}\n", idx); auto p_raw = vector_get(world->raws.plants.all, idx); if (p_raw) { - DEBUG(log, out).print("Filter/exclude raw: %s\n", p_raw->id.c_str()); + DEBUG(log, out).print("Filter/exclude raw: {}\n", p_raw->id); if (p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Filter/exclude plant raw was grass: %d (%s)\n", idx, p_raw->id.c_str()); + out.printerr("Filter/exclude plant raw was grass: {} ({})\n", idx, p_raw->id); return CR_FAILURE; } else if (options.grow && !p_raw->flags.is_set(plant_raw_flags::TREE)) { // User might copy-paste filters between grow and remove, so just log this - DEBUG(log, out).print("Filter/exclude shrub with grow: %d (%s)\n", idx, p_raw->id.c_str()); + DEBUG(log, out).print("Filter/exclude shrub with grow: {} ({})\n", idx, p_raw->id); } } else { - out.printerr("Plant raw not found for filter/exclude: %d\n", idx); + out.printerr("Plant raw not found for filter/exclude: {}\n", idx); return CR_FAILURE; } } @@ -689,12 +689,12 @@ command_result df_plant(color_ostream &out, vector ¶meters) bounds.addPos(world->map.x_count-1, world->map.y_count-1, 0); } - DEBUG(log, out).print("bounds = (%d:%d, %d:%d, %d:%d)\n", + DEBUG(log, out).print("bounds = ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index ae8337859d..eca7b888de 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -73,7 +73,7 @@ static void on_new_active_unit(color_ostream& out, void* data); static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, "Manage room assignments for off-map units and noble roles.", @@ -92,12 +92,12 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { EventManager::unregisterAll(plugin_self); } - DEBUG(control, out).print("now %s\n", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("now {}\n", is_enabled ? "enabled" : "disabled"); return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -265,7 +265,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!World::isFortressMode() || !Core::getInstance().isMapLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -344,17 +344,17 @@ static void assign_nobles(color_ostream &out) { zone->spec_sub_flag.bits.active = true; Buildings::setOwner(zone, unit); assigned = true; - INFO(cycle,out).print("preserve-rooms: assigning %s to a %s-associated %s\n", - DF2CONSOLE(Units::getReadableName(unit)).c_str(), - toLower_cp437(code).c_str(), - ENUM_KEY_STR(civzone_type, zone->type).c_str()); + INFO(cycle,out).print("preserve-rooms: assigning {} to a {}-associated {}\n", + DF2CONSOLE(Units::getReadableName(unit)), + toLower_cp437(code), + ENUM_KEY_STR(civzone_type, zone->type)); break; } if (assigned) break; } if (!assigned && (zone->spec_sub_flag.bits.active || zone->assigned_unit_id != -1)) { - DEBUG(cycle,out).print("noble zone now reserved for eventual office holder: %d\n", zone_id); + DEBUG(cycle,out).print("noble zone now reserved for eventual office holder: {}\n", zone_id); zone->spec_sub_flag.bits.active = false; Buildings::setOwner(zone, NULL); } @@ -403,8 +403,8 @@ static void scrub_reservations(color_ostream &out) { continue; } } - DEBUG(cycle,out).print("removed reservation for dead, culled, or non-army hfid %d: %s\n", hfid, - hf ? DF2CONSOLE(Units::getReadableName(hf)).c_str() : "culled"); + DEBUG(cycle,out).print("removed reservation for dead, culled, or non-army hfid {}: {}\n", hfid, + hf ? DF2CONSOLE(Units::getReadableName(hf)) : "culled"); hfids_to_scrub.push_back(hfid); for (int32_t zone_id : zone_ids) { if (scrub_id_from_entries(hfid, zone_id, reserved_zones)) { @@ -461,9 +461,9 @@ static void handle_missing_assignments(color_ostream &out, if (spouse_hf && share_with_spouse) { if (spouse && Units::isActive(spouse) && !Units::isDead(spouse) && active_unit_ids.contains(spouse->id)) { - DEBUG(cycle,out).print("assigning zone %d (%s) to spouse %s\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), - DF2CONSOLE(Units::getReadableName(spouse)).c_str()); + DEBUG(cycle,out).print("assigning zone {} ({}) to spouse {}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), + DF2CONSOLE(Units::getReadableName(spouse))); Buildings::setOwner(zone, spouse); continue; } @@ -471,22 +471,22 @@ static void handle_missing_assignments(color_ostream &out, if (hf->died_year > -1) continue; // register the hf ids for reassignment and reserve the room - DEBUG(cycle,out).print("registering primary unit for reassignment to zone %d (%s): %d %s\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), hf->unit_id, - DF2CONSOLE(Units::getReadableName(hf)).c_str()); + DEBUG(cycle,out).print("registering primary unit for reassignment to zone {} ({}): {} {}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), hf->unit_id, + DF2CONSOLE(Units::getReadableName(hf))); pending_reassignment[hfid].push_back(zone_id); reserved_zones[zone_id].push_back(hfid); if (share_with_spouse && spouse) { - DEBUG(cycle,out).print("registering spouse unit for reassignment to zone %d (%s): hfid=%d\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), spouse_hfid); + DEBUG(cycle,out).print("registering spouse unit for reassignment to zone {} ({}): hfid={}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), spouse_hfid); pending_reassignment[spouse_hfid].push_back(zone_id); reserved_zones[zone_id].push_back(spouse_hfid); } - INFO(cycle,out).print("preserve-rooms: reserving %s for the return of %s%s%s\n", - toLower_cp437(ENUM_KEY_STR(civzone_type, zone->type)).c_str(), - DF2CONSOLE(Units::getReadableName(hf)).c_str(), + INFO(cycle,out).print("preserve-rooms: reserving {} for the return of {}{}\n", + toLower_cp437(ENUM_KEY_STR(civzone_type, zone->type)), + DF2CONSOLE(Units::getReadableName(hf)), spouse_hf ? " or their spouse, " : "", - spouse_hf ? DF2CONSOLE(Units::getReadableName(spouse_hf)).c_str() : ""); + spouse_hf ? DF2CONSOLE(Units::getReadableName(spouse_hf)) : ""); zone->spec_sub_flag.bits.active = false; } @@ -504,7 +504,7 @@ static void process_rooms(color_ostream &out, for (auto zone : vec) { auto idx = linear_index(df::global::world->buildings.all, (df::building*)(zone)); if (idx == -1) { - WARN(cycle, out).print("invalid building pointer %p in building vector\n", zone); + WARN(cycle, out).print("invalid building pointer {} in building vector\n", static_cast(zone)); continue; } if (zone->assigned_unit_id == -1) { @@ -513,7 +513,7 @@ static void process_rooms(color_ostream &out, } auto owner = Buildings::getOwner(zone); if (!owner) { - DEBUG(cycle, out).print("building %d has owner id %d but no such unit exists\n", zone->id, zone->assigned_unit_id); + DEBUG(cycle, out).print("building {} has owner id {} but no such unit exists\n", zone->id, zone->assigned_unit_id); continue; } auto hf = df::historical_figure::find(owner->hist_figure_id); @@ -530,7 +530,7 @@ static void process_rooms(color_ostream &out, static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; - TRACE(cycle,out).print("running %s cycle\n", plugin_name); + TRACE(cycle,out).print("running {} cycle\n", plugin_name); if (config.get_bool(CONFIG_TRACK_MISSIONS)) { unordered_set active_unit_ids; @@ -542,7 +542,7 @@ static void do_cycle(color_ostream &out) { process_rooms(out, active_unit_ids, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); process_rooms(out, active_unit_ids, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, false); - DEBUG(cycle,out).print("tracking zone assignments: bedrooms: %zd, offices: %zd, dining halls: %zd, tombs: %zd\n", + DEBUG(cycle,out).print("tracking zone assignments: bedrooms: {}, offices: {}, dining halls: {}, tombs: {}\n", last_known_assignments_bedroom.size(), last_known_assignments_office.size(), last_known_assignments_dining.size(), last_known_assignments_tomb.size()); @@ -584,15 +584,15 @@ static void on_new_active_unit(color_ostream& out, void* data) { auto unit = df::unit::find(unit_id); if (!unit || unit->hist_figure_id < 0) return; - TRACE(event,out).print("unit %d (%s) arrived on map (hfid: %d, in pending: %d)\n", - unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str(), unit->hist_figure_id, + TRACE(event,out).print("unit {} ({}) arrived on map (hfid: {}, in pending: {})\n", + unit->id, DF2CONSOLE(Units::getReadableName(unit)), unit->hist_figure_id, pending_reassignment.contains(unit->hist_figure_id)); auto hfid = unit->hist_figure_id; auto it = pending_reassignment.find(hfid); if (it == pending_reassignment.end()) return; - INFO(event,out).print("preserve-rooms: restoring room ownership for %s\n", - DF2CONSOLE(Units::getReadableName(unit)).c_str()); + INFO(event,out).print("preserve-rooms: restoring room ownership for {}\n", + DF2CONSOLE(Units::getReadableName(unit))); for (auto zone_id : it->second) { reserved_zones.erase(zone_id); auto zone = virtual_cast(df::building::find(zone_id)); @@ -601,7 +601,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { zone->spec_sub_flag.bits.active = true; if (zone->assigned_unit_id != -1 || spouse_has_sharable_room(out, hfid, zone->type)) continue; - DEBUG(event,out).print("reassigning zone %d\n", zone->id); + DEBUG(event,out).print("reassigning zone {}\n", zone->id); Buildings::setOwner(zone, unit); } pending_reassignment.erase(it); @@ -617,8 +617,8 @@ static void preserve_rooms_cycle(color_ostream &out) { } static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string feature) { - DEBUG(control,out).print("preserve_rooms_setFeature: enabled=%d, feature=%s\n", - enabled, feature.c_str()); + DEBUG(control,out).print("preserve_rooms_setFeature: enabled={}, feature={}\n", + enabled, feature); if (feature == "track-missions") { config.set_bool(CONFIG_TRACK_MISSIONS, enabled); if (is_enabled && enabled) @@ -633,7 +633,7 @@ static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string f } static bool preserve_rooms_getFeature(color_ostream &out, string feature) { - TRACE(control,out).print("preserve_rooms_getFeature: feature=%s\n", feature.c_str()); + TRACE(control,out).print("preserve_rooms_getFeature: feature={}\n", feature); if (feature == "track-missions") return config.get_bool(CONFIG_TRACK_MISSIONS); if (feature == "track-roles") @@ -642,7 +642,7 @@ static bool preserve_rooms_getFeature(color_ostream &out, string feature) { } static bool preserve_rooms_resetFeatureState(color_ostream &out, string feature) { - DEBUG(control,out).print("preserve_rooms_resetFeatureState: feature=%s\n", feature.c_str()); + DEBUG(control,out).print("preserve_rooms_resetFeatureState: feature={}\n", feature); if (feature == "track-missions") { vector zone_ids; std::transform(reserved_zones.begin(), reserved_zones.end(), std::back_inserter(zone_ids), [](auto & elem){ return elem.first; }); @@ -667,7 +667,7 @@ static int preserve_rooms_assignToRole(lua_State *L) { virtual_cast(df::building::find(zone_id)); if (!zone) return 0; - DEBUG(control,*out).print("preserve_rooms_assignToRole: zone_id=%d\n", zone->id); + DEBUG(control,*out).print("preserve_rooms_assignToRole: zone_id={}\n", zone->id); vector group_codes; Lua::GetVector(L, group_codes); @@ -689,7 +689,7 @@ static int preserve_rooms_assignToRole(lua_State *L) { static string get_role_assignment(color_ostream &out, df::building_civzonest * zone) { if (!zone) return ""; - TRACE(control,out).print("get_role_assignment: zone_id=%d\n", zone->id); + TRACE(control,out).print("get_role_assignment: zone_id={}\n", zone->id); auto it = noble_zones.find(zone->id); if (it == noble_zones.end() || it->second.empty()) return ""; @@ -709,7 +709,7 @@ static bool preserve_rooms_isReserved(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return false; - TRACE(control,out).print("preserve_rooms_isReserved: zone_id=%d\n", zone->id); + TRACE(control,out).print("preserve_rooms_isReserved: zone_id={}\n", zone->id); auto it = reserved_zones.find(zone->id); return it != reserved_zones.end() && it->second.size() > 0; } @@ -718,7 +718,7 @@ static string preserve_rooms_getReservationName(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return ""; - TRACE(control,out).print("preserve_rooms_getReservationName: zone_id=%d\n", zone->id); + TRACE(control,out).print("preserve_rooms_getReservationName: zone_id={}\n", zone->id); auto it = reserved_zones.find(zone->id); if (it != reserved_zones.end() && it->second.size() > 0) { if (auto hf = df::historical_figure::find(it->second.front())) { @@ -732,7 +732,7 @@ static bool preserve_rooms_clearReservation(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return false; - DEBUG(control,out).print("preserve_rooms_clearReservation: zone_id=%d\n", zone->id); + DEBUG(control,out).print("preserve_rooms_clearReservation: zone_id={}\n", zone->id); clear_reservation(out, zone->id, zone); return true; } diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 40b9eb56fd..a695b22e31 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -53,25 +53,25 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector & params) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot use %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot use {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (params.size() == 0 || params[0] == "status") { - out.print("%s is currently %s\n", plugin_name, is_enabled ? "enabled" : "disabled"); + out.print("{} is currently {}\n", plugin_name, is_enabled ? "enabled" : "disabled"); if (is_enabled) { out.print("tracked tomb assignments:\n"); std::for_each(tomb_assignments.begin(), tomb_assignments.end(), [&out](const auto& p){ auto& [unit_id, building_id] = p; auto* unit = df::unit::find(unit_id); std::string name = unit ? DF2CONSOLE(Units::getReadableName(unit)) : "UNKNOWN UNIT" ; - out.print("%s (id %d) -> building %d\n", name.c_str(), unit_id, building_id); + out.print("{} (id {}) -> building {}\n", name, unit_id, building_id); }); } return CR_OK; } if (params[0] == "now") { if (!is_enabled) { - out.printerr("Cannot update %s when not enabled", plugin_name); + out.printerr("Cannot update {} when not enabled", plugin_name); return CR_FAILURE; } update_tomb_assignments(out); @@ -96,13 +96,13 @@ static void do_disable() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) @@ -110,7 +110,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { else do_disable(); } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -118,7 +118,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); // PluginManager handles unregistering our handler from EventManager, // so we don't have to do that here @@ -136,7 +136,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { } is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); if (is_enabled) do_enable(out); @@ -146,7 +146,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; do_disable(); @@ -176,16 +176,16 @@ void onUnitDeath(color_ostream& out, void* ptr) { int32_t building_id = it->second; if (!assign_to_tomb(unit, building_id)) { if (unit) { - WARN(event, out).print("%s died - but failed to assign them back to their tomb %d\n", + WARN(event, out).print("{} died - but failed to assign them back to their tomb {}\n", DF2CONSOLE(Units::getReadableName(unit)).c_str(), building_id); } else { - WARN(event, out).print("Unit %d died - but failed to assign them back to their tomb %d\n", unit_id, building_id); + WARN(event, out).print("Unit {} died - but failed to assign them back to their tomb {}\n", unit_id, building_id); } return; } // success, print status update and remove assignment from our memo-list - INFO(event, out).print("%s died - assigning them back to their tomb\n", + INFO(event, out).print("{} died - assigning them back to their tomb\n", DF2CONSOLE(Units::getReadableName(unit)).c_str()); tomb_assignments.erase(it); } @@ -206,10 +206,10 @@ static void update_tomb_assignments(color_ostream &out) { if (it == tomb_assignments.end()) { tomb_assignments.emplace(tomb->assigned_unit_id, tomb->id); - DEBUG(cycle, out).print("%s new tomb assignment, unit %d to tomb %d\n", + DEBUG(cycle, out).print("{} new tomb assignment, unit {} to tomb {}\n", plugin_name, tomb->assigned_unit_id, tomb->id); } else if (it->second != tomb->id) { - DEBUG(cycle, out).print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", + DEBUG(cycle, out).print("{} tomb assignment to {} changed, (old: {}, new: {})\n", plugin_name, tomb->assigned_unit_id, it->second, tomb->id); it->second = tomb->id; } @@ -221,17 +221,17 @@ static void update_tomb_assignments(color_ostream &out) { auto bld = df::building::find(building_id); if (!bld) { - DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); + DEBUG(cycle, out).print("{} tomb missing: {} - removing\n", plugin_name, building_id); return true; } auto tomb = virtual_cast(bld); if (!tomb || !tomb->flags.bits.exists) { - DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); + DEBUG(cycle, out).print("{} tomb missing: {} - removing\n", plugin_name, building_id); return true; } if (tomb->assigned_unit_id != unit_id) { - DEBUG(cycle, out).print("%s unit %d unassigned from tomb %d - removing\n", plugin_name, unit_id, building_id); + DEBUG(cycle, out).print("{} unit {} unassigned from tomb {} - removing\n", plugin_name, unit_id, building_id); return true; } diff --git a/plugins/probe.cpp b/plugins/probe.cpp index eff0c98c86..f49e167867 100644 --- a/plugins/probe.cpp +++ b/plugins/probe.cpp @@ -41,7 +41,7 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) if (!unit) return CR_FAILURE; - out.print("Creature %d, race %d (0x%x), civ %d (0x%x)\n", + out.print("Creature {}, race {} (0x{:x}), civ {} (0x{:x})\n", unit->id, unit->race, unit->race, unit->civ_id, unit->civ_id); for (auto inv_item : unit->inventory) { @@ -62,25 +62,25 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) } static void describeTile(color_ostream &out, df::tiletype tiletype) { - out.print("%d", tiletype); + out.print("{}", static_cast(tiletype)); if (tileName(tiletype)) - out.print(" = %s", tileName(tiletype)); - out.print(" (%s)", ENUM_KEY_STR(tiletype, tiletype).c_str()); + out.print(" = {}", tileName(tiletype)); + out.print(" ({})", ENUM_KEY_STR(tiletype, tiletype).c_str()); out.print("\n"); df::tiletype_shape shape = tileShape(tiletype); df::tiletype_material material = tileMaterial(tiletype); df::tiletype_special special = tileSpecial(tiletype); df::tiletype_variant variant = tileVariant(tiletype); - out.print("%-10s: %4d %s\n","Class" ,shape, - ENUM_KEY_STR(tiletype_shape, shape).c_str()); - out.print("%-10s: %4d %s\n","Material" , - material, ENUM_KEY_STR(tiletype_material, material).c_str()); - out.print("%-10s: %4d %s\n","Special" , - special, ENUM_KEY_STR(tiletype_special, special).c_str()); - out.print("%-10s: %4d %s\n" ,"Variant" , - variant, ENUM_KEY_STR(tiletype_variant, variant).c_str()); - out.print("%-10s: %s\n" ,"Direction", + out.print("{:>10}: {:4} {}\n","Class" ,static_cast(shape), + ENUM_KEY_STR(tiletype_shape, shape)); + out.print("{:>10}: {:4} {}\n","Material" , static_cast(material), + ENUM_KEY_STR(tiletype_material, material)); + out.print("{:>10}: {:4} {}\n","Special" , static_cast(special), + ENUM_KEY_STR(tiletype_special, special)); + out.print("{:>10}: {:4} {}\n","Variant" , static_cast(variant), + ENUM_KEY_STR(tiletype_variant, variant)); + out.print("{:>10}: {}\n" ,"Direction", tileDirection(tiletype).getStr()); out.print("\n"); } @@ -129,7 +129,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) } auto &block = *b->getRaw(); - out.print("block addr: %p\n\n", &block); + out.print("block addr: {}\n\n", static_cast(&block)); df::tiletype tiletype = mc.tiletypeAt(cursor); df::tile_designation &des = block.designation[tileX][tileY]; @@ -143,8 +143,8 @@ static command_result df_probe(color_ostream &out, vector & parameters) out.print("base: "); describeTile(out, mc.baseTiletypeAt(cursor)); - out.print("temperature1: %d U\n", mc.temperature1At(cursor)); - out.print("temperature2: %d U\n", mc.temperature2At(cursor)); + out.print("temperature1: {} U\n", mc.temperature1At(cursor)); + out.print("temperature2: {} U\n", mc.temperature2At(cursor)); int offset = block.region_offset[des.bits.biome]; int bx = clip_range(block.region_pos.x + (offset % 3) - 1, 0, world->world_data->world_width-1); @@ -223,12 +223,12 @@ static command_result df_probe(color_ostream &out, vector & parameters) if (des.bits.water_stagnant) out << "stagnant" << std::endl; - #define PRINT_FLAG( FIELD, BIT ) out.print("%-16s= %c\n", #BIT , ( FIELD.bits.BIT ? 'Y' : ' ' ) ) + #define PRINT_FLAG( FIELD, BIT ) out.print("{:<16} = {}\n", #BIT , ( FIELD.bits.BIT ? 'Y' : ' ' ) ) - out.print("%-16s= %s\n", "dig", enum_item_key(des.bits.dig).c_str()); + out.print("{:<16} = {}\n", "dig", enum_item_key(des.bits.dig)); PRINT_FLAG(occ, dig_marked); PRINT_FLAG(occ, dig_auto); - out.print("%-16s= %s\n", "traffic", enum_item_key(des.bits.traffic).c_str()); + out.print("{:<16} = {}\n", "traffic", enum_item_key(des.bits.traffic)); PRINT_FLAG(occ, carve_track_north); PRINT_FLAG(occ, carve_track_south); PRINT_FLAG(occ, carve_track_east); @@ -241,7 +241,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) PRINT_FLAG( des, water_table ); PRINT_FLAG( des, rained ); PRINT_FLAG( occ, monster_lair); - out.print("%-16s= %d\n", "fog_of_war", fog_of_war); + out.print("{:<16} = {}\n", "fog_of_war", fog_of_war); df::coord2d pc(blockX, blockY); @@ -251,19 +251,20 @@ static command_result df_probe(color_ostream &out, vector & parameters) PRINT_FLAG( des, feature_local ); if(local.type != -1) { - out.print("%-16s", ""); - out.print(" %4d", block.local_feature); - out.print(" (%2d)", local.type); - out.print(" addr %p ", local.origin); - out.print(" %s\n", sa_feature(local.type)); + out.print("{:<16}", ""); + out.print(" {:4}", block.local_feature); + out.print(" ({:2})", static_cast(local.type)); + out.print(" addr {}", static_cast(local.origin)); + out.print(" {}", sa_feature(local.type)); } PRINT_FLAG( des, feature_global ); if(global.type != -1) { - out.print("%-16s", ""); - out.print(" %4d", block.global_feature); - out.print(" (%2d)", global.type); - out.print(" %s\n", sa_feature(global.type)); + out.print("{:<16}", ""); + out.print(" {:4}", block.global_feature); + out.print(" ({:2})", static_cast(global.type)); + out.print(" {}", static_cast(global.origin)); + out.print(" {}", sa_feature(global.type)); } out << "local feature idx: " << block.local_feature << std::endl; @@ -272,7 +273,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) out << std::endl; out << "Occupancy:" << std::endl; - out.print("%-16s= %s\n", "building", enum_item_key(occ.bits.building).c_str()); + out.print("{:<16} = {}\n", "building", enum_item_key(occ.bits.building)); PRINT_FLAG(occ, unit); PRINT_FLAG(occ, unit_grounded); PRINT_FLAG(occ, item); @@ -331,67 +332,67 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) Subtype subtype{bld->getSubtype()}; int32_t custom = bld->getCustomType(); - out.print("Building %i, \"%s\", type %s (%i)", + out.print("Building {:<4}, \"{}\", type {} ({})", bld->id, - name.c_str(), - ENUM_KEY_STR(building_type, bld_type).c_str(), - bld_type); + name, + ENUM_KEY_STR(building_type, bld_type), + static_cast(bld_type)); switch (bld_type) { case df::building_type::Civzone: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(civzone_type, subtype.civzone_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(civzone_type, subtype.civzone_type), subtype.subtype); break; case df::building_type::Furnace: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(furnace_type, subtype.furnace_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(furnace_type, subtype.furnace_type), subtype.subtype); if (subtype.furnace_type == df::furnace_type::Custom) - out.print(", custom type %s (%i)", - world->raws.buildings.all[custom]->code.c_str(), + out.print(", custom type {} ({})", + world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Workshop: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(workshop_type, subtype.workshop_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(workshop_type, subtype.workshop_type), subtype.subtype); if (subtype.workshop_type == df::workshop_type::Custom) - out.print(", custom type %s (%i)", - world->raws.buildings.all[custom]->code.c_str(), + out.print(", custom type {} ({})", + world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Construction: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(construction_type, subtype.construction_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(construction_type, subtype.construction_type), subtype.subtype); break; case df::building_type::Shop: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(shop_type, subtype.shop_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(shop_type, subtype.shop_type), subtype.subtype); break; case df::building_type::SiegeEngine: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(siegeengine_type, subtype.siegeengine_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(siegeengine_type, subtype.siegeengine_type), subtype.subtype); break; case df::building_type::Trap: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(trap_type, subtype.trap_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(trap_type, subtype.trap_type), subtype.subtype); break; case df::building_type::NestBox: { df::building_nest_boxst* nestbox = virtual_cast(bld); if (nestbox) - out.print(", claimed:(%i), items:%zu", nestbox->claimed_by, nestbox->contained_items.size()); + out.print(", claimed:({}), items:({})", nestbox->claimed_by, nestbox->contained_items.size()); break; } default: if (subtype.subtype != -1) - out.print(", subtype %i", subtype.subtype); + out.print(", subtype {}", subtype.subtype); break; } diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index e8883fde59..035d6cdee1 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -230,7 +230,7 @@ void printVeins(color_ostream &con, MatMap &mat_map, df::inorganic_raw *gloss = vector_get(inorganics, kv.first); if (!gloss) { - con.printerr("invalid material gloss: %hi\n", kv.first); + con.printerr("invalid material gloss: {}\n", kv.first); continue; } @@ -298,7 +298,7 @@ static df::world_region_details *get_details(df::world_data *data, df::coord2d p bool estimate_underground(color_ostream &out, EmbarkTileLayout &tile, df::world_region_details *details, int x, int y) { if (x < 0 || y < 0 || x > 15 || y > 15) { - out.printerr("Invalid embark coordinates: x=%i, y=%i\n", x, y); + out.printerr("Invalid embark coordinates: x={}, y={}\n", x, y); return false; } // Find actual biome @@ -428,7 +428,7 @@ bool estimate_materials(color_ostream &out, EmbarkTileLayout &tile, MatMap &laye if (!geo_biome) { - out.printerr("Region geo-biome not found: (%d,%d)\n", + out.printerr("Region geo-biome not found: ({}, {})\n", tile.biome_pos.x, tile.biome_pos.y); return false; } diff --git a/plugins/regrass.cpp b/plugins/regrass.cpp index d40e1bc6dc..a17f5fb7e2 100644 --- a/plugins/regrass.cpp +++ b/plugins/regrass.cpp @@ -111,7 +111,7 @@ static bool valid_tile(color_ostream &out, regrass_options options, df::map_bloc else if (block->occupancy[tx][ty].bits.building > (options.buildings ? tile_building_occ::Passable : tile_building_occ::None)) { // Avoid stockpiles and planned/passable buildings unless enabled. - TRACE(log, out).print("Invalid tile: Building (%s)\n", + TRACE(log, out).print("Invalid tile: Building ({})\n", ENUM_KEY_STR(tile_building_occ, block->occupancy[tx][ty].bits.building).c_str()); return false; } @@ -180,7 +180,7 @@ static void grasses_for_tile(color_ostream &out, vector &vec, df::map_b for (auto p_raw : world->raws.plants.grasses) { // Sorted by df::plant_raw::index if (p_raw->flags.is_set(plant_raw_flags::BIOME_SUBTERRANEAN_WATER)) { - TRACE(log, out).print("Cave grass: %s\n", p_raw->id.c_str()); + TRACE(log, out).print("Cave grass: {}\n", p_raw->id); vec.push_back(p_raw->index); } } @@ -209,7 +209,7 @@ static void grasses_for_tile(color_ostream &out, vector &vec, df::map_b (evil || !flags.is_set(plant_raw_flags::EVIL)) && (savage || !flags.is_set(plant_raw_flags::SAVAGE))) { - TRACE(log, out).print("Surface grass: %s\n", p_raw->id.c_str()); + TRACE(log, out).print("Surface grass: {}\n", p_raw->id); vec.push_back(p_raw->index); } } @@ -241,7 +241,7 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d } else if (gr_ev->amount[tx][ty] > 0) { // Refill first non-zero grass. - DEBUG(log, out).print("Refilling existing grass (ID %d).\n", gr_ev->plant_index); + DEBUG(log, out).print("Refilling existing grass (ID {}).\n", gr_ev->plant_index); gr_ev->amount[tx][ty] = 100; return true; } @@ -263,12 +263,12 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d for (auto gr_ev : consider_grev) { // These grass events are already present. if (erase_from_vector(new_grasses, gr_ev->plant_index)) { - TRACE(log, out).print("Grass (ID %d) already present.\n", gr_ev->plant_index); + TRACE(log, out).print("Grass (ID {}) already present.\n", gr_ev->plant_index); } } for (auto id : new_grasses) { - DEBUG(log, out).print("Allocating new grass event (ID %d).\n", id); + DEBUG(log, out).print("Allocating new grass event (ID {}).\n", id); auto gr_ev = df::allocate(); if (!gr_ev) { out.printerr("Failed to allocate new grass event! Aborting loop.\n"); @@ -297,7 +297,7 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d for (size_t i = consider_grev.size(); i-- > 0;) { // Iterate backwards and remove invalid events from consideration. if (!vector_contains(valid_grasses, consider_grev[i]->plant_index)) { - TRACE(log, out).print("Removing grass (ID %d) from consideration.\n", + TRACE(log, out).print("Removing grass (ID {}) from consideration.\n", consider_grev[i]->plant_index); consider_grev.erase(consider_grev.begin() + i); } @@ -327,14 +327,14 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d if (options.max_grass) { // Don't need random choice. - DEBUG(log, out).print("Done with max regrass, %s.\n", + DEBUG(log, out).print("Done with max regrass, {}.\n", success ? "success" : "failure"); return success; } TRACE(log, out).print("Selecting random valid grass event...\n"); if (auto rand_grev = vector_get_random(consider_grev)) { - DEBUG(log, out).print("Refilling random grass (ID %d).\n", rand_grev->plant_index); + DEBUG(log, out).print("Refilling random grass (ID {}).\n", rand_grev->plant_index); rand_grev->amount[tx][ty] = 100; return true; } @@ -346,11 +346,11 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo { // Regrass single tile. Return 1 if tile success, else 0. CHECK_NULL_POINTER(block); if (!is_valid_tile_coord(df::coord2d(tx, ty))) { - out.printerr("(%d, %d) not in range 0-15!\n", tx, ty); + out.printerr("({}, {}) not in range 0-15!\n", tx, ty); return 0; } - DEBUG(log, out).print("Regrass tile (%d, %d, %d)\n", + DEBUG(log, out).print("Regrass tile ({}, {}, {})\n", block->map_pos.x + tx, block->map_pos.y + ty, block->map_pos.z); if (!regrass_events(out, options, block, tx, ty)) return 0; @@ -377,7 +377,7 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo { // Ramp or stairs. auto new_mat = (rand() & 1) ? tiletype_material::GRASS_LIGHT : tiletype_material::GRASS_DARK; auto new_tt = findTileType(shape, new_mat, tiletype_variant::NONE, tiletype_special::NONE, nullptr); - DEBUG(log, out).print("Tiletype to %s.\n", ENUM_KEY_STR(tiletype, new_tt).c_str()); + DEBUG(log, out).print("Tiletype to {}.\n", ENUM_KEY_STR(tiletype, new_tt)); block->tiletype[tx][ty] = new_tt; } return 1; @@ -385,12 +385,12 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo int regrass_cuboid(color_ostream &out, const regrass_options &options, const cuboid &bounds) { // Regrass all tiles in the defined cuboid. - DEBUG(log, out).print("Regrassing cuboid (%d:%d, %d:%d, %d:%d), clipped to map.\n", + DEBUG(log, out).print("Regrassing cuboid ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); int count = 0; bounds.forBlock([&](df::map_block *block, cuboid intersect) { - DEBUG(log, out).print("Cuboid regrass block at (%d, %d, %d)\n", + DEBUG(log, out).print("Cuboid regrass block at ({}, {}, {})\n", block->map_pos.x, block->map_pos.y, block->map_pos.z); intersect.forCoord([&](df::coord pos) { count += regrass_tile(out, options, block, pos.x&15, pos.y&15); @@ -404,7 +404,7 @@ int regrass_cuboid(color_ostream &out, const regrass_options &options, const cub int regrass_block(color_ostream &out, const regrass_options &options, df::map_block *block) { // Regrass all tiles in a single block. CHECK_NULL_POINTER(block); - DEBUG(log, out).print("Regrass block at (%d, %d, %d)\n", + DEBUG(log, out).print("Regrass block at ({}, {}, {})\n", block->map_pos.x, block->map_pos.y, block->map_pos.z); int count = 0; @@ -443,11 +443,11 @@ command_result df_regrass(color_ostream &out, vector ¶meters) else if (options.forced_plant == -2) { // Print all grass raw IDs. for (auto p_raw : world->raws.plants.grasses) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); return CR_OK; } - DEBUG(log, out).print("pos_1 = (%d, %d, %d)\npos_2 = (%d, %d, %d)\n", + DEBUG(log, out).print("pos_1 = ({}, {}, {})\npos_2 = ({}, {}, {})\n", pos_1.x, pos_1.y, pos_1.z, pos_2.x, pos_2.y, pos_2.z); if (options.block && options.zlevel) { @@ -464,17 +464,17 @@ command_result df_regrass(color_ostream &out, vector ¶meters) } if (options.force) { - DEBUG(log, out).print("forced_plant = %d\n", options.forced_plant); + DEBUG(log, out).print("forced_plant = {}\n", options.forced_plant); auto p_raw = vector_get(world->raws.plants.all, options.forced_plant); if (p_raw) { if (!p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Plant raw wasn't grass: %d (%s)\n", options.forced_plant, p_raw->id.c_str()); + out.printerr("Plant raw wasn't grass: {} ({})\n", options.forced_plant, p_raw->id); return CR_FAILURE; } - out.print("Forced grass: %s\n", p_raw->id.c_str()); + out.print("Forced grass: {}\n", p_raw->id); } else { - out.printerr("Plant raw not found for --force: %d\n", options.forced_plant); + out.printerr("Plant raw not found for --force: {}\n", options.forced_plant); return CR_FAILURE; } } @@ -484,7 +484,7 @@ command_result df_regrass(color_ostream &out, vector ¶meters) { // Specified z-levels or viewport z. auto z1 = pos_1.isValid() ? pos_1.z : Gui::getViewportPos().z; auto z2 = pos_2.isValid() ? pos_2.z : z1; - DEBUG(log, out).print("Regrassing z-levels %d to %d...\n", z1, z2); + DEBUG(log, out).print("Regrassing z-levels {} to {}...\n", z1, z2); count = regrass_cuboid(out, options, cuboid(0, 0, z1, world->map.x_count-1, world->map.y_count-1, z2)); } @@ -519,6 +519,6 @@ command_result df_regrass(color_ostream &out, vector ¶meters) DEBUG(log, out).print("Regrassing map...\n"); count = regrass_map(out, options); } - out.print("Regrew %d tiles of grass.\n", count); + out.print("Regrew {} tiles of grass.\n", count); return CR_OK; } diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index a7bfccd7ce..60c04ac25b 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -213,7 +213,7 @@ command_result loadArtImageChunk(color_ostream &out, std::vector & { int index = atoi(parameters[0].c_str()); auto chunk = GetArtImageChunk(&(world->art_image_chunks.all), index); - out.print("Loaded chunk id: %d\n", chunk->id); + out.print("Loaded chunk id: {}\n", chunk->id); } return CR_OK; } @@ -627,7 +627,7 @@ static command_result CheckHashes(color_ostream &stream, const EmptyMessage *in) } clock_t end = clock(); double elapsed_secs = double(end - start) / CLOCKS_PER_SEC; - stream.print("Checking all hashes took %f seconds.", elapsed_secs); + stream.print("Checking all hashes took {} seconds.", elapsed_secs); return CR_OK; } @@ -2262,8 +2262,7 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w out->set_map_y(pos_y); out->set_world_width(17); out->set_world_height(17); - char name[256]; - sprintf(name, "Region %d, %d", pos_x, pos_y); + std::string name = fmt::format("Region {}, {}", pos_x, pos_y); out->set_name_english(name); out->set_name(name); auto poles = worldData->flip_latitude; @@ -2366,8 +2365,7 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w int pos_y = worldRegionDetails->pos.y; out->set_map_x(pos_x); out->set_map_y(pos_y); - char name[256]; - sprintf(name, "Region %d, %d", pos_x, pos_y); + std::string name = fmt::format("Region {}, {}", pos_x, pos_y); out->set_name_english(name); out->set_name(name); diff --git a/plugins/rendermax/renderer_light.cpp b/plugins/rendermax/renderer_light.cpp index 469c849091..b905cd93c5 100644 --- a/plugins/rendermax/renderer_light.cpp +++ b/plugins/rendermax/renderer_light.cpp @@ -1185,12 +1185,12 @@ void lightingEngineViewscreen::loadSettings() int ret=luaL_loadfile(s,settingsfile.c_str()); if(ret==LUA_ERRFILE) { - out.printerr("File not found:%s\n",settingsfile.c_str()); + out.printerr("File not found:{}\n",settingsfile); lua_pop(s,1); } else if(ret==LUA_ERRSYNTAX) { - out.printerr("Syntax error:\n\t%s\n",lua_tostring(s,-1)); + out.printerr("Syntax error:\n\t{}\n",lua_tostring(s,-1)); } else { @@ -1202,38 +1202,38 @@ void lightingEngineViewscreen::loadSettings() lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu materials loaded\n",matDefs.size()); + out.print("{} materials loaded\n",matDefs.size()); lua_pushcfunction(s, parseSpecial); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu day light colors loaded\n",dayColors.size()); + out.print("{} day light colors loaded\n",dayColors.size()); lua_pushcfunction(s, parseBuildings); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu buildings loaded\n",buildingDefs.size()); + out.print("{} buildings loaded\n",buildingDefs.size()); lua_pushcfunction(s, parseCreatures); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu creatures loaded\n",creatureDefs.size()); + out.print("{} creatures loaded\n",creatureDefs.size()); lua_pushcfunction(s, parseItems); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu items loaded\n",itemDefs.size()); + out.print("{} items loaded\n",itemDefs.size()); } } } catch(std::exception& e) { - out.printerr("%s",e.what()); + out.printerr("{}\n",e.what()); } lua_pop(s,1); } diff --git a/plugins/rendermax/rendermax.cpp b/plugins/rendermax/rendermax.cpp index 5515bb39a2..39e59e4f6b 100644 --- a/plugins/rendermax/rendermax.cpp +++ b/plugins/rendermax/rendermax.cpp @@ -433,7 +433,7 @@ static command_result rendermax(color_ostream &out, vector & parameters else if(cmd=="disable") { if(current_mode==MODE_DEFAULT) - out.print("%s\n","Not installed, doing nothing."); + out.print("{}\n","Not installed, doing nothing."); else removeOld(); gps->force_full_display_count++; diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index 994ac067ac..7eecabd2c5 100644 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -69,7 +69,7 @@ static PersistentDataItem & ensure_seed_config(color_ostream &out, int id) { if (watched_seeds.count(id)) return watched_seeds[id]; string keyname = SEED_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for seed type %d\n", id); + DEBUG(control,out).print("creating new persistent key for seed type {}\n", id); watched_seeds.emplace(id, World::GetPersistentSiteData(keyname, true)); watched_seeds[id].set_int(SEED_CONFIG_ID, id); return watched_seeds[id]; @@ -77,7 +77,7 @@ static PersistentDataItem & ensure_seed_config(color_ostream &out, int id) { static void remove_seed_config(color_ostream &out, int id) { if (!watched_seeds.count(id)) return; - DEBUG(control,out).print("removing persistent key for seed type %d\n", id); + DEBUG(control,out).print("removing persistent key for seed type {}\n", id); World::DeletePersistentData(watched_seeds[id]); watched_seeds.erase(id); } @@ -90,12 +90,12 @@ static bool validate_seed_config(color_ostream& out, PersistentDataItem c) int seed_id = c.get_int(SEED_CONFIG_ID); auto plant = df::plant_raw::find(seed_id); if (!plant) { - WARN(control,out).print("discarded invalid seed id: %d\n", seed_id); + WARN(control,out).print("discarded invalid seed id: {}\n", seed_id); return false; } bool valid = (!plant->flags.is_set(df::enums::plant_raw_flags::TREE)); if (!valid) { - DEBUG(control, out).print("invalid configuration for %s discarded\n", plant->id.c_str()); + DEBUG(control, out).print("invalid configuration for {} discarded\n", plant->id); } return valid; } @@ -108,7 +108,7 @@ static void do_cycle(color_ostream &out, int32_t *num_enabled_seeds = NULL, int3 static void seedwatch_setTarget(color_ostream &out, string name, int32_t num); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -144,19 +144,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -304,7 +304,7 @@ static void scan_seeds(color_ostream &out, unordered_map *acce } static void do_cycle(color_ostream &out, int32_t *num_enabled_seed_types, int32_t *num_disabled_seed_types) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -325,14 +325,14 @@ static void do_cycle(color_ostream &out, int32_t *num_enabled_seed_types, int32_ if (accessible_counts[id] <= target && (Kitchen::isPlantCookeryAllowed(id) || Kitchen::isSeedCookeryAllowed(id))) { - DEBUG(cycle,out).print("disabling seed mat: %d\n", id); + DEBUG(cycle,out).print("disabling seed mat: {}\n", id); if (num_disabled_seed_types) ++*num_disabled_seed_types; Kitchen::denyPlantSeedCookery(id); } else if (target + TARGET_BUFFER < accessible_counts[id] && (!Kitchen::isPlantCookeryAllowed(id) || !Kitchen::isSeedCookeryAllowed(id))) { - DEBUG(cycle,out).print("enabling seed mat: %d\n", id); + DEBUG(cycle,out).print("enabling seed mat: {}\n", id); if (num_enabled_seed_types) ++*num_enabled_seed_types; Kitchen::allowPlantSeedCookery(id); @@ -351,7 +351,7 @@ static void set_target(color_ostream &out, int32_t id, int32_t target) { else { if (id < 0 || (size_t)id >= world->raws.plants.all.size()) { WARN(control,out).print( - "cannot set target for unknown plant id: %d\n", id); + "cannot set target for unknown plant id: {}\n", id); return; } PersistentDataItem &c = ensure_seed_config(out, id); @@ -385,7 +385,7 @@ static void seedwatch_setTarget(color_ostream &out, string name, int32_t num) { if (!world_plant_ids.count(token)) { token = toUpper_cp437(token); if (!world_plant_ids.count(token)) { - out.printerr("%s has not been found as a material.\n", token.c_str()); + out.printerr("{} has not been found as a material.\n", token); return; } } diff --git a/plugins/showmood.cpp b/plugins/showmood.cpp index 198dd190e0..977c767aeb 100644 --- a/plugins/showmood.cpp +++ b/plugins/showmood.cpp @@ -31,7 +31,7 @@ REQUIRE_GLOBAL(world); command_result df_showmood (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -65,7 +65,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) out.printerr("Dwarf with strange mood does not have a mood type!\n"); continue; } - out.print("%s is currently ", DF2CONSOLE(out, Units::getReadableName(unit)).c_str()); + out.print("{} is currently ", DF2CONSOLE(out, Units::getReadableName(unit))); switch (unit->mood) { case mood_type::Macabre: @@ -143,7 +143,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) out.print("do something else..."); break; } - out.print(" and become a legendary %s", ENUM_ATTR_STR(job_skill, caption_noun, unit->job.mood_skill)); + out.print(" and become a legendary {}", ENUM_ATTR_STR(job_skill, caption_noun, unit->job.mood_skill)); if (unit->mood == mood_type::Possessed) out.print(" (but not really)"); break; @@ -160,7 +160,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) { string name; building->getName(&name); - out.print("claimed a %s and wants", name.c_str()); + out.print("claimed a {} and wants", name.c_str()); } else out.print("not yet claimed a workshop but will want"); @@ -169,7 +169,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) for (size_t i = 0; i < job->job_items.elements.size(); i++) { df::job_item *item = job->job_items.elements[i]; - out.print("Item %zu: ", i + 1); + out.print("Item {}: ", i + 1); MaterialInfo matinfo(item->mat_type, item->mat_index); @@ -178,37 +178,37 @@ command_result df_showmood (color_ostream &out, vector & parameters) switch (item->item_type) { case item_type::BOULDER: - out.print("%s boulder", mat_name.c_str()); + out.print("{} boulder", mat_name); break; case item_type::BLOCKS: - out.print("%s blocks", mat_name.c_str()); + out.print("{} blocks", mat_name); break; case item_type::WOOD: - out.print("%s logs", mat_name.c_str()); + out.print("{} logs", mat_name); break; case item_type::BAR: if (matinfo.isInorganicWildcard()) mat_name = "metal"; if (matinfo.inorganic && matinfo.inorganic->flags.is_set(inorganic_flags::WAFERS)) - out.print("%s wafers", mat_name.c_str()); + out.print("{} wafers", mat_name); else - out.print("%s bars", mat_name.c_str()); + out.print("{} bars", mat_name); break; case item_type::SMALLGEM: - out.print("%s cut gems", mat_name.c_str()); + out.print("{} cut gems", mat_name); break; case item_type::ROUGH: if (matinfo.isAnyInorganic()) { if (matinfo.isInorganicWildcard()) mat_name = "any"; - out.print("%s rough gems", mat_name.c_str()); + out.print("{} rough gems", mat_name); } else - out.print("raw %s", mat_name.c_str()); + out.print("raw {}", mat_name); break; case item_type::SKIN_TANNED: - out.print("%s leather", mat_name.c_str()); + out.print("{} leather", mat_name); break; case item_type::CLOTH: if (matinfo.isNone()) @@ -220,50 +220,51 @@ command_result df_showmood (color_ostream &out, vector & parameters) else if (item->flags2.bits.yarn) mat_name = "any yarn"; } - out.print("%s cloth", mat_name.c_str()); + out.print("{} cloth", mat_name); break; case item_type::REMAINS: - out.print("%s remains", mat_name.c_str()); + out.print("{} remains", mat_name); break; case item_type::CORPSE: - out.print("%s %scorpse", mat_name.c_str(), (item->flags1.bits.murdered ? "murdered " : "")); + out.print("{} {}corpse", mat_name, (item->flags1.bits.murdered ? "murdered " : "")); break; case item_type::NONE: if (item->flags2.bits.body_part) { if (item->flags2.bits.bone) - out.print("%s bones", mat_name.c_str()); + out.print("{} bones", mat_name); else if (item->flags2.bits.shell) - out.print("%s shells", mat_name.c_str()); + out.print("{} shells", mat_name); else if (item->flags2.bits.horn) - out.print("%s horns", mat_name.c_str()); + out.print("{} horns", mat_name); else if (item->flags2.bits.pearl) - out.print("%s pearls", mat_name.c_str()); + out.print("{} pearls", mat_name); else if (item->flags2.bits.ivory_tooth) - out.print("%s ivory/teeth", mat_name.c_str()); + out.print("{} ivory/teeth", mat_name); else - out.print("%s unknown body parts (%s:%s:%s)", - mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("{} unknown body parts ({}:{}:{})", + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); } else - out.print("indeterminate %s item (%s:%s:%s)", - mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("indeterminate {} item ({}:{}:{})", + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); break; default: { ItemTypeInfo itinfo(item->item_type, item->item_subtype); - out.print("item %s material %s flags (%s:%s:%s)", - itinfo.toString().c_str(), mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("item {} material {} flags ({}:{}:{})", + itinfo.toString(), + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); break; } } @@ -280,7 +281,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) if (job->items[j]->job_item_idx == int32_t(i)) count_got += 1; } - out.print(", got %i of %i\n", count_got, + out.print(", got {} of {}\n", count_got, item->quantity < divisor ? item->quantity : item->quantity/divisor); } } diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index ca30fc76b4..3d9fbd537d 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -1503,7 +1503,7 @@ static void releaseTiredWorker(EngineInfo *engine, df::job *job, df::unit *worke if (Job::removeWorker(job)) { color_ostream_proxy out(Core::getInstance().getConsole()); - out.print("Released tired operator %d from siege engine.\n", worker->id); + out.print("Released tired operator {} from siege engine.\n", worker->id); if (process_jobs) *process_jobs = true; diff --git a/plugins/sort.cpp b/plugins/sort.cpp index 7706750fef..f1b700c5af 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -56,7 +56,7 @@ static bool do_filter(const char *module_name, const char *fn_name, const item_o ret = lua_toboolean(L, 1); } ); - TRACE(log).print("filter result for %s: %d\n", Units::getReadableName(unit).c_str(), ret); + TRACE(log).print("filter result for {}: {}\n", Units::getReadableName(unit), ret); return !ret; } @@ -85,7 +85,7 @@ static int32_t our_filter_idx(filter_func* filter, df::widget_unit_list* unitlis auto t = fn.target(); if (t && *t == filter) { - TRACE(log).print("found our filter function at idx %d\n", idx); + TRACE(log).print("found our filter function at idx {}\n", idx); return idx; } ++idx; @@ -172,7 +172,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector= 0) { - DEBUG(log,out).print("removing %s filter function\n", which); + DEBUG(log,out).print("removing {} filter function\n", which); filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); vector_erase_at(*filter_vec, idx); } @@ -182,7 +182,7 @@ static void remove_sort_function(color_ostream &out, const char *which, df::widg std::vector *sorting_by = reinterpret_cast *>(&unitlist->sorting_by); int32_t idx = our_sort_idx(*sorting_by); if (idx >= 0) { - DEBUG(log).print("removing %s sort function\n", which); + DEBUG(log).print("removing {} sort function\n", which); vector_erase_at(*sorting_by, idx); } } diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index ae287f6000..072ad813c7 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -178,12 +178,12 @@ static class UnitHistory { void add_to_history(color_ostream &out, int32_t unit_id) { if (offset > 0) { - DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + DEBUG(cycle,out).print("trimming history forward of offset {}\n", offset); history.resize(history.size() - offset); offset = 0; } if (history.size() && history.back() == unit_id) { - DEBUG(cycle,out).print("unit %d is already current unit; not adding to history\n", unit_id); + DEBUG(cycle,out).print("unit {} is already current unit; not adding to history\n", unit_id); } else { history.push_back(unit_id); if (history.size() > MAX_HISTORY) { @@ -191,19 +191,19 @@ static class UnitHistory { history.pop_front(); } } - DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); + DEBUG(cycle,out).print("history now has {} entries\n", history.size()); } void add_and_follow(color_ostream &out, df::unit *unit) { // if we're currently following a unit, add it to the history if it's not already there if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { - DEBUG(cycle,out).print("currently following unit %d that is not in history; adding\n", plotinfo->follow_unit); + DEBUG(cycle,out).print("currently following unit {} that is not in history; adding\n", plotinfo->follow_unit); add_to_history(out, plotinfo->follow_unit); } int32_t id = unit->id; add_to_history(out, id); - DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + DEBUG(cycle,out).print("now following unit {}: {}\n", id, DF2CONSOLE(Units::getReadableName(unit))); Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_item = -1; plotinfo->follow_unit = id; @@ -216,7 +216,7 @@ static class UnitHistory { } ++offset; int unit_id = get_cur_unit_id(); - DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); + DEBUG(cycle,out).print("scanning back to unit {} at offset {}\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_item = -1; @@ -232,7 +232,7 @@ static class UnitHistory { --offset; int unit_id = get_cur_unit_id(); - DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); + DEBUG(cycle,out).print("scanning forward to unit {} at offset {}\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_item = -1; @@ -281,7 +281,7 @@ static class RecentUnits { static void on_new_active_unit(color_ostream& out, void* data) { int32_t unit_id = reinterpret_cast(data); - DEBUG(event,out).print("unit %d has arrived on map\n", unit_id); + DEBUG(event,out).print("unit {} has arrived on map\n", unit_id); recent_units.add(unit_id); } @@ -294,7 +294,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static void follow_a_dwarf(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -320,19 +320,19 @@ static void set_next_cycle_unpaused_ms(color_ostream &out, bool has_active_comba delay_ms = distribution(rng); delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } - DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); + DEBUG(cycle,out).print("next cycle in {} ms\n", delay_ms); next_cycle_unpaused_ms = Core::getInstance().getUnpausedMs() + delay_ms; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); if (enable) { config.reset(); @@ -340,7 +340,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { WARN(control,out).print("Failed to refresh config\n"); } if (is_squads_open()) { - out.printerr("Cannot enable %s while the squads screen is open.\n", plugin_name); + out.printerr("Cannot enable {} while the squads screen is open.\n", plugin_name); Lua::CallLuaModuleFunction(out, "plugins.spectate", "show_squads_warning"); is_enabled = false; return CR_FAILURE; @@ -358,7 +358,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { // don't reset the unit history since we may want to re-enable } } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -366,7 +366,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); on_disable(out); return CR_OK; } @@ -378,7 +378,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan break; case SC_WORLD_UNLOADED: if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; on_disable(out, true); @@ -456,7 +456,7 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - TRACE(cycle, out).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + TRACE(cycle, out).print("unit {} is in combat: {}\n", unit->id, DF2CONSOLE(Units::getReadableName(unit))); if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) citizen_combat_units.push_back(unit); else @@ -498,17 +498,17 @@ static void add_bucket(const vector &bucket, vector &units } #define DUMP_BUCKET(name) \ - DEBUG(cycle,out).print("bucket: " #name ", size: %zd\n", name.size()); \ + DEBUG(cycle,out).print("bucket: " #name ", size: {}\n", name.size()); \ if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ for (auto u : name) { \ - DEBUG(cycle,out).print(" unit %d: %s\n", u->id, DF2CONSOLE(Units::getReadableName(u)).c_str()); \ + DEBUG(cycle,out).print(" unit {}: {}\n", u->id, DF2CONSOLE(Units::getReadableName(u))); \ } \ } #define DUMP_FLOAT_VECTOR(name) \ DEBUG(cycle,out).print(#name ":\n"); \ for (float f : name) { \ - DEBUG(cycle,out).print(" %d\n", (int)f); \ + DEBUG(cycle,out).print(" {}\n", (int)f); \ } static void follow_a_dwarf(color_ostream &out) { @@ -551,7 +551,7 @@ static void follow_a_dwarf(color_ostream &out) { DUMP_BUCKET(other_units); DUMP_FLOAT_VECTOR(intervals); DUMP_FLOAT_VECTOR(weights); - DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); + DEBUG(cycle,out).print("selected unit idx {}\n", unit_idx); } unit_history.add_and_follow(out, unit); @@ -561,7 +561,7 @@ static void follow_a_dwarf(color_ostream &out) { // Lua API static void spectate_setSetting(color_ostream &out, string name, int val) { - DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); + DEBUG(control,out).print("entering spectate_setSetting {} = {}\n", name, val); if (name == "auto-unpause") { if (val && !config.auto_unpause) { @@ -593,7 +593,7 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { } config.follow_ms = val * 1000; } else { - WARN(control,out).print("Unknown setting: %s\n", name.c_str()); + WARN(control,out).print("Unknown setting: {}\n", name); } } @@ -610,9 +610,9 @@ static void spectate_followNext(color_ostream &out) { }; static void spectate_addToHistory(color_ostream &out, int32_t unit_id) { - DEBUG(control,out).print("entering spectate_addToHistory; unit_id=%d\n", unit_id); + DEBUG(control,out).print("entering spectate_addToHistory; unit_id={}\n", unit_id); if (!df::unit::find(unit_id)) { - WARN(control,out).print("unit with id %d not found; not adding to history\n", unit_id); + WARN(control,out).print("unit with id {} not found; not adding to history\n", unit_id); return; } unit_history.add_to_history(out, unit_id); diff --git a/plugins/steam-engine.cpp b/plugins/steam-engine.cpp index befd53f10a..06b14aa6c0 100644 --- a/plugins/steam-engine.cpp +++ b/plugins/steam-engine.cpp @@ -870,7 +870,7 @@ static bool find_engines(color_ostream &out) if (!ws.gear_tiles.empty()) engines.push_back(ws); else - out.printerr("%s has no gear tiles - ignoring.\n", wslist[i]->code.c_str()); + out.printerr("{} has no gear tiles - ignoring.\n", wslist[i]->code); } return !engines.empty(); diff --git a/plugins/stockflow.cpp b/plugins/stockflow.cpp index 1671d08991..524bd16896 100644 --- a/plugins/stockflow.cpp +++ b/plugins/stockflow.cpp @@ -284,7 +284,7 @@ static bool apply_hooks(color_ostream &out, bool enabling) { } if (!INTERPOSE_HOOK(stockflow_hook, feed).apply(enabling) || !INTERPOSE_HOOK(stockflow_hook, render).apply(enabling)) { - out.printerr("Could not %s stockflow hooks!\n", enabling? "insert": "remove"); + out.printerr("Could not {} stockflow hooks!\n", enabling? "insert": "remove"); return false; } @@ -336,7 +336,7 @@ static command_result stockflow_cmd(color_ostream &out, vector & parame } } - out.print("Stockflow is %s %s%s.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled", fast? ", in fast mode": ""); + out.print("Stockflow is {} {}{}.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled", fast? ", in fast mode": ""); enabled = desired; return CR_OK; } diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 879353f3b0..1392e8c12c 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -22,7 +22,7 @@ DBG_EXTERN(stockpiles, log); */ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat) { - DEBUG(log, out).print("food_lookup: food_idx(%zd)\n", food_idx); + DEBUG(log, out).print("food_lookup: food_idx({})\n", food_idx); auto& raws = world->raws; df::special_mat_table table = raws.mat_table; int32_t main_idx = table.organic_indexes[mat_category][food_idx]; @@ -32,11 +32,11 @@ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category: mat_category == organic_mat_category::Eggs) { food_mat.creature = raws.creatures.all[type]; food_mat.caste = food_mat.creature->caste[main_idx]; - DEBUG(log, out).print("special creature type(%d) caste(%d)\n", type, main_idx); + DEBUG(log, out).print("special creature type({}) caste({})\n", type, main_idx); } else { food_mat.material.decode(type, main_idx); - DEBUG(log, out).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str()); + DEBUG(log, out).print("type({}) index({}) token({})\n", type, main_idx, food_mat.material.getToken()); } } std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, const FoodMat& food_mat) { @@ -80,22 +80,22 @@ int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_cate std::vector tokens; split_string(&tokens, token, ":"); if (tokens.size() != 2) { - WARN(log, out).print("creature invalid CREATURE:CASTE token: %s\n", token.c_str()); + WARN(log, out).print("creature invalid CREATURE:CASTE token: {}\n", token); return -1; } int16_t creature_idx = find_creature(tokens[0]); if (creature_idx < 0) { - WARN(log, out).print("creature invalid token %s\n", tokens[0].c_str()); + WARN(log, out).print("creature invalid token {}\n", tokens[0]); return -1; } int16_t food_idx = linear_index(table.organic_types[mat_category], creature_idx); if (tokens[1] == "MALE") food_idx += 1; if (table.organic_types[mat_category][food_idx] == creature_idx) { - DEBUG(log, out).print("creature %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx); + DEBUG(log, out).print("creature {} caste {} creature_idx({}) food_idx({})\n", token, tokens[1], creature_idx, food_idx); return food_idx; } - WARN(log, out).print("creature caste not found: %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx); + WARN(log, out).print("creature caste not found: {} caste {} creature_idx({}) food_idx({})\n", token, tokens[1], creature_idx, food_idx); return -1; } @@ -106,12 +106,12 @@ int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_cate int32_t index = mat_info.index; auto it = food_index[mat_category].find(std::make_pair(type, index)); if (it != food_index[mat_category].end()) { - DEBUG(log, out).print("matinfo: %s type(%d) idx(%d) food_idx(%zd)\n", token.c_str(), mat_info.type, mat_info.index, it->second); + DEBUG(log, out).print("matinfo: {} type({}) idx({}) food_idx({})\n", token, mat_info.type, mat_info.index, it->second); return it->second; } - WARN(log, out).print("matinfo: %s type(%d) idx(%d) food_idx not found :(\n", token.c_str(), mat_info.type, mat_info.index); + WARN(log, out).print("matinfo: {} type({}) idx({}) food_idx not found :(\n", token, mat_info.type, mat_info.index); return -1; } diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index ad76977f1a..46efd95044 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -180,8 +180,8 @@ bool StockpileSettingsSerializer::serialize_to_ostream(color_ostream& out, std:: bool StockpileSettingsSerializer::serialize_to_file(color_ostream& out, const string& file, uint32_t includedElements) { std::fstream output(file, std::ios::out | std::ios::binary | std::ios::trunc); if (output.fail()) { - WARN(log, out).print("ERROR: failed to open file for writing: '%s'\n", - file.c_str()); + WARN(log, out).print("ERROR: failed to open file for writing: '{}'\n", + file); return false; } return serialize_to_ostream(out, &output, includedElements); @@ -202,8 +202,8 @@ bool StockpileSettingsSerializer::parse_from_istream(color_ostream &out, std::is bool StockpileSettingsSerializer::unserialize_from_file(color_ostream &out, const string& file, DeserializeMode mode, const vector& filters) { std::fstream input(file, std::ios::in | std::ios::binary); if (input.fail()) { - WARN(log, out).print("failed to open file for reading: '%s'\n", - file.c_str()); + WARN(log, out).print("failed to open file for reading: '{}'\n", + file); return false; } return parse_from_istream(out, &input, mode, filters); @@ -224,7 +224,7 @@ static typename df::enum_traits::base_type token_to_enum_val(const string& to static bool matches_filter(color_ostream& out, const vector& filters, const string& name) { for (auto & filter : filters) { - DEBUG(log, out).print("searching for '%s' in '%s'\n", filter.c_str(), name.c_str()); + DEBUG(log, out).print("searching for '{}' in '{}'\n", filter, name); if (std::search(name.begin(), name.end(), filter.begin(), filter.end(), [](unsigned char ch1, unsigned char ch2) { return std::toupper(ch1) == std::toupper(ch2); } ) != name.end()) @@ -235,7 +235,7 @@ static bool matches_filter(color_ostream& out, const vector& filters, co static void set_flag(color_ostream& out, const char* name, const vector& filters, bool all, char val, bool enabled, bool& elem) { if ((all || enabled) && matches_filter(out, filters, name)) { - DEBUG(log, out).print("setting %s to %d\n", name, val); + DEBUG(log, out).print("setting {} to {}\n", name, val); elem = val; } } @@ -243,7 +243,7 @@ static void set_flag(color_ostream& out, const char* name, const vector& static void set_filter_elem(color_ostream& out, const char* subcat, const vector& filters, char val, const string& name, const string& id, char& elem) { if (matches_filter(out, filters, subcat + ((*subcat ? "/" : "") + name))) { - DEBUG(log, out).print("setting %s (%s) to %d\n", name.c_str(), id.c_str(), val); + DEBUG(log, out).print("setting {} ({}) to {}\n", name, id, val); elem = val; } } @@ -252,7 +252,7 @@ template static void set_filter_elem(color_ostream& out, const char* subcat, const vector& filters, T_val val, const string& name, T_id id, T_val& elem) { if (matches_filter(out, filters, subcat + ((*subcat ? "/" : "") + name))) { - DEBUG(log, out).print("setting %s (%d) to %d\n", name.c_str(), (int32_t)id, val); + DEBUG(log, out).print("setting {} ({}) to {}\n", name, id, val); elem = val; } } @@ -287,7 +287,7 @@ static bool serialize_list_itemdef(color_ostream& out, FuncWriteExport add_value ItemTypeInfo ii; if (!ii.decode(type, i)) continue; - DEBUG(log, out).print("adding itemdef type %s\n", ii.getToken().c_str()); + DEBUG(log, out).print("adding itemdef type {}\n", ii.getToken()); add_value(ii.getToken()); } return all; @@ -312,7 +312,7 @@ static void unserialize_list_itemdef(color_ostream& out, const char* subcat, boo if (!ii.find(id)) continue; if (ii.subtype < 0 || size_t(ii.subtype) >= pile_list.size()) { - WARN(log, out).print("item type index invalid: %d\n", ii.subtype); + WARN(log, out).print("item type index invalid: {}\n", ii.subtype); continue; } set_filter_elem(out, subcat, filters, val, id, ii.subtype, pile_list.at(ii.subtype)); @@ -332,7 +332,7 @@ static bool serialize_list_quality(color_ostream& out, FuncWriteExport add_value } const string f_type(quality_traits::key_table[i]); add_value(f_type); - DEBUG(log, out).print("adding quality %s\n", f_type.c_str()); + DEBUG(log, out).print("adding quality {}\n", f_type); } return all; } @@ -356,7 +356,7 @@ static void unserialize_list_quality(color_ostream& out, const char* subcat, boo const string quality = read_value(i); df::enum_traits::base_type idx = token_to_enum_val(quality); if (idx < 0) { - WARN(log, out).print("invalid quality token: %s\n", quality.c_str()); + WARN(log, out).print("invalid quality token: {}\n", quality); continue; } set_filter_elem(out, subcat, filters, val, quality, idx, pile_list[idx]); @@ -392,11 +392,11 @@ static bool serialize_list_other_mats(color_ostream& out, } const string token = other_mats_index(other_mats, i); if (token.empty()) { - WARN(log, out).print("invalid other material with index %zd\n", i); + WARN(log, out).print("invalid other material with index {}\n", i); continue; } add_value(token); - DEBUG(log, out).print("other mats %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("other mats {} is {}\n", i, token); } return all; } @@ -416,11 +416,11 @@ static void unserialize_list_other_mats(color_ostream& out, const char* subcat, const string token = read_value(i); size_t idx = other_mats_token(other_mats, token); if (idx < 0) { - WARN(log, out).print("invalid other mat with token %s\n", token.c_str()); + WARN(log, out).print("invalid other mat with token {}\n", token); continue; } if (idx >= num_elems) { - WARN(log, out).print("other_mats index too large! idx[%zd] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("other_mats index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); @@ -447,7 +447,7 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v DEBUG(log, out).print("food mat invalid :(\n"); continue; } - DEBUG(log, out).print("organic_material %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("organic_material {} is {}\n", i, token); add_value(token); } return all; @@ -489,7 +489,7 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, const string token = read_value(i); int16_t idx = OrganicMatLookup::food_idx_by_token(out, cat, token); if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("organic mat index too large! idx[%d] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("organic mat index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } OrganicMatLookup::FoodMat food_mat; @@ -542,7 +542,7 @@ static void unserialize_list_color(color_ostream& out, const char* subcat, bool size_t color = find_color_by_token(value); if (color == SIZE_MAX) { - WARN(log, out).print("unknown color %s", value.c_str()); + WARN(log, out).print("unknown color {}\n", value); continue; } set_filter_elem(out, subcat, filters, val, value, color, pile_list[color]); @@ -556,7 +556,7 @@ static bool serialize_list_item_type(color_ostream& out, FuncItemAllowed is_allo bool all = true; size_t num_item_types = list.size(); - DEBUG(log, out).print("item_type size = %zd size limit = %d typecasted: %zd\n", + DEBUG(log, out).print("item_type size = {} size limit = {} typecasted: {}\n", num_item_types, type_traits::last_item_value, (size_t)type_traits::last_item_value); for (size_t i = 0; i <= (size_t)type_traits::last_item_value; ++i) { @@ -569,7 +569,7 @@ static bool serialize_list_item_type(color_ostream& out, FuncItemAllowed is_allo if (!is_allowed(type)) continue; add_value(r_type); - DEBUG(log, out).print("item_type key_table[%zd] type[%d] is %s\n", i + 1, (int16_t)type, r_type.c_str()); + DEBUG(log, out).print("item_type key_table[{}] type[{}] is {}\n", i + 1, (int16_t)type, r_type); } return all; } @@ -599,7 +599,7 @@ static void unserialize_list_item_type(color_ostream& out, const char* subcat, b if (!is_allowed((item_type)idx)) continue; if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("error item_type index too large! idx[%d] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("error item_type index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); @@ -618,7 +618,7 @@ static bool serialize_list_material(color_ostream& out, FuncMaterialAllowed is_a mi.decode(0, i); if (!is_allowed(mi)) continue; - DEBUG(log, out).print("adding material %s\n", mi.getToken().c_str()); + DEBUG(log, out).print("adding material {}\n", mi.getToken()); add_value(mi.getToken()); } return all; @@ -652,7 +652,7 @@ static void unserialize_list_material(color_ostream& out, const char* subcat, bo if (!mi.find(id) || !is_allowed(mi)) continue; if (mi.index < 0 || size_t(mi.index) >= pile_list.size()) { - WARN(log, out).print("material type index invalid: %d\n", mi.index); + WARN(log, out).print("material type index invalid: {}\n", mi.index); continue; } set_filter_elem(out, subcat, filters, val, id, mi.index, pile_list.at(mi.index)); @@ -671,7 +671,7 @@ static bool serialize_list_creature(color_ostream& out, FuncWriteExport add_valu if (r->flags.is_set(creature_raw_flags::GENERATED) || r->creature_id == "EQUIPMENT_WAGON") continue; - DEBUG(log, out).print("adding creature %s\n", r->creature_id.c_str()); + DEBUG(log, out).print("adding creature {}\n", r->creature_id); add_value(r->creature_id); } return all; @@ -701,7 +701,7 @@ static void unserialize_list_creature(color_ostream& out, const char* subcat, bo string id = read_value(i); int idx = find_creature(id); if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("animal index invalid: %d\n", idx); + WARN(log, out).print("animal index invalid: {}\n", idx); continue; } auto r = find_creature(idx); @@ -721,7 +721,7 @@ static void write_cat(color_ostream& out, const char* name, bool include_types, T_cat_set* cat_set = mutable_cat_fn(); if (!include_types) { - DEBUG(log, out).print("including all for %s since only category is being recorded\n", name); + DEBUG(log, out).print("including all for {} since only category is being recorded\n", name); cat_set->set_all(true); return; } @@ -729,7 +729,7 @@ static void write_cat(color_ostream& out, const char* name, bool include_types, if (write_cat_fn(out, cat_set)) { // all fields were set. clear them and use the "all" flag instead so "all" can be applied // to other worlds with other generated types - DEBUG(log, out).print("including all for %s since all fields were enabled\n", name); + DEBUG(log, out).print("including all for {} since all fields were enabled\n", name); cat_set->Clear(); cat_set->set_all(true); } @@ -742,7 +742,7 @@ void StockpileSettingsSerializer::write(color_ostream& out, uint32_t includedEle if (!(includedElements & INCLUDED_ELEMENTS_CATEGORIES)) return; - DEBUG(log, out).print("GROUP SET %s\n", + DEBUG(log, out).print("GROUP SET {}\n", bitfield_to_string(mSettings->flags).c_str()); bool include_types = 0 != (includedElements & INCLUDED_ELEMENTS_TYPES); @@ -893,7 +893,7 @@ static void read_elem(color_ostream& out, const char* name, DeserializeMode mode bool is_set = elem_fn() != 0; if (mode == DESERIALIZE_MODE_SET || is_set) { T_elem val = (mode == DESERIALIZE_MODE_DISABLE) ? 0 : elem_fn(); - DEBUG(log, out).print("setting %s to %d\n", name, val); + DEBUG(log, out).print("setting {} to {}\n", name, val); setting = val; } } @@ -907,7 +907,7 @@ static void read_category(color_ostream& out, const char* name, DeserializeMode std::function clear_fn, std::function set_fn) { if (mode == DESERIALIZE_MODE_SET) { - DEBUG(log, out).print("clearing %s\n", name); + DEBUG(log, out).print("clearing {}\n", name); cat_flags &= ~cat_mask; clear_fn(); } @@ -923,7 +923,7 @@ static void read_category(color_ostream& out, const char* name, DeserializeMode bool all = cat_fn().all(); char val = (mode == DESERIALIZE_MODE_DISABLE) ? (char)0 : (char)1; - DEBUG(log, out).print("setting %s %s elements to %d\n", + DEBUG(log, out).print("setting {} {} elements to {}\n", all ? "all" : "marked", name, val); set_fn(all, val); } @@ -970,7 +970,7 @@ void StockpileSerializer::read_general(color_ostream& out, DeserializeMode mode) if (!mBuffer.has_use_links_only()) return; bool use_links_only = mBuffer.use_links_only(); - DEBUG(log, out).print("setting use_links_only to %d\n", use_links_only); + DEBUG(log, out).print("setting use_links_only to {}\n", use_links_only); mPile->stockpile_flag.bits.use_links_only = use_links_only; } @@ -991,7 +991,7 @@ bool StockpileSettingsSerializer::write_ammo(color_ostream& out, StockpileSettin mSettings->ammo.mats) && all; if (mSettings->ammo.other_mats.size() > 2) { - WARN(log, out).print("ammo other materials > 2: %zd\n", + WARN(log, out).print("ammo other materials > 2: {}\n", mSettings->ammo.other_mats.size()); } @@ -1004,7 +1004,7 @@ bool StockpileSettingsSerializer::write_ammo(color_ostream& out, StockpileSettin } const string token = i == 0 ? "WOOD" : "BONE"; ammo->add_other_mats(token); - DEBUG(log, out).print("other mats %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("other mats {} is {}\n", i, token); } all = serialize_list_quality(out, @@ -1843,7 +1843,7 @@ bool StockpileSettingsSerializer::write_furniture(color_ostream& out, StockpileS } string f_type{ENUM_KEY_STR(furniture_type, furniture_type(i))}; furniture->add_type(f_type); - DEBUG(log, out).print("furniture_type %zd is %s\n", i, f_type.c_str()); + DEBUG(log, out).print("furniture_type {} is {}\n", i, f_type); } all = serialize_list_material(out, @@ -1896,7 +1896,7 @@ void StockpileSettingsSerializer::read_furniture(color_ostream& out, Deserialize const string token = bfurniture.type(i); df::enum_traits::base_type idx = token_to_enum_val(token); if (idx < 0 || size_t(idx) >= pfurniture.type.size()) { - WARN(log, out).print("furniture type index invalid %s, idx=%d\n", token.c_str(), idx); + WARN(log, out).print("furniture type index invalid {}, idx={}\n", token, idx); continue; } set_filter_elem(out, "type", filters, val, token, idx, pfurniture.type.at(idx)); @@ -1956,7 +1956,7 @@ bool StockpileSettingsSerializer::write_gems(color_ostream& out, StockpileSettin mi.decode(i, -1); if (!gem_other_mat_is_allowed(mi)) continue; - DEBUG(log, out).print("gem rough_other mat %zd is %s\n", i, mi.getToken().c_str()); + DEBUG(log, out).print("gem rough_other mat {} is {}\n", i, mi.getToken()); gems->add_rough_other_mats(mi.getToken()); } @@ -1970,7 +1970,7 @@ bool StockpileSettingsSerializer::write_gems(color_ostream& out, StockpileSettin mi.decode(0, i); if (!gem_other_mat_is_allowed(mi)) continue; - DEBUG(log, out).print("gem cut_other mat %zd is %s\n", i, mi.getToken().c_str()); + DEBUG(log, out).print("gem cut_other mat {} is {}\n", i, mi.getToken()); gems->add_cut_other_mats(mi.getToken()); } @@ -2384,7 +2384,7 @@ bool StockpileSettingsSerializer::write_wood(color_ostream& out, StockpileSettin if (!wood_mat_is_allowed(plant)) continue; wood->add_mats(plant->id); - DEBUG(log, out).print("plant %zd is %s\n", i, plant->id.c_str()); + DEBUG(log, out).print("plant {} is {}\n", i, plant->id); } return all; } @@ -2415,7 +2415,7 @@ void StockpileSettingsSerializer::read_wood(color_ostream& out, DeserializeMode const string token = bwood.mats(i); const size_t idx = find_plant(token); if (idx < 0 || (size_t)idx >= num_elems) { - WARN(log, out).print("wood mat index invalid %s idx=%zd\n", token.c_str(), idx); + WARN(log, out).print("wood mat index invalid {}, idx={}\n", token, idx); continue; } set_filter_elem(out, "", filters, val, token, idx, pwood.mats.at(idx)); diff --git a/plugins/stockpiles/stockpiles.cpp b/plugins/stockpiles/stockpiles.cpp index 33ce855b42..7165449cca 100644 --- a/plugins/stockpiles/stockpiles.cpp +++ b/plugins/stockpiles/stockpiles.cpp @@ -29,7 +29,7 @@ DBG_DECLARE(stockpiles, log, DebugCategory::LINFO); static command_result do_command(color_ostream& out, vector& parameters); DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { - DEBUG(log, out).print("initializing %s\n", plugin_name); + DEBUG(log, out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -62,7 +62,7 @@ static df::building_stockpilest* get_stockpile(int id) { static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t includedElements) { df::building_stockpilest* sp = get_stockpile(id); if (!sp) { - out.printerr("Specified building isn't a stockpile: %d.\n", id); + out.printerr("Specified building isn't a stockpile: {}\n", id); return false; } @@ -72,12 +72,12 @@ static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t try { StockpileSerializer cereal(sp); if (!cereal.serialize_to_file(out, fname, includedElements)) { - out.printerr("could not save to '%s'\n", fname.c_str()); + out.printerr("could not save to '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("serialization failed: protobuf exception: %s\n", e.what()); + out.printerr("serialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -87,7 +87,7 @@ static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t static bool stockpiles_import(color_ostream& out, string fname, int id, string mode_str, string filter) { df::building_stockpilest* sp = get_stockpile(id); if (!sp) { - out.printerr("Specified building isn't a stockpile: %d.\n", id); + out.printerr("Specified building isn't a stockpile: {}\n", id); return false; } @@ -95,7 +95,7 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m fname += ".dfstock"; if (!Filesystem::exists(fname)) { - out.printerr("ERROR: file doesn't exist: '%s'\n", fname.c_str()); + out.printerr("ERROR: file doesn't exist: '{}'\n", fname); return false; } @@ -111,12 +111,12 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m try { StockpileSerializer cereal(sp); if (!cereal.unserialize_from_file(out, fname, mode, filters)) { - out.printerr("deserialization failed: '%s'\n", fname.c_str()); + out.printerr("deserialization failed: '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("deserialization failed: protobuf exception: %s\n", e.what()); + out.printerr("deserialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -126,13 +126,13 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m static df::stockpile_settings * get_stop_settings(color_ostream& out, int route_id, int stop_id) { auto route = df::hauling_route::find(route_id); if (!route) { - out.printerr("Specified hauling route not found: %d.\n", route_id); + out.printerr("Specified hauling route not found: {}\n", route_id); return NULL; } df::hauling_stop *stop = binsearch_in_vector(route->stops, &df::hauling_stop::id, stop_id); if (!stop) { - out.printerr("Specified hauling stop not found in route %d: %d.\n", route_id, stop_id); + out.printerr("Specified hauling stop not found in route {}: {}.\n", route_id, stop_id); return NULL; } @@ -150,12 +150,12 @@ static bool stockpiles_route_export(color_ostream& out, string fname, int route_ try { StockpileSettingsSerializer cereal(settings); if (!cereal.serialize_to_file(out, fname, includedElements)) { - out.printerr("could not save to '%s'\n", fname.c_str()); + out.printerr("could not save to '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("serialization failed: protobuf exception: %s\n", e.what()); + out.printerr("serialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -171,7 +171,7 @@ static bool stockpiles_route_import(color_ostream& out, string fname, int route_ fname += ".dfstock"; if (!Filesystem::exists(fname)) { - out.printerr("ERROR: file doesn't exist: '%s'\n", fname.c_str()); + out.printerr("ERROR: file doesn't exist: '{}'\n", fname); return false; } @@ -187,12 +187,12 @@ static bool stockpiles_route_import(color_ostream& out, string fname, int route_ try { StockpileSettingsSerializer cereal(settings); if (!cereal.unserialize_from_file(out, fname, mode, filters)) { - out.printerr("deserialization failed: '%s'\n", fname.c_str()); + out.printerr("deserialization failed: '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("deserialization failed: protobuf exception: %s\n", e.what()); + out.printerr("deserialization failed: protobuf exception: {}\n", e.what()); return false; } diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 3bdd9431df..2ab221195d 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -149,7 +149,7 @@ int getCreatedMetalBars (int32_t idx) command_result df_strangemood (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -202,7 +202,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) type = mood_type::Macabre; else { - out.printerr("Mood type '%s' not recognized!\n", parameters[i].c_str()); + out.printerr("Mood type '{}' not recognized!\n", parameters[i]); return CR_WRONG_USAGE; } } @@ -260,13 +260,13 @@ command_result df_strangemood (color_ostream &out, vector & parameters) skill = job_skill::MECHANICS; else { - out.printerr("Mood skill '%s' not recognized!\n", parameters[i].c_str()); + out.printerr("Mood skill '{}' not recognized!\n", parameters[i]); return CR_WRONG_USAGE; } } else { - out.printerr("Unrecognized parameter: %s\n", parameters[i].c_str()); + out.printerr("Unrecognized parameter: {}\n", parameters[i]); return CR_WRONG_USAGE; } } @@ -428,7 +428,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) if (unit->job.current_job) { // TODO: cancel job - out.printerr("Chosen unit '%s' has active job, cannot start mood!\n", + out.printerr("Chosen unit '{}' has active job, cannot start mood!\n", DF2CONSOLE(Units::getReadableName(unit)).c_str()); return CR_FAILURE; } diff --git a/plugins/suspendmanager.cpp b/plugins/suspendmanager.cpp index 0756e10832..13a37d1901 100644 --- a/plugins/suspendmanager.cpp +++ b/plugins/suspendmanager.cpp @@ -471,7 +471,7 @@ class SuspendManager { static bool riskBlocking(color_ostream &out, df::job* job) { if (job->job_type != job_type::ConstructBuilding) return false; - TRACE(cycle,out).print("risk blocking: check construction job %d\n", job->id); + TRACE(cycle,out).print("risk blocking: check construction job {}\n", job->id); auto building = Job::getHolder(job); if (!building || !isImpassable(building)) @@ -484,7 +484,7 @@ class SuspendManager { return false; auto risk = riskOfStuckConstructionAt(pos); - TRACE(cycle,out).print(" risk is %d\n", risk); + TRACE(cycle,out).print(" risk is {}\n", risk); // TOTHINK: on a large grid, this will compute the same risk up to 5 times for (auto npos : neighbors | transform(around(pos))) { @@ -504,7 +504,7 @@ class SuspendManager { if (!building || building->getType() != df::building_type::Construction) return false; - TRACE(cycle,out).print("check (construction) construction job %d for support\n", job->id); + TRACE(cycle,out).print("check (construction) construction job {} for support\n", job->id); coord pos(building->centerx,building->centery,building->z); @@ -647,7 +647,7 @@ class SuspendManager { void refresh(color_ostream &out) { - DEBUG(cycle,out).print("starting refresh, prevent blocking is %s\n", + DEBUG(cycle,out).print("starting refresh, prevent blocking is {}\n", prevent_blocking ? "true" : "false"); suspensions.clear(); leadsToDeadend.clear(); @@ -693,7 +693,7 @@ class SuspendManager { if (building && buildingOnDesignation(building)) suspensions[job->id]=Reason::ERASE_DESIGNATION; } - DEBUG(cycle,out).print("finished refresh: found %zu reasons for suspension\n",suspensions.size()); + DEBUG(cycle,out).print("finished refresh: found {} reasons for suspension\n",suspensions.size()); } void do_cycle (color_ostream &out, bool unsuspend_everything = false) @@ -720,7 +720,7 @@ class SuspendManager { } } } - DEBUG(cycle,out).print("suspended %zu constructions and unsuspended %zu constructions\n", + DEBUG(cycle,out).print("suspended {} constructions and unsuspended {} constructions\n", num_suspend, num_unsuspend); } @@ -772,7 +772,7 @@ static void do_cycle(color_ostream &out); static void jobCompletedHandler(color_ostream& out, void* ptr); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); suspendmanager_instance = std::make_unique(); eventhandler_instance = std::make_unique(plugin_self,jobCompletedHandler,1); @@ -793,13 +793,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector prevent_blocking = config.get_bool(CONFIG_PREVENT_BLOCKING); - DEBUG(control,out).print("loading persisted state: enabled is %s / prevent_blocking is %s\n", + DEBUG(control,out).print("loading persisted state: enabled is {} / prevent_blocking is {}\n", is_enabled ? "true" : "false", suspendmanager_instance->prevent_blocking ? "true" : "false"); if(is_enabled) { @@ -854,7 +854,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -870,16 +870,16 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.size() == 0) { if (!is_enabled) { - out.print("%s is disabled\n", plugin_name); + out.print("{} is disabled\n", plugin_name); } else { out.print( - "%s is enabled %s supending blocking jobs\n", plugin_name, + "{} is enabled {} suspending blocking jobs\n", plugin_name, suspendmanager_instance->prevent_blocking ? "and" : "but not"); } return CR_OK; @@ -896,7 +896,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) config.set_bool(CONFIG_PREVENT_BLOCKING, true); if (is_enabled) { do_cycle(out); - out.print("%s", suspendmanager_instance->getStatus(out).c_str()); + out.print("{}", suspendmanager_instance->getStatus(out)); } return CR_OK; } else if (parameters[2] == "false") { @@ -904,7 +904,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) config.set_bool(CONFIG_PREVENT_BLOCKING, false); if (is_enabled) { do_cycle(out); - out.print("%s", suspendmanager_instance->getStatus(out).c_str()); + out.print("{}", suspendmanager_instance->getStatus(out)); } return CR_OK; } else @@ -923,7 +923,7 @@ static void jobCompletedHandler(color_ostream& out, void* ptr) { TRACE(cycle,out).print("job completed/initiated handler called\n"); df::job* job = static_cast(ptr); if (SuspendManager::isConstructionJob(job)) { - DEBUG(cycle,out).print("construction job initiated/completed (tick: %d)\n", world->frame_counter); + DEBUG(cycle,out).print("construction job initiated/completed (tick: {})\n", world->frame_counter); cycle_needed = true; } @@ -938,7 +938,7 @@ static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; cycle_needed = false; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); suspendmanager_instance->do_cycle(out); } diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index c66d314431..efd9044024 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -228,8 +228,8 @@ class Tailor { df::item_type t; int size; std::tie(t, size) = i.first; - DEBUG(cycle).print("tailor: %d %s of size %d found\n", - i.second, ENUM_KEY_STR(item_type, t).c_str(), size); + DEBUG(cycle).print("tailor: {} {} of size {} found\n", + i.second, ENUM_KEY_STR(item_type, t), size); } } } @@ -248,7 +248,7 @@ class Tailor { // only count dyed std::string d; i->getItemDescription(&d, 0); - TRACE(cycle).print("tailor: skipping undyed %s\n", DF2CONSOLE(d).c_str()); + TRACE(cycle).print("tailor: skipping undyed {}\n", DF2CONSOLE(d)); continue; } MaterialInfo mat(i); @@ -268,7 +268,7 @@ class Tailor { { std::string d; i->getItemDescription(&d, 0); - DEBUG(cycle).print("tailor: weird cloth item found: %s (%d)\n", DF2CONSOLE(d).c_str(), i->id); + DEBUG(cycle).print("tailor: weird cloth item found: {} ({})\n", DF2CONSOLE(d), i->id); } } } @@ -280,7 +280,7 @@ class Tailor { supply[M_LEATHER] += i->getStackSize(); } - DEBUG(cycle).print("tailor: available silk %d yarn %d cloth %d leather %d adamantine %d\n", + DEBUG(cycle).print("tailor: available silk {} yarn {} cloth {} leather {} adamantine {}\n", supply[M_SILK], supply[M_YARN], supply[M_CLOTH], supply[M_LEATHER], supply[M_ADAMANTINE]); } @@ -331,9 +331,9 @@ class Tailor { { if (ordered.count(ty) == 0) { - DEBUG(cycle).print ("tailor: %s (size %d) worn by %s (size %d) needs replacement\n", - DF2CONSOLE(description).c_str(), isize, - DF2CONSOLE(Units::getReadableName(u)).c_str(), usize); + DEBUG(cycle).print ("tailor: {} (size {}) worn by {} (size {}) needs replacement\n", + DF2CONSOLE(description), isize, + DF2CONSOLE(Units::getReadableName(u)), usize); needed[std::make_pair(ty, usize)] += 1; ordered.insert(ty); } @@ -346,10 +346,10 @@ class Tailor { bool confiscated = Items::setOwner(w, NULL); INFO(cycle).print( - "tailor: %s %s from %s.\n", + "tailor: {} {} from {}.\n", (confiscated ? "confiscated" : "could not confiscate"), - DF2CONSOLE(description).c_str(), - DF2CONSOLE(Units::getReadableName(u)).c_str() + DF2CONSOLE(description), + DF2CONSOLE(Units::getReadableName(u)) ); } @@ -363,10 +363,10 @@ class Tailor { { if (equipped.count(ty) == 0 && ordered.count(ty) == 0) { - TRACE(cycle).print("tailor: one %s of size %d needed to cover %s\n", - ENUM_KEY_STR(item_type, ty).c_str(), + TRACE(cycle).print("tailor: one {} of size {} needed to cover {}\n", + ENUM_KEY_STR(item_type, ty), usize, - DF2CONSOLE(Units::getReadableName(u)).c_str()); + DF2CONSOLE(Units::getReadableName(u))); needed[std::make_pair(ty, usize)] += 1; } } @@ -423,7 +423,7 @@ class Tailor { { const df::job_type j = jj->second; orders[std::make_tuple(j, sub, size)] += count; - DEBUG(cycle).print("tailor: %s times %d of size %d ordered\n", ENUM_KEY_STR(job_type, j).c_str(), count, size); + DEBUG(cycle).print("tailor: {} times {} of size {} ordered\n", ENUM_KEY_STR(job_type, j), count, size); } } } @@ -441,8 +441,8 @@ class Tailor { if (o->material_category.whole == m.job_material.whole) { supply[m] -= o->amount_left; - TRACE(cycle).print("tailor: supply of %s reduced by %d due to being required for an existing order\n", - DF2CONSOLE(m.name).c_str(), o->amount_left); + TRACE(cycle).print("tailor: supply of {} reduced by {} due to being required for an existing order\n", + DF2CONSOLE(m.name), o->amount_left); } } } @@ -574,7 +574,7 @@ class Tailor { auto [_, n] = get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); if (n > 0) { - INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); + INFO(cycle).print("tailor: ordered {} {}\n", c, DF2CONSOLE(descr)); } return n; } @@ -687,7 +687,7 @@ class Tailor { if (sizes.count(size) == 0) { - WARN(cycle).print("tailor: cannot determine race for clothing of size %d, skipped\n", + WARN(cycle).print("tailor: cannot determine race for clothing of size {}, skipped\n", size); continue; } @@ -738,11 +738,11 @@ class Tailor { if (!can_make) { - INFO(cycle).print("tailor: civilization cannot make %s, skipped\n", DF2CONSOLE(name_p).c_str()); + INFO(cycle).print("tailor: civilization cannot make {}, skipped\n", DF2CONSOLE(name_p)); continue; } - DEBUG(cycle).print("tailor: ordering %d %s\n", count, DF2CONSOLE(name_p).c_str()); + DEBUG(cycle).print("tailor: ordering {} {}\n", count, DF2CONSOLE(name_p)); for (auto& m : material_order) { @@ -757,8 +757,8 @@ class Tailor { if (supply[m] < count + res) { c = supply[m] - res; - TRACE(cycle).print("tailor: order reduced from %d to %d to protect reserves of %s\n", - count, c, DF2CONSOLE(m.name).c_str()); + TRACE(cycle).print("tailor: order reduced from {} to {} to protect reserves of {}\n", + count, c, DF2CONSOLE(m.name)); skipped += (count - c); } supply[m] -= c; @@ -767,13 +767,12 @@ class Tailor { if (n > 0) { - INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", + INFO(cycle).print("tailor: added order #{} for {} {} {}, sized for {}\n", order->id, n, - bitfield_to_string(order->material_category).c_str(), - DF2CONSOLE((c > 1) ? name_p : name_s).c_str(), - DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1]).c_str() - ); + bitfield_to_string(order->material_category), + DF2CONSOLE((c > 1) ? name_p : name_s), + DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1])); count -= n; ordered += n; @@ -782,7 +781,7 @@ class Tailor { else { skipped += count; - DEBUG(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); + DEBUG(cycle).print("tailor: material {} skipped due to lack of reserves, {} available\n", DF2CONSOLE(m.name), supply[m]); } } @@ -791,7 +790,7 @@ class Tailor { if (skipped > 0) { - INFO(cycle).print("tailor: %d item%s not ordered due to a lack of materials\n", skipped, skipped != 1 ? "s" : ""); + INFO(cycle).print("tailor: {} item{} not ordered due to a lack of materials\n", skipped, skipped != 1 ? "s" : ""); if (automate_dye) { @@ -799,23 +798,23 @@ class Tailor { int available_dyeable_cloth = count_dyeables(df::item_type::CLOTH); int dye_cloth_orders = count_dye_cloth_orders(); - DEBUG(cycle).print("tailor: available dyes = %d, available dyeable cloth = %d, dye cloth orders = %d\n", + DEBUG(cycle).print("tailor: available dyes = {}, available dyeable cloth = {}, dye cloth orders = {}\n", available_dyes, available_dyeable_cloth, dye_cloth_orders); int to_dye = std::min(skipped, std::min(available_dyes, available_dyeable_cloth) - dye_cloth_orders); - DEBUG(cycle).print("tailor: to dye = %d\n", to_dye); + DEBUG(cycle).print("tailor: to dye = {}\n", to_dye); if (to_dye > 0) { int dyed = order_dye_cloth(to_dye); if (dyed > 0) { - INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); + INFO(cycle).print("tailor: dyeing {} cloth\n", to_dye); } } int dyes_to_make = available_dyes - to_dye; if (dyes_to_make > 0) { - INFO(cycle).print("tailor: ordering up to %d dyes\n", dyes_to_make); + INFO(cycle).print("tailor: ordering up to {} dyes\n", dyes_to_make); make_dyes(dyes_to_make); } } @@ -843,7 +842,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static int do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); tailor_instance = std::make_unique(); @@ -858,19 +857,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector set_confiscate(config.get_bool(CONFIG_CONFISCATE)); - DEBUG(control,out).print("loading persisted confiscation state: %s\n", + DEBUG(control,out).print("loading persisted confiscation state: {}\n", tailor_instance->get_confiscate() ? "true" : "false"); tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); - DEBUG(control, out).print("loading persisted dye automation state: %s\n", + DEBUG(control, out).print("loading persisted dye automation state: {}\n", tailor_instance->get_automate_dye() ? "true" : "false"); tailor_instance->sync_material_order(); @@ -933,7 +932,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -945,14 +944,14 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (world->frame_counter - cycle_timestamp >= CYCLE_TICKS) { int ordered = do_cycle(out); if (0 < ordered) - out.print("tailor: ordered %d items of clothing\n", ordered); + out.print("tailor: ordered {} items of clothing\n", ordered); } return CR_OK; } static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -975,7 +974,7 @@ static int do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); return tailor_instance->do_cycle(); } @@ -986,7 +985,7 @@ static int do_cycle(color_ostream &out) { static void tailor_doCycle(color_ostream &out) { DEBUG(control,out).print("entering tailor_doCycle\n"); - out.print("ordered %d items of clothing\n", do_cycle(out)); + out.print("ordered {} items of clothing\n", do_cycle(out)); } // remember, these are ONE-based indices from Lua @@ -1008,7 +1007,7 @@ static void tailor_setMaterialPreferences(color_ostream &out, int32_t silkIdx, static void tailor_setConfiscate(color_ostream& out, bool enable) { - DEBUG(control,out).print("%s confiscation of tattered clothing \n", enable ? "enabling" : "disabling"); + DEBUG(control,out).print("{} confiscation of tattered clothing \n", enable ? "enabling" : "disabling"); config.set_bool(CONFIG_CONFISCATE, enable); tailor_instance->set_confiscate(enable); } @@ -1020,7 +1019,7 @@ static bool tailor_getConfiscate(color_ostream& out) static void tailor_setAutomateDye(color_ostream& out, bool enable) { - DEBUG(control, out).print("%s automation of dye\n", enable ? "enabling" : "disabling"); + DEBUG(control, out).print("{} automation of dye\n", enable ? "enabling" : "disabling"); config2.set_bool(CONFIG_AUTOMATE_DYE, enable); tailor_instance->set_automate_dye(enable); } diff --git a/plugins/tiletypes.cpp b/plugins/tiletypes.cpp index 95c5bd4424..8c8407d189 100644 --- a/plugins/tiletypes.cpp +++ b/plugins/tiletypes.cpp @@ -1156,7 +1156,7 @@ command_result executePaintJob(color_ostream &out, } if (!opts.quiet) - out.print("Cursor coords: (%d, %d, %d)\n", + out.print("Cursor coords: ({}, {}, {})\n", cursor.x, cursor.y, cursor.z); MapExtras::MapCache map; @@ -1196,9 +1196,9 @@ command_result executePaintJob(color_ostream &out, } if (failures > 0) - out.printerr("Could not update %d tiles of %zu.\n", failures, all_tiles.size()); + out.printerr("Could not update {} tiles of {}.\n", failures, all_tiles.size()); else if (!opts.quiet) - out.print("Processed %zu tiles.\n", all_tiles.size()); + out.print("Processed {} tiles.\n", all_tiles.size()); if (map.WriteAll()) { @@ -1399,57 +1399,57 @@ command_result df_tiletypes_here_point (color_ostream &out, vector & pa static bool setTile(color_ostream& out, df::coord pos, TileType target) { if (!Maps::isValidTilePos(pos)) { - out.printerr("Invalid map position: %d, %d, %d\n", pos.x, pos.y, pos.z); + out.printerr("Invalid map position: ({}, {}, {})\n", pos.x, pos.y, pos.z); return false; } if (!is_valid_enum_item(target.shape)) { - out.printerr("Invalid shape type: %d\n", target.shape); + out.printerr("Invalid shape type: {}\n", ENUM_AS_STR(target.shape)); return false; } if (!is_valid_enum_item(target.material)) { - out.printerr("Invalid material type: %d\n", target.material); + out.printerr("Invalid material type: {}\n", ENUM_AS_STR(target.material)); return false; } if (!is_valid_enum_item(target.special)) { - out.printerr("Invalid special type: %d\n", target.special); + out.printerr("Invalid special type: {}\n", ENUM_AS_STR(target.special)); return false; } if (!is_valid_enum_item(target.variant)) { - out.printerr("Invalid variant type: %d\n", target.variant); + out.printerr("Invalid variant type: {}\n", ENUM_AS_STR(target.variant)); return false; } if (target.hidden < -1 || target.hidden > 1) { - out.printerr("Invalid hidden value: %d\n", target.hidden); + out.printerr("Invalid hidden value: {}\n", target.hidden); return false; } if (target.light < -1 || target.light > 1) { - out.printerr("Invalid light value: %d\n", target.light); + out.printerr("Invalid light value: {}\n", target.light); return false; } if (target.subterranean < -1 || target.subterranean > 1) { - out.printerr("Invalid subterranean value: %d\n", target.subterranean); + out.printerr("Invalid subterranean value: {}\n", target.subterranean); return false; } if (target.skyview < -1 || target.skyview > 1) { - out.printerr("Invalid skyview value: %d\n", target.skyview); + out.printerr("Invalid skyview value: {}\n", target.skyview); return false; } if (target.aquifer < -1 || target.aquifer > 2) { - out.printerr("Invalid aquifer value: %d\n", target.aquifer); + out.printerr("Invalid aquifer value: {}\n", target.aquifer); return false; } if (target.autocorrect < 0 || target.autocorrect > 1) { - out.printerr("Invalid autocorrect value: %d\n", target.autocorrect); + out.printerr("Invalid autocorrect value: {}\n", target.autocorrect); return false; } if (target.material == df::tiletype_material::STONE) { if (target.stone_material != -1 && !isStoneInorganic(target.stone_material)) { - out.printerr("Invalid stone material: %d\n", target.stone_material); + out.printerr("Invalid stone material: {}\n", target.stone_material); return false; } if (!is_valid_enum_item(target.vein_type)) { - out.printerr("Invalid vein type: %d\n", target.vein_type); + out.printerr("Invalid vein type: {}\n", ENUM_AS_STR(target.vein_type)); return false; } } diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index a5e5b04c80..deb6c45e4d 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -78,7 +78,7 @@ static void on_new_active_unit(color_ostream& out, void* data); static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -162,7 +162,7 @@ static void do_disable() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -170,7 +170,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) { @@ -181,7 +181,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { do_disable(); } } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -189,7 +189,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -221,7 +221,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { if (config.get_bool(CONFIG_IS_ENABLED)) { plugin_enable(out, true); } - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; @@ -230,7 +230,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); do_disable(); } @@ -246,7 +246,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -270,7 +270,7 @@ static void record_coverage(color_ostream &out) { if (coverage_slot >= NUM_COVERAGE_TICKS) return; if (!tick_coverage[coverage_slot]) { - DEBUG(cycle,out).print("recording coverage for slot: %u", coverage_slot); + DEBUG(cycle,out).print("recording coverage for slot: {}", coverage_slot); } tick_coverage[coverage_slot] = true; } @@ -616,7 +616,7 @@ static void adjust_items(color_ostream &out, int32_t timeskip) { } static void do_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -647,7 +647,7 @@ static void do_cycle(color_ostream &out) { // don't let our deficit grow unbounded if we can never catch up timeskip_deficit = std::min(desired_timeskip - float(timeskip), 100.0F); - DEBUG(cycle,out).print("cur_year_tick: %d, real_fps: %d, timeskip: (%d, +%.2f)\n", *cur_year_tick, real_fps, timeskip, timeskip_deficit); + DEBUG(cycle,out).print("cur_year_tick: {}, real_fps: {}, timeskip: ({}, +{:.2f})\n", *cur_year_tick, real_fps, timeskip, timeskip_deficit); if (timeskip <= 0) return; @@ -669,7 +669,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { auto unit = df::unit::find(unit_id); if (!unit) return; - DEBUG(event,out).print("registering new unit %d (%s)\n", unit->id, Units::getReadableName(unit).c_str()); + DEBUG(event,out).print("registering new unit {} ({})\n", unit->id, Units::getReadableName(unit)); register_birthday(unit); } @@ -678,7 +678,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { // static void timestream_setFps(color_ostream &out, int fps) { - DEBUG(cycle,out).print("timestream_setFps: %d\n", fps); + DEBUG(cycle,out).print("timestream_setFps: {}\n", fps); config.set_int(CONFIG_TARGET_FPS, clamp_fps_to_valid(fps)); } diff --git a/plugins/tubefill.cpp b/plugins/tubefill.cpp index 46f7101fab..cab0241bd4 100644 --- a/plugins/tubefill.cpp +++ b/plugins/tubefill.cpp @@ -116,6 +116,6 @@ command_result tubefill(color_ostream &out, std::vector & params) } } } - out.print("Found and changed %" PRId64 " tiles.\n", count); + out.print("Found and changed {} tiles.\n", count); return CR_OK; } diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index dd7edbf3e1..0e69da9df6 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -107,14 +107,14 @@ static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vect if (disable) { hook.remove(); if (!quiet) - out.print("Disabled tweak %s (%s)\n", parameters[0].c_str(), hook.name()); + out.print("Disabled tweak {} ({})\n", parameters[0], hook.name()); } else { if (hook.apply()) { if (!quiet) - out.print("Enabled tweak %s (%s)\n", parameters[0].c_str(), hook.name()); + out.print("Enabled tweak {} ({})\n", parameters[0], hook.name()); } else - out.printerr("Could not activate tweak %s (%s)\n", parameters[0].c_str(), hook.name()); + out.printerr("Could not activate tweak {} ({})\n", parameters[0], hook.name()); } } @@ -134,13 +134,13 @@ static command_result enable_tweak(string tweak, color_ostream &out, vector ¶meters) { if (parameters.empty() || parameters[0] == "list") { out.print("tweaks:\n"); for (auto & entry : get_status()) - out.print(" %25s: %s\n", entry.first.c_str(), entry.second ? "enabled" : "disabled"); + out.print(" {:25}: {}\n", entry.first, entry.second ? "enabled" : "disabled"); return CR_OK; } diff --git a/plugins/work-now.cpp b/plugins/work-now.cpp index ca9f1ffe06..6844a23623 100644 --- a/plugins/work-now.cpp +++ b/plugins/work-now.cpp @@ -44,13 +44,13 @@ static void cleanup() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(log,out).print("%s from the API; persisting\n", + DEBUG(log,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) @@ -58,7 +58,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { else cleanup(); } else { - DEBUG(log,out).print("%s from the API, but already %s; no action\n", + DEBUG(log,out).print("{} from the API, but already {} ; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -81,7 +81,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { } is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(log,out).print("loading persisted enabled state: %s\n", + DEBUG(log,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; @@ -98,7 +98,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan poke_idlers(); } else if (e == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(log,out).print("world unloaded; disabling %s\n", + DEBUG(log,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -109,12 +109,12 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan static command_result work_now(color_ostream& out, vector& parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.empty() || parameters[0] == "status") { - out.print("work_now is %sactively poking idle dwarves.\n", is_enabled ? "" : "not "); + out.print("work_now is {}actively poking idle dwarves.\n", is_enabled ? "" : "not "); return CR_OK; } diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 2b1d91b826..24c3a67a3d 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -410,7 +410,7 @@ static void stop_protect(color_ostream &out) pending_recover.clear(); if (!known_jobs.empty()) - out.print("Unprotecting %zd jobs.\n", known_jobs.size()); + out.print("Unprotecting {} jobs.\n", known_jobs.size()); for (TKnownJobs::iterator it = known_jobs.begin(); it != known_jobs.end(); ++it) delete it->second; @@ -437,7 +437,7 @@ static void start_protect(color_ostream &out) check_lost_jobs(out, 0); if (!known_jobs.empty()) - out.print("Protecting %zd jobs.\n", known_jobs.size()); + out.print("Protecting {} jobs.\n", known_jobs.size()); } static void init_state(color_ostream &out) @@ -456,7 +456,7 @@ static void init_state(color_ostream &out) if (get_constraint(out, items[i].val(), &items[i])) continue; - out.printerr("Lost constraint %s\n", items[i].val().c_str()); + out.printerr("Lost constraint {}\n", items[i].val()); World::DeletePersistentData(items[i]); } @@ -503,8 +503,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) pj->holder = df::building::find(pj->building_id); if (!pj->holder) { - out.printerr("Forgetting job %d (%s): holder building lost.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Forgetting job {} ({}): holder building lost.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); forget_job(out, pj); return true; } @@ -512,8 +512,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) // Check its state and postpone or cancel if invalid if (pj->holder->jobs.size() >= 10) { - out.printerr("Forgetting job %d (%s): holder building has too many jobs.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Forgetting job {} ({}): holder building has too many jobs.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); forget_job(out, pj); return true; } @@ -535,8 +535,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) { Job::deleteJobStruct(recovered); - out.printerr("Inconsistency: job %d (%s) already in list.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Inconsistency: job {} ({}) already in list.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); return true; } @@ -664,7 +664,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str if (tokens[0] == "ANY_CRAFT" || tokens[0] == "CRAFTS") { is_craft = true; } else if (!item.find(tokens[0]) || !item.isValid()) { - out.printerr("Cannot find item type: %s\n", tokens[0].c_str()); + out.printerr("Cannot find item type: {}\n", tokens[0]); return NULL; } @@ -674,7 +674,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str df::dfhack_material_category mat_mask; std::string maskstr = vector_get(tokens,1); if (!maskstr.empty() && !parseJobMaterialCategory(&mat_mask, maskstr)) { - out.printerr("Cannot decode material mask: %s\n", maskstr.c_str()); + out.printerr("Cannot decode material mask: {}\n", maskstr); return NULL; } @@ -684,7 +684,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str MaterialInfo material; std::string matstr = vector_get(tokens,2); if (!matstr.empty() && (!material.find(matstr) || !material.isValid())) { - out.printerr("Cannot find material: %s\n", matstr.c_str()); + out.printerr("Cannot find material: {}\n", matstr); return NULL; } @@ -692,7 +692,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str weight += (material.index >= 0 ? 5000 : 1000); if (mat_mask.whole && material.isValid() && !material.matches(mat_mask)) { - out.printerr("Material %s doesn't match mask %s\n", matstr.c_str(), maskstr.c_str()); + out.printerr("Material {} doesn't match mask {}\n", matstr, maskstr); return NULL; } @@ -724,7 +724,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str if (!found) { - out.printerr("Cannot parse token: %s\n", token.c_str()); + out.printerr("Cannot parse token: {}\n", token); return NULL; } } @@ -1184,9 +1184,9 @@ static void setJobResumed(color_ostream &out, ProtectedJob *pj, bool goal) if (goal != current) { - out.print("%s %s%s\n", + out.print("{} {}{}\n", (goal ? "Resuming" : "Suspending"), - shortJobDescription(pj->actual_job).c_str(), + shortJobDescription(pj->actual_job), (!goal || pj->isActuallyResumed() ? "" : " (delayed)")); } } @@ -1817,7 +1817,7 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet if (deleteConstraint(parameters[1])) return CR_OK; - out.printerr("Constraint not found: %s\n", parameters[1].c_str()); + out.printerr("Constraint not found: {}\n", parameters[1]); return CR_FAILURE; } else if (cmd == "unlimit-all") From d5518b988a93eabe5063ca75f2f6b94b14c5bc0f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 17:46:24 -0600 Subject: [PATCH 0662/1082] corrections whitespace; `std::format` -> `fmt::format` --- library/DataDefs.cpp | 2 +- library/Process.cpp | 2 +- library/include/ColorText.h | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 7a9ae4ae35..54cdfff982 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -262,7 +262,7 @@ const std::string bit_container_identity::getFullName(const type_identity *) con const std::string df::buffer_container_identity::getFullName(const type_identity *item) const { return (item ? item->getFullName() : std::string("void")) + - (size > 0 ? std::format("[{}]", size) : std::string("[]")); + (size > 0 ? fmt::format("[{}]", size) : std::string("[]")); } union_identity::union_identity(size_t size, const TAllocateFn alloc, diff --git a/library/Process.cpp b/library/Process.cpp index 47d110ba92..00664c74de 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -215,7 +215,7 @@ Process::Process(const VersionInfoFactory& known_versions) : identified(false) } free(wd); #else /* WIN32 */ - cerr << "PE timestamp: " << std::format("{:#0x}", my_pe) << endl; + cerr << "PE timestamp: " << fmt::format("{:#0x}", my_pe) << endl; #endif /* WIN32 */ } } diff --git a/library/include/ColorText.h b/library/include/ColorText.h index 3fce782ecf..15aca0cf59 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -191,4 +191,3 @@ namespace DFHack }; } - From 31dcd41624c24fb4c6ca83b0fde276fcd893de5c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 17:54:09 -0600 Subject: [PATCH 0663/1082] add missing include --- library/Process.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/Process.cpp b/library/Process.cpp index 00664c74de..2f3471d4a5 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -35,6 +35,8 @@ distribution. #include #include +#include + #ifndef WIN32 #include #include From 4fd76d64e61728d44fe1f74f2c4ab5fb85620a11 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 18:02:23 -0600 Subject: [PATCH 0664/1082] add another missing include --- library/Process.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Process.cpp b/library/Process.cpp index 2f3471d4a5..57ea6a7c0c 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -36,6 +36,7 @@ distribution. #include #include +#include #ifndef WIN32 #include From 809b3adb75ab8a2155cddb2a547c19ee7242a7ce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 18:12:51 -0600 Subject: [PATCH 0665/1082] use `reinterpret_cast` to print function pointers c++ standard does not allow static cast of a function pointer to `void*` --- library/modules/EventManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 4a4d47e765..fad6b77678 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -72,7 +72,7 @@ static const int32_t ticksPerYear = 403200; void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler) { DEBUG(log).print("registering handler {} from plugin {} for event {}\n", - static_cast(handler.eventHandler), + reinterpret_cast(handler.eventHandler), handler.plugin ? handler.plugin->getName() : "", static_cast(e)); handlers[e].insert(pair(handler.plugin, handler)); @@ -91,7 +91,7 @@ int32_t DFHack::EventManager::registerTick(EventHandler handler, int32_t when, b handler.freq = when; tickQueue.insert(pair(handler.freq, handler)); DEBUG(log).print("registering handler {} from plugin {} for event TICK\n", - static_cast(handler.eventHandler), + reinterpret_cast(handler.eventHandler), handler.plugin ? handler.plugin->getName() : ""); handlers[EventType::TICK].insert(pair(handler.plugin,handler)); return when; From dda124b7f08088fcafd2a75d3da64f0a243293c5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 18:16:58 -0600 Subject: [PATCH 0666/1082] missed one :( --- library/modules/EventManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index fad6b77678..3c926645a2 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -119,7 +119,7 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl continue; } DEBUG(log).print("unregistering handler {} from plugin {} for event {}\n", - static_cast(handler.eventHandler), + reinterpret_cast(handler.eventHandler), handler.plugin ? handler.plugin->getName() : "", static_cast(e)); i = handlers[e].erase(i); From 5bd91da0452bf1ceeda5b6ad7bb3f560dae8b6e6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 18:32:59 -0600 Subject: [PATCH 0667/1082] add `fmt` to `dfhack_test` --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e8fe082b7f..caaf20c3e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -436,7 +436,7 @@ macro(dfhack_test name files) if(BUILD_LIBRARY AND UNIX AND NOT APPLE) # remove this once our MSVC build env has been updated add_executable(${name} ${files}) target_include_directories(${name} PUBLIC depends/googletest/googletest/include) - target_link_libraries(${name} dfhack gtest) + target_link_libraries(${name} dfhack fmt gtest) add_test(NAME ${name} COMMAND ${name}) endif() endmacro() From 5224d655ed5e19ebc65ba82be8150b6d2abc2df9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 22:45:20 -0600 Subject: [PATCH 0668/1082] pin fmtlib to 12.1.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index caaf20c3e8..2cbe4fd686 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,7 +351,7 @@ INCLUDE(FetchContent) FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git - GIT_TAG master + GIT_TAG 12.1.0 ) FetchContent_MakeAvailable(fmt) From 41c85e77060f4e0135346a2a7accfb1fb917fc22 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 23:45:30 -0600 Subject: [PATCH 0669/1082] change fmtlib pin 12.1.0 generates a warning when built with msvc also there are build time improvements after 12.1.0 was released --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2cbe4fd686..de3e0f3132 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,7 +351,7 @@ INCLUDE(FetchContent) FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git - GIT_TAG 12.1.0 + GIT_TAG 790b9389ae99c4ddebdd2736a8602eca1fec684e # 12.1.0 + bugfix for MSVC warning + build time improvements ) FetchContent_MakeAvailable(fmt) From 47d6dd25d4f5f5558756a2ca1f61f4777ab0f1ad Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 01:22:05 -0600 Subject: [PATCH 0670/1082] adjustments to facilitating to std::format when it's ready, which it's not... --- CMakeLists.txt | 9 ++++++--- library/CMakeLists.txt | 11 ++++++----- library/Core.cpp | 13 +++++++------ library/Error.cpp | 2 +- library/LuaTools.cpp | 2 +- library/Process.cpp | 3 +-- library/VersionInfoFactory.cpp | 4 ++-- library/include/ColorText.h | 4 +--- library/include/DataDefs.h | 3 +-- library/include/Format.h | 15 +++++++++++++++ 10 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 library/include/Format.h diff --git a/CMakeLists.txt b/CMakeLists.txt index de3e0f3132..528f6f0d93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,8 +46,8 @@ option(REMOVE_SYMBOLS_FROM_DF_STUBS "Remove debug symbols from DF stubs. (Reduce macro(CHECK_GCC compiler_path) execute_process(COMMAND ${compiler_path} -dumpversion OUTPUT_VARIABLE GCC_VERSION_OUT) string(STRIP "${GCC_VERSION_OUT}" GCC_VERSION_OUT) - if(${GCC_VERSION_OUT} VERSION_LESS "10") - message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 10 or later") + if(${GCC_VERSION_OUT} VERSION_LESS "11") + message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 11 or later") endif() endmacro() @@ -347,6 +347,7 @@ if(BUILD_LIBRARY) endif() endif() +# this can be made conditional once we get to better platform support for std::format INCLUDE(FetchContent) FetchContent_Declare( fmt @@ -354,6 +355,8 @@ FetchContent_Declare( GIT_TAG 790b9389ae99c4ddebdd2736a8602eca1fec684e # 12.1.0 + bugfix for MSVC warning + build time improvements ) FetchContent_MakeAvailable(fmt) +set(FMTLIB fmt) +add_definitions("-DUSE_FMTLIB") if(APPLE) # libstdc++ (GCC 4.8.5 for OS X 10.6) @@ -436,7 +439,7 @@ macro(dfhack_test name files) if(BUILD_LIBRARY AND UNIX AND NOT APPLE) # remove this once our MSVC build env has been updated add_executable(${name} ${files}) target_include_directories(${name} PUBLIC depends/googletest/googletest/include) - target_link_libraries(${name} dfhack fmt gtest) + target_link_libraries(${name} dfhack ${FMTLIB} gtest) add_test(NAME ${name} COMMAND ${name}) endif() endmacro() diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 471d2f808a..9e9babb241 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -58,6 +58,7 @@ set(MAIN_HEADERS include/DebugManager.h include/Error.h include/Export.h + include/Format.h include/Hooks.h include/LuaTools.h include/LuaWrapper.h @@ -320,12 +321,12 @@ if(UNIX) endif() if(APPLE) - set(PROJECT_LIBS dl dfhack-md5 fmt ${DFHACK_TINYXML}) + set(PROJECT_LIBS dl dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) elseif(UNIX) - set(PROJECT_LIBS rt dl dfhack-md5 fmt ${DFHACK_TINYXML}) + set(PROJECT_LIBS rt dl dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) else(WIN32) # FIXME: do we really need psapi? - set(PROJECT_LIBS psapi dbghelp dfhack-md5 fmt ${DFHACK_TINYXML}) + set(PROJECT_LIBS psapi dbghelp dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) endif() set(VERSION_SRCS DFHackVersion.cpp) @@ -391,7 +392,7 @@ 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 fmt) + target_link_libraries(dfhooks_dfhack dfhack ${FMTLIB}) endif() # effectively disables debug builds... @@ -420,7 +421,7 @@ endif() target_link_libraries(dfhack protobuf-lite clsocket lua jsoncpp_static dfhack-version ${PROJECT_LIBS}) set_target_properties(dfhack PROPERTIES INTERFACE_LINK_LIBRARIES "") -target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static fmt) +target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static ${FMTLIB}) if(WIN32) target_link_libraries(dfhack-client dbghelp) endif() diff --git a/library/Core.cpp b/library/Core.cpp index 6960c12eb9..5e32f1b0d5 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -41,6 +41,7 @@ distribution. #include "LuaTools.h" #include "DFHackVersion.h" #include "md5wrapper.h" +#include "Format.h" #include "modules/DFSDL.h" #include "modules/DFSteam.h" @@ -87,7 +88,6 @@ distribution. #include #include -#include #ifdef _WIN32 #define NOMINMAX @@ -525,18 +525,19 @@ std::filesystem::path Core::findScript(std::string name) { std::vector paths; getScriptPaths(&paths); - for (auto it = paths.begin(); it != paths.end(); ++it) + for (auto& path : paths) { std::error_code ec; - std::filesystem::path path = std::filesystem::weakly_canonical(*it / name, ec); + auto raw_path = path / name; + std::filesystem::path load_path = std::filesystem::weakly_canonical(raw_path, ec); if (ec) { - con.printerr("Error loading '{}' ({})\n", *it / name, ec.message()); + con.printerr("Error loading '{}' ({})\n", raw_path, ec.message()); continue; } - if (Filesystem::isfile(path)) - return path; + if (Filesystem::isfile(load_path)) + return load_path; } return {}; } diff --git a/library/Error.cpp b/library/Error.cpp index fa32daf90e..9abb9ea7c9 100644 --- a/library/Error.cpp +++ b/library/Error.cpp @@ -1,7 +1,7 @@ #include "Error.h" +#include "Format.h" #include "MiscUtils.h" -#include #include using namespace DFHack::Error; diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index e6083b0a4e..69242ede5f 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -249,7 +249,7 @@ static void signal_typeid_error(color_ostream* out, lua_State* state, int val_index, bool perr, bool signal) { std::string typestr = type ? type->getFullName() : "any pointer"; - std::string error = fmt::format(fmt::runtime(msg), typestr); + std::string error = fmt::vformat(msg, fmt::make_format_args(typestr)); //FIXME: C++26 if (signal) { diff --git a/library/Process.cpp b/library/Process.cpp index 57ea6a7c0c..c5e28b179e 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -35,8 +35,7 @@ distribution. #include #include -#include -#include +#include "Format.h" #ifndef WIN32 #include diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index 6b2c1e62f6..b4f6eefc78 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -209,7 +209,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "md5-hash") { const char *cstr_value = pMemEntry->Attribute("value"); - fmt::print(stderr, "{} ({}): MD5: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); + std::cerr << fmt::format("{} ({}): MD5: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addMD5(cstr_value); @@ -217,7 +217,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "binary-timestamp") { const char *cstr_value = pMemEntry->Attribute("value"); - fmt::print(stderr, "{} ({}): PE: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); + std::cerr << fmt::format("{} ({}): PE: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addPE(strtol(cstr_value, 0, 16)); diff --git a/library/include/ColorText.h b/library/include/ColorText.h index 15aca0cf59..330a2d50be 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -24,6 +24,7 @@ distribution. #pragma once #include "Export.h" +#include "Format.h" #include #include @@ -33,9 +34,6 @@ distribution. #include #include -#include -#include - namespace dfproto { class CoreTextNotification; diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 182148d635..08d8d02097 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -35,8 +35,7 @@ distribution. #include "BitArray.h" #include "Export.h" - -#include +#include "Format.h" struct lua_State; diff --git a/library/include/Format.h b/library/include/Format.h new file mode 100644 index 0000000000..52d4e0e3c6 --- /dev/null +++ b/library/include/Format.h @@ -0,0 +1,15 @@ +#pragma once + +#ifdef USE_FMTLIB + +#include +#include +#include + +#else + +#include + +namespace fmt = std; + +#endif From b1fd0b98b8915ff28c50e962da71b7ae31d51a2d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 01:42:37 -0600 Subject: [PATCH 0671/1082] missed a file on last commit --- plugins/Plugins.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 107f9a8068..82439f69a5 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -124,7 +124,7 @@ macro(dfhack_plugin) target_include_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proto") target_link_libraries(${PLUGIN_NAME} protobuf-lite) endif() - target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version fmt ${PLUGIN_LINK_LIBRARIES}) + target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version ${FMTLIB} ${PLUGIN_LINK_LIBRARIES}) if(UNIX) set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_GCC}") From 59a262ba7bbb64669ea7b7d97b7586d2460d609e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 25 Nov 2025 15:16:41 -0600 Subject: [PATCH 0672/1082] Add Commands.cpp (from #5659) --- library/Commands.cpp | 581 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 library/Commands.cpp diff --git a/library/Commands.cpp b/library/Commands.cpp new file mode 100644 index 0000000000..b7eb0e424d --- /dev/null +++ b/library/Commands.cpp @@ -0,0 +1,581 @@ + +#include "Commands.h" + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "RemoteTools.h" + +#include "modules/Gui.h" +#include "modules/World.h" + +#include "df/viewscreen_new_regionst.h" + +#include +#include +#include + + +namespace DFHack +{ + command_result Commands::help(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!parts.size()) + { + if (con.is_console()) + { + con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" + "Some basic editing capabilities are included (single-line text editing).\n" + "The console also has a command history - you can navigate it with Up and Down keys.\n" + "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" + "by clicking on the program icon in the top bar of the window.\n\n"); + } + con.print("Here are some basic commands to get you started:\n" + " help|?|man - This text.\n" + " help - Usage help for the given plugin, command, or script.\n" + " tags - List the tags that the DFHack tools are grouped by.\n" + " ls|dir [] - List commands, optionally filtered by a tag or substring.\n" + " Optional parameters:\n" + " --notags: skip printing tags for each command.\n" + " --dev: include commands intended for developers and modders.\n" + " cls|clear - Clear the console.\n" + " fpause - Force DF to pause.\n" + " die - Force DF to close immediately, without saving.\n" + " keybinding - Modify bindings of commands to in-game key shortcuts.\n" + "\n" + "See more commands by running 'ls'.\n\n" + ); + + con.print("DFHack version {}\n", dfhack_version_desc()); + } + else + { + DFHack::help_helper(con, parts[0]); + } + return CR_OK; + } + + command_result Commands::load(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool all = false; + bool load = (first == "load"); + bool unload = (first == "unload"); + bool reload = (first == "reload"); + auto plug_mgr = core.getPluginManager(); + if (parts.size()) + { + for (const auto& p : parts) + { + if (p.size() && p[0] == '-') + { + if (p.find('a') != std::string::npos) + all = true; + } + } + auto ret = CR_OK; + if (all) + { + if (load && !plug_mgr->loadAll()) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unloadAll()) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reloadAll()) + ret = CR_FAILURE; + } + else + { + for (auto& p : parts) + { + if (p.empty() || p[0] == '-') + continue; + if (load && !plug_mgr->load(p)) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unload(p)) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reload(p)) + ret = CR_FAILURE; + } + } + if (ret != CR_OK) + con.printerr("{} failed\n", first.c_str()); + return ret; + } + else + { + con.printerr("{}: no arguments\n", first.c_str()); + return CR_FAILURE; + } + + } + + command_result Commands::enable(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + CoreSuspender suspend; + bool enable = (first == "enable"); + auto plug_mgr = core.getPluginManager(); + + if (parts.size()) + { + command_result res{CR_FAILURE}; + + for (auto& part_ : parts) + { + // have to copy to modify as passed argument is const + std::string part(part_); + + if (has_backslashes(part)) + { + con.printerr("Replacing backslashes with forward slashes in \"{}\"\n", part); + replace_backslashes_with_forwardslashes(part); + } + + auto alias = core.GetAliasCommand(part, true); + + Plugin* plug = (*plug_mgr)[alias]; + + if (!plug) + { + std::filesystem::path lua = core.findScript(part + ".lua"); + if (!lua.empty()) + { + res = core.enableLuaScript(con, part, enable); + } + else + { + res = CR_NOT_FOUND; + con.printerr("No such plugin or Lua script: {}\n", part); + } + } + else if (!plug->can_set_enabled()) + { + res = CR_NOT_IMPLEMENTED; + con.printerr("Cannot {} plugin: {}\n", first, part); + } + else + { + res = plug->set_enabled(con, enable); + + if (res != CR_OK || plug->is_enabled() != enable) + con.printerr("Could not {} plugin: {}\n", first, part); + } + } + + return res; + } + else + { + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + if (!plug->can_be_enabled()) continue; + + con.print( + "{:>21} {:<3}{}\n", + (key + ":").c_str(), + plug->is_enabled() ? "on" : "off", + plug->can_set_enabled() ? "" : " (controlled internally)" + ); + } + + Lua::CallLuaModuleFunction(con, "script-manager", "list"); + + return CR_OK; + } + } + + command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + constexpr auto header_format = "{:30} {:10} {:4} {:8}\n"; + constexpr auto row_format = header_format; + + con.print(header_format, "Name", "State", "Cmds", "Enabled"); + + auto plug_mgr = core.getPluginManager(); + + plug_mgr->refresh(); + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + + if (parts.size() && std::ranges::find(parts, key) == parts.end()) + continue; + + color_value color; + switch (plug->getState()) + { + case Plugin::PS_LOADED: + color = COLOR_RESET; + break; + case Plugin::PS_UNLOADED: + case Plugin::PS_UNLOADING: + color = COLOR_YELLOW; + break; + case Plugin::PS_LOADING: + color = COLOR_LIGHTBLUE; + break; + case Plugin::PS_BROKEN: + color = COLOR_LIGHTRED; + break; + default: + color = COLOR_LIGHTMAGENTA; + break; + } + con.color(color); + con.print(row_format, + plug->getName(), + Plugin::getStateDescription(plug->getState()), + plug->size(), + (plug->can_be_enabled() + ? (plug->is_enabled() ? "enabled" : "disabled") + : "n/a") + ); + con.color(COLOR_RESET); + } + + return CR_OK; + } + + command_result Commands::type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + auto plug_mgr = core.getPluginManager(); + + if (!parts.size()) + { + con.printerr("type: no argument\n"); + return CR_WRONG_USAGE; + } + con << parts[0]; + bool builtin = is_builtin(con, parts[0]); + std::filesystem::path lua_path = core.findScript(parts[0] + ".lua"); + Plugin* plug = plug_mgr->getPluginByCommand(parts[0]); + if (builtin) + { + con << " is a built-in command"; + con << std::endl; + } + else if (core.IsAlias(parts[0])) + { + con << " is an alias: " << core.GetAliasCommand(parts[0]) << std::endl; + } + else if (plug) + { + con << " is a command implemented by the plugin " << plug->getName() << std::endl; + } + else if (!lua_path.empty()) + { + con << " is a Lua script: " << lua_path << std::endl; + } + else + { + con << " is not a recognized command." << std::endl; + plug = plug_mgr->getPluginByName(parts[0]); + if (plug) + con << "Plugin " << parts[0] << " exists and implements " << plug->size() << " commands." << std::endl; + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) + { + std::string keystr = parts[1]; + if (parts[0] == "set") + core.ClearKeyBindings(keystr); + // for (int i = parts.size()-1; i >= 2; i--) + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) + { + if (!core.AddKeyBinding(keystr, part)) + { + con.printerr("Invalid key spec: {}\n", keystr); + return CR_FAILURE; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") + { + // for (size_t i = 1; i < parts.size(); i++) + for (const auto& part : parts | std::views::drop(1)) + { + if (!core.ClearKeyBindings(part)) + { + con.printerr("Invalid key spec: {}\n", part); + return CR_FAILURE; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") + { + std::vector list = core.ListKeyBindings(parts[1]); + if (list.empty()) + con << "No bindings." << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; + } + else + { + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + } + + return CR_OK; + } + + command_result Commands::alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) + { + const std::string& name = parts[1]; + std::vector cmd(parts.begin() + 2, parts.end()); + if (!core.AddAlias(name, cmd, parts[0] == "replace")) + { + con.printerr("Could not add alias {} - already exists\n", name); + return CR_FAILURE; + } + } + else if (parts.size() >= 2 && (parts[0] == "delete" || parts[0] == "clear")) + { + if (!core.RemoveAlias(parts[1])) + { + con.printerr("Could not remove alias {}\n", parts[1]); + return CR_FAILURE; + } + } + else if (parts.size() >= 1 && (parts[0] == "list")) + { + auto aliases = core.ListAliases(); + for (auto p : aliases) + { + con << p.first << ": " << join_strings(" ", p.second) << std::endl; + } + } + else + { + con << "Usage: " << std::endl + << " alias add|replace " << std::endl + << " alias delete|clear " << std::endl + << " alias list" << std::endl; + } + + return CR_OK; + } + + command_result Commands::fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (auto scr = Gui::getViewscreenByType()) + { + if (scr->doing_mods || scr->doing_simple_params || scr->doing_params) + { + con.printerr("Cannot pause now.\n"); + return CR_FAILURE; + } + scr->abort_world_gen_dialogue = true; + } + else + { + World::SetPauseState(true); + } + con.print("The game was forced to pause!\n"); + return CR_OK; + } + + command_result Commands::clear(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (con.can_clear()) + { + con.clear(); + return CR_OK; + } + else + { + con.printerr("No console to clear, or this console does not support clearing.\n"); + return CR_NEEDS_CONSOLE; + } + } + + command_result Commands::kill_lua(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool force = std::ranges::any_of(parts, [] (const std::string& part) { return part == "force"; }); + if (!Lua::Interrupt(force)) + { + con.printerr( + "Failed to register hook. This can happen if you have" + " lua profiling or coverage monitoring enabled. Use" + " 'kill-lua force' to force, but this may disable" + " profiling and coverage monitoring.\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + core.loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); + return CR_OK; + } + else + { + con << "Usage:" << std::endl + << " script " << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::show(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().show()) + { + con.printerr("Could not show console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::hide(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().hide()) + { + con.printerr("Could not hide console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::sc_script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.empty() || parts[0] == "help" || parts[0] == "?") + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + con << "Valid event names (SC_ prefix is optional):" << std::endl; + for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) + { + std::string name = sc_event_name((state_change_event)i); + if (name != "SC_UNKNOWN") + con << " " << name << std::endl; + } + return CR_OK; + } + else if (parts[0] == "list") + { + std::string event_name = parts.size() >= 2 ? parts[1] : ""; + if (event_name.size() && sc_event_id(event_name) == SC_UNKNOWN) + { + con << "Unrecognized event name: " << parts[1] << std::endl; + return CR_WRONG_USAGE; + } + for (const auto& state_script : core.getStateChangeScripts()) + { + if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) + { + con.print("{} ({}): {}{}\n", sc_event_name(state_script.event), + state_script.save_specific ? "save-specific" : "global", + state_script.save_specific ? "/raw/" : "/", + state_script.path); + } + } + return CR_OK; + } + else if (parts[0] == "add") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript script(evt, parts[2], save_specific); + for (const auto& state_script : core.getStateChangeScripts()) + { + if (script == state_script) + { + con << "Script already registered" << std::endl; + return CR_FAILURE; + } + } + core.addStateChangeScript(script); + return CR_OK; + } + else if (parts[0] == "remove") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript tmp(evt, parts[2], save_specific); + if (core.removeStateChangeScript(tmp)) + { + return CR_OK; + } + else + { + con << "Unrecognized script" << std::endl; + return CR_FAILURE; + } + } + else + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::dump_rpc(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + std::ofstream file(parts[0]); + CoreService coreSvc; + coreSvc.dumpMethods(file); + + for (auto& it : *core.getPluginManager()) + { + Plugin* plug = it.second; + if (!plug) + continue; + + std::unique_ptr svc(plug->rpc_connect(con)); + if (!svc) + continue; + + file << "// Plugin: " << plug->getName() << std::endl; + svc->dumpMethods(file); + } + } + else + { + con << "Usage: devel/dump-rpc \"filename\"" << std::endl; + return CR_WRONG_USAGE; + } + return CR_OK; + } +} From c6914c227cdd94f880415d2d70c704206dfdfabf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 25 Nov 2025 15:32:24 -0600 Subject: [PATCH 0673/1082] Include information about `FetchContent` in offline build section --- docs/dev/compile/Compile.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/dev/compile/Compile.rst b/docs/dev/compile/Compile.rst index 3eea95d880..2ad2c52efb 100644 --- a/docs/dev/compile/Compile.rst +++ b/docs/dev/compile/Compile.rst @@ -442,3 +442,14 @@ a command starting with ``cmake .. -G Ninja`` on Linux and macOS, following the instructions in the sections above. CMake should automatically locate files that you placed in ``CMake/downloads``, and use them instead of attempting to download them. + +In addition, some packages used by DFHack are managed using CMake's ``FetchContent`` +feature, which requires an online connection during builds. The simplest way to address +this is to have a connection during the first build (during which CMake will download the +dependencies), and then to use CMake's ``FETCHCONTENT_FULLY_DISCONNECTED`` or +``FETCHCONTENT_UPDATES_DISCONNECTED`` defines to control how CMake manages cached +dependencies. If you need even the first-time build be an offline build, you will need +to provide a CMake dependency provider. We do not provide one, but CMake's own documentation +includes a simple provider. For more information about CMake's ``FetchContent`` feature +and how to use it in offline builds, see the +`CMake documentation `_. From 0f3726c74f2f577274254b5ae6fb4a6e4d21a2fe Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 26 Nov 2025 07:20:43 +0000 Subject: [PATCH 0674/1082] 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 24322fe4a3..76b912392d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 24322fe4a30209399c1c5fe903726828edd9032b +Subproject commit 76b912392d461138726bb0c0d82fcb47a5638cd3 From fc1b58899e0592e873d97a71982b76254c3584fd Mon Sep 17 00:00:00 2001 From: Optical Disc <88067218+opticaldisc@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:36:50 -0800 Subject: [PATCH 0675/1082] Add more detail about --@ lines in the Lua API doc. Includes a block of examples showing what will work and what will not. --- docs/dev/Lua API.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index a4a039f409..cc7a90bb3f 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -7468,6 +7468,15 @@ Importing scripts --@ module = true + In order to be recognized, this line **must** begin with ``--@`` with no + whitespace characters before it:: + + --@ module = true OK + --@module = true OK + -- @module = true NOT OK (no --@ found due to space after --) + --@module = true NOT OK (leading space, --@ is not at the beginning of the line) + ---@module = true NOT OK (leading dash, --@ is not at the beginning of the line) + 2. Include a check for ``dfhack_flags.module``, and avoid running any code that has side-effects if this flag is true. For instance:: From 60cd9b713f2f7980c8ce7133003019b11b6e7248 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 14:47:39 -0500 Subject: [PATCH 0676/1082] Add new plugin edgescroll to automate panning of game/region maps --- docs/changelog.txt | 1 + docs/plugins/edgescroll.rst | 13 +++ plugins/CMakeLists.txt | 1 + plugins/edgescroll.cpp | 197 ++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 docs/plugins/edgescroll.rst create mode 100644 plugins/edgescroll.cpp diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a..45cbc95137 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ Template for new versions: # Future ## New Tools +- ``edgescroll``: Introduced plugin to pan the view automatically when the mouse reaches the screen border. ## New Features diff --git a/docs/plugins/edgescroll.rst b/docs/plugins/edgescroll.rst new file mode 100644 index 0000000000..4522ff36bf --- /dev/null +++ b/docs/plugins/edgescroll.rst @@ -0,0 +1,13 @@ +edgescroll +========== + +.. dfhack-tool:: + :summary: Scroll the game world and region maps when the mouse reaches the window border. + :tags: interface + +Usage +----- + +:: + + enable edgescroll diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index a49fdf7a9f..0ac4fe9b57 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -75,6 +75,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(dwarfvet dwarfvet.cpp LINK_LIBRARIES lua) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) + dfhack_plugin(edgescroll edgescroll.cpp) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp new file mode 100644 index 0000000000..7c25287c82 --- /dev/null +++ b/plugins/edgescroll.cpp @@ -0,0 +1,197 @@ +#include "ColorText.h" +#include "PluginManager.h" +#include "MemAccess.h" + +#include "df/world_generatorst.h" +#include "modules/Gui.h" + +#include "df/enabler.h" +#include "df/gamest.h" +#include "df/graphic.h" +#include "df/graphic_viewportst.h" +#include "df/renderer_2d.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/viewscreen_worldst.h" +#include "df/viewscreen_new_regionst.h" +#include "df/world.h" +#include "df/world_data.h" + +#include +#include + +using namespace DFHack; + +DFHACK_PLUGIN("edgescroll"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(enabler); +REQUIRE_GLOBAL(game); +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(gps); + +// Cooldown between edge scroll actions +constexpr uint32_t cooldown_ms = 100; +// Number of pixels from border to trigger edgescroll +constexpr int border_range = 5; + +// Controls how much edge scroll moves +constexpr int map_scroll_pixels = 100; +constexpr int world_scroll_tiles = 3; +constexpr int world_scroll_tiles_zoomed = 6; + +DFhackCExport command_result plugin_init([[maybe_unused]]color_ostream &out, [[maybe_unused]] std::vector &commands) { + return CR_OK; +} + +DFhackCExport command_result plugin_enable([[maybe_unused]]color_ostream &out, bool enable) { + is_enabled = enable; + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out) { + return CR_OK; +} + +template +static void apply_scroll(T* out, T diff, T min, T max) { + *out = std::min(std::max(*out + diff, min), max); +} + +static void scroll_dwarfmode(int xdiff, int ydiff) { + using df::global::window_x; + using df::global::window_y; + using df::global::game; + // Scale the movement by pixels, to keep scroll speeds visually consistent + int tilesize = gps->viewport_zoom_factor / 4; + int width = gps->main_viewport->dim_x; + int height = gps->main_viewport->dim_y; + + // Ensure the map doesn't go fully off-screen + int min_x = -width / 2; + int min_y = -height / 2; + int max_x = world->map.x_count - (width / 2); + int max_y = world->map.y_count - (height / 2); + apply_scroll(window_x, xdiff * std::max(1, map_scroll_pixels / tilesize), min_x, max_x); + apply_scroll(window_y, ydiff * std::max(1, map_scroll_pixels / tilesize), min_y, max_y); + + // Force a minimap update + game->minimap.update = 1; + game->minimap.mustmake = 1; +} + +template +static void scroll_world(T* screen, int xdiff, int ydiff) { + if constexpr(std::is_same_v) { + if (screen->zoomed_in) { + int max_x = (world->world_data->world_width * 16)-1; + int max_y = (world->world_data->world_height * 16)-1; + apply_scroll(&screen->zoom_cent_x, xdiff * world_scroll_tiles_zoomed, 0, max_x); + apply_scroll(&screen->zoom_cent_y, ydiff * world_scroll_tiles_zoomed, 0, max_y); + return; + } + } + + int32_t *x, *y; + if constexpr(std::is_same_v) { + x = &world->worldgen_status.cursor_x; + y = &world->worldgen_status.cursor_y; + } else { + x = &screen->region_cent_x; + y = &screen->region_cent_y; + } + int max_x = world->world_data->world_width-1; + int max_y = world->world_data->world_height-1; + apply_scroll(x, xdiff * world_scroll_tiles, 0, max_x); + apply_scroll(y, ydiff * world_scroll_tiles, 0, max_y); +} + +template +struct overloads : Ts... { using Ts::operator()...; }; + +using world_map = std::variant; +static std::optional get_map() { + df::viewscreen* screen = Gui::getCurViewscreen(true); + screen = Gui::getDFViewscreen(true, screen); // Get the first non-dfhack viewscreen + if (auto start_site = virtual_cast(screen)) + return start_site; + if (auto world_map = virtual_cast(screen)) + return world_map; + if (auto worldgen_map = virtual_cast(screen)) { + if (!world || world->worldgen_status.state <= df::world_generatorst::Initializing) + return {}; // Map isn't displayed yet + return worldgen_map; + } + return {}; +} + +static void scroll_world(world_map screen, int xdiff, int ydiff) { + const auto visitor = overloads { + [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world(s, xdiff, ydiff);}, + }; + std::visit(visitor, screen); +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + + // Ensure either a map viewscreen or the main viewport are visible + auto worldmap = get_map(); + if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) + return CR_OK; + + // FIXME: Once dfhooks_sdl_loop is hooked up in Core, use SDL_GetMouseState + // to determine the correct mouse position without forcing the screen to + // render slightly un-centered. + // origin_x/y are already zero if not fitting the interface to the grid + + // Force the origin_x/y values to zero to workaround df marking any + // mouse position within the margin register as invalid + auto renderer = virtual_cast(enabler->renderer); + if (renderer && (renderer->origin_x != 0 || renderer->origin_y != 0)) { + renderer->origin_x = 0; + renderer->origin_y = 0; + } + + auto dim_x = gps->screen_pixel_x; + auto dim_y = gps->screen_pixel_y; + auto x = gps->precise_mouse_x; + auto y = gps->precise_mouse_y; + if (x == -1 || y == -1) + return CR_OK; // Invalid mouse position + + // Apply a cooldown to any potential edgescrolls + auto& core = Core::getInstance(); + static uint32_t last_action = 0; + uint32_t now = core.p->getTickCount(); + if (now < last_action + cooldown_ms) + return CR_OK; + + + int xdiff = 0; + int ydiff = 0; + if (x <= border_range) { + xdiff--; + } else if (x >= dim_x - border_range) { + xdiff++; + } + if (y <= border_range) { + ydiff--; + } else if (y >= dim_y - border_range) { + ydiff++; + } + + if (xdiff == 0 && ydiff == 0) + return CR_OK; // No work to do + + // Dispatch scrolling to active scrollables + if (worldmap.has_value()) + scroll_world(worldmap.value(), xdiff, ydiff); + else if (gps->main_viewport->flag.bits.active) + scroll_dwarfmode(xdiff, ydiff); + + // Update cooldown + last_action = now; + + return CR_OK; +} From 04da99d53970be81bdcc6ec434eb8facd6be67fb Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 21:27:23 -0500 Subject: [PATCH 0677/1082] Setup dfhooks_sdl_loop and use it to read exact mouse position for edgescroll --- library/Core.cpp | 4 ++ library/Hooks.cpp | 1 + library/include/Core.h | 2 + library/include/modules/DFSDL.h | 10 +++ library/modules/DFSDL.cpp | 36 +++++++++++ plugins/edgescroll.cpp | 108 ++++++++++++++++++++------------ 6 files changed, 120 insertions(+), 41 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index bb631414c6..3be676644e 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2508,6 +2508,10 @@ bool Core::DFH_SDL_Event(SDL_Event* ev) { return ret; } +void Core::DFH_SDL_Loop() { + DFHack::runRenderThreadCallbacks(); +} + bool Core::doSdlInputEvent(SDL_Event* ev) { // this should only ever be called from the render thread diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 951472eba6..0f957fea06 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -68,6 +68,7 @@ DFhackCExport void dfhooks_sdl_loop() { if (disabled) return; // TODO: wire this up to the new SDL-based console once it is merged + DFHack::Core::getInstance().DFH_SDL_Loop(); } // called from the main thread for each utf-8 char read from the ncurses input diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef1..9791464c28 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -151,6 +151,7 @@ namespace DFHack friend void ::dfhooks_update(); friend void ::dfhooks_prerender(); friend bool ::dfhooks_sdl_event(SDL_Event* event); + friend void ::dfhooks_sdl_loop(); friend bool ::dfhooks_ncurses_key(int key); public: /// Get the single Core instance or make one. @@ -238,6 +239,7 @@ namespace DFHack int Update (void); int Shutdown (void); bool DFH_SDL_Event(SDL_Event* event); + void DFH_SDL_Loop(); bool ncurses_wgetch(int in, int & out); bool DFH_ncurses_key(int key); diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index ff8e81ab2c..3d371d3ea5 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -3,10 +3,13 @@ #include "Export.h" #include "ColorText.h" +#include +#include #include struct SDL_Surface; struct SDL_Rect; +struct SDL_Renderer; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; @@ -55,6 +58,10 @@ namespace DFHack::DFSDL DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char* title, const char* message, SDL_Window* window); + DFHACK_EXPORT uint32_t DFSDL_GetMouseState(int* x, int* y); + DFHACK_EXPORT void DFSDL_RenderWindowToLogical(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); + DFHACK_EXPORT void DFSDL_RenderLogicalToWindow(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); + // submitted and returned text is UTF-8 // see wrapper functions below for cp-437 variants DFHACK_EXPORT char* DFSDL_GetClipboardText(); @@ -76,4 +83,7 @@ namespace DFHack DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); + // Queue a cb to be run on the render thread, with optional userdata + DFHACK_EXPORT void runOnRenderThread(std::function cb, void* userdata); + DFHACK_EXPORT void runRenderThreadCallbacks(); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 9da2bd8057..666844f6cb 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -8,6 +8,8 @@ #include +#include + #ifdef WIN32 # include #endif @@ -61,6 +63,9 @@ SDL_Surface* (*g_SDL_CreateRGBSurfaceWithFormat)(uint32_t flags, int width, int int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char *message, SDL_Window *window) = nullptr; char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; char* (*g_SDL_GetBasePath)() = nullptr; +uint32_t (*g_SDL_GetMouseState)(int* x, int* y) = nullptr; +void (*g_SDL_RenderWindowToLogical)(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); +void (*g_SDL_RenderLogicalToWindow)(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -106,6 +111,9 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_ShowSimpleMessageBox); bind(g_sdl_handle, SDL_GetPrefPath); bind(g_sdl_handle, SDL_GetBasePath); + bind(g_sdl_handle, SDL_GetMouseState); + bind(g_sdl_handle, SDL_RenderWindowToLogical); + bind(g_sdl_handle, SDL_RenderLogicalToWindow); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -190,6 +198,18 @@ char* DFSDL::DFSDL_GetBasePath() return g_SDL_GetBasePath(); } +uint32_t DFSDL::DFSDL_GetMouseState(int* x, int* y) { + return g_SDL_GetMouseState(x, y); +} + +void DFSDL::DFSDL_RenderWindowToLogical(SDL_Renderer *renderer, int windowX, int windowY, float *logicalX, float *logicalY) { + g_SDL_RenderWindowToLogical(renderer, windowX, windowY, logicalX, logicalY); +} + +void DFSDL::DFSDL_RenderLogicalToWindow(SDL_Renderer *renderer, float logicalX, float logicalY, int *windowX, int *windowY) { + g_SDL_RenderLogicalToWindow(renderer, logicalX, logicalY, windowX, windowY); +} + int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window) { if (!g_SDL_ShowSimpleMessageBox) return -1; @@ -266,3 +286,19 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { } return 0 == DFHack::DFSDL::DFSDL_SetClipboardText(str.str().c_str()); } + +static std::recursive_mutex render_cb_lock; +static std::vector, void*>> render_cb_queue; + +DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { + std::lock_guard l(render_cb_lock); + render_cb_queue.push_back({cb, userdata}); +} + +DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { + std::lock_guard l(render_cb_lock); + for (auto& cb : render_cb_queue) { + std::get<0>(cb)(std::get<1>(cb)); + } + render_cb_queue.clear(); +} diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 7c25287c82..2a26797281 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -4,6 +4,7 @@ #include "df/world_generatorst.h" #include "modules/Gui.h" +#include "modules/DFSDL.h" #include "df/enabler.h" #include "df/gamest.h" @@ -52,6 +53,62 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out return CR_OK; } +static std::atomic_bool request_queued = false; + +struct scroll_state { + int8_t xdiff; + int8_t ydiff; +}; + +static const scroll_state state_default(0, 0); + +static scroll_state state = state_default; +static scroll_state queued = state_default; + +static void render_thread_cb([[maybe_unused]] void* _) { + queued = state_default; + // Ignore the mouse if outside the window + if (!enabler->mouse_focus) { + request_queued.store(false); + return; + } + + // Determine window border location in window coordinates + auto* renderer = virtual_cast(enabler->renderer); + int origin_x, origin_y = 0; + int end_x, end_y; + DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->origin_x, renderer->origin_y, &origin_x, &origin_y); + DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->cur_w - renderer->origin_x, renderer->cur_h - renderer->origin_y, &end_x, &end_y); + + int mx, my; + DFSDL::DFSDL_GetMouseState(&mx, &my); + + if (mx <= origin_x + border_range) { + queued.xdiff--; + } else if (mx >= end_x - border_range) { + queued.xdiff++; + } + if (my <= origin_y + border_range) { + queued.ydiff--; + } else if (my >= end_y - border_range) { + queued.ydiff++; + } + + request_queued.store(false); +} + +static bool update_mouse_pos() { + if (request_queued.load()) + return false; // No new inputs, and a request for more is already placed + + state = queued; + queued = state_default; + DFHack::runOnRenderThread(render_thread_cb, nullptr); + request_queued.store(true); + return true; +} + +// Scrolling behavior template static void apply_scroll(T* out, T diff, T min, T max) { *out = std::min(std::max(*out + diff, min), max); @@ -134,32 +191,6 @@ static void scroll_world(world_map screen, int xdiff, int ydiff) { } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - - // Ensure either a map viewscreen or the main viewport are visible - auto worldmap = get_map(); - if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) - return CR_OK; - - // FIXME: Once dfhooks_sdl_loop is hooked up in Core, use SDL_GetMouseState - // to determine the correct mouse position without forcing the screen to - // render slightly un-centered. - // origin_x/y are already zero if not fitting the interface to the grid - - // Force the origin_x/y values to zero to workaround df marking any - // mouse position within the margin register as invalid - auto renderer = virtual_cast(enabler->renderer); - if (renderer && (renderer->origin_x != 0 || renderer->origin_y != 0)) { - renderer->origin_x = 0; - renderer->origin_y = 0; - } - - auto dim_x = gps->screen_pixel_x; - auto dim_y = gps->screen_pixel_y; - auto x = gps->precise_mouse_x; - auto y = gps->precise_mouse_y; - if (x == -1 || y == -1) - return CR_OK; // Invalid mouse position - // Apply a cooldown to any potential edgescrolls auto& core = Core::getInstance(); static uint32_t last_action = 0; @@ -167,28 +198,23 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (now < last_action + cooldown_ms) return CR_OK; + // Update mouse_x/y to values from the render thread + if (!update_mouse_pos()) + return CR_OK; - int xdiff = 0; - int ydiff = 0; - if (x <= border_range) { - xdiff--; - } else if (x >= dim_x - border_range) { - xdiff++; - } - if (y <= border_range) { - ydiff--; - } else if (y >= dim_y - border_range) { - ydiff++; - } + // Ensure either a map viewscreen or the main viewport are visible + auto worldmap = get_map(); + if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) + return CR_OK; - if (xdiff == 0 && ydiff == 0) + if (state.xdiff == 0 && state.ydiff == 0) return CR_OK; // No work to do // Dispatch scrolling to active scrollables if (worldmap.has_value()) - scroll_world(worldmap.value(), xdiff, ydiff); + scroll_world(worldmap.value(), state.xdiff, state.ydiff); else if (gps->main_viewport->flag.bits.active) - scroll_dwarfmode(xdiff, ydiff); + scroll_dwarfmode(state.xdiff, state.ydiff); // Update cooldown last_action = now; From bfd9bc4a46a27ddda326292dfa679722c734f5c3 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 23:52:02 -0500 Subject: [PATCH 0678/1082] Release render_cb_lock earlier --- library/modules/DFSDL.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 666844f6cb..2a67351d14 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -287,18 +287,24 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { return 0 == DFHack::DFSDL::DFSDL_SetClipboardText(str.str().c_str()); } +// Queue to run callbacks on the render thread. +// Semantics loosely based on SDL3's SDL_RunOnMainThread static std::recursive_mutex render_cb_lock; static std::vector, void*>> render_cb_queue; DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { std::lock_guard l(render_cb_lock); - render_cb_queue.push_back({cb, userdata}); + render_cb_queue.push_back({std::move(cb), userdata}); } DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { - std::lock_guard l(render_cb_lock); - for (auto& cb : render_cb_queue) { + static decltype(render_cb_queue) local_queue; + { + std::lock_guard l(render_cb_lock); + std::swap(local_queue, render_cb_queue); + } + for (auto& cb : local_queue) { std::get<0>(cb)(std::get<1>(cb)); } - render_cb_queue.clear(); + local_queue.clear(); } From 8f9688b439b4a5a7cd636aa979763feb699933ab Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 28 Nov 2025 22:54:07 -0500 Subject: [PATCH 0679/1082] Code cleanup and implement review suggestions --- library/include/modules/DFSDL.h | 4 +- library/modules/DFSDL.cpp | 8 ++-- plugins/edgescroll.cpp | 71 +++++++++++++++++++-------------- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 3d371d3ea5..be40eff2c5 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -83,7 +83,7 @@ namespace DFHack DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); - // Queue a cb to be run on the render thread, with optional userdata - DFHACK_EXPORT void runOnRenderThread(std::function cb, void* userdata); + // Queue a cb to be run on the render thread + DFHACK_EXPORT void runOnRenderThread(std::function cb); DFHACK_EXPORT void runRenderThreadCallbacks(); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 2a67351d14..9543f81387 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -290,11 +290,11 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { // Queue to run callbacks on the render thread. // Semantics loosely based on SDL3's SDL_RunOnMainThread static std::recursive_mutex render_cb_lock; -static std::vector, void*>> render_cb_queue; +static std::vector> render_cb_queue; -DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { +DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb) { std::lock_guard l(render_cb_lock); - render_cb_queue.push_back({std::move(cb), userdata}); + render_cb_queue.push_back(std::move(cb)); } DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { @@ -304,7 +304,7 @@ DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { std::swap(local_queue, render_cb_queue); } for (auto& cb : local_queue) { - std::get<0>(cb)(std::get<1>(cb)); + cb(); } local_queue.clear(); } diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 2a26797281..08a50430f8 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -1,8 +1,7 @@ #include "ColorText.h" -#include "PluginManager.h" #include "MemAccess.h" +#include "PluginManager.h" -#include "df/world_generatorst.h" #include "modules/Gui.h" #include "modules/DFSDL.h" @@ -16,6 +15,7 @@ #include "df/viewscreen_new_regionst.h" #include "df/world.h" #include "df/world_data.h" +#include "df/world_generatorst.h" #include #include @@ -53,33 +53,37 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out return CR_OK; } -static std::atomic_bool request_queued = false; +static std::atomic_bool callback_queued = false; struct scroll_state { int8_t xdiff; int8_t ydiff; }; -static const scroll_state state_default(0, 0); +static scroll_state state; +static scroll_state queued; -static scroll_state state = state_default; -static scroll_state queued = state_default; - -static void render_thread_cb([[maybe_unused]] void* _) { - queued = state_default; +static void render_thread_cb() { + queued = {0}; // Ignore the mouse if outside the window if (!enabler->mouse_focus) { - request_queued.store(false); + callback_queued.store(false); return; } - // Determine window border location in window coordinates + // Calculate the render rect in window coordinates auto* renderer = virtual_cast(enabler->renderer); - int origin_x, origin_y = 0; + int origin_x, origin_y; int end_x, end_y; - DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->origin_x, renderer->origin_y, &origin_x, &origin_y); - DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->cur_w - renderer->origin_x, renderer->cur_h - renderer->origin_y, &end_x, &end_y); - + DFSDL::DFSDL_RenderLogicalToWindow( + (SDL_Renderer*)renderer->sdl_renderer, (float)renderer->origin_x, + (float)renderer->origin_y, &origin_x, &origin_y); + DFSDL::DFSDL_RenderLogicalToWindow( + (SDL_Renderer*)renderer->sdl_renderer, + (float)renderer->cur_w - (float)renderer->origin_x, + (float)(renderer->cur_h - renderer->origin_y), &end_x, &end_y); + + // Get the mouse location in window coordinates int mx, my; DFSDL::DFSDL_GetMouseState(&mx, &my); @@ -94,26 +98,28 @@ static void render_thread_cb([[maybe_unused]] void* _) { queued.ydiff++; } - request_queued.store(false); + callback_queued.store(false); } static bool update_mouse_pos() { - if (request_queued.load()) - return false; // No new inputs, and a request for more is already placed + if (callback_queued.load()) + return false; // Queued callback not complete, check back later + // Queued callback complete, save the results and enqueue again state = queued; - queued = state_default; - DFHack::runOnRenderThread(render_thread_cb, nullptr); - request_queued.store(true); + queued = {0}; + DFHack::runOnRenderThread(render_thread_cb); + callback_queued.store(true); return true; } -// Scrolling behavior +// Apply scroll whilst maintaining boundaries template static void apply_scroll(T* out, T diff, T min, T max) { *out = std::min(std::max(*out + diff, min), max); } +// Scroll main fortress/adventure world views static void scroll_dwarfmode(int xdiff, int ydiff) { using df::global::window_x; using df::global::window_y; @@ -137,7 +143,7 @@ static void scroll_dwarfmode(int xdiff, int ydiff) { } template -static void scroll_world(T* screen, int xdiff, int ydiff) { +static void scroll_world_internal(T* screen, int xdiff, int ydiff) { if constexpr(std::is_same_v) { if (screen->zoomed_in) { int max_x = (world->world_data->world_width * 16)-1; @@ -169,6 +175,9 @@ using world_map = std::variant get_map() { df::viewscreen* screen = Gui::getCurViewscreen(true); screen = Gui::getDFViewscreen(true, screen); // Get the first non-dfhack viewscreen + if(!screen) + return {}; + if (auto start_site = virtual_cast(screen)) return start_site; if (auto world_map = virtual_cast(screen)) @@ -183,9 +192,9 @@ static std::optional get_map() { static void scroll_world(world_map screen, int xdiff, int ydiff) { const auto visitor = overloads { - [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world(s, xdiff, ydiff);}, - [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world(s, xdiff, ydiff);}, - [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world_internal(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world_internal(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world_internal(s, xdiff, ydiff);}, }; std::visit(visitor, screen); } @@ -198,18 +207,18 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (now < last_action + cooldown_ms) return CR_OK; - // Update mouse_x/y to values from the render thread + // Update mouse_x/y from values read in render thread callback if (!update_mouse_pos()) - return CR_OK; + return CR_OK; // No new input to process + + if (state.xdiff == 0 && state.ydiff == 0) + return CR_OK; // No work to do // Ensure either a map viewscreen or the main viewport are visible auto worldmap = get_map(); if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) return CR_OK; - if (state.xdiff == 0 && state.ydiff == 0) - return CR_OK; // No work to do - // Dispatch scrolling to active scrollables if (worldmap.has_value()) scroll_world(worldmap.value(), state.xdiff, state.ydiff); From 7e6f7f9c801a5e2b84e6ba28a3fb2e0a0a48d30c Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 29 Nov 2025 10:15:49 -0800 Subject: [PATCH 0680/1082] library/lua/dfhack.lua interactive interpreter banner: list shortcuts When the `lua` interactive interpreter is started, the help banner now lists the shortcuts (that is, variable names with special handling) already implemented in `library/lua/utils.lua` `df_shortcut_var()`. These keywords allow the interpreter to use the currently-selected object in the DF UI. They are currently undocumented, as far as I can tell. (The `lua` interpreter documentation does contain two hints in the Examples.) --- library/lua/dfhack.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 15910b46f1..24fb10e226 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -855,6 +855,8 @@ function dfhack.interpreter(prompt,hfile,env) " '^ foo' => 'printall_recurse(foo)'\n".. " '@ foo' => 'printall_ipairs(foo)'\n".. "All of these save the first result as '_'.") + print("These keywords refer to the currently-selected object in the game:") + print(" unit item plant building bld job workshop_job wsjob screen scr") print_banner = false end From bcb22b54de666f814b5587a7df4c3e03d025905f Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 29 Nov 2025 17:20:06 -0800 Subject: [PATCH 0681/1082] update changelog (Is there a better way to word this?) --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a..8cacfc9c56 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## API ## Lua +- The ``Lua interactive interpreter`` banner now documents keywords such as ``unit`` and ``item`` which reference the currently-selected object in the DF UI. ## Removed From d6eaea10e438026bdc72a9b1ead7cc3b0bc58621 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 29 Nov 2025 18:41:45 -0500 Subject: [PATCH 0682/1082] Ensure the dfhack signal handler doesn't appear in crashlogs --- library/Crashlog.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index c560666f99..681e4fd4c5 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,4 +1,5 @@ #include "DFHackVersion.h" + #include #include #include @@ -42,7 +43,8 @@ void signal_crashlog_complete() { std::thread crashlog_thread; -extern "C" void dfhack_crashlog_handle_signal(int sig) { +// Force this method to be inlined so that it doesn't create a stack frame +[[gnu::always_inline]] inline void handle_signal_internal(int sig) { if (shutdown.load() || crashed.exchange(true) || crashlog_ready.load()) { // Ensure the signal handler doesn't try to write a crashlog // whilst the crashlog thread is unavailable. @@ -61,8 +63,12 @@ extern "C" void dfhack_crashlog_handle_signal(int sig) { std::quick_exit(1); } +extern "C" void dfhack_crashlog_handle_signal(int sig) { + handle_signal_internal(sig); +} + void dfhack_crashlog_handle_terminate() { - dfhack_crashlog_handle_signal(0); + handle_signal_internal(0); } std::string signal_name(int sig) { @@ -122,8 +128,9 @@ void dfhack_save_crashlog() { crashlog << "Signal " << signal << "\n"; } - for (int i = 0; i < crash_info.backtrace_entries; i++) { - crashlog << i << "> " << backtrace_strings[i] << "\n"; + // Skip the first backtrace entry as it will always be dfhack_crashlog_handle_(signal|terminate) + for (int i = 1; i < crash_info.backtrace_entries; i++) { + crashlog << i - 1 << "> " << backtrace_strings[i] << "\n"; } } catch (...) {} From 0bce84e60caff072285f268122d2fe1a6e68b2b7 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 30 Nov 2025 09:47:50 -0800 Subject: [PATCH 0683/1082] library/lua/dfhack.lua interactive interpreter banner: highlight keywords When the interactive interpreter is started, highlight the various commands and keywords in yellow. This includes banner and the 'Type quit to exit interactive lua interpreter.' message. --- library/lua/dfhack.lua | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 24fb10e226..0dadc4fa9c 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -845,18 +845,26 @@ function dfhack.interpreter(prompt,hfile,env) return nil, 'not interactive' end - print("Type quit to exit interactive lua interpreter.") + local function print_keyword(pre, keyword, post) + dfhack.color(COLOR_RESET) + if pre ~= nil then dfhack.print(pre) end + dfhack.color(COLOR_YELLOW) + dfhack.print(keyword) + dfhack.color(COLOR_RESET) + if post ~= nil then print(post) end + end + print_keyword("Type ", "quit", " to exit interactive lua interpreter.") if print_banner then - print("Shortcuts:\n".. - " '= foo' => '_1,_2,... = foo'\n".. - " '! foo' => 'print(foo)'\n".. - " '~ foo' => 'printall(foo)'\n".. - " '^ foo' => 'printall_recurse(foo)'\n".. - " '@ foo' => 'printall_ipairs(foo)'\n".. - "All of these save the first result as '_'.") + print("Shortcuts:") + print_keyword(" '", "= foo", "' => '_1,_2,... = foo'") + print_keyword(" '", "! foo", "' => 'print(foo)'") + print_keyword(" '", "~ foo", "' => 'printall(foo)'") + print_keyword(" '", "^ foo", "' => 'printall_recurse(foo)'") + print_keyword(" '", "@ foo", "' => 'printall_ipairs(foo)'") + print_keyword("All of these save the first result as '", "_", "'.") print("These keywords refer to the currently-selected object in the game:") - print(" unit item plant building bld job workshop_job wsjob screen scr") + print_keyword(" ", "unit item plant building bld job workshop_job wsjob screen scr", "") print_banner = false end From fe7b67fe02134ae24a23e10f027d048541011b4f Mon Sep 17 00:00:00 2001 From: Quietust Date: Sun, 30 Nov 2025 14:19:32 -0600 Subject: [PATCH 0684/1082] createitem - add a few more building targets (table, nest, bookcase, display furniture, and offering place) --- docs/changelog.txt | 1 + plugins/createitem.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a..53e2ace9ac 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `createitem`: created items can now be placed onto/into tables, nests, bookcases, display cases, and altars ## Documentation diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index fba821b270..7f8ef6e2ce 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -280,6 +280,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { } switch (building->getType()) { using namespace df::enums::building_type; + case Table: case Coffin: case Furnace: case TradeDepot: @@ -294,8 +295,12 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { case AnimalTrap: case Cage: case Wagon: + case Nest: case NestBox: case Hive: + case Bookcase: + case DisplayFurniture: + case OfferingPlace: break; default: out.printerr("The selected building cannot be used for item storage!\n"); From 124aa4590b348ca2e9d1429a9e2bb790e23c2b2e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:13:07 +0000 Subject: [PATCH 0685/1082] [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.34.1 → 0.35.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.1...0.35.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 70277354ad..cc38b7e9ab 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.34.1 + rev: 0.35.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 71a1a32972a92fd97e7487cf40f18f07d15a43e6 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 2 Dec 2025 20:55:00 +0100 Subject: [PATCH 0686/1082] 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 0687/1082] 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 0688/1082] 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 8ad478055629bce381623f4c39a39ed141f7f465 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 3 Dec 2025 11:56:10 -0500 Subject: [PATCH 0689/1082] Convert build-windows workflow to run on windows natively This should resolve the majority of spurious failures that occurred when building the windows release under Wine. --- .github/workflows/build-windows.yml | 81 +++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 9257a21081..41eb7a94e5 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -66,12 +66,15 @@ on: jobs: build-win64: name: Build win64 - runs-on: ubuntu-22.04 + runs-on: windows-2022 steps: - - name: Install dependencies + - name: Install build dependencies run: | - sudo apt-get update - sudo apt-get install ccache + choco install sccache + - name: Install doc dependencies + if: inputs.docs + run: | + pip install sphinx - name: Clone DFHack uses: actions/checkout@v4 with: @@ -108,49 +111,80 @@ jobs: ref: main ssh-key: ${{ secrets.DFHACK_3RDPARTY_TOKEN }} path: depends/steam + - name: Prepare output directories + run: | + mkdir output + mkdir pdb - name: Fetch ccache if: inputs.platform-files uses: actions/cache/restore@v4 with: - path: build/win64-cross/ccache + path: C:\\Users\\runneradmin\\AppData\\Local\\Mozilla\\sccache\\cache key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} restore-keys: | win-msvc-${{ inputs.cache-id }} win-msvc + - uses: ilammy/msvc-dev-cmd@v1 + - name: Configure DFHack + run: | + cmake ` + -S . ` + -B build ` + -GNinja ` + -DDFHACK_BUILD_ARCH=64 ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_INSTALL_PREFIX=output ` + -DCMAKE_C_COMPILER_LAUNCHER=sccache ` + -DCMAKE_CXX_COMPILER_LAUNCHER=sccache ` + -DBUILD_PDBS:BOOL=${{ inputs.cache-id == 'release' }} ` + -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id}}' ` + -DBUILD_LIBRARY=${{ inputs.platform-files }} ` + -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files && inputs.plugins }} ` + -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} ` + -DBUILD_DOCS:BOOL=${{ inputs.docs }} ` + -DBUILD_DOCS_NO_HTML:BOOL=${{ !inputs.html }} ` + -DINSTALL_DATA_FILES:BOOL=${{ inputs.common-files }} ` + -DBUILD_DFLAUNCH:BOOL=${{ inputs.launchdf }} ` + -DBUILD_TESTS:BOOL=${{ inputs.tests }} ` + -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} ` + ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || '' }} - name: Cross-compile env: - CMAKE_EXTRA_ARGS: -DBUILD_PDBS:BOOL=${{ inputs.cache-id == 'release' }} -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' -DBUILD_LIBRARY=${{ inputs.platform-files }} -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files && inputs.plugins }} -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} -DBUILD_DOCS:BOOL=${{ inputs.docs }} -DBUILD_DOCS_NO_HTML:BOOL=${{ !inputs.html }} -DINSTALL_DATA_FILES:BOOL=${{ inputs.common-files }} -DINSTALL_SCRIPTS:BOOL=${{ inputs.common-files }} -DBUILD_DFLAUNCH:BOOL=${{ inputs.launchdf }} -DBUILD_TESTS:BOOL=${{ inputs.tests }} -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || '' }} + SCCACHE_CACHE_SIZE: 500M run: | - cd build - bash -x build-win64-from-linux.sh + ninja install -C build - name: Finalize cache run: | cd build - ccache -d win64-cross/ccache --show-stats --verbose - ccache -d win64-cross/ccache --max-size ${{ inputs.cache-id == 'release' && '500M' || '150M' }} - ccache -d win64-cross/ccache --cleanup - ccache -d win64-cross/ccache --max-size ${{ inputs.cache-id == 'release' && '2G' || '500M' }} - ccache -d win64-cross/ccache --zero-stats + sccache --show-stats + sccache --zero-stats - name: Save ccache if: inputs.platform-files && !inputs.cache-readonly uses: actions/cache/save@v4 with: - path: build/win64-cross/ccache + path: C:\\Users\\runneradmin\\AppData\\Local\\Mozilla\\sccache\\cache key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} - name: Format artifact name if: inputs.artifact-name id: artifactname run: | - if test "false" = "${{ inputs.append-date-and-hash }}"; then - echo name=${{ inputs.artifact-name }} >> $GITHUB_OUTPUT - else - echo name=${{ inputs.artifact-name }}-$(date +%Y%m%d)-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT - fi + if ("${{ inputs.append-date-and-hash }}" -eq "false") { + "name=${{ inputs.artifact-name }}" | Out-File -Append $env:GITHUB_OUTPUT + } else { + $date = Get-Date -Format "yyyMMdd" + $hash = git rev-parse --short HEAD + "name=${{ inputs.artifact-name}}-$date-$hash" | Out-File -Append $env:GITHUB_OUTPUT + } + - name: Prep pdbs + if: inputs.artifact-name && inputs.cache-id == 'release' + run: | + Get-ChildItem -Recurse -File -Path "build" -Filter *.pdb | + Copy-Item -Destination "pdb" - name: Prep artifact - if: inputs.artifact-name run: | - cd build/win64-cross/output - tar cjf ../../../${{ steps.artifactname.outputs.name }}.tar.bz2 . + cd output + 7z a -ttar -so -an . | + 7z a -si -tbzip2 ../${{ steps.artifactname.outputs.name }}.tar.bz2 - name: Upload artifact if: inputs.artifact-name uses: actions/upload-artifact@v4 @@ -162,4 +196,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ steps.artifactname.outputs.name }}_pdb - path: build/win64-cross/pdb + path: pdb + From 3071d397265ce65dc1a7ad43bde8978bd8a7b3c6 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 3 Dec 2025 14:28:18 -0500 Subject: [PATCH 0690/1082] Fix line ending --- .github/workflows/build-windows.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 41eb7a94e5..0a46737809 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -197,4 +197,3 @@ jobs: with: name: ${{ steps.artifactname.outputs.name }}_pdb path: pdb - From 911f949bbfc548cfdb0644b65503b3c0daabf00f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 4 Dec 2025 12:04:29 -0600 Subject: [PATCH 0691/1082] correct changelog.txt moved a 53.06 change to correct section --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f7d146dc45..29c6ef5365 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -78,7 +78,6 @@ Template for new versions: ## New Features ## Fixes -- `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements @@ -99,6 +98,7 @@ Template for new versions: - compatibility with 53.05 ## Fixes +- `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements From 5687f5c0f0ee465d31a6a9b82e1920919b911422 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 15:01:47 -0500 Subject: [PATCH 0692/1082] Make a failure to obtain relevant crash backtrace obvious --- library/Crashlog.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 681e4fd4c5..3860e0106e 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -111,10 +111,6 @@ std::filesystem::path get_crashlog_path() { void dfhack_save_crashlog() { char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); - if (!backtrace_strings) { - // Allocation failed, give up - return; - } try { std::filesystem::path crashlog_path = get_crashlog_path(); std::ofstream crashlog(crashlog_path); @@ -128,9 +124,14 @@ void dfhack_save_crashlog() { crashlog << "Signal " << signal << "\n"; } - // Skip the first backtrace entry as it will always be dfhack_crashlog_handle_(signal|terminate) - for (int i = 1; i < crash_info.backtrace_entries; i++) { - crashlog << i - 1 << "> " << backtrace_strings[i] << "\n"; + if (crash_info.backtrace_entries >= 2 && backtrace_strings != nullptr) { + // Skip the first backtrace entry as it will always be dfhack_crashlog_handle_(signal|terminate) + for (int i = 1; i < crash_info.backtrace_entries; i++) { + crashlog << i - 1 << "> " << backtrace_strings[i] << "\n"; + } + } else { + // Make it clear if no relevant backtrace was able to be obtained + crashlog << "Failed to obtain relevant backtrace\n"; } } catch (...) {} From cd825e0f0b672ef997ffe1ae460fd4d969a82015 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 15:27:01 -0500 Subject: [PATCH 0693/1082] Default initialize scroll_state properly --- plugins/edgescroll.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 08a50430f8..cdc0c349d2 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -56,15 +56,15 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out static std::atomic_bool callback_queued = false; struct scroll_state { - int8_t xdiff; - int8_t ydiff; + int8_t xdiff = 0; + int8_t ydiff = 0; }; static scroll_state state; static scroll_state queued; static void render_thread_cb() { - queued = {0}; + queued = {}; // Ignore the mouse if outside the window if (!enabler->mouse_focus) { callback_queued.store(false); @@ -107,7 +107,7 @@ static bool update_mouse_pos() { // Queued callback complete, save the results and enqueue again state = queued; - queued = {0}; + queued = {}; DFHack::runOnRenderThread(render_thread_cb); callback_queued.store(true); return true; From 71d830ac7161a1e3c712b7fe387e35086e9bdfb8 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 16:14:31 -0500 Subject: [PATCH 0694/1082] Determine LOCALAPPDATA path dynamically --- .github/workflows/build-windows.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 0a46737809..0864b716f9 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -115,11 +115,13 @@ jobs: run: | mkdir output mkdir pdb + - name: Get sccache path + run: echo ("SCCACHE_DIR=" + $env:LOCALAPPDATA + "\Mozilla\sccache\cache") >> $env:GITHUB_ENV - name: Fetch ccache if: inputs.platform-files uses: actions/cache/restore@v4 with: - path: C:\\Users\\runneradmin\\AppData\\Local\\Mozilla\\sccache\\cache + path: ${{ env.SCCACHE_DIR }} key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} restore-keys: | win-msvc-${{ inputs.cache-id }} @@ -137,7 +139,7 @@ jobs: -DCMAKE_C_COMPILER_LAUNCHER=sccache ` -DCMAKE_CXX_COMPILER_LAUNCHER=sccache ` -DBUILD_PDBS:BOOL=${{ inputs.cache-id == 'release' }} ` - -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id}}' ` + -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' ` -DBUILD_LIBRARY=${{ inputs.platform-files }} ` -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files && inputs.plugins }} ` -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} ` @@ -148,7 +150,7 @@ jobs: -DBUILD_TESTS:BOOL=${{ inputs.tests }} ` -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} ` ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || '' }} - - name: Cross-compile + - name: Build DFHack env: SCCACHE_CACHE_SIZE: 500M run: | @@ -162,7 +164,7 @@ jobs: if: inputs.platform-files && !inputs.cache-readonly uses: actions/cache/save@v4 with: - path: C:\\Users\\runneradmin\\AppData\\Local\\Mozilla\\sccache\\cache + path: ${{ env.SCCACHE_DIR }} key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} - name: Format artifact name if: inputs.artifact-name From f65e4bdcc49de55979bc576f68158bae50c392ff Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 16:17:45 -0500 Subject: [PATCH 0695/1082] Remove redundant lock causing deadlock when listing keybinds --- library/modules/Hotkey.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index d7ef91ed10..e5fbf77f78 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -260,7 +260,6 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { - std::lock_guard l(lock); std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return {}; From 0e341b76b5b96d7b27616c77bace2df69728e4b7 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 16:33:33 -0500 Subject: [PATCH 0696/1082] Relocate keybinding command handling --- library/Commands.cpp | 79 +++++++++++++++++--------------- library/include/modules/Hotkey.h | 2 - library/modules/Hotkey.cpp | 57 ----------------------- 3 files changed, 43 insertions(+), 95 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 796a1f2701..289219c594 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -9,6 +9,7 @@ #include "RemoteTools.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/World.h" #include "df/viewscreen_new_regionst.h" @@ -282,56 +283,62 @@ namespace DFHack command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; + using Hotkey::KeySpec; + auto hotkey_mgr = core.getHotkeyManager(); + std::string parse_error; + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { + const std::string& keystr = parts[1]; if (parts[0] == "set") - core.ClearKeyBindings(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!core.AddKeyBinding(keystr, part)) - { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - return CR_FAILURE; + hotkey_mgr->removeKeybind(keystr); + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { + auto spec = KeySpec::parse(keystr, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + break; + } + if (!hotkey_mgr->addKeybind(spec.value(), part)) { + con.printerr("Invalid command: '%s'\n", part.c_str()); + break; } } } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!core.ClearKeyBindings(part)) - { - con.printerr("Invalid key spec: %s\n", part.c_str()); - return CR_FAILURE; + else if (parts.size() >= 2 && parts[0] == "clear") { + for (const auto& part : parts | std::views::drop(1)) { + auto spec = KeySpec::parse(part, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + } + if (!hotkey_mgr->removeKeybind(spec.value())) { + con.printerr("No matching keybinds to remove\n"); + break; } } } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = core.ListKeyBindings(parts[1]); + else if (parts.size() == 2 && parts[0] == "list") { + auto spec = KeySpec::parse(parts[1], &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + return CR_FAILURE; + } + std::vector list = hotkey_mgr->listKeybinds(spec.value()); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) con << " " << kb << std::endl; } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl + else { + con << "Usage:\n" + << " keybinding list \n" + << " keybinding clear [@context]...\n" + << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" + << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" + << "Later adds, and earlier items within one command have priority.\n" + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" + << "Context may be used to limit the scope of the binding, by\n" + << "requiring the current context to have a certain prefix.\n" + << "Current UI context is: \n" << join_strings("\n", Gui::getCurFocus(true)) << std::endl; } - return CR_OK; } diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 78978f29f6..25129b2ff5 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -33,7 +33,6 @@ namespace DFHack { }; } class DFHACK_EXPORT HotkeyManager { - friend class Core; public: HotkeyManager(); ~HotkeyManager(); @@ -74,6 +73,5 @@ namespace DFHack { std::map> bindings; void hotkey_thread_fn(); - void handleKeybindingCommand(color_ostream& out, const std::vector& parts); }; } diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index e5fbf77f78..6433879d85 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -387,63 +387,6 @@ std::string HotkeyManager::getKeybindingInput() { return requested_keybind; } -void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { - std::string parse_error; - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { - const std::string& keystr = parts[1]; - if (parts[0] == "set") - removeKeybind(keystr); - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - auto spec = KeySpec::parse(keystr, &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - break; - } - if (!addKeybind(spec.value(), part)) { - con.printerr("Invalid command: '%s'\n", part.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") { - for (const auto& part : parts | std::views::drop(1)) { - auto spec = KeySpec::parse(part, &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - } - if (!removeKeybind(spec.value())) { - con.printerr("No matching keybinds to remove\n"); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") { - auto spec = KeySpec::parse(parts[1], &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - return; - } - std::vector list = listKeybinds(spec.value()); - if (list.empty()) - con << "No bindings.\n"; - for (const auto& kb : list) - con << " " << kb << "\n"; - } - else { - con << "Usage:\n" - << " keybinding list \n" - << " keybinding clear [@context]...\n" - << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" - << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" - << "Later adds, and earlier items within one command have priority.\n" - << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" - << "Context may be used to limit the scope of the binding, by\n" - << "requiring the current context to have a certain prefix.\n" - << "Current UI context is: \n" - << join_strings("\n", Gui::getCurFocus(true)) << "\n"; - } -} - HotkeyManager::HotkeyManager() { this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); } From 522eb7b443b1a6a06975de2705300b9d63453433 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 5 Dec 2025 07:22:40 +0000 Subject: [PATCH 0697/1082] 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 76b912392d..40ac7ee39f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 76b912392d461138726bb0c0d82fcb47a5638cd3 +Subproject commit 40ac7ee39f67c1de8bb63ef307e8218e3d3fd14a diff --git a/plugins/stonesense b/plugins/stonesense index 9213b47546..50190a34de 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9213b475463c39251e93a5d7c93d1b0f38a0f537 +Subproject commit 50190a34de760732a4d8f926f28df9048fd0a5e2 diff --git a/scripts b/scripts index dd60bf218e..163f2c46ee 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit dd60bf218e6f4a9c421327d65094998fb6459995 +Subproject commit 163f2c46eec0d10f722b7ea178b76730ba52b611 From e09948a20f892f5f804b8795be1c282936a314ab Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 6 Dec 2025 07:19:23 +0000 Subject: [PATCH 0698/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 163f2c46ee..db609669d4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 163f2c46eec0d10f722b7ea178b76730ba52b611 +Subproject commit db609669d4076a4d165d913ab98df487899acb20 From e36860793be04b18d5612cbdff1b9d495b34cacf Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 17:49:00 +0100 Subject: [PATCH 0699/1082] 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 0700/1082] 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 0701/1082] 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 0702/1082] 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 0703/1082] 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 0704/1082] 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 0705/1082] 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 0706/1082] 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 0707/1082] 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 0708/1082] 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 0709/1082] 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 0710/1082] 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 76477ae446ef4f2e0f0094f460ec5b5054982fda Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Mon, 8 Dec 2025 23:23:15 -0500 Subject: [PATCH 0711/1082] infinite-sky: Add support for invasion maps --- docs/changelog.txt | 1 + plugins/CMakeLists.txt | 2 +- plugins/infinite-sky.cpp | 56 +++++++++++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e9b68fb94a..baf28404be 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Tools - ``edgescroll``: Introduced plugin to pan the view automatically when the mouse reaches the screen border. +- `infinite-sky`: Re-enabled with compatibility with new siege map data. ## New Features - `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 0ac4fe9b57..355cc0ac4c 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -87,7 +87,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(generated-creature-renamer generated-creature-renamer.cpp) dfhack_plugin(getplants getplants.cpp) dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua) - #dfhack_plugin(infinite-sky infinite-sky.cpp LINK_LIBRARIES lua) + dfhack_plugin(infinite-sky infinite-sky.cpp LINK_LIBRARIES lua) #dfhack_plugin(jobutils jobutils.cpp) dfhack_plugin(lair lair.cpp) dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 158dea2a42..402a4a7a7b 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -10,8 +10,13 @@ #include "df/block_column_print_infost.h" #include "df/construction.h" +#include "df/entity_plot_invasion_mapst.h" +#include "df/historical_entity.h" +#include "df/invasion_info.h" #include "df/map_block.h" #include "df/map_block_column.h" +#include "df/plotinfost.h" +#include "df/plot_invasion_mapst.h" #include "df/world.h" #include "df/z_level_flags.h" @@ -28,6 +33,7 @@ using namespace df::enums; DFHACK_PLUGIN("infinite-sky"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); +REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); namespace DFHack { @@ -130,7 +136,7 @@ static void constructionEventHandler(color_ostream &out, void *ptr) { doInfiniteSky(out, 1); } -void doInfiniteSky(color_ostream& out, int32_t howMany) { +void addBlockColumns(color_ostream& out, int32_t quantity) { int32_t z_count_block = world->map.z_count_block; df::map_block ****block_index = world->map.block_index; @@ -141,14 +147,14 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { last_air_layer.forCoord([&](df::coord bpos) { // Allocate a new block column and copy over data from the old df::map_block **blockColumn = - new df::map_block *[z_count_block + howMany]; + new df::map_block *[z_count_block + quantity]; memcpy(blockColumn, block_index[bpos.x][bpos.y], z_count_block * sizeof(df::map_block *)); delete[] block_index[bpos.x][bpos.y]; block_index[bpos.x][bpos.y] = blockColumn; df::map_block *last_air_block = blockColumn[bpos.z]; - for (int32_t count = 0; count < howMany; count++) { + for (int32_t count = 0; count < quantity; count++) { df::map_block *air_block = new df::map_block(); std::fill(&air_block->tiletype[0][0], &air_block->tiletype[0][0] + (16 * 16), @@ -209,19 +215,55 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { }); // Update global z level flags - df::z_level_flags *flags = new df::z_level_flags[z_count_block + howMany]; + df::z_level_flags *flags = new df::z_level_flags[z_count_block + quantity]; memcpy(flags, world->map_extras.z_level_flags, z_count_block * sizeof(df::z_level_flags)); - for (int32_t count = 0; count < howMany; count++) { + for (int32_t count = 0; count < quantity; count++) { flags[z_count_block + count].whole = 0; flags[z_count_block + count].bits.update = 1; } - world->map.z_count_block += howMany; - world->map.z_count += howMany; + world->map.z_count_block += quantity; + world->map.z_count += quantity; delete[] world->map_extras.z_level_flags; world->map_extras.z_level_flags = flags; } +void updateInvasionMap(color_ostream &out, int32_t new_height, df::plot_invasion_mapst& map) { + if (map.blockz == 0) + return; // Unused invasion map + if (map.blockz >= new_height) + return; // No change required + + cuboid blocks(0, 0, 0, map.blockx - 1, map.blocky - 1, 0); + blocks.forCoord([&](df::coord bpos) { + // Create new vertical block + df::pim_blockst **new_block = new df::pim_blockst *[new_height](); + memcpy(new_block, map.block_index[bpos.x][bpos.y], map.blockz * sizeof(df::pim_blockst*)); + // Fill new block with nullptr (no information) + std::fill_n(&new_block[map.blockz], new_height - map.blockz, nullptr); + delete[] map.block_index[bpos.x][bpos.y]; + map.block_index[bpos.x][bpos.y] = new_block; + return true; + }); + + map.blockz = new_height; +} + +void doInfiniteSky(color_ostream &out, int32_t quantity) { + addBlockColumns(out, quantity); + + for (auto& invasion : plotinfo->invasions.list) { + updateInvasionMap(out, world->map.z_count, invasion->map); + } + for (auto& entity : world->entities.all) { + for (auto& map : entity->plot_invasion_map) { + if (map->site_id != plotinfo->site_id) + continue; + updateInvasionMap(out, world->map.z_count, map->map); + } + } +} + struct infinitesky_options { // whether to display help bool help = false; From d8eb61e1b8eb0401f00c5a64428137d70867432d Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:19:38 +0100 Subject: [PATCH 0712/1082] 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 0713/1082] 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 0714/1082] 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 0715/1082] 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 0716/1082] 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 0717/1082] 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 0718/1082] 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 0719/1082] 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 0720/1082] 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 501d01a09761354f9f173a996dd746cabe8d9bb0 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:47:36 +0000 Subject: [PATCH 0721/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index db609669d4..578f6fe8fa 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit db609669d4076a4d165d913ab98df487899acb20 +Subproject commit 578f6fe8faed4d8e156787c25eadc39cdd0bc358 From 1b42dfadb8973133107f9a0395f09df330b71bed Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 15 Dec 2025 08:01:54 -0600 Subject: [PATCH 0722/1082] 53.06-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 528f6f0d93..ea49529bc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.06") -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 0645b1241d12bdee3265a6357c3f0388f265824e Mon Sep 17 00:00:00 2001 From: Quietust Date: Wed, 17 Dec 2025 09:27:20 -0600 Subject: [PATCH 0723/1082] 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 0724/1082] 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 a98e9ccca44fa8c811d5ac75cf8de69a71c2f5fd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 18 Dec 2025 12:49:54 -0600 Subject: [PATCH 0725/1082] updates for 53.07-r1 --- CMakeLists.txt | 6 +++--- library/xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ea49529bc8..33289b2c98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.06") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DF_VERSION "53.07") +set(DFHACK_RELEASE "r1") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) diff --git a/library/xml b/library/xml index 40ac7ee39f..58afa78efb 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 40ac7ee39f67c1de8bb63ef307e8218e3d3fd14a +Subproject commit 58afa78efbd6d5ca9790b9591db9df045a9020a5 From f8f665c6eca2d8a040f57f3b2d43a3059948e22a Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:14:19 +0000 Subject: [PATCH 0726/1082] Auto-update structures ref for 53.07 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 58afa78efb..30c1a17836 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 58afa78efbd6d5ca9790b9591db9df045a9020a5 +Subproject commit 30c1a1783652a8d24fd6d24e2aa0aaf62ac98e4e From c0d6e5f823ff9b382307ba22dda2d0e480f07a3a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 18 Dec 2025 13:26:19 -0600 Subject: [PATCH 0727/1082] changelog for 53.07-r1 --- docs/changelog.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index baf28404be..51a0d7fb5d 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.07-r1 + ## New Tools - ``edgescroll``: Introduced plugin to pan the view automatically when the mouse reaches the screen border. - `infinite-sky`: Re-enabled with compatibility with new siege map data. @@ -67,8 +85,8 @@ Template for new versions: ## Misc Improvements - `createitem`: created items can now be placed onto/into tables, nests, bookcases, display cases, and altars -- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. - The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). +- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. ## Documentation From b1c7abab5c52b54fca1cf137ed50df56741ae61a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 18 Dec 2025 13:27:43 -0600 Subject: [PATCH 0728/1082] update submodules --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 30c1a17836..0e1e5cc853 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 30c1a1783652a8d24fd6d24e2aa0aaf62ac98e4e +Subproject commit 0e1e5cc8537cfb1a6580125dc5e0a2541e4db777 From 95fb7dd962f82203af6da1ed22e651aa812a8a05 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:42:19 +0000 Subject: [PATCH 0729/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 578f6fe8fa..00d826f0eb 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 578f6fe8faed4d8e156787c25eadc39cdd0bc358 +Subproject commit 00d826f0eb81e837912c841438eba522149d93fc From 8319aa31acbe96204165e645048b1458046d03a6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 19 Dec 2025 10:51:03 -0600 Subject: [PATCH 0730/1082] update CMakeLists for 53.08-r1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 33289b2c98..6aafbe107f 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.07") +set(DF_VERSION "53.08") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From f8d2b95d78c9c27584c8a08d1e4b4b2f95feffee Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:07:14 +0000 Subject: [PATCH 0731/1082] Auto-update structures ref for 53.08 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 0e1e5cc853..e97044d60e 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 0e1e5cc8537cfb1a6580125dc5e0a2541e4db777 +Subproject commit e97044d60e6facf1d5e27be979e061573d78cbc5 From 43100e7fd48e765603e779bd1487dbaff4ab760c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:14:02 +0000 Subject: [PATCH 0732/1082] 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 e97044d60e..da0b52eb3a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e97044d60e6facf1d5e27be979e061573d78cbc5 +Subproject commit da0b52eb3ad79866b1228a880be3b734cfac7b55 From 67c591cfae28f766089de48412237e766ed3cd9f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 19 Dec 2025 11:28:13 -0600 Subject: [PATCH 0733/1082] add changelog for 53.08-r1 --- docs/changelog.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 51a0d7fb5d..2eb584644f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,25 @@ Template for new versions: ## Removed +# 53.08-r1 + +## New Tools + +## New Features +- compatibility with DF 53.08 + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 53.07-r1 ## New Tools From c474f7555a9afff7a23e627e40385b3069011440 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 30 Dec 2025 11:19:02 +0100 Subject: [PATCH 0734/1082] 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 0735/1082] 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 0736/1082] 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 0737/1082] 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 0738/1082] 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 0739/1082] 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 0740/1082] 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 0741/1082] 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 0742/1082] 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 0743/1082] 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 0744/1082] 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 0745/1082] 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 0746/1082] 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 0747/1082] 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 0748/1082] 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 0749/1082] [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 0750/1082] 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 0751/1082] 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 0752/1082] 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 0753/1082] 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 0754/1082] 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 0755/1082] 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 0756/1082] 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 0757/1082] 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 0758/1082] 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 0759/1082] 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 0760/1082] 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 0761/1082] 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 0762/1082] 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 0763/1082] 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 0764/1082] 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 0765/1082] 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 0766/1082] 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 0767/1082] 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 0768/1082] 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 0769/1082] 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 0770/1082] 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 0771/1082] 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 0772/1082] 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 0773/1082] 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 0774/1082] 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 0775/1082] 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 0776/1082] 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 0777/1082] [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 0778/1082] 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 0779/1082] 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 0780/1082] 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 0781/1082] 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 0782/1082] 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 0783/1082] 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 0784/1082] 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 0785/1082] 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 0786/1082] 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 0787/1082] 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 0788/1082] 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 0789/1082] 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 0790/1082] 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 0791/1082] 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 0792/1082] 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 0793/1082] 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 0794/1082] 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 0795/1082] 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 0796/1082] 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 0797/1082] 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 0798/1082] 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 0799/1082] 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 0800/1082] 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 0801/1082] 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 0802/1082] 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 0803/1082] 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 0804/1082] 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 0805/1082] 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 0806/1082] 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 0807/1082] [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 0808/1082] 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 0809/1082] 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 0810/1082] 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 0811/1082] 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 0812/1082] 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 0813/1082] 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 0814/1082] 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 0815/1082] 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 0816/1082] 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 0817/1082] 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 0818/1082] 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 0819/1082] 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 0820/1082] 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 0821/1082] 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 0822/1082] 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 0823/1082] 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 0824/1082] 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 0825/1082] 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 0826/1082] 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 0827/1082] 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 0828/1082] 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 0829/1082] 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 0830/1082] 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 0831/1082] 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 0832/1082] 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 0833/1082] 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 0834/1082] 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 0835/1082] 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 0836/1082] 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 0837/1082] 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 0838/1082] [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 0839/1082] 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 0840/1082] 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 0841/1082] 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 0842/1082] 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 0843/1082] 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 0844/1082] 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 0845/1082] 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 0846/1082] 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 0847/1082] 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 0848/1082] 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 0849/1082] 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 0850/1082] 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 0851/1082] 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 0852/1082] 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 0853/1082] 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 0854/1082] `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 0855/1082] 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 0856/1082] 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 0857/1082] 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 0858/1082] 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 0859/1082] 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 0860/1082] 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 0861/1082] 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 0862/1082] 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 0863/1082] 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 0864/1082] 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 0865/1082] 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 0866/1082] 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 0867/1082] 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 0868/1082] 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 0869/1082] 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 0870/1082] 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 0871/1082] 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 0872/1082] 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 0873/1082] 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 0874/1082] 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 0875/1082] 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 0876/1082] 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 0877/1082] 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 0878/1082] 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 0879/1082] [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 0880/1082] 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 0881/1082] 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 0882/1082] 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 0883/1082] 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 0884/1082] 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 0885/1082] 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 0886/1082] 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 0887/1082] 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 0888/1082] 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 0889/1082] 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 0890/1082] 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 0891/1082] 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 0892/1082] 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 0893/1082] 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 0894/1082] 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 0895/1082] 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 0896/1082] 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 0897/1082] 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 0898/1082] `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 0899/1082] 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 0900/1082] 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 0901/1082] 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 0902/1082] 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 0903/1082] 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 0904/1082] 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 0905/1082] 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 0906/1082] 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 0907/1082] 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 0908/1082] 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 0909/1082] 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 0910/1082] 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 0911/1082] 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 0912/1082] 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 0913/1082] 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 0914/1082] 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 0915/1082] 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 0916/1082] 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 0917/1082] 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 0918/1082] 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 0919/1082] 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 0920/1082] 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 0921/1082] 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 0922/1082] [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 0923/1082] 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 0924/1082] 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 0925/1082] 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 0926/1082] 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 0927/1082] 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 0928/1082] 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 0929/1082] 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 0930/1082] 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 0931/1082] 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 0932/1082] 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 0933/1082] 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 0934/1082] 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 0935/1082] 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 0936/1082] 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 0937/1082] 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 0938/1082] 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 0939/1082] 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 0940/1082] 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 0941/1082] 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 0942/1082] `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 0943/1082] 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 0944/1082] 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 0945/1082] 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 0946/1082] 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 0947/1082] 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 0948/1082] 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 0949/1082] 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 0950/1082] 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 0951/1082] 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 0952/1082] 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 0953/1082] 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 0954/1082] 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 0955/1082] 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 0956/1082] 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 0957/1082] 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 0958/1082] 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 0959/1082] 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 0960/1082] 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 0961/1082] 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 0962/1082] 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 0963/1082] 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 0964/1082] 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 0965/1082] 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 0966/1082] 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 0967/1082] 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 0968/1082] `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 0969/1082] `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 0970/1082] `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 0971/1082] `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 0972/1082] `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 0973/1082] `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 0974/1082] `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 0975/1082] 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 0976/1082] 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 0977/1082] 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 0978/1082] 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 0979/1082] 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 0980/1082] `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 0981/1082] 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 0982/1082] 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 0983/1082] `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 0984/1082] `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 0985/1082] 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 0986/1082] 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 0987/1082] 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 0988/1082] 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 0989/1082] 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 0990/1082] 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 0991/1082] 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 0992/1082] 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 0993/1082] 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 0994/1082] 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 0995/1082] 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 0996/1082] 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 0997/1082] 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 0998/1082] 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 0999/1082] 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 1000/1082] 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 1001/1082] 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 1002/1082] 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 1003/1082] 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 1004/1082] 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 1005/1082] 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 1006/1082] 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 1007/1082] 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 1008/1082] [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 1009/1082] 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 1010/1082] `>=` 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 1011/1082] 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 1012/1082] 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 1013/1082] 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 1014/1082] 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 1015/1082] `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 1016/1082] 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 1017/1082] 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 1018/1082] 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 1019/1082] 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 1020/1082] 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 1021/1082] 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 1022/1082] 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 1023/1082] 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 1024/1082] 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 1025/1082] 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 1026/1082] 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 1027/1082] 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 1028/1082] 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 1029/1082] 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 1030/1082] 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 1031/1082] 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 9075ba493f66b2c5ec11784e385ac95fa300c909 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:34:10 +0200 Subject: [PATCH 1032/1082] add elevation export --- plugins/export-map.cpp | 89 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 4c72af1b90..e2d4768e51 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -57,8 +58,8 @@ struct std::hash // were only dealing with 2D coordinates in this file using coord = df::coord2d; -static int wdim = 768; // dimension of a world tile -static int rdim = 48; // dimension of a region tile +constexpr int wdim = 768; // dimension of a world tile +constexpr int rdim = 48; // dimension of a region tile /** * Takes a vector of coordinates interpreted as global region tile coordinates @@ -117,6 +118,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { @@ -137,6 +139,9 @@ static command_result do_command(color_ostream &out, vector ¶meters) else if (parameters.size() && parameters[0] == "regions") { return export_region_tiles(out); } + else if (parameters.size() && parameters[0] == "elevation") { + return export_elevation(out); + } else { auto ok_region = export_region_tiles(out); @@ -409,12 +414,12 @@ static command_result export_region_tiles(color_ostream &out) { auto [left, right] = ahead(component, current_position, current_direction); if (left && right) { - // in front of a wall: turn right + // in front of a wall: turn left path.push_back(current_position); current_direction = turn_left(current_direction); } else if (!left && !right) { - // no walls ahead: turn left + // no walls ahead: turn right path.push_back(current_position); current_direction = turn_right(current_direction); } @@ -635,5 +640,81 @@ static command_result export_rivers(color_ostream &out) } + return CR_OK; +} + +/********************************************************************** */ + +static std::string vrtTemplate = R"( + + EPSG:3857 + 0,48,0,0,0,-48 + + elevation.dat + 0 + 2 + {LINE_OFFSET} + LSB + + +)"; + + +template +struct matrix { + std::size_t ncols; + std::size_t nrows; + std::vector _data; + + matrix(std::size_t cols, std::size_t rows) : ncols(cols), nrows(rows), _data(cols * rows) {}; + + T& operator()(std::size_t col, std::size_t row) { + return _data[row * ncols + col]; + }; + + T* data() { return _data.data(); }; + std::size_t size() { return _data.size(); }; +}; + + +static command_result export_elevation(color_ostream &out) +{ + // ensure that we have an output file + std::string data_filename("elevation.dat"); + std::ofstream data_file(data_filename, std::ios::out | std::ios::trunc | std::ios::binary); + + std::string vrt_filename("elevation.vrt"); + std::ofstream vrt_file(vrt_filename, std::ios::out | std::ios::trunc); + if (!data_file || !vrt_file) { + return CR_FAILURE; + } + + + auto world_width = world->world_data->world_width * 16; + auto world_height = world->world_data->world_height * 16; + + matrix height_map(world_width, world_height); + + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto world_x = region_details->pos.x; + auto world_y = region_details->pos.y; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) + { + auto elevation = region_details->elevation[region_x][region_y]; + height_map(16 * world_x + region_x, 16 * world_y + region_y) = elevation; + } + } + } + + data_file.write( + reinterpret_cast(height_map.data()), height_map.size() * sizeof(int16_t)); + + auto vrt = std::regex_replace(vrtTemplate, std::regex("\\{WIDTH\\}"), std::to_string(world_width)); + vrt = std::regex_replace(vrt, std::regex("\\{HEIGHT\\}"), std::to_string(world_height)); + vrt = std::regex_replace(vrt, std::regex("\\{LINE_OFFSET\\}"), std::to_string(world_width * sizeof(int16_t))); + + vrt_file << vrt; + return CR_OK; } From deb3c57cccf56b8c649d008bff2132b14484f84b Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Thu, 16 Jul 2026 23:18:18 -0500 Subject: [PATCH 1033/1082] 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 1034/1082] 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 1035/1082] 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 1036/1082] 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 1037/1082] 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 1038/1082] Remove MaterialInfo consts in favor of using df::builtin_mats enum entries Also adjust some comparisons to use more easily understandable values (e.g. <= CREATURE_200 instead of < HIST_FIG_1) --- docs/changelog.txt | 3 ++- library/LuaApi.cpp | 3 ++- library/RemoteTools.cpp | 6 ++--- library/include/modules/Materials.h | 7 ----- library/lua/gui/materials.lua | 9 ++++--- library/lua/tile-material.lua | 4 +-- library/modules/Items.cpp | 2 +- library/modules/Kitchen.cpp | 2 +- library/modules/MapCache.cpp | 4 +-- library/modules/Materials.cpp | 22 ++++++++-------- plugins/autochop.cpp | 2 +- plugins/buildingplan/buildingplan.cpp | 2 +- .../remotefortressreader.cpp | 26 +++++++++---------- 13 files changed, 44 insertions(+), 48 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index af1e77905d..3f1340168f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,12 +68,13 @@ Template for new versions: ## API - ``Screen``: new functions ``paintMapPortTile`` and ``readMapPortTile`` to write and read world and region map tiles. +- ``Materials``: ``MaterialInfo`` constants ``NUM_BUILTIN``, ``GROUP_SIZE``, ``CREATURE_BASE``, ``FIGURE_BASE``, ``PLANT_BASE``, and ``END_BASE`` removed. New plugins should use appropriate members of the ``df::builtin_mats`` enum. ## Lua - Added ``Screen::paintMapPortTile`` as ``dfhack.screen.paintMapPortTile`` - Added ``Screen::readMapPortTile`` as ``dfhack.screen.readMapPortTile`` - +- Deprecated ``gui.materials.CREATURE_BASE`` and ``gui.materials.PLANT_BASE`` - scripts should instead use ``df.builtin_mats.CREATURE_1`` and ``df.builtin_mats.PLANT_1``, respectively. ## Removed diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d321ca7891..6a08756156 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -72,6 +72,7 @@ distribution. #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" #include "df/building_workshopst.h" +#include "df/builtin_mats.h" #include "df/burrow.h" #include "df/caravan_state.h" #include "df/construction.h" @@ -491,7 +492,7 @@ static bool decode_matinfo(lua_State *state, MaterialInfo *info, bool numpair = if (auto item = Lua::GetDFObject(state, 1)) return info->decode(item); if (auto plant = Lua::GetDFObject(state, 1)) - return info->decode(MaterialInfo::PLANT_BASE, plant->material); + return info->decode(df::builtin_mats::PLANT_1, plant->material); if (auto mvec = Lua::GetDFObject(state, 1)) return info->decode(*mvec, luaL_checkint(state, 2)); } diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index 5dab76e8cb..30e24c51b5 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -530,7 +530,7 @@ static command_result ListMaterials(color_ostream &stream, if (in->builtin()) { - for (int i = 0; i < MaterialInfo::NUM_BUILTIN; i++) + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) listMaterial(out, i, -1, mask); } @@ -549,7 +549,7 @@ static command_result ListMaterials(color_ostream &stream, auto praw = vec[i]; for (size_t j = 0; j < praw->material.size(); j++) - listMaterial(out, MaterialInfo::CREATURE_BASE+j, i, mask); + listMaterial(out, df::builtin_mats::CREATURE_1+j, i, mask); } } @@ -561,7 +561,7 @@ static command_result ListMaterials(color_ostream &stream, auto praw = vec[i]; for (size_t j = 0; j < praw->material.size(); j++) - listMaterial(out, MaterialInfo::PLANT_BASE+j, i, mask); + listMaterial(out, df::builtin_mats::PLANT_1+j, i, mask); } } diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 1f87f548ff..61763ae295 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -66,13 +66,6 @@ namespace DFHack struct DFHACK_EXPORT MaterialInfo { - static const int NUM_BUILTIN = 19; - static const int GROUP_SIZE = 200; - static const int CREATURE_BASE = NUM_BUILTIN; - static const int FIGURE_BASE = NUM_BUILTIN + GROUP_SIZE; - static const int PLANT_BASE = NUM_BUILTIN + GROUP_SIZE * 2; - static const int END_BASE = NUM_BUILTIN + GROUP_SIZE * 3; - int16_t type; int32_t index; diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua index 7429a78237..99e4e6449b 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -8,8 +8,9 @@ local dlg = require('gui.dialogs') ARROW = string.char(26) -CREATURE_BASE = 19 -PLANT_BASE = 419 +-- For backwards compatibility with older scripts +CREATURE_BASE = df.builtin_mats.CREATURE_1 +PLANT_BASE = df.builtin_mats.PLANT_1 MaterialDialog = defclass(MaterialDialog, gui.FramedScreen) @@ -127,7 +128,7 @@ function MaterialDialog:initCreatureMode() local choices = {} for i,v in ipairs(df.global.world.raws.creatures.all) do - self:addObjectChoice(choices, v, v.name[0], CREATURE_BASE, i) + self:addObjectChoice(choices, v, v.name[0], df.builtin_mats.CREATURE_1, i) end self:pushContext('Creature materials', choices) @@ -137,7 +138,7 @@ function MaterialDialog:initPlantMode() local choices = {} for i,v in ipairs(df.global.world.raws.plants.all) do - self:addObjectChoice(choices, v, v.name, PLANT_BASE, i) + self:addObjectChoice(choices, v, v.name, df.builtin_mats.PLANT_1, i) end self:pushContext('Plant materials', choices) diff --git a/library/lua/tile-material.lua b/library/lua/tile-material.lua index c0fe2e7cb6..5c847c463d 100644 --- a/library/lua/tile-material.lua +++ b/library/lua/tile-material.lua @@ -194,7 +194,7 @@ function GetTreeMat(x, y, z) for _, tree in ipairs(df.global.world.plants.all) do if tree.tree_info ~= nil then if coordInTree(pos, tree) then - return dfhack.matinfo.decode(419, tree.material) + return dfhack.matinfo.decode(df.builtin_mats.PLANT_1, tree.material) end end end @@ -209,7 +209,7 @@ function GetShrubMat(x, y, z) for _, shrub in ipairs(df.global.world.plants.all) do if shrub.tree_info == nil then if shrub.pos.x == pos.x and shrub.pos.y == pos.y and shrub.pos.z == pos.z then - return dfhack.matinfo.decode(419, shrub.material) + return dfhack.matinfo.decode(df.builtin_mats.PLANT_1, shrub.material) end end end diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 6b5b3ee1d2..0e7f1ffdf3 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1756,7 +1756,7 @@ int32_t Items::pickGrowthPrint(int16_t subtype, int16_t mat, int32_t matg) { int growth_print = -1; // Make sure it's made of a valid plant material, then grab its definition - if (mat >= 419 && mat <= 618 && matg >= 0 && (unsigned)matg < world->raws.plants.all.size()) + if (mat >= df::builtin_mats::PLANT_1 && mat <= df::builtin_mats::PLANT_200 && matg >= 0 && (unsigned)matg < world->raws.plants.all.size()) { auto plant_def = world->raws.plants.all[matg]; // Make sure it subtype is also valid diff --git a/library/modules/Kitchen.cpp b/library/modules/Kitchen.cpp index c9d4c7d5c6..fd1bfe002f 100644 --- a/library/modules/Kitchen.cpp +++ b/library/modules/Kitchen.cpp @@ -39,7 +39,7 @@ void Kitchen::debug_print(color_ostream &out) plotinfo->kitchen.mat_types[i], plotinfo->kitchen.mat_indices[i], plotinfo->kitchen.exc_types[i].whole, - (plotinfo->kitchen.mat_types[i] >= 419 && plotinfo->kitchen.mat_types[i] <= 618) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id : "n/a" + (plotinfo->kitchen.mat_types[i] >= df::builtin_mats::PLANT_1 && plotinfo->kitchen.mat_types[i] <= df::builtin_mats::PLANT_200) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id : "n/a" ); } out.print("\n"); diff --git a/library/modules/MapCache.cpp b/library/modules/MapCache.cpp index 84ee4affa0..ff0bea9eb4 100644 --- a/library/modules/MapCache.cpp +++ b/library/modules/MapCache.cpp @@ -943,7 +943,7 @@ t_matpair MapExtras::BlockInfo::getBaseMaterial(df::tiletype tt, df::coord2d pos case ROOT: case TREE: case PLANT: - rv.mat_type = MaterialInfo::PLANT_BASE; + rv.mat_type = df::builtin_mats::PLANT_1; if (auto plant = plants[block->map_pos + df::coord(x,y,0)]) { if (auto raw = df::plant_raw::find(plant->material)) @@ -958,7 +958,7 @@ t_matpair MapExtras::BlockInfo::getBaseMaterial(df::tiletype tt, df::coord2d pos case GRASS_DARK: case GRASS_DRY: case GRASS_DEAD: - rv.mat_type = MaterialInfo::PLANT_BASE; + rv.mat_type = df::builtin_mats::PLANT_1; if (auto raw = df::plant_raw::find(grass[x][y])) { rv.mat_type = raw->material_defs.type[plant_material_def::basic_mat]; diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index dba8a78132..f91882337a 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -108,7 +108,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) { material = raws.mat_table.builtin[type]; } - else if (type == 0) + else if (type == df::builtin_mats::INORGANIC) { mode = Inorganic; inorganic = df::inorganic_raw::find(index); @@ -116,23 +116,23 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; material = &inorganic->material; } - else if (type < CREATURE_BASE) + else if (type < df::builtin_mats::CREATURE_1) { material = raws.mat_table.builtin[type]; } - else if (type < FIGURE_BASE) + else if (type <= df::builtin_mats::CREATURE_200) { mode = Creature; - subtype = type - CREATURE_BASE; + subtype = type - df::builtin_mats::CREATURE_1; creature = df::creature_raw::find(index); if (!creature || size_t(subtype) >= creature->material.size()) return false; material = creature->material[subtype]; } - else if (type < PLANT_BASE) + else if (type <= df::builtin_mats::HIST_FIG_200) { mode = Creature; - subtype = type - FIGURE_BASE; + subtype = type - df::builtin_mats::HIST_FIG_1; figure = df::historical_figure::find(index); if (!figure) return false; @@ -141,10 +141,10 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; material = creature->material[subtype]; } - else if (type < END_BASE) + else if (type <= df::builtin_mats::PLANT_200) { mode = Plant; - subtype = type - PLANT_BASE; + subtype = type - df::builtin_mats::PLANT_1; plant = df::plant_raw::find(index); if (!plant || size_t(subtype) >= plant->material.size()) return false; @@ -219,7 +219,7 @@ bool MaterialInfo::findBuiltin(const std::string& token) } auto& raws = world->raws; - for (int i = 0; i < NUM_BUILTIN; i++) + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) { auto obj = raws.mat_table.builtin[i]; if (obj && obj->id == token) @@ -266,7 +266,7 @@ bool MaterialInfo::findPlant(const std::string& token, const std::string& subtok for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(PLANT_BASE + j, i); + return decode(df::builtin_mats::PLANT_1 + j, i); break; } @@ -286,7 +286,7 @@ bool MaterialInfo::findCreature(const std::string& token, const std::string& sub for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(CREATURE_BASE + j, i); + return decode(df::builtin_mats::CREATURE_1 + j, i); break; } diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index 811a3d1cb0..f85bbdd756 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -301,7 +301,7 @@ static int32_t estimate_logs(const df::plant *plant) { return 0; MaterialInfo mi; - mi.decode(MaterialInfo::PLANT_BASE, plant->material); + mi.decode(df::builtin_mats::PLANT_1, plant->material); bool is_shroom = mi.plant->flags.is_set(df::plant_raw_flags::TREE_HAS_MUSHROOM_CAP); int32_t trunks = 0, parent_dir = 0; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 6f5a13a8c7..61ee882071 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -167,7 +167,7 @@ static void load_organic_material_cache(df::organic_mat_category cat) { static void load_material_cache() { auto &raws = world->raws; - for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) + for (int i = 1; i < df::builtin_mats::CREATURE_1; ++i) if (raws.mat_table.builtin[i]) cache_matched(i, -1); diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index f8b54f01fb..9a8f034c5b 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -634,12 +634,12 @@ static command_result CheckHashes(color_ostream &stream, const EmptyMessage *in) void CopyMat(RemoteFortressReader::MatPair * mat, int type, int index) { - if (type >= MaterialInfo::FIGURE_BASE && type < MaterialInfo::PLANT_BASE) + if (type >= df::builtin_mats::HIST_FIG_1 && type <= df::builtin_mats::HIST_FIG_200) { df::historical_figure * figure = df::historical_figure::find(index); if (figure) { - type -= MaterialInfo::GROUP_SIZE; + type = (type - df::builtin_mats::HIST_FIG_1) + df::builtin_mats::CREATURE_1; index = figure->race; } } @@ -817,9 +817,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage MaterialInfo mat; for (size_t i = 0; i < raws->inorganics.all.size(); i++) { - mat.decode(0, i); + mat.decode(df::builtin_mats::INORGANIC, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(0); + mat_def->mutable_mat_pair()->set_mat_type(df::builtin_mats::INORGANIC); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; @@ -828,11 +828,11 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage ConvertDFColorDescriptor(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)], mat_def->mutable_state_color()); } } - for (int i = 0; i < 19; i++) + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) { int k = -1; - if (i == 7) - k = 1;// for coal. + if (i == df::builtin_mats::COAL) + k = 1;// for coke and charcoal for (int j = -1; j <= k; j++) { mat.decode(i, j); @@ -852,9 +852,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage df::creature_raw * creature = raws->creatures.all[i]; for (size_t j = 0; j < creature->material.size(); j++) { - mat.decode(j + MaterialInfo::CREATURE_BASE, i); + mat.decode(j + df::builtin_mats::CREATURE_1, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(j + 19); + mat_def->mutable_mat_pair()->set_mat_type(j + df::builtin_mats::CREATURE_1); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; @@ -869,9 +869,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage df::plant_raw * plant = raws->plants.all[i]; for (size_t j = 0; j < plant->material.size(); j++) { - mat.decode(j + 419, i); + mat.decode(j + df::builtin_mats::PLANT_1, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(j + 419); + mat_def->mutable_mat_pair()->set_mat_type(j + df::builtin_mats::PLANT_1); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; @@ -2096,14 +2096,14 @@ static void SetRegionTile(RegionTile * out, df::region_map_entry * e1) auto plantMat = out->add_plant_materials(); plantMat->set_mat_index(pop->plant); - plantMat->set_mat_type(419); + plantMat->set_mat_type(df::builtin_mats::PLANT_1); } else if (pop->type == world_population_type::Tree) { auto plantMat = out->add_tree_materials(); plantMat->set_mat_index(pop->plant); - plantMat->set_mat_type(419); + plantMat->set_mat_type(df::builtin_mats::PLANT_1); } } #if DF_VERSION_INT >= 43005 From c9a137c6dca3330e424dc37ef83524b762d2567c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 30 Jul 2026 17:05:37 -0500 Subject: [PATCH 1039/1082] extend DFSDL API to provide the DFLIbrary so a plugin that needs to use an SDL method not already mapped by DFSDL doesn't have to manually locate and open the SDL libs itself --- docs/changelog.txt | 2 +- library/include/modules/DFSDL.h | 7 +++++++ library/modules/DFSDL.cpp | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3f1340168f..300ae435f2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,7 +66,7 @@ Template for new versions: ## Documentation ## API - +- ``DFSDL``: added ``obtain_library_handle`` and ``obtain_image_library_handle`` so that a plugin can obtain DFHack's already-open handle to these libs instead of having to do it itself. - ``Screen``: new functions ``paintMapPortTile`` and ``readMapPortTile`` to write and read world and region map tiles. - ``Materials``: ``MaterialInfo`` constants ``NUM_BUILTIN``, ``GROUP_SIZE``, ``CREATURE_BASE``, ``FIGURE_BASE``, ``PLANT_BASE``, and ``END_BASE`` removed. New plugins should use appropriate members of the ``df::builtin_mats`` enum. diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 7d53242ad0..d47d588fc6 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -35,6 +35,13 @@ namespace DFHack::DFSDL */ void cleanup(); + /** + * Obtain DFHack's handle to the SDL or IMG libraries, in case a plugin needs + * to map a SDL API not mapped here + */ + DFHACK_EXPORT DFLibrary* obtain_library_handle(); + DFHACK_EXPORT DFLibrary* obtain_image_library_handle(); + DFHACK_EXPORT SDL_Surface* DFIMG_Load(const char* file); DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceFrom(void* pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index f23cde5511..ae0e6abbd6 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -25,6 +25,17 @@ using std::vector; static DFLibrary *g_sdl_handle = nullptr; static DFLibrary *g_sdl_image_handle = nullptr; + +DFLibrary* obtain_library_handle() +{ + return g_sdl_handle; +} + +DFLibrary* obtain_image_library_handle() +{ + return g_sdl_image_handle; +} + static const vector SDL_LIBS { #ifdef WIN32 "SDL2.dll" From e872d74a763fce333d87c0d70b0f14b1a6202724 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 30 Jul 2026 17:12:33 -0500 Subject: [PATCH 1040/1082] add missing header oops --- library/include/modules/DFSDL.h | 1 + 1 file changed, 1 insertion(+) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index d47d588fc6..95837b6889 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -2,6 +2,7 @@ #include "ColorText.h" #include "Export.h" +#include "PluginManager.h" #include #include From d460446d2fe332869d602db65c9ded6810dacaa8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 30 Jul 2026 18:41:13 -0500 Subject: [PATCH 1041/1082] correct namespace for these functions do not commit while hungry --- library/modules/DFSDL.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index ae0e6abbd6..01b89720cf 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -26,12 +26,12 @@ using std::vector; static DFLibrary *g_sdl_handle = nullptr; static DFLibrary *g_sdl_image_handle = nullptr; -DFLibrary* obtain_library_handle() +DFLibrary* DFHack::DFSDL::obtain_library_handle() { return g_sdl_handle; } -DFLibrary* obtain_image_library_handle() +DFLibrary* DFHack::DFSDL::obtain_image_library_handle() { return g_sdl_image_handle; } From fb8d4e900d228d02da1e483aec45d8f3036d32e1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 1 Aug 2026 13:56:45 -0500 Subject: [PATCH 1042/1082] some updates to `Contributing.rst` --- docs/dev/Contributing.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/dev/Contributing.rst b/docs/dev/Contributing.rst index 3ab620c1f9..216d204969 100644 --- a/docs/dev/Contributing.rst +++ b/docs/dev/Contributing.rst @@ -120,6 +120,37 @@ Pull request guidelines or add "WIP" to the title. Otherwise, your pull request may be reviewed and/or merged prematurely. +* Avoid using force pushes to your pull request branch after it has been reviewed, + as this can make it difficult for reviewers to see what has changed since their + last review. If you need to make changes, consider creating a new commit instead + of amending or rebasing. We neither enforce nor recommend a "single commit" rule; if you do + choose to squash your commits, please ensure that the commit message is clear and descriptive of the changes made. + If your pull request has an unusually large number of commits, a maintainer may + request that you squash your commits into a smaller number of commits before merging. + +* All pull requests must be accompanied by a description of the changes made, and + any relevant information for reviewers. If your pull request addresses an + issue, please include a reference to that issue in the description (e.g. + "Fixes #1234"). If your pull request is related to another pull request, please + include a reference to that pull request in the description (e.g. "Related to + #1234"). + +* All pull requests which have user facing changes, including all new features, bug fixes, or + changes to existing functionality, must include an entry in the "Future" section of + the changelog for the relevant repository. If your pull request is merged, this entry + will be added to the appropriate changelog. These entries are used when preparing the release + notes for each release, so please be sure to include a clear and concise description + of the changes made. See `build-changelog` for more information on the changelog format. + Changes that do not require a changelog entry are mainly those that are purely internal, + such as refactoring not intended to change semantics, code cleanup, changes to CI implementation + or to documentation, or changes directly related to the release process. When in doubt, + assume a changelog entry will be required. + +* Pull requests that add or modify tools must include a corresponding update to the documentation + for that tool. Similarly, pull requests that add or modify either the C++ or Lua APIs + must include a corresponding update to the appropriate API documentation. + See `docs-standards` for details. + Other ways to help ================== DFHack is a software project, but there's a lot more to it than programming. From b6a3824ae70f03922a0908bad1fdd426017313b6 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:33:20 +0200 Subject: [PATCH 1043/1082] improve code quality --- plugins/export-map.cpp | 233 ++++++++++++++++++++++------------------- 1 file changed, 126 insertions(+), 107 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index e2d4768e51..1284ea6048 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -61,6 +63,16 @@ using coord = df::coord2d; constexpr int wdim = 768; // dimension of a world tile constexpr int rdim = 48; // dimension of a region tile +static std::ofstream open_output_file( + const std::filesystem::path& filename, + std::ios_base::openmode mode = std::ios::out | std::ios::trunc) +{ + auto base = Core::getInstance().getConfigPath() / "map-export"; + std::filesystem::create_directories(base); + return std::ofstream(base / filename, mode); +} + + /** * Takes a vector of coordinates interpreted as global region tile coordinates * (i.e. 16 region tiles per world tile) and emits a WKT path in GIS-compatible @@ -74,21 +86,21 @@ auto print_path(std::ostream &out, const std::vector &path) { print_range(out, path, print_point, "(", ",", ")"); } -df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { - switch (dir) { - case 1: world_x-- ; world_y++; break; - case 2: ; world_y++; break; - case 3: world_x++ ; world_y++; break; - case 4: world_x-- ; ; break; - // case 5 induces no change - case 6: world_x++ ; ; break; - case 7: world_x-- ; world_y--; break; - case 8: ; world_y--; break; - case 9: world_x++ ; world_y--; break; - } - world_x = std::clamp(world_x,(int16_t)0,(int16_t)(world->world_data->world_width - 1)); - world_y = std::clamp(world_y,(int16_t)0,(int16_t)(world->world_data->world_height - 1)); - return { world_x, world_y }; +df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t offset_dir) { + constexpr auto biome_offset = std::to_array>({ + {-1, 1}, {0, 1}, {1, 1}, + {-1, 0}, {0, 0}, {1, 0}, + {-1,-1}, {0,-1}, {1,-1} + }); + + auto [diff_x, diff_y] = biome_offset[std::clamp(offset_dir, (int8_t)1, (int8_t)9) - 1]; + return { + (int16_t)std::clamp(world_x + diff_x,0,world->world_data->world_width - 1), + (int16_t)std::clamp(world_y + diff_y,0,world->world_data->world_height - 1) + }; +} +df::coord2d get_world_index(coord world_pos, int8_t offset_dir) { + return get_world_index(world_pos.x, world_pos.y, offset_dir); } const char* describe_surroundings(int savagery, int evilness) { @@ -102,10 +114,14 @@ const char* describe_surroundings(int savagery, int evilness) { return surroundings[3 * evilness_index + savagery_index]; } - static command_result do_command(color_ostream &out, vector ¶meters); +static command_result export_regions(color_ostream &out); +static command_result export_sites(color_ostream &out); +static command_result export_rivers(color_ostream &out); +static command_result export_elevation(color_ostream &out); + DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(log,out).print("initializing %s\n", plugin_name); + DEBUG(log,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -115,11 +131,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { CoreSuspender suspend; @@ -129,38 +140,30 @@ static command_result do_command(color_ostream &out, vector ¶meters) return CR_WRONG_USAGE; } - if (parameters.size() && parameters[0] == "sites") - { - return export_sites(out); - } - else if (parameters.size() && parameters[0] == "rivers") { - return export_rivers(out); - } - else if (parameters.size() && parameters[0] == "regions") { - return export_region_tiles(out); - } - else if (parameters.size() && parameters[0] == "elevation") { - return export_elevation(out); - } - else - { - auto ok_region = export_region_tiles(out); - auto ok_sites = ok_region == CR_OK ? export_sites(out) : ok_region; - return ok_sites == CR_OK ? export_rivers(out) : ok_sites; - } + const bool run_all = parameters.empty() || std::ranges::find(parameters, "all") != parameters.end(); + + auto result = CR_WRONG_USAGE; + const auto run_if_selected = [&](std::string name, command_result(*export_fn)(color_ostream &out)) { + if (result != CR_FAILURE && (run_all || std::ranges::find(parameters, name) != parameters.end())) + { + result = export_fn(out); + } + }; + + run_if_selected("regions", &export_regions); + run_if_selected("sites", &export_sites); + run_if_selected("rivers", &export_rivers); + run_if_selected("elevation", &export_elevation); + + return result; } /********************************************************************** */ static command_result export_sites(color_ostream &out) { - out.print("exporting sites... "); - out.flush(); - const auto start{std::chrono::steady_clock::now()}; - // ensure that we have an output file - std::string filename("sites.csv"); - std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + auto out_file = open_output_file("sites.csv"); if (!out_file) { return CR_FAILURE; } @@ -185,7 +188,7 @@ static command_result export_sites(color_ostream &out) df::creature_raw *race = nullptr; if (owner){ race = df::creature_raw::find(owner->race); - DEBUG(warning, out).print("owner (%d) of site (%d) has undefined race (%d)", owner->id, site->id, owner->race); + DEBUG(warning, out).print("owner ({}) of site ({}) has undefined race ({})", owner->id, site->id, owner->race); if (!race) { df::creature_raw::find(civ->race); } @@ -215,17 +218,12 @@ static command_result export_sites(color_ostream &out) print_range(out_file, std::vector>{path}, print_path , "POLYGON(", ",", ")\n" ); } - - const auto finish{std::chrono::steady_clock::now()}; - const std::chrono::duration elapsed_seconds{finish - start}; - out.print("done in %.2fs !\n", elapsed_seconds.count()); return CR_OK; } /********************************************************************** */ - - +// reverse lex-ordering (topmost leftmost tile is smallest) bool region_order(coord p1, coord p2) { return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); }; @@ -259,6 +257,14 @@ coord advance(coord pos, direction dir) { return pos + as_offset(dir); } + +/** + * Look-ahead relative to the direction of movement: + * + * L ↑ R | L + * - + - → + → + * ↑ | R + */ std::pair ahead(const std::vector &component, coord pos, direction dir) { auto test = [&](int16_t x, int16_t y){ coord offset{x,y}; @@ -279,19 +285,17 @@ std::pair ahead(const std::vector &component, coord pos, direc } } -static command_result export_region_tiles(color_ostream &out) +static command_result export_regions(color_ostream &out) { - out.print("%lu / %d region map tiles loaded\n", + out.print("{} / {} region map tiles loaded\n", world->world_data->midmap_data.region_details.size(), world->world_data->world_width * world->world_data->world_height ); - out.print("exporting map... \n"); - out.flush(); + out.print("exporting map... "); const auto start{std::chrono::steady_clock::now()}; // ensure that we have an output file - std::string filename("map.csv"); - std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + auto out_file = open_output_file("map.csv"); if (!out_file) { return CR_FAILURE; } @@ -308,25 +312,23 @@ static command_result export_region_tiles(color_ostream &out) /* Preprocessing: cluster region tiles by the world tile used for the biome information */ - // map world tile coord -> vector of region tiles referencing of world title + // map world tile coord -> vector of region tiles referencing world title for biome information std::unordered_map> world_tile_region; // iterating over the region details allows the user to do partial map exports - // by manually scrolling on the embark site selection + // by manually scrolling on the zoomed embark selection map for (auto const region_details : world->world_data->midmap_data.region_details) { - auto world_x = region_details->pos.x; - auto world_y = region_details->pos.y; + auto &world_pos = region_details->pos; for (int region_x = 0; region_x < 16; ++region_x) { for (int region_y = 0; region_y < 16; ++region_y) { - auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); - world_tile_region[biome_tile].emplace_back(16 * world_x + region_x, 16 * world_y + region_y); + auto biome_tile = get_world_index(world_pos, region_details->biome[region_x][region_y]); + world_tile_region[biome_tile].emplace_back(world_pos * 16 + coord(region_x, region_y)); } } } - out.print("processing %ld world tile regions\n", world_tile_region.size()); - + // out.print("processing {} world tile regions\n", world_tile_region.size()); static std::array directions{ as_offset(direction::North), @@ -342,13 +344,17 @@ static command_result export_region_tiles(color_ostream &out) // sorting the region provides O(log n) membership test. std::ranges::sort(region, region_order); - /* Phase I : compute the connected components of the world tile region */ + /** + * Phase I : compute the connected components of the world tile region using DFS algorithm. + * (except for the southern and eastern map edge, all world tile regions + * should have a single component) + */ - // component_assignment[i] is the component id of region[i] + // component_assignment[i] is the component id of region[i] (0 means unassigned) std::vector component_assignment; component_assignment.resize(region.size(),0); - // (indices of) blocks in the current component that have been discovered but not yet explored + // (indices of) region tiles in the current component that have been discovered but not yet explored std::deque agenda; unsigned int current_component = 0; @@ -367,9 +373,9 @@ static command_result export_region_tiles(color_ostream &out) auto pos = region[pos_idx]; for (const auto& offset : directions) { - auto lb = std::ranges::lower_bound(region, pos + offset, region_order); - if (lb != region.end() && *lb == pos + offset) { - auto n_idx = std::distance(region.begin(), lb); + const auto it = std::ranges::lower_bound(region, pos + offset, region_order); + if (it != region.end() && *it == pos + offset) { + auto n_idx = std::distance(region.begin(), it); if (component_assignment[n_idx] == 0) { component_assignment[n_idx] = current_component; agenda.push_back(n_idx); @@ -487,7 +493,7 @@ static command_result export_region_tiles(color_ostream &out) const auto finish{std::chrono::steady_clock::now()}; const std::chrono::duration elapsed_seconds{finish - start}; - out.print("done in %f s !\n", elapsed_seconds.count()); + out.print("done in {} s !\n", elapsed_seconds.count()); return CR_OK; } @@ -507,18 +513,37 @@ struct river_tile { struct gate { int active,min,max; - static gate get(const df::world_region_details *const region_details, int region_x, int region_y, direction dir) { + static gate get( + const df::world_region_details *const region_details, + int region_x, int region_y, direction dir + ) { auto& vertical = region_details->rivers_vertical; auto& horizontal = region_details->rivers_horizontal; switch (dir) { case direction::North: - return { vertical.active[region_x][region_y], vertical.x_min[region_x][region_y], vertical.x_max[region_x][region_y] }; + return { + vertical.active[region_x][region_y], + vertical.x_min[region_x][region_y], + vertical.x_max[region_x][region_y] + }; case direction::West: - return { horizontal.active[region_x][region_y], horizontal.y_min[region_x][region_y], horizontal.y_max[region_x][region_y] }; + return { + horizontal.active[region_x][region_y], + horizontal.y_min[region_x][region_y], + horizontal.y_max[region_x][region_y] + }; case direction::South: - return { vertical.active[region_x][region_y+1], vertical.x_min[region_x][region_y+1], vertical.x_max[region_x][region_y+1] }; + return { + vertical.active[region_x][region_y+1], + vertical.x_min[region_x][region_y+1], + vertical.x_max[region_x][region_y+1] + }; case direction::East: - return { horizontal.active[region_x+1][region_y], horizontal.y_min[region_x+1][region_y], horizontal.y_max[region_x+1][region_y] }; + return { + horizontal.active[region_x+1][region_y], + horizontal.y_min[region_x+1][region_y], + horizontal.y_max[region_x+1][region_y] + }; default: assert(false); return {}; @@ -543,8 +568,7 @@ bool is_land(const df::world_region_details *const region_details, int16_t regio static command_result export_rivers(color_ostream &out) { // ensure that we have an output file - std::string filename("rivers.csv"); - std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + auto out_file = open_output_file("rivers.csv"); if (!out_file) { return CR_FAILURE; } @@ -645,27 +669,13 @@ static command_result export_rivers(color_ostream &out) /********************************************************************** */ -static std::string vrtTemplate = R"( - - EPSG:3857 - 0,48,0,0,0,-48 - - elevation.dat - 0 - 2 - {LINE_OFFSET} - LSB - - -)"; - - template -struct matrix { +class matrix { std::size_t ncols; std::size_t nrows; std::vector _data; +public: matrix(std::size_t cols, std::size_t rows) : ncols(cols), nrows(rows), _data(cols * rows) {}; T& operator()(std::size_t col, std::size_t row) { @@ -680,16 +690,12 @@ struct matrix { static command_result export_elevation(color_ostream &out) { // ensure that we have an output file - std::string data_filename("elevation.dat"); - std::ofstream data_file(data_filename, std::ios::out | std::ios::trunc | std::ios::binary); - - std::string vrt_filename("elevation.vrt"); - std::ofstream vrt_file(vrt_filename, std::ios::out | std::ios::trunc); + auto data_file = open_output_file("elevation.dat", std::ios::out | std::ios::trunc | std::ios::binary); + auto vrt_file = open_output_file("elevation.vrt"); if (!data_file || !vrt_file) { return CR_FAILURE; } - auto world_width = world->world_data->world_width * 16; auto world_height = world->world_data->world_height * 16; @@ -707,13 +713,26 @@ static command_result export_elevation(color_ostream &out) } } - data_file.write( - reinterpret_cast(height_map.data()), height_map.size() * sizeof(int16_t)); + data_file.write(reinterpret_cast(height_map.data()), height_map.size() * sizeof(int16_t)); + + // provide an interpretation for the elevation map consistent with the remaining exports + const std::string vrtTemplate = +R"( +EPSG:3857 +0,48,0,0,0,-48 + + elevation.dat + 0 + 2 + {LINE_OFFSET} + LSB + + +)"; auto vrt = std::regex_replace(vrtTemplate, std::regex("\\{WIDTH\\}"), std::to_string(world_width)); vrt = std::regex_replace(vrt, std::regex("\\{HEIGHT\\}"), std::to_string(world_height)); vrt = std::regex_replace(vrt, std::regex("\\{LINE_OFFSET\\}"), std::to_string(world_width * sizeof(int16_t))); - vrt_file << vrt; return CR_OK; From e868d02edd4991b9a74891fe2ce67fad953f7bbd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 1 Aug 2026 21:48:48 -0500 Subject: [PATCH 1044/1082] add a safety check to `manageUnitAttackEvent` to avoid a potential crash based on a bug report in which DFHack crashed due to getting an erroneous result from `binsearch`, presumably due to crud in the DF vector --- docs/changelog.txt | 1 + library/modules/EventManager.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 300ae435f2..92f023062f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: - `buildingplan`: restore planner UI elements: hollow constructions, only engraved slabs, only empty cages, weapon count ## Misc Improvements +- `EventManager`: add safety check to potentially avoid a DFHack crash when DF's ``reports`` table is out of order ## Documentation diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 3c926645a2..12aa4e7ed5 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -1136,6 +1136,11 @@ static void manageUnitAttackEvent(color_ostream& out) { multimap copy(handlers[EventType::UNIT_ATTACK].begin(), handlers[EventType::UNIT_ATTACK].end()); std::vector& reports = df::global::world->status.reports; size_t idx = df::report::binsearch_index(reports, lastReportUnitAttack, false); + if (idx >= reports.size()) + { + WARN(log, out).print("manageUnitAttackEvent: last reported unit attack lookup failed ({} -> {})\n", lastReportUnitAttack, idx); + return; + } // returns the index to the key equal to or greater than the key provided idx = reports[idx]->id == lastReportUnitAttack ? idx + 1 : idx; // we need the index after (where the new stuff is) From 2f1bb5f6a402cfbcba896cb4b8c436a005f73d5a Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 2 Aug 2026 16:33:10 +0200 Subject: [PATCH 1045/1082] fix river confluence points --- plugins/export-map.cpp | 105 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 1284ea6048..b325132924 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -22,11 +22,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -159,6 +161,9 @@ static command_result do_command(color_ostream &out, vector ¶meters) } /********************************************************************** */ +/* Site Export */ +/********************************************************************** */ + static command_result export_sites(color_ostream &out) { @@ -221,6 +226,8 @@ static command_result export_sites(color_ostream &out) return CR_OK; } +/********************************************************************** */ +/* Region Map Export */ /********************************************************************** */ // reverse lex-ordering (topmost leftmost tile is smallest) @@ -497,19 +504,101 @@ static command_result export_regions(color_ostream &out) return CR_OK; } +/********************************************************************** */ +/* River Export */ /********************************************************************** */ +// // used for global coordinates at local tile granularity (129*768 = 99072 doesn't fit into df::coord2d) +template struct gcoord { - int x,y; + T x, y; + + gcoord() = default; + gcoord(T x, T y) : x(x), y(y) {} + + template + explicit gcoord(const gcoord &other) : x(static_cast(other.x)), y(static_cast(other.y)) {}; + + gcoord operator+(const gcoord &other) const + { + return {x + other.x, y + other.y}; + } + + gcoord operator-(const gcoord &other) const + { + return {x - other.x, y - other.y}; + } + + gcoord operator*(T s) const + { + return {x * s, y * s}; + } + + gcoord operator/(T s) const + { + return {x / s, y / s}; + } + + static T dotp(const gcoord& a, const gcoord& b) + { + return a.x * b.x + a.y * b.y; + } }; +// linear interpolation between two points +static gcoord lerp(gcoord A, gcoord B, double t) +{ + return A + (B - A) * t; +} + +// "orthogonal" projection of the point P onto the line segment AB +static gcoord project_onto_line(gcoord A, gcoord B, gcoord P) +{ + auto AB = B - A; + auto AP = P - A; + auto t = gcoord::dotp(AP, AB) / gcoord::dotp(AB, AB); + return A + AB * std::clamp(t, 0.0, 1.0); +} + + struct river_tile { - using polygon_t = std::list; + using polygon_t = std::vector>; polygon_t polygon; - //std::optional north, south, west, east; }; +/** + * To get reasonably-looking river confluences, we project the centroid of all + * river gates onto the line segments between the river gates and then + * interpolate between the centroid and the projection point. + */ +static void fix_confluence_tiles(river_tile::polygon_t& polygon) +{ + assert(polygon.size() > 4 && polygon.size() % 2 == 0); + + auto centroid = + gcoord(std::reduce(polygon.begin(), polygon.end())) / static_cast(polygon.size()); + + river_tile::polygon_t inset_polygon; + + for (size_t i = 0; i + 1 < polygon.size(); i += 2) { + auto pair_start = polygon[i]; + auto pair_end = polygon[i + 1]; + auto next_pair_start = polygon[(i + 2) % polygon.size()]; + + inset_polygon.emplace_back(pair_start); + inset_polygon.emplace_back(pair_end); + + auto projection = project_onto_line(gcoord(pair_end), gcoord(next_pair_start), centroid); + auto inset_point = lerp(projection, centroid, 0.6); + inset_polygon.emplace_back(gcoord(inset_point)); + } + + polygon = std::move(inset_polygon); +} + + + struct gate { int active,min,max; @@ -598,7 +687,7 @@ static command_result export_rivers(color_ostream &out) for (int region_x = 0; region_x < 16; ++region_x) { for (int region_y = 0; region_y < 16; ++region_y) { - gcoord base = { world_x * wdim + region_x * rdim, world_y * wdim + region_y * rdim }; + gcoord base = { world_x * wdim + region_x * rdim, world_y * wdim + region_y * rdim }; auto north = gate::get(region_details, region_x, region_y, direction::North); auto west = gate::get(region_details, region_x, region_y, direction::West); @@ -632,6 +721,10 @@ static command_result export_rivers(color_ostream &out) tile.polygon.emplace_back(base.x + rdim, base.y + east.min); } + if (tile.polygon.size() > 4) { + fix_confluence_tiles(tile.polygon); + } + auto r_idx = world_river.at({world_x, world_y}); tile_index[r_idx][ coord(world_x * 16 + region_x, world_y * 16 + region_y) ] = std::move(tile); } @@ -650,7 +743,7 @@ static command_result export_rivers(color_ostream &out) bool first = true; for (auto &[tile_pos, tile] : river_index) { tile.polygon.emplace_back(*tile.polygon.begin()); - auto print_position = [](std::ostream &out, gcoord pos) { + auto print_position = [](std::ostream &out, gcoord pos) { out << pos.x << " " << -pos.y; }; if (first) { @@ -667,6 +760,8 @@ static command_result export_rivers(color_ostream &out) return CR_OK; } +/********************************************************************** */ +/* Elevation Map Export */ /********************************************************************** */ template From 7c23828c6c50d22391acccd3163e729add69da66 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:40:29 +0200 Subject: [PATCH 1046/1082] more cleanup --- plugins/export-map.cpp | 69 +++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index b325132924..d790364d19 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -80,7 +80,7 @@ static std::ofstream open_output_file( * (i.e. 16 region tiles per world tile) and emits a WKT path in GIS-compatible * local tile coordinates (negative y-coordinates, 48 stepts per region tile) */ -auto print_path(std::ostream &out, const std::vector &path) { +static void print_path(std::ostream &out, const std::vector &path) { auto scale = rdim; assert(path.size()); auto print_point = [scale](std::ostream &out, const coord &pos){ @@ -88,7 +88,7 @@ auto print_path(std::ostream &out, const std::vector &path) { print_range(out, path, print_point, "(", ",", ")"); } -df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t offset_dir) { +static df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t offset_dir) { constexpr auto biome_offset = std::to_array>({ {-1, 1}, {0, 1}, {1, 1}, {-1, 0}, {0, 0}, {1, 0}, @@ -101,7 +101,7 @@ df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t offset_dir) (int16_t)std::clamp(world_y + diff_y,0,world->world_data->world_height - 1) }; } -df::coord2d get_world_index(coord world_pos, int8_t offset_dir) { +static df::coord2d get_world_index(coord world_pos, int8_t offset_dir) { return get_world_index(world_pos.x, world_pos.y, offset_dir); } @@ -167,6 +167,10 @@ static command_result do_command(color_ostream &out, vector ¶meters) static command_result export_sites(color_ostream &out) { + out.print("exporting sites ... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + // ensure that we have an output file auto out_file = open_output_file("sites.csv"); if (!out_file) { @@ -223,6 +227,10 @@ static command_result export_sites(color_ostream &out) print_range(out_file, std::vector>{path}, print_path , "POLYGON(", ",", ")\n" ); } + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in {:2f} s !\n", elapsed_seconds.count()); + return CR_OK; } @@ -237,15 +245,15 @@ bool region_order(coord p1, coord p2) { enum class direction : int { North = 0, West = 1, South = 2, East = 3 }; -direction turn_left(direction dir) { +static constexpr direction turn_left(direction dir) { return (direction)(((int)dir+1) % 4); } -direction turn_right(direction dir) { +static constexpr direction turn_right(direction dir) { return (direction)(((int)dir+3) % 4); } -coord as_offset(direction dir) { +static coord as_offset(direction dir) { switch (dir) { case direction::North: return { 0, -1 }; @@ -260,7 +268,7 @@ coord as_offset(direction dir) { } } -coord advance(coord pos, direction dir) { +static coord advance(coord pos, direction dir) { return pos + as_offset(dir); } @@ -272,7 +280,7 @@ coord advance(coord pos, direction dir) { * - + - → + → * ↑ | R */ -std::pair ahead(const std::vector &component, coord pos, direction dir) { +static std::pair ahead(const std::vector &component, coord pos, direction dir) { auto test = [&](int16_t x, int16_t y){ coord offset{x,y}; return std::ranges::binary_search(component, pos + offset, region_order); @@ -298,7 +306,8 @@ static command_result export_regions(color_ostream &out) world->world_data->midmap_data.region_details.size(), world->world_data->world_width * world->world_data->world_height ); - out.print("exporting map... "); + out.print("exporting map ... "); + out.flush(); const auto start{std::chrono::steady_clock::now()}; // ensure that we have an output file @@ -500,7 +509,7 @@ static command_result export_regions(color_ostream &out) const auto finish{std::chrono::steady_clock::now()}; const std::chrono::duration elapsed_seconds{finish - start}; - out.print("done in {} s !\n", elapsed_seconds.count()); + out.print("done in {:2f} s !\n", elapsed_seconds.count()); return CR_OK; } @@ -645,7 +654,7 @@ struct gate { }; -bool is_land(const df::world_region_details *const region_details, int16_t region_x, int16_t region_y) { +static bool is_land(const df::world_region_details *const region_details, int16_t region_x, int16_t region_y) { CHECK_NULL_POINTER(region_details); auto [world_x, world_y] = region_details->pos; auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); @@ -656,13 +665,26 @@ bool is_land(const df::world_region_details *const region_details, int16_t regio static command_result export_rivers(color_ostream &out) { + out.print("exporting rivers ... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + // ensure that we have an output file auto out_file = open_output_file("rivers.csv"); if (!out_file) { return CR_FAILURE; } - // create lookup table for rivers based on world tile coordinates + + /** + * In DwarfFortress, a world tile can only have at most one river. As a + * consequence, rivers do not end at a confluence point but they already + * change their name when they enter the world tile containing the + * confluence. Grouping river tiles by world tile allows us to only output + * one multipolygon feature per river. + */ + + // create lookup table for rivers based on world tile coordinates (index into world_data->rivers) std::unordered_map world_river; // assign river end first, so that it can be overridden by proper path elements @@ -679,8 +701,8 @@ static command_result export_rivers(color_ostream &out) } } - // river idx -> region tile coord -> tile - std::unordered_map> tile_index; + // river idx -> river tiles + std::unordered_map> tile_index; for (auto const region_details : world->world_data->midmap_data.region_details) { auto [world_x, world_y] = region_details->pos; @@ -725,8 +747,9 @@ static command_result export_rivers(color_ostream &out) fix_confluence_tiles(tile.polygon); } + // locate the river using world coordinates and assign the tile auto r_idx = world_river.at({world_x, world_y}); - tile_index[r_idx][ coord(world_x * 16 + region_x, world_y * 16 + region_y) ] = std::move(tile); + tile_index[r_idx].push_back(std::move(tile)); } } } @@ -735,13 +758,14 @@ static command_result export_rivers(color_ostream &out) // generate output out_file << "name_df;name_en;geometry_wkt\n"; - for (auto& [r_idx, river_index] : tile_index) { + for (auto& [r_idx, river_tiles] : tile_index) { auto river = world->world_data->rivers.at(r_idx); out_file << DF2UTF(Translation::translateName(&river->name, false)) << ";"; out_file << DF2UTF(Translation::translateName(&river->name, true)) << ";"; out_file << "MULTIPOLYGON("; bool first = true; - for (auto &[tile_pos, tile] : river_index) { + for (auto &tile : river_tiles) { + // close the polygon tile.polygon.emplace_back(*tile.polygon.begin()); auto print_position = [](std::ostream &out, gcoord pos) { out << pos.x << " " << -pos.y; @@ -756,6 +780,9 @@ static command_result export_rivers(color_ostream &out) out_file << ")\n"; } + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in {:2f} s !\n", elapsed_seconds.count()); return CR_OK; } @@ -784,6 +811,10 @@ class matrix { static command_result export_elevation(color_ostream &out) { + out.print("exporting elevation ... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + // ensure that we have an output file auto data_file = open_output_file("elevation.dat", std::ios::out | std::ios::trunc | std::ios::binary); auto vrt_file = open_output_file("elevation.vrt"); @@ -830,5 +861,9 @@ R"( vrt = std::regex_replace(vrt, std::regex("\\{LINE_OFFSET\\}"), std::to_string(world_width * sizeof(int16_t))); vrt_file << vrt; + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in {:2f} s !\n", elapsed_seconds.count()); + return CR_OK; } From 394718a764cc6ffbafbeaaed4cd1083d85cae7c9 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:05:42 +0200 Subject: [PATCH 1047/1082] refactor region output into multiple functions --- plugins/export-map.cpp | 208 ++++++++++++++++++++++------------------- 1 file changed, 111 insertions(+), 97 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index d790364d19..7c6f307461 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -300,6 +300,112 @@ static std::pair ahead(const std::vector &component, coord pos } } +// standard DFS-based connected components algorithm. +static auto connected_components(std::vector ®ion) { + + static std::array directions{ + as_offset(direction::North), + as_offset(direction::West), + as_offset(direction::South), + as_offset(direction::East) + }; + + // component_assignment[i] is the component id of region[i] (0 means unassigned) + std::vector component_assignment; + component_assignment.resize(region.size(),0); + + // (indices of) region tiles in the current component that have been discovered but not yet explored + std::deque agenda; + unsigned int current_component = 0; + + for (size_t i = 0; i < region.size(); ++i) { + if (component_assignment[i]) { + // skip region tiles that have already been assigned a component + continue; + } else { + // start a new component for tiles that haven't been assigned yet + ++current_component; + component_assignment[i] = current_component; + agenda.push_back(i); + } + while(!agenda.empty()) { + auto pos_idx = agenda.front(); agenda.pop_front(); + auto pos = region[pos_idx]; + + for (const auto& offset : directions) { + const auto it = std::ranges::lower_bound(region, pos + offset, region_order); + if (it != region.end() && *it == pos + offset) { + auto n_idx = std::distance(region.begin(), it); + if (component_assignment[n_idx] == 0) { + component_assignment[n_idx] = current_component; + agenda.push_back(n_idx); + } + } + } + } + } + + // check that all parts of the region are accounted for + assert(std::ranges::all_of(component_assignment, [](int comp){ return comp > 0;})); + + // distribute region tiles according to their component assignment (preserves region order) + std::vector> components; + components.resize(current_component); + for (size_t i = 0; i < region.size(); ++i) { + components.at(component_assignment.at(i) - 1).push_back(region.at(i)); + } + + return components; +} + + +// create outlines around connected components using a clockwise walk around the perimeter +// (exploits that components do not have inclusions) +static auto create_outlines(color_ostream &out, std::vector> components) { + std::vector> paths; + for (auto const &component : components) { + + // start at the NW corner of the west-most tile of the northmost row... + auto start = component.at(0); + std::vector path; + path.push_back(start); + + // ... ensuring that a step to the east is a valid clockwise step along the boundary. + auto current_direction = direction::East; + auto current_position = advance(start,current_direction); + + while (current_position != start) + { + auto [left, right] = ahead(component, current_position, current_direction); + if (left && right) { + // in front of a wall: turn left + path.push_back(current_position); + current_direction = turn_left(current_direction); + } + else if (!left && !right) { + // no walls ahead: turn right + path.push_back(current_position); + current_direction = turn_right(current_direction); + } + else if (left && !right) { + // diagonal step: turn right (following the outline of the inclusion) + // this does not seem to happen with the maps currently generated by DF + DEBUG(warning,out).print("Region has self-intersecting outline"); + path.push_back(current_position); + current_direction = turn_right(current_direction); + } + // case !left && right requires no turn; advance the position in all cases + current_position = advance(current_position, current_direction); + } + // close the path + path.push_back(current_position); + paths.push_back(std::move(path)); + path.clear(); + } + assert(paths.size() > 0); + return paths; +} + static command_result export_regions(color_ostream &out) { out.print("{} / {} region map tiles loaded\n", @@ -344,15 +450,6 @@ static command_result export_regions(color_ostream &out) } } - // out.print("processing {} world tile regions\n", world_tile_region.size()); - - static std::array directions{ - as_offset(direction::North), - as_offset(direction::West), - as_offset(direction::South), - as_offset(direction::East) - }; - for (auto& [biome_tile, region] : world_tile_region) { assert(region.size() > 0); @@ -361,55 +458,12 @@ static command_result export_regions(color_ostream &out) std::ranges::sort(region, region_order); /** - * Phase I : compute the connected components of the world tile region using DFS algorithm. - * (except for the southern and eastern map edge, all world tile regions - * should have a single component) + * Phase I : compute the connected components of the world tile region + * using DFS algorithm. (except for the southern and eastern map edge, + * all world tile regions should have a single component) */ - // component_assignment[i] is the component id of region[i] (0 means unassigned) - std::vector component_assignment; - component_assignment.resize(region.size(),0); - - // (indices of) region tiles in the current component that have been discovered but not yet explored - std::deque agenda; - unsigned int current_component = 0; - - for (size_t i = 0; i < region.size(); ++i) { - if (component_assignment[i]) { - // skip region tiles that have already been assigned a component - continue; - } else { - // start a new component for tiles that haven't been assigned yet - ++current_component; - component_assignment[i] = current_component; - agenda.push_back(i); - } - while(!agenda.empty()) { - auto pos_idx = agenda.front(); agenda.pop_front(); - auto pos = region[pos_idx]; - - for (const auto& offset : directions) { - const auto it = std::ranges::lower_bound(region, pos + offset, region_order); - if (it != region.end() && *it == pos + offset) { - auto n_idx = std::distance(region.begin(), it); - if (component_assignment[n_idx] == 0) { - component_assignment[n_idx] = current_component; - agenda.push_back(n_idx); - } - } - } - } - } - - // check that all parts of the region are accounted for - assert(std::ranges::all_of(component_assignment, [](int comp){ return comp > 0;})); - - // distribute region tiles according to their component assignment (preserves region order) - std::vector> components; - components.resize(current_component); - for (size_t i = 0; i < region.size(); ++i) { - components.at(component_assignment.at(i) - 1).push_back(region.at(i)); - } + auto components = connected_components(region); /* Phase II : create paths by clockwise traversal along the outside of every component */ /** @@ -420,47 +474,7 @@ static command_result export_regions(color_ostream &out) * https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry */ - std::vector> paths; - for (auto const &component : components) { - - // start at the NW corner of the west-most tile of the northmost row... - auto start = component.at(0); - std::vector path; - path.push_back(start); - - // ... ensuring that a step to the east is a valid clockwise step along the boundary. - auto current_direction = direction::East; - auto current_position = advance(start,current_direction); - - while (current_position != start) - { - auto [left, right] = ahead(component, current_position, current_direction); - if (left && right) { - // in front of a wall: turn left - path.push_back(current_position); - current_direction = turn_left(current_direction); - } - else if (!left && !right) { - // no walls ahead: turn right - path.push_back(current_position); - current_direction = turn_right(current_direction); - } - else if (left && !right) { - // diagonal step: turn right (following the outline of the inclusion) - // this does not seem to happen with the maps currently generated by DF - DEBUG(warning,out).print("Region has self-intersecting outline"); - path.push_back(current_position); - current_direction = turn_right(current_direction); - } - // case !left && right requires no turn; advance the position in all cases - current_position = advance(current_position, current_direction); - } - // close the path - path.push_back(current_position); - paths.push_back(std::move(path)); - path.clear(); - } - assert(paths.size() > 0); + auto paths = create_outlines(out, components); /* Phase III: output the CSV line */ From 70cb75e98716e42a52f4963d650512b14f229bd6 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:55:48 +0200 Subject: [PATCH 1048/1082] documentation and changelog --- docs/changelog.txt | 1 + docs/plugins/export-map.rst | 74 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 docs/plugins/export-map.rst diff --git a/docs/changelog.txt b/docs/changelog.txt index 300ae435f2..4a135736b3 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ Template for new versions: # Future ## New Tools +- `export-map`: Export world map data for GIS and other external tools. ## New Features diff --git a/docs/plugins/export-map.rst b/docs/plugins/export-map.rst new file mode 100644 index 0000000000..1002971cbd --- /dev/null +++ b/docs/plugins/export-map.rst @@ -0,0 +1,74 @@ +export-map +========== + +.. dfhack-tool:: + :summary: Export world map data for GIS and other external tools. + :tags: inspection embark map + +This plugin exports world-map information to files in the DFHack configuration +folder under the ``map-export`` directory. It must be run from the +embark-selection screen. The exports are generated at the granularity of +individual region tiles (16 x 16 per world tile). Those tiles are only generated +by DF as you scroll around on the zoomed-in embark map, which thus needs to be +done prior to running this export tool. + +The supported export targets are: + +- ``regions``: writes world tile regions and their biome information to ``map.csv``. +- ``sites``: writes site extents and ownership/civilization information to ``sites.csv``. +- ``rivers``: writes rivers to ``rivers.csv``. +- ``elevation``: writes elevation data to ``elevation.dat`` and ``elevation.vrt``. + +Unless you have software such as ArcGIS or QGIS/GDAL available, these exports +will be of little use. + +Usage +----- + +:: + + export-map + export-map + export-map ... + export-map all + +If no topic is given, or if ``all`` is supplied, every supported export target +is run. + +Examples +-------- + +``export-map`` + Export all supported datasets. + +``export-map regions`` + Export only region boundary geometries. + +``export-map sites rivers`` + Export site and river geometries only. + + +Notes on file formats +--------------------- + +To avoid depending on external libraries such as GDAL, the export formats are +kept as simple as possible to generate. This means raw bitmap data for the +elevation export and CSV files for the others. Before these files can be used +reasonably in software such as QGIS, they need to be assigned a coordinate +reference system (CRS) and converted into formats that can be accessed more +efficiently than CSV. While any CRS will do, WGS 84 / Pseudo-Mercator +(EPSG:3857) is the preferred choice. + +For detailed map data (``regions`` and ``rivers``), Parquet is by far the +preferred format. The elevation export provides a VRT file to facilitate the +transformation into a GeoTIFF with the aforementioned CRS. + +Assuming a recent version of GDAL built with Parquet support and with all +command-line tools in your PATH, this can be achieved with the following +commands: + +:: + + ogr2ogr -a_srs "EPSG:3857" -of Parquet map.parquet -oo 'GEOM_POSSIBLE_NAMES=*wkt' -oo 'KEEP_GEOM_COLUMNS=NO' -oo 'AUTODETECT_TYPE=YES' map.csv + ogr2ogr -a_srs "EPSG:3857" -of Parquet rivers.parquet -oo 'GEOM_POSSIBLE_NAMES=*wkt' -oo 'KEEP_GEOM_COLUMNS=NO' -oo 'AUTODETECT_TYPE=YES' rivers.csv + gdal raster convert elevation.vrt elevation.tif From 4c2130595c54fac19b30ce713918f2e84fbda97c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Aug 2026 09:46:33 -0500 Subject: [PATCH 1049/1082] version to 53.15-r3 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 61bddcc0b4..87b47f2d04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. set(DF_VERSION "53.15") -set(DFHACK_RELEASE "r2") +set(DFHACK_RELEASE "r3") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 58c2636545d7b22e7b78fa74aa79a1d5a830cdac Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Aug 2026 09:58:26 -0500 Subject: [PATCH 1050/1082] Changelog for 53.15-r3 --- docs/changelog.txt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 92f023062f..14efc37d1a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.15-r3 + +## New Tools + +## New Features + ## Fixes - `buildingplan`: restore planner UI elements: hollow constructions, only engraved slabs, only empty cages, weapon count @@ -72,7 +90,6 @@ Template for new versions: - ``Materials``: ``MaterialInfo`` constants ``NUM_BUILTIN``, ``GROUP_SIZE``, ``CREATURE_BASE``, ``FIGURE_BASE``, ``PLANT_BASE``, and ``END_BASE`` removed. New plugins should use appropriate members of the ``df::builtin_mats`` enum. ## Lua - - Added ``Screen::paintMapPortTile`` as ``dfhack.screen.paintMapPortTile`` - Added ``Screen::readMapPortTile`` as ``dfhack.screen.readMapPortTile`` - Deprecated ``gui.materials.CREATURE_BASE`` and ``gui.materials.PLANT_BASE`` - scripts should instead use ``df.builtin_mats.CREATURE_1`` and ``df.builtin_mats.PLANT_1``, respectively. From 679a99a99b4e047a71929a1d6c4ef4794ca20233 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Aug 2026 09:59:00 -0500 Subject: [PATCH 1051/1082] library/xml for 53.15-r3 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a06e4c1c81..a488fe8f5a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a06e4c1c81f94eaf0d735cfe5a8ed2c64e0bcb81 +Subproject commit a488fe8f5ae3475d7731eb8169498900a32e4b0b From e069b16a8362e53b304daaccb5cfb2074455cdc8 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:38:38 +0000 Subject: [PATCH 1052/1082] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a488fe8f5a..47bacb367d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a488fe8f5ae3475d7731eb8169498900a32e4b0b +Subproject commit 47bacb367d26cadca1a99a1ecd74fde88e4ed1a4 From 7f5304709fba9c3cbc13ebf4b1882bc99ba3fde7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 4 Aug 2026 10:43:21 -0500 Subject: [PATCH 1053/1082] block gcc 16 for being used for builds gcc 16 has a defect in which enabling `-Warray-bounds` causes it to spuriously warn about object slicing in a case where no object slicing can possibly occur will reenable when gcc fixes this defect --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 87b47f2d04..852d21f07d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,12 @@ macro(CHECK_GCC compiler_path) if(${GCC_VERSION_OUT} VERSION_LESS "11") message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 11 or later") endif() + # GCC 16 currently has a defect that prevents it from compiling DFHack + # -Warray-bounds is broken in GCC 16 and we'd rather disable the compiler than remove this warning + # will reconsider when this defect is fixed in a future GCC release + if(${GCC_VERSION_OUT} VERSION_GREATER_EQUAL "16") + message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 15 or earlier") + endif() endmacro() if(UNIX) From 8e98ed9c530530e0562471ad80baaaa48b86e65f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:13:36 +0000 Subject: [PATCH 1054/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 68b39325b2..769a842ba5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 68b39325b2a94e6401ab5dc32770e5ccf0f0bf52 +Subproject commit 769a842ba5064d91d893373ba493710fea9958a5 From 69cb7a9c6a084a26cbeeda927701b7a923d182bb Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:35:18 +0000 Subject: [PATCH 1055/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 769a842ba5..eb0d5d633d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 769a842ba5064d91d893373ba493710fea9958a5 +Subproject commit eb0d5d633d74b77ab6287393d61ea866b190b814 From e274bc76eb8ef2f1d1937e6e1fee2dca4a5ec695 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 03:59:31 -0500 Subject: [PATCH 1056/1082] prevent out of bound color values in `doSetTile_char` some Lua code is leaking a bg of -1 into this function, resulting in an out of bounds reference to `uccolor`. this logic prohibits such out of bound reads the upstream problem still needs to be found --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 3 +++ .../widgets/text_area/text_area_content.lua | 2 +- library/modules/Screen.cpp | 19 +++++++++++-------- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 14efc37d1a..49518fdcb4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- a safety check was added to ``Screen::doSetTile_char`` fur out of bound pen color values ## Misc Improvements diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index f729be2743..e8a1cd9f67 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3754,6 +3754,9 @@ environment by the mandatory init file dfhack.lua: ``COLOR_GREY`` and ``COLOR_DARKGREY`` can also be spelled ``COLOR_GRAY`` and ``COLOR_DARKGRAY``. + Note: ``COLOR_RESET`` is not valid in a `Pen `, and using it in a Pen color field + will result in runtime warnings and may result in color flashing or other unexpected results. + * State change event codes, used by ``dfhack.onStateChange`` Available only in the `core context `, as is the event itself: diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index 83099fac76..cd35f751ee 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -37,7 +37,7 @@ function TextAreaContent:init() self.cursor = nil self.main_pen = dfhack.pen.parse({ - bg=COLOR_RESET, + bg=COLOR_BLACK, bold=true }, self.text_pen) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 338a66925e..c5451ce806 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -54,6 +54,8 @@ distribution. #include "df/renderer.h" #include "df/plant.h" +#include +#include #include #include #include @@ -209,14 +211,15 @@ static bool doSetTile_char(const Pen &pen, int x, int y, bool use_graphics) *texpos_lower = df::global::init->texpos_border_interior; // basic black background } - auto rgb_fg = &gps->uccolor[fg][0]; - auto rgb_bg = &gps->uccolor[bg][0]; - screen[1] = rgb_fg[0]; - screen[2] = rgb_fg[1]; - screen[3] = rgb_fg[2]; - screen[4] = rgb_bg[0]; - screen[5] = rgb_bg[1]; - screen[6] = rgb_bg[2]; + if (fg >= 0 && fg <= COLOR_MAX) + std::ranges::copy(gps->uccolor[fg], &screen[1]); + else + WARN(screen).print("in doSetTile_char, fg {} out of range\n", fg); + + if (bg >= 0 && bg <= COLOR_MAX) + std::ranges::copy(gps->uccolor[bg], &screen[4]); + else + WARN(screen).print("in doSetTile_char, bg {} out of range\n", bg); return true; } From a33cad6400b4074a248aafcbe094ede011dc6751 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 07:30:51 -0500 Subject: [PATCH 1057/1082] revise changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 49518fdcb4..9ad9dd18dd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: ## Fixes - a safety check was added to ``Screen::doSetTile_char`` fur out of bound pen color values +- ``TextArea`` widget corrected to use ``COLOR_BLACK`` instead of ``COLOR_RESET`` as default background color ## Misc Improvements From 9b3bfcb8a41fa23a3312f7c89626944070d62017 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:29:57 +0000 Subject: [PATCH 1058/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index eb0d5d633d..7549711a99 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit eb0d5d633d74b77ab6287393d61ea866b190b814 +Subproject commit 7549711a993e03bef19e90b27427096c1099853e From 7f26bd9568687e6bedf9b425363686d6741c3983 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:37:19 +0000 Subject: [PATCH 1059/1082] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 47bacb367d..a79647e433 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 47bacb367d26cadca1a99a1ecd74fde88e4ed1a4 +Subproject commit a79647e4338976351ef469bef30a6af04b3d1502 From d89463c5ef24d6fb7013c83e5cc78ce0fecf967d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 11:46:57 -0500 Subject: [PATCH 1060/1082] add `std::filesystem::file_time_type` as an opaque type --- library/DataIdentity.cpp | 1 + library/include/DataIdentity.h | 1 + 2 files changed, 2 insertions(+) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index 0a797c2069..111166d90e 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -65,6 +65,7 @@ namespace df { OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(std::filesystem::file_time_type); OPAQUE_IDENTITY_TRAITS(wchar_t*); const buffer_container_identity buffer_container_identity::base_instance; diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index cf486e6188..c48dd299fd 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -632,6 +632,7 @@ namespace df OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(std::filesystem::file_time_type); #ifdef BUILD_DFHACK_LIB template From c3de892042c639d1dc7aa82e8fb9d3b47758c459 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:06:18 +0000 Subject: [PATCH 1061/1082] Auto-update structures ref for 53.16 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a79647e433..f89b44a8c7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a79647e4338976351ef469bef30a6af04b3d1502 +Subproject commit f89b44a8c76f556060ddbec1238a81fa2db3b1e2 From 9aa84eae2886788fccefdfe296e1a7a05e33f3da Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 12:14:03 -0500 Subject: [PATCH 1062/1082] Update version number and changelog for 53.16-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 852d21f07d..1d9b17943e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. -set(DF_VERSION "53.15") -set(DFHACK_RELEASE "r3") +set(DF_VERSION "53.16") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ad9dd18dd..a290eee5b5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,10 +59,28 @@ Template for new versions: ## New Features ## Fixes -- a safety check was added to ``Screen::doSetTile_char`` fur out of bound pen color values + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.16-r1 + +## New Tools + +## New Features + +## Fixes - ``TextArea`` widget corrected to use ``COLOR_BLACK`` instead of ``COLOR_RESET`` as default background color ## Misc Improvements +- a safety check was added to ``Screen::doSetTile_char`` for out of bound pen color values ## Documentation From c80747da32aad3bdd5915c60105136d6558aca39 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 12:19:13 -0500 Subject: [PATCH 1063/1082] update xml for 53.16-r1 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f89b44a8c7..1dd01aad64 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f89b44a8c76f556060ddbec1238a81fa2db3b1e2 +Subproject commit 1dd01aad64219afa0578f1328cf15bd0c6006d5a From 3bde79692a4cd5d8c10ba063ccad0030728538a7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 5 Aug 2026 12:41:23 -0500 Subject: [PATCH 1064/1082] Update download-df.sh bay12 no longer publishes a "small" download for windows so don't attempt to download it for testing --- ci/download-df.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/download-df.sh b/ci/download-df.sh index ed8b373a25..a07f75f2a1 100755 --- a/ci/download-df.sh +++ b/ci/download-df.sh @@ -14,7 +14,7 @@ fi df_url="https://www.bay12games.com/dwarves/df_${minor}_${patch}" if test "$OS_TARGET" = "windows"; then WGET="C:/msys64/usr/bin/wget.exe" - df_url="${df_url}_win_s.zip" + df_url="${df_url}_win.zip" df_archive_name="df.zip" df_extract_cmd="unzip -d ${DF_FOLDER}" elif test "$OS_TARGET" = "ubuntu"; then From 9397f275306013f1ec740419f8c31613ac2d03bf Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:12:13 +0200 Subject: [PATCH 1065/1082] allow grouping of exports by world name and date --- plugins/CMakeLists.txt | 2 +- plugins/export-map.cpp | 69 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f99b576fad..a240639cb6 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -81,7 +81,7 @@ if(BUILD_SUPPORTED) #add_subdirectory(embark-assistant) dfhack_plugin(edgescroll edgescroll.cpp) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) - dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique) + dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique LINK_LIBRARIES lua) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 7c6f307461..d72e2633e9 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -1,6 +1,7 @@ #include "Debug.h" #include "Error.h" #include "MiscUtils.h" +#include "PluginLua.h" #include "PluginManager.h" #include "modules/Maps.h" @@ -65,16 +66,6 @@ using coord = df::coord2d; constexpr int wdim = 768; // dimension of a world tile constexpr int rdim = 48; // dimension of a region tile -static std::ofstream open_output_file( - const std::filesystem::path& filename, - std::ios_base::openmode mode = std::ios::out | std::ios::trunc) -{ - auto base = Core::getInstance().getConfigPath() / "map-export"; - std::filesystem::create_directories(base); - return std::ofstream(base / filename, mode); -} - - /** * Takes a vector of coordinates interpreted as global region tile coordinates * (i.e. 16 region tiles per world tile) and emits a WKT path in GIS-compatible @@ -122,6 +113,42 @@ static command_result export_sites(color_ostream &out); static command_result export_rivers(color_ostream &out); static command_result export_elevation(color_ostream &out); +struct Config { + bool group_by_world = false; + bool group_by_date = false; +}; + +static Config config{}; + +static auto exportmap_getWorldFolderName() { + auto& world_name = world->world_data->name; + return fmt::format("{} ({})", + DF2UTF(Translation::translateName(&world_name, true)), + DF2UTF(Translation::translateName(&world_name, false))); +} + +static auto exportmap_getDateFolderName() { + return fmt::format("{}-{}", + *df::global::cur_year, + *df::global::cur_year_tick / 33600 + 1); +} + +static std::ofstream open_output_file( + const std::filesystem::path& filename, + std::ios_base::openmode mode = std::ios::out | std::ios::trunc) +{ + auto base = Core::getInstance().getConfigPath() / "map-export"; + if (config.group_by_world || config.group_by_date) { + base /= std::filesystem::path(exportmap_getWorldFolderName()); + } + if (config.group_by_date) { + base /= std::filesystem::path(exportmap_getDateFolderName()); + } + std::filesystem::create_directories(base); + return std::ofstream(base / filename, mode); +} + + DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { DEBUG(log,out).print("initializing {}\n", plugin_name); @@ -142,11 +169,23 @@ static command_result do_command(color_ostream &out, vector ¶meters) return CR_WRONG_USAGE; } - const bool run_all = parameters.empty() || std::ranges::find(parameters, "all") != parameters.end(); + auto has_param = [¶meters](const string &option) { + return std::ranges::find(parameters, option) != parameters.end(); + }; + + config = {}; + if (has_param("--group-by-world")) { + config.group_by_world = true; + } + if (has_param("--group-by-date")) { + config.group_by_date = true; + } + + const bool run_all = parameters.empty() || has_param("all"); auto result = CR_WRONG_USAGE; const auto run_if_selected = [&](std::string name, command_result(*export_fn)(color_ostream &out)) { - if (result != CR_FAILURE && (run_all || std::ranges::find(parameters, name) != parameters.end())) + if (result != CR_FAILURE && (run_all || has_param(name))) { result = export_fn(out); } @@ -881,3 +920,9 @@ R"( return CR_OK; } + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(exportmap_getWorldFolderName), + DFHACK_LUA_FUNCTION(exportmap_getDateFolderName), + DFHACK_LUA_END +}; From 33a04e695df47181f7dc554ef2225f497fbd097b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 7 Aug 2026 23:18:35 -0500 Subject: [PATCH 1066/1082] fix incorrect use of `std::extent` in `stockpiles` unintended consequence of #5847 --- docs/changelog.txt | 1 + plugins/stockpiles/StockpileSerializer.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index a290eee5b5..6dbc91f6e2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `stockpiles` will no longer incorrectly size the "rough gem" and "cut gem" vectors, which avoids a crash when viewing stockpile settings. This potentially affects anything that uses blueprints to create a stockpile, including `gui/quantum` ## Misc Improvements diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 6e90cb797e..f1b1a3bd3e 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -1984,24 +1984,24 @@ void StockpileSettingsSerializer::read_gems(color_ostream& out, DeserializeMode std::bind(&StockpileSettings::gems, mBuffer), mSettings->flags.whole, mSettings->flags.mask_gems, - [&]() { + [&] () { pgems.cut_other_mats.clear(); pgems.cut_mats.clear(); pgems.rough_other_mats.clear(); pgems.rough_mats.clear(); }, - [&](bool all, char val) { - auto & bgems = mBuffer.gems(); + [&] (bool all, char val) { + auto& bgems = mBuffer.gems(); unserialize_list_material(out, "mats/rough", all, val, filters, gem_mat_is_allowed, - [&](const size_t& idx) -> const string& { return bgems.rough_mats(idx); }, + [&] (const size_t& idx) -> const string& { return bgems.rough_mats(idx); }, bgems.rough_mats_size(), pgems.rough_mats); unserialize_list_material(out, "mats/cut", all, val, filters, gem_cut_mat_is_allowed, - [&](const size_t& idx) -> const string& { return bgems.cut_mats(idx); }, + [&] (const size_t& idx) -> const string& { return bgems.cut_mats(idx); }, bgems.cut_mats_size(), pgems.cut_mats); - const size_t builtin_size = std::extentraws.mat_table.builtin)>::value; + const size_t builtin_size = world->raws.mat_table.builtin.size(); pgems.rough_other_mats.resize(builtin_size, '\0'); pgems.cut_other_mats.resize(builtin_size, '\0'); if (all) { From b638b59d0876d9bdf8b5f97e52714206ab7f3266 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 7 Aug 2026 23:37:03 -0500 Subject: [PATCH 1067/1082] CMakeLists and changelog for 53.16-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d9b17943e..1495bea2b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. set(DF_VERSION "53.16") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 6dbc91f6e2..904fcc86fb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.16-r1.1 + +## New Tools + +## New Features + ## Fixes - `stockpiles` will no longer incorrectly size the "rough gem" and "cut gem" vectors, which avoids a crash when viewing stockpile settings. This potentially affects anything that uses blueprints to create a stockpile, including `gui/quantum` From 93a23af5f1cc9fd2df5c89dccbd1502aa0c58972 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:41:58 +0200 Subject: [PATCH 1068/1082] parallelize processing of region tiles --- plugins/export-map.cpp | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index d72e2633e9..1ec6ef7064 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -25,14 +25,16 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include +#include +#include #include #include @@ -400,7 +402,7 @@ static auto connected_components(std::vector ®ion) { // create outlines around connected components using a clockwise walk around the perimeter // (exploits that components do not have inclusions) -static auto create_outlines(color_ostream &out, std::vector> components) { +static auto create_outlines(std::vector> components) { std::vector> paths; for (auto const &component : components) { @@ -429,7 +431,6 @@ static auto create_outlines(color_ostream &out, std::vector out_lines; + out_lines.reserve(world_tile_region.size()); + + std::for_each( + std::execution::par, world_tile_region.begin(), world_tile_region.end(), + [&out_lines,&out_mutex](auto &wtr) { + auto& [biome_tile, region] = wtr; + + std::ostringstream out_line; + assert(region.size() > 0); // sorting the region provides O(log n) membership test. @@ -513,7 +526,7 @@ static command_result export_regions(color_ostream &out) * https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry */ - auto paths = create_outlines(out, components); + auto paths = create_outlines(components); /* Phase III: output the CSV line */ @@ -521,7 +534,7 @@ static command_result export_regions(color_ostream &out) auto world_region = df::world_region::find(region_map_entry.region_id); auto landmass = df::world_landmass::find(region_map_entry.landmass_id); - auto print_csv = [&out_file](auto ...args){ ([&]{ out_file << args << ";" ; }() ,...); }; + auto print_csv = [&out_line](auto ...args){ ([&]{ out_line << args << ";" ; }() ,...); }; print_csv( // "world_x", "world_y", "num_tiles", "num_components", "biome_type", biome_tile.x, @@ -554,10 +567,17 @@ static command_result export_regions(color_ostream &out) // output geometry as WKT if (paths.size() == 1) { - print_range(out_file, paths, print_path , "POLYGON(", ",", ")\n" ); + print_range(out_line, paths, print_path , "POLYGON(", ",", ")\n" ); } else { - print_range(out_file, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); + print_range(out_line, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); } + + std::lock_guard lock(out_mutex); + out_lines.push_back(std::move(out_line)); + }); + + for (const auto& line : out_lines) { + out_file << line.view(); } const auto finish{std::chrono::steady_clock::now()}; From bbebf84086135b655386f0587b7221e5b99ff6d8 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:53:30 +0200 Subject: [PATCH 1069/1082] Move functions to Maps module and update changelog --- docs/changelog.txt | 4 ++++ library/include/modules/Maps.h | 4 ++++ library/modules/Maps.cpp | 27 +++++++++++++++++++++++++++ plugins/CMakeLists.txt | 2 +- plugins/export-map.cpp | 34 +++------------------------------- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index c23136b91f..4b0c3111a8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,10 @@ Template for new versions: ## Documentation ## API +- `MiscUtils`: new helper function ``print_range`` to print ranges with prefix, separator, and suffix +- `Maps`: new function ``getSiteTypeName`` to print the sites classification as a string (e.g. "mountain halls") +- `Maps`: new function ``addRegionBiomeOffset`` to shift world region coordinate by region_details biome reference +- `Maps`: new function ``describeSurroundings`` to get surroundings classification from savagery and evilness ## Lua diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index d28c40e238..6da4986899 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -352,6 +352,8 @@ inline df::tiletype *getTileType(df::coord pos) { return getTileType(pos.x, pos. inline df::tile_designation *getTileDesignation(df::coord pos) { return getTileDesignation(pos.x, pos.y, pos.z); } inline df::tile_occupancy *getTileOccupancy(df::coord pos) { return getTileOccupancy(pos.x, pos.y, pos.z); } +// shift world region coordinate by region_details biome reference +DFHACK_EXPORT df::coord2d addRegionBiomeOffset(df::coord2d world_pos, int8_t offset_dir); // Returns biome info about the specified world region. DFHACK_EXPORT df::region_map_entry *getRegionBiome(df::coord2d rgn_pos); @@ -413,6 +415,8 @@ DFHACK_EXPORT int removeAreaAquifer(df::coord pos1, df::coord pos2, DFHACK_EXPORT void addBlockColumns(int32_t new_height); +// Get surroundings classification from savagery and evilness +DFHACK_EXPORT const char* describeSurroundings(int savagery, int evilness); /** * A single function does not merit a "Sites" module, hence we collect site functions here in the meantime. diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 6dc5ecbaf8..95ab00b2d7 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -436,6 +436,22 @@ df::tile_occupancy *Maps::getTileOccupancy(int32_t x, int32_t y, int32_t z) return block ? &block->occupancy[x&15][y&15] : NULL; } +df::coord2d Maps::addRegionBiomeOffset(df::coord2d world_pos, int8_t offset_dir) { + // Note 1: textual order of offfsets is upside down compared to the keypad order used by the biome offset + // Note 2: also upside down compared to (fort) map block region offsets + constexpr auto biome_offset = std::to_array>({ + {-1, 1}, {0, 1}, {1, 1}, + {-1, 0}, {0, 0}, {1, 0}, + {-1,-1}, {0,-1}, {1,-1} + }); + + auto [diff_x, diff_y] = biome_offset[std::clamp(offset_dir, (int8_t)1, (int8_t)9) - 1]; + return { + (int16_t)std::clamp(world_pos.x + diff_x,0,world->world_data->world_width - 1), + (int16_t)std::clamp(world_pos.y + diff_y,0,world->world_data->world_height - 1) + }; +} + df::region_map_entry *Maps::getRegionBiome(df::coord2d rgn_pos) { auto data = world->world_data; @@ -1535,6 +1551,17 @@ int Maps::removeAreaAquifer(df::coord pos1, df::coord pos2, std::functionsurroundings{ + "Serene", "Mirthful", "Joyous Wilds", + "Calm", "Wilderness", "Untamed Wilds", + "Sinister", "Haunted", "Terrifying" + }; + auto savagery_index = savagery < 33 ? 0 : (savagery > 65 ? 2 : 1); + auto evilness_index = evilness < 33 ? 0 : (evilness > 65 ? 2 : 1); + return surroundings[3 * evilness_index + savagery_index]; +} + void Maps::addBlockColumns(int32_t new_height) { auto quantity = new_height - world->map.z_count_block; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index a240639cb6..6478db7fa4 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -81,7 +81,7 @@ if(BUILD_SUPPORTED) #add_subdirectory(embark-assistant) dfhack_plugin(edgescroll edgescroll.cpp) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) - dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique LINK_LIBRARIES lua) + dfhack_plugin(export-map export-map.cpp LINK_LIBRARIES lua) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 1ec6ef7064..e63cc29d3a 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -81,33 +81,6 @@ static void print_path(std::ostream &out, const std::vector &path) { print_range(out, path, print_point, "(", ",", ")"); } -static df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t offset_dir) { - constexpr auto biome_offset = std::to_array>({ - {-1, 1}, {0, 1}, {1, 1}, - {-1, 0}, {0, 0}, {1, 0}, - {-1,-1}, {0,-1}, {1,-1} - }); - - auto [diff_x, diff_y] = biome_offset[std::clamp(offset_dir, (int8_t)1, (int8_t)9) - 1]; - return { - (int16_t)std::clamp(world_x + diff_x,0,world->world_data->world_width - 1), - (int16_t)std::clamp(world_y + diff_y,0,world->world_data->world_height - 1) - }; -} -static df::coord2d get_world_index(coord world_pos, int8_t offset_dir) { - return get_world_index(world_pos.x, world_pos.y, offset_dir); -} - -const char* describe_surroundings(int savagery, int evilness) { - constexpr std::arraysurroundings{ - "Serene", "Mirthful", "Joyous Wilds", - "Calm", "Wilderness", "Untamed Wilds", - "Sinister", "Haunted", "Terrifying" - }; - auto savagery_index = savagery < 33 ? 0 : (savagery > 65 ? 2 : 1); - auto evilness_index = evilness < 33 ? 0 : (evilness > 65 ? 2 : 1); - return surroundings[3 * evilness_index + savagery_index]; -} static command_result do_command(color_ostream &out, vector ¶meters); static command_result export_regions(color_ostream &out); @@ -484,7 +457,7 @@ static command_result export_regions(color_ostream &out) for (int region_x = 0; region_x < 16; ++region_x) { for (int region_y = 0; region_y < 16; ++region_y) { - auto biome_tile = get_world_index(world_pos, region_details->biome[region_x][region_y]); + auto biome_tile = Maps::addRegionBiomeOffset(world_pos, region_details->biome[region_x][region_y]); world_tile_region[biome_tile].emplace_back(world_pos * 16 + coord(region_x, region_y)); } } @@ -559,7 +532,7 @@ static command_result export_regions(color_ostream &out) region_map_entry.rainfall, region_map_entry.salinity, // "surroundings", "elevation", "reanimating", "has_bogeymen" - describe_surroundings(region_map_entry.savagery, region_map_entry.evilness), + Maps::describeSurroundings(region_map_entry.savagery, region_map_entry.evilness), region_map_entry.elevation, world_region->reanimating, world_region->has_bogeymen @@ -729,8 +702,7 @@ struct gate { static bool is_land(const df::world_region_details *const region_details, int16_t region_x, int16_t region_y) { CHECK_NULL_POINTER(region_details); - auto [world_x, world_y] = region_details->pos; - auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); + auto biome_tile = Maps::addRegionBiomeOffset(region_details->pos, region_details->biome[region_x][region_y]); auto region_map_entry = Maps::getRegionBiome(biome_tile); CHECK_NULL_POINTER(region_map_entry); return region_map_entry->elevation >= 100 && !region_map_entry->flags.is_set(df::enums::region_map_entry_flags::is_lake); From d08da221c25512abe58a269634cc90d79ffa3477 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:16:39 +0200 Subject: [PATCH 1070/1082] fix changelog links --- docs/changelog.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4b0c3111a8..9af57c6adb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,10 +66,10 @@ Template for new versions: ## Documentation ## API -- `MiscUtils`: new helper function ``print_range`` to print ranges with prefix, separator, and suffix -- `Maps`: new function ``getSiteTypeName`` to print the sites classification as a string (e.g. "mountain halls") -- `Maps`: new function ``addRegionBiomeOffset`` to shift world region coordinate by region_details biome reference -- `Maps`: new function ``describeSurroundings`` to get surroundings classification from savagery and evilness +- Added ``MiscUtils::print_range`` to print ranges with prefix, separator, and suffix +- Added ``Maps::getSiteTypeName`` to print the sites classification as a string (e.g. "mountain halls") +- Added ``Maps::addRegionBiomeOffset`` to shift world region coordinate by region_details biome reference +- Added ``Maps::describeSurroundings`` to get surroundings classification from savagery and evilness ## Lua From 7da854b2c66d7815a23b492d5e0e1a329c7f0288 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:21:46 +0200 Subject: [PATCH 1071/1082] Rename tool from `export-map` to `export-world-map` --- docs/changelog.txt | 2 +- .../{export-map.rst => export-world-map.rst} | 30 ++++++++++++------- plugins/CMakeLists.txt | 2 +- .../{export-map.cpp => export-world-map.cpp} | 4 +-- 4 files changed, 24 insertions(+), 14 deletions(-) rename docs/plugins/{export-map.rst => export-world-map.rst} (83%) rename plugins/{export-map.cpp => export-world-map.cpp} (99%) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9af57c6adb..00dfa16b48 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,7 +55,7 @@ Template for new versions: # Future ## New Tools -- `export-map`: Export world map data for GIS and other external tools. +- `export-world-map`: Export world map data for GIS and other external tools. ## New Features diff --git a/docs/plugins/export-map.rst b/docs/plugins/export-world-map.rst similarity index 83% rename from docs/plugins/export-map.rst rename to docs/plugins/export-world-map.rst index 1002971cbd..e85cbd26d5 100644 --- a/docs/plugins/export-map.rst +++ b/docs/plugins/export-world-map.rst @@ -1,5 +1,5 @@ -export-map -========== +export-world-map +================ .. dfhack-tool:: :summary: Export world map data for GIS and other external tools. @@ -14,7 +14,7 @@ done prior to running this export tool. The supported export targets are: -- ``regions``: writes world tile regions and their biome information to ``map.csv``. +- ``regions``: writes world tile regions and their biome information to ``regions.csv``. - ``sites``: writes site extents and ownership/civilization information to ``sites.csv``. - ``rivers``: writes rivers to ``rivers.csv``. - ``elevation``: writes elevation data to ``elevation.dat`` and ``elevation.vrt``. @@ -27,24 +27,34 @@ Usage :: - export-map - export-map - export-map ... - export-map all + export-world-map + export-world-map + export-world-map ... + export-world-map all If no topic is given, or if ``all`` is supplied, every supported export target is run. +Options +------- + +``--group-by-world`` + Create a folder named after the world in the ``map-export`` directory. + +``--group-by-date`` + Create a folder with name ``year-month`` inside the world folder. Implies + ``--group-by-world``. + Examples -------- -``export-map`` +``export-world-map`` Export all supported datasets. -``export-map regions`` +``export-world-map regions`` Export only region boundary geometries. -``export-map sites rivers`` +``export-world-map sites rivers`` Export site and river geometries only. diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 6478db7fa4..6d374b3ba5 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -81,7 +81,7 @@ if(BUILD_SUPPORTED) #add_subdirectory(embark-assistant) dfhack_plugin(edgescroll edgescroll.cpp) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) - dfhack_plugin(export-map export-map.cpp LINK_LIBRARIES lua) + dfhack_plugin(export-world-map export-world-map.cpp LINK_LIBRARIES lua) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) diff --git a/plugins/export-map.cpp b/plugins/export-world-map.cpp similarity index 99% rename from plugins/export-map.cpp rename to plugins/export-world-map.cpp index e63cc29d3a..2ccf8daec5 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-world-map.cpp @@ -43,7 +43,7 @@ using std::vector; using namespace DFHack; -DFHACK_PLUGIN("export-map"); +DFHACK_PLUGIN("export-world-map"); REQUIRE_GLOBAL(world); @@ -430,7 +430,7 @@ static command_result export_regions(color_ostream &out) const auto start{std::chrono::steady_clock::now()}; // ensure that we have an output file - auto out_file = open_output_file("map.csv"); + auto out_file = open_output_file("regions.csv"); if (!out_file) { return CR_FAILURE; } From b4bf4aae51fd1aba9b903eed3c1ba368052dcc8b Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:00:29 +0200 Subject: [PATCH 1072/1082] fix export of Lua folder helpers --- plugins/export-world-map.cpp | 12 ++++++------ plugins/lua/export-world-map.lua | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 plugins/lua/export-world-map.lua diff --git a/plugins/export-world-map.cpp b/plugins/export-world-map.cpp index 2ccf8daec5..867d11fe50 100644 --- a/plugins/export-world-map.cpp +++ b/plugins/export-world-map.cpp @@ -95,14 +95,14 @@ struct Config { static Config config{}; -static auto exportmap_getWorldFolderName() { +static auto getWorldFolderName() { auto& world_name = world->world_data->name; return fmt::format("{} ({})", DF2UTF(Translation::translateName(&world_name, true)), DF2UTF(Translation::translateName(&world_name, false))); } -static auto exportmap_getDateFolderName() { +static auto getDateFolderName() { return fmt::format("{}-{}", *df::global::cur_year, *df::global::cur_year_tick / 33600 + 1); @@ -114,10 +114,10 @@ static std::ofstream open_output_file( { auto base = Core::getInstance().getConfigPath() / "map-export"; if (config.group_by_world || config.group_by_date) { - base /= std::filesystem::path(exportmap_getWorldFolderName()); + base /= std::filesystem::path(getWorldFolderName()); } if (config.group_by_date) { - base /= std::filesystem::path(exportmap_getDateFolderName()); + base /= std::filesystem::path(getDateFolderName()); } std::filesystem::create_directories(base); return std::ofstream(base / filename, mode); @@ -914,7 +914,7 @@ R"( } DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(exportmap_getWorldFolderName), - DFHACK_LUA_FUNCTION(exportmap_getDateFolderName), + DFHACK_LUA_FUNCTION(getWorldFolderName), + DFHACK_LUA_FUNCTION(getDateFolderName), DFHACK_LUA_END }; diff --git a/plugins/lua/export-world-map.lua b/plugins/lua/export-world-map.lua new file mode 100644 index 0000000000..672f141e43 --- /dev/null +++ b/plugins/lua/export-world-map.lua @@ -0,0 +1,12 @@ +local _ENV = mkmodule('plugins.export-world-map') + +--[[ + +Native functions: + +- getWorldFolderName() +- getDateFolderName() + +]] + +return _ENV From 6516229e6e0f23e433dac1a08319b809c4fe660b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 18 Aug 2026 07:38:27 +0000 Subject: [PATCH 1073/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 7549711a99..4ac3e364e7 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 7549711a993e03bef19e90b27427096c1099853e +Subproject commit 4ac3e364e7f0a0b1e23df7c4bb50998d172e61c6 From 6327518b43a89e20783a54da0c168529f30e9cce Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:45:05 +0000 Subject: [PATCH 1074/1082] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 1dd01aad64..86441613f9 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1dd01aad64219afa0578f1328cf15bd0c6006d5a +Subproject commit 86441613f95908af833a8416ba80371314d749c2 From be63a6747dbf9d5238b191044c2c1a4ba1f49a94 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:53:59 +0000 Subject: [PATCH 1075/1082] 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 86441613f9..5aee008756 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 86441613f95908af833a8416ba80371314d749c2 +Subproject commit 5aee008756c9f8514bfdbb1248deac77c8833262 From e4cf597ee321725be43af066c3e64b8afa2c6880 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 20 Aug 2026 19:14:10 -0500 Subject: [PATCH 1076/1082] Refactor includes This takes `global_objects.h` out of `DataDefs.h` which significantly reduces the number of translation units that include this header. It also makes the transitive dependencies a lot less complicated. --- library/Hooks.cpp | 1 + library/include/DataDefs.h | 37 +------- library/include/LuaTools.h | 2 + library/include/Types.h | 90 ++++++++++++++----- library/include/modules/Burrows.h | 6 +- library/include/modules/Designations.h | 2 + library/include/modules/Maps.h | 3 + library/include/modules/Materials.h | 1 + library/include/modules/World.h | 7 +- library/modules/Filesystem.cpp | 6 +- library/modules/Renderer.cpp | 2 + library/modules/Textures.cpp | 1 + plugins/3dveins.cpp | 1 + plugins/aquifer.cpp | 2 + plugins/buildingplan/buildingplan.cpp | 1 + plugins/cleanconst.cpp | 5 +- .../check-structures-sanity/dispatch.cpp | 1 + .../check-structures-sanity/validate.cpp | 2 + plugins/devel/dumpmats.cpp | 9 +- plugins/devel/eventExample.cpp | 7 +- plugins/devel/frozen.cpp | 1 + plugins/devel/tilesieve.cpp | 14 +-- plugins/eventful.cpp | 10 +-- plugins/flows.cpp | 1 + plugins/getplants.cpp | 5 +- plugins/infinite-sky.cpp | 1 + plugins/lair.cpp | 1 + plugins/stockpiles/OrganicMatLookup.cpp | 1 + plugins/stockpiles/StockpileUtils.h | 1 + plugins/stockpiles/stockpiles.cpp | 1 + 30 files changed, 140 insertions(+), 82 deletions(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 0bb6b04426..761de48778 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -2,6 +2,7 @@ #include "Export.h" #include "df/gamest.h" +#include "df/global_objects.h" #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 37892391a4..b3500f3d92 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -997,6 +997,7 @@ namespace DFHack { DFHACK_EXPORT const struct_field_info *find_union_tag(const struct_identity *structure, const struct_field_info *union_field); } + #define ENUM_ATTR(enum,attr,val) (df::enum_traits::attrs(val).attr) #define ENUM_ATTR_STR(enum,attr,val) DFHack::ifnull(ENUM_ATTR(enum,attr,val),"?") #define ENUM_KEY_STR(enum,val) (DFHack::enum_item_key(val)) @@ -1013,42 +1014,6 @@ namespace DFHack { */ // Global object pointers -#include "df/global_objects.h" - #define DF_GLOBAL_VALUE(name,defval) (df::global::name ? *df::global::name : defval) #define DF_GLOBAL_FIELD(name,fname,defval) (df::global::name ? df::global::name->fname : defval) -// A couple of headers that have to be included at once -#include "df/coord2d.h" -#include "df/coord.h" - -namespace std { - template <> - struct hash { - std::size_t operator()(const df::coord& c) const { - return c(); - } - }; -} - -template <> -struct fmt::formatter : fmt::formatter -{ - template - auto format(const df::coord& c, FormatContext& ctx) const - { - return fmt::formatter::format( - fmt::format("({}, {}, {})", c.x, c.y, c.z), ctx); - } -}; - -template <> -struct fmt::formatter : fmt::formatter -{ - template - auto format(const df::coord2d& c, FormatContext& ctx) const - { - return fmt::formatter::format( - fmt::format("({}, {})", c.x, c.y), ctx); - } -}; diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index f95095d55b..4e40789c4a 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -39,6 +39,8 @@ distribution. #include "ColorText.h" #include "DataDefs.h" +#include "df/coord.h" +#include "df/coord2d.h" #include "df/interface_key.h" #include diff --git a/library/include/Types.h b/library/include/Types.h index f367946e56..17114faf6d 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -30,9 +30,11 @@ distribution. #include #include "Export.h" -#include "DataDefs.h" +#include "df/coord.h" +#include "df/coord2d.h" #include "df/general_ref_type.h" +#include "df/global_objects.h" #include "df/specific_ref_type.h" namespace df { @@ -47,16 +49,16 @@ namespace DFHack { int16_t type; int32_t index; - bool operator<(const t_matglossPair &b) const + bool operator<(const t_matglossPair& b) const { if (type != b.type) return (type < b.type); return (index < b.index); } - bool operator==(const t_matglossPair &b) const + bool operator==(const t_matglossPair& b) const { return (type == b.type) && (index == b.index); } - bool operator!=(const t_matglossPair &b) const + bool operator!=(const t_matglossPair& b) const { return (type != b.type) || (index != b.index); } @@ -95,10 +97,14 @@ namespace DFHack std::string name; uint32_t xpNxtLvl; }; +} - typedef std::pair rect2d; +namespace DFHack +{ + using rect2d = std::pair; - inline rect2d intersect(rect2d a, rect2d b) { + inline rect2d intersect(rect2d a, rect2d b) + { df::coord2d g1 = a.first, g2 = a.second; df::coord2d c1 = b.first, c2 = b.second; df::coord2d rc1 = df::coord2d(std::max(g1.x, c1.x), std::max(g1.y, c1.y)); @@ -106,28 +112,70 @@ namespace DFHack return rect2d(rc1, rc2); } - inline rect2d mkrect_xy(int x1, int y1, int x2, int y2) { + inline rect2d mkrect_xy(int x1, int y1, int x2, int y2) + { return rect2d(df::coord2d(x1, y1), df::coord2d(x2, y2)); } - inline rect2d mkrect_wh(int x, int y, int w, int h) { - return rect2d(df::coord2d(x, y), df::coord2d(x+w-1, y+h-1)); + inline rect2d mkrect_wh(int x, int y, int w, int h) + { + return rect2d(df::coord2d(x, y), df::coord2d(x + w - 1, y + h - 1)); } - inline df::coord2d rect_size(const rect2d &rect) { - return rect.second - rect.first + df::coord2d(1,1); + inline df::coord2d rect_size(const rect2d& rect) + { + return rect.second - rect.first + df::coord2d(1, 1); } - DFHACK_EXPORT int getdir(std::filesystem::path dir, std::vector &files); - DFHACK_EXPORT bool hasEnding (std::string const &fullString, std::string const &ending); + DFHACK_EXPORT int getdir(std::filesystem::path dir, std::vector& files); + DFHACK_EXPORT bool hasEnding(std::string const& fullString, std::string const& ending); - DFHACK_EXPORT df::general_ref *findRef(std::vector &vec, df::general_ref_type type); - DFHACK_EXPORT bool removeRef(std::vector &vec, df::general_ref_type type, int id); + DFHACK_EXPORT df::general_ref* findRef(std::vector& vec, df::general_ref_type type); + DFHACK_EXPORT bool removeRef(std::vector& vec, df::general_ref_type type, int id); - DFHACK_EXPORT df::item *findItemRef(std::vector &vec, df::general_ref_type type); - DFHACK_EXPORT df::building *findBuildingRef(std::vector &vec, df::general_ref_type type); - DFHACK_EXPORT df::unit *findUnitRef(std::vector &vec, df::general_ref_type type); + DFHACK_EXPORT df::item* findItemRef(std::vector& vec, df::general_ref_type type); + DFHACK_EXPORT df::building* findBuildingRef(std::vector& vec, df::general_ref_type type); + DFHACK_EXPORT df::unit* findUnitRef(std::vector& vec, df::general_ref_type type); - DFHACK_EXPORT df::specific_ref *findRef(std::vector &vec, df::specific_ref_type type); - DFHACK_EXPORT bool removeRef(std::vector &vec, df::specific_ref_type type, void *ptr); -}// namespace DFHack + DFHACK_EXPORT df::specific_ref* findRef(std::vector& vec, df::specific_ref_type type); + DFHACK_EXPORT bool removeRef(std::vector& vec, df::specific_ref_type type, void* ptr); + +} + +// A couple of headers that have to be included at once +#include "df/coord2d.h" +#include "df/coord.h" + +namespace std +{ + template <> + struct hash + { + std::size_t operator()(const df::coord& c) const + { + return c(); + } + }; +} + +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const df::coord& c, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {}, {})", c.x, c.y, c.z), ctx); + } +}; + +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const df::coord2d& c, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {})", c.x, c.y), ctx); + } +}; diff --git a/library/include/modules/Burrows.h b/library/include/modules/Burrows.h index dbd2e51dd6..2662826aad 100644 --- a/library/include/modules/Burrows.h +++ b/library/include/modules/Burrows.h @@ -23,11 +23,15 @@ distribution. */ #pragma once + +#include + #include "Export.h" #include "DataDefs.h" #include "modules/Maps.h" -#include +#include "df/coord.h" +#include "df/coord2d.h" /** * \defgroup grp_burrows Burrows module and its types diff --git a/library/include/modules/Designations.h b/library/include/modules/Designations.h index 914605e58a..5eb0e7cb86 100644 --- a/library/include/modules/Designations.h +++ b/library/include/modules/Designations.h @@ -1,5 +1,7 @@ #pragma once +#include "df/coord.h" + namespace df { struct plant; } diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index aa8907ac7e..9c6cc26d1d 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -36,7 +36,10 @@ distribution. #include "df/biome_type.h" #include "df/block_flags.h" +#include "df/coord.h" +#include "df/coord2d.h" #include "df/feature_type.h" +#include "df/flow_info.h" #include "df/flow_type.h" #include "df/matter_state.h" #include "df/tile_dig_designation.h" diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 61763ae295..652ed5fc2a 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -33,6 +33,7 @@ distribution. #include "DataDefs.h" #include "df/craft_material_class.h" +#include "df/item_type.h" namespace df { diff --git a/library/include/modules/World.h b/library/include/modules/World.h index a82ceef9a4..46b8f2e351 100644 --- a/library/include/modules/World.h +++ b/library/include/modules/World.h @@ -31,9 +31,14 @@ distribution. * @ingroup grp_modules */ +#include "DataDefs.h" #include "Export.h" + #include "modules/Persistence.h" -#include "DataDefs.h" + +#include "df/game_mode.h" +#include "df/game_type.h" +#include "df/unit.h" namespace df diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 6d49c6a3b7..226d7bc830 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -46,16 +46,18 @@ SOFTWARE. */ #include -#include -#include #include +#include #include +#include #include "modules/DFSDL.h" #include "modules/Filesystem.h" +#include "df/global_objects.h" #include "df/init.h" + using namespace DFHack; static bool initialized = false; diff --git a/library/modules/Renderer.cpp b/library/modules/Renderer.cpp index e134fa4f3c..dc80390960 100644 --- a/library/modules/Renderer.cpp +++ b/library/modules/Renderer.cpp @@ -2,6 +2,8 @@ #include "MiscUtils.h" #include "modules/Renderer.h" +#include "df/global_objects.h" + using namespace DFHack; using df::global::enabler; using df::global::gps; diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index 4a1e29aeab..dfa4c458fe 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -14,6 +14,7 @@ #include "VTableInterpose.h" #include "df/enabler.h" +#include "df/global_objects.h" #include "df/viewscreen_adopt_regionst.h" #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_new_arenast.h" diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp index 07c1ca795d..db25c75fbc 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -8,6 +8,7 @@ #include "modules/Random.h" #include "modules/World.h" +#include "df/global_objects.h" #include "df/inorganic_raw.h" #include "df/map_block.h" #include "df/world.h" diff --git a/plugins/aquifer.cpp b/plugins/aquifer.cpp index afb8bbe54f..ca00689628 100644 --- a/plugins/aquifer.cpp +++ b/plugins/aquifer.cpp @@ -3,9 +3,11 @@ #include "PluginManager.h" #include "PluginLua.h" #include "TileTypes.h" +#include "Types.h" #include "modules/Maps.h" +#include "df/global_objects.h" #include "df/map_block.h" #include "df/world.h" diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 61ee882071..8af8992450 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -13,6 +13,7 @@ #include "df/construction_type.h" #include "df/burrow.h" +#include "df/global_objects.h" #include "df/item.h" #include "df/job_item.h" #include "df/organic_mat_category.h" diff --git a/plugins/cleanconst.cpp b/plugins/cleanconst.cpp index 9e05ecae46..12fa7f1293 100644 --- a/plugins/cleanconst.cpp +++ b/plugins/cleanconst.cpp @@ -2,11 +2,14 @@ // and flags the constructions to recreate their components upon disassembly #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" +#include "Types.h" + #include "modules/Maps.h" -#include "DataDefs.h" +#include "df/global_objects.h" #include "df/item.h" #include "df/world.h" #include "df/construction.h" diff --git a/plugins/devel/check-structures-sanity/dispatch.cpp b/plugins/devel/check-structures-sanity/dispatch.cpp index 162ea7bbd4..a013048e8c 100644 --- a/plugins/devel/check-structures-sanity/dispatch.cpp +++ b/plugins/devel/check-structures-sanity/dispatch.cpp @@ -3,6 +3,7 @@ #include #include +#include "df/global_objects.h" #include "df/large_integer.h" Checker::Checker(color_ostream & out) : diff --git a/plugins/devel/check-structures-sanity/validate.cpp b/plugins/devel/check-structures-sanity/validate.cpp index d6137eff1f..37e739bdb4 100644 --- a/plugins/devel/check-structures-sanity/validate.cpp +++ b/plugins/devel/check-structures-sanity/validate.cpp @@ -1,5 +1,7 @@ #include "check-structures-sanity.h" +#include "df/global_objects.h" + #ifdef WIN32 #define WIN32_LEAN_AND_MEAN #define _WIN32_WINNT 0x0501 diff --git a/plugins/devel/dumpmats.cpp b/plugins/devel/dumpmats.cpp index 91b70a2676..b7bc7d4350 100644 --- a/plugins/devel/dumpmats.cpp +++ b/plugins/devel/dumpmats.cpp @@ -1,16 +1,17 @@ // Dump all hardcoded materials #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" -#include "DataDefs.h" -#include "df/world.h" -#include "df/material.h" #include "df/builtin_mats.h" -#include "df/matter_state.h" #include "df/descriptor_color.h" +#include "df/global_objects.h" #include "df/item_type.h" +#include "df/material.h" +#include "df/matter_state.h" #include "df/strain_type.h" +#include "df/world.h" using std::string; using std::vector; diff --git a/plugins/devel/eventExample.cpp b/plugins/devel/eventExample.cpp index 3b0bd860d4..c9715a6875 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -1,4 +1,7 @@ +#include +#include + #include "PluginManager.h" #include "DataDefs.h" @@ -8,6 +11,7 @@ #include "df/caste_body_info.h" #include "df/construction.h" #include "df/coord.h" +#include "df/global_objects.h" #include "df/item.h" #include "df/item_actual.h" #include "df/job.h" @@ -16,9 +20,6 @@ #include "df/unit_wound_layerst.h" #include "df/world.h" -#include -#include - using namespace DFHack; using namespace std; diff --git a/plugins/devel/frozen.cpp b/plugins/devel/frozen.cpp index a6d862a5a5..e97a7ebf0a 100644 --- a/plugins/devel/frozen.cpp +++ b/plugins/devel/frozen.cpp @@ -6,6 +6,7 @@ #include "modules/Maps.h" #include "df/block_square_event_frozen_liquidst.h" +#include "df/global_objects.h" #include "df/map_block.h" #include "df/world.h" diff --git a/plugins/devel/tilesieve.cpp b/plugins/devel/tilesieve.cpp index 9f21d98368..2360a0574a 100644 --- a/plugins/devel/tilesieve.cpp +++ b/plugins/devel/tilesieve.cpp @@ -1,16 +1,18 @@ // This is a generic plugin that does nothing useful apart from acting as an example... of a plugin that does nothing :D -// some headers required for a plugin. Nothing special, just the basics. -#include -#include -#include #include -// DF data structure definition headers + +#include "Console.h" #include "DataDefs.h" +#include "Export.h" +#include "PluginManager.h" +#include "TileTypes.h" + #include "modules/Maps.h" + +#include "df/global_objects.h" #include "df/map_block.h" #include "df/world.h" -#include "TileTypes.h" using namespace DFHack; using namespace df::enums; diff --git a/plugins/eventful.cpp b/plugins/eventful.cpp index 8dd3bbc85d..859649c470 100644 --- a/plugins/eventful.cpp +++ b/plugins/eventful.cpp @@ -1,3 +1,7 @@ +#include +#include +#include + #include "LuaTools.h" #include "PluginManager.h" #include "PluginLua.h" @@ -9,6 +13,7 @@ #include "df/building_furnacest.h" #include "df/building_workshopst.h" #include "df/construction.h" +#include "df/global_objects.h" #include "df/item.h" #include "df/item_actual.h" #include "df/job.h" @@ -22,11 +27,6 @@ #include "df/unit_wound.h" #include "df/world.h" -#include -#include -#include -#include - using std::vector; using std::string; using std::stack; diff --git a/plugins/flows.cpp b/plugins/flows.cpp index c13e355b6c..c99d21c33b 100644 --- a/plugins/flows.cpp +++ b/plugins/flows.cpp @@ -5,6 +5,7 @@ #include "PluginManager.h" #include "DataDefs.h" +#include "df/global_objects.h" #include "df/world.h" #include "df/map_block.h" #include "df/tile_liquid.h" diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 53219276bb..16fe5019d9 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "PluginManager.h" #include "TileTypes.h" @@ -7,6 +9,7 @@ #include "modules/Materials.h" #include "modules/Random.h" +#include "df/global_objects.h" #include "df/map_block.h" #include "df/map_block_column.h" #include "df/material.h" @@ -21,8 +24,6 @@ #include "df/world_object_data.h" #include "df/world_site.h" -#include - using std::string; using std::vector; using std::set; diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 50c0563ef3..4977dd3e30 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -11,6 +11,7 @@ #include "df/block_column_print_infost.h" #include "df/construction.h" #include "df/entity_plot_invasion_mapst.h" +#include "df/global_objects.h" #include "df/historical_entity.h" #include "df/invasion_info.h" #include "df/map_block.h" diff --git a/plugins/lair.cpp b/plugins/lair.cpp index ac5b88c4f9..dcfe3863fb 100644 --- a/plugins/lair.cpp +++ b/plugins/lair.cpp @@ -3,6 +3,7 @@ #include "modules/Maps.h" +#include "df/global_objects.h" #include "df/map_block.h" #include "df/world.h" diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 1392e8c12c..b722883804 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -5,6 +5,7 @@ #include "df/creature_raw.h" #include "df/caste_raw.h" +#include "df/global_objects.h" #include "df/world.h" using namespace DFHack; diff --git a/plugins/stockpiles/StockpileUtils.h b/plugins/stockpiles/StockpileUtils.h index e78ffccd8c..4899001d5f 100644 --- a/plugins/stockpiles/StockpileUtils.h +++ b/plugins/stockpiles/StockpileUtils.h @@ -5,6 +5,7 @@ #include "df/world.h" #include "df/creature_raw.h" +#include "df/global_objects.h" #include "df/plant_raw.h" // Utility Functions {{{ diff --git a/plugins/stockpiles/stockpiles.cpp b/plugins/stockpiles/stockpiles.cpp index 7165449cca..f9df8d7367 100644 --- a/plugins/stockpiles/stockpiles.cpp +++ b/plugins/stockpiles/stockpiles.cpp @@ -9,6 +9,7 @@ #include "df/building.h" #include "df/building_stockpilest.h" +#include "df/global_objects.h" #include "df/hauling_route.h" #include "df/hauling_stop.h" From ce919a1ee7205c783543b166eb42bc59b8981371 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 20 Aug 2026 19:22:42 -0500 Subject: [PATCH 1077/1082] remove blank line from `DataDefs.h` --- library/include/DataDefs.h | 1 - 1 file changed, 1 deletion(-) diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index b3500f3d92..5e03e9ab63 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -1016,4 +1016,3 @@ namespace DFHack { // Global object pointers #define DF_GLOBAL_VALUE(name,defval) (df::global::name ? *df::global::name : defval) #define DF_GLOBAL_FIELD(name,fname,defval) (df::global::name ? df::global::name->fname : defval) - From c5902a1c79cdad450b9edb4b29decaf2140fc6c8 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 22 Aug 2026 07:32:25 +0000 Subject: [PATCH 1078/1082] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 4ac3e364e7..de85988d55 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4ac3e364e7f0a0b1e23df7c4bb50998d172e61c6 +Subproject commit de85988d550f352d1be16b4d411c48d53fdbe469 From 868cbbef085f5805244a26afb9f25aed8841652f Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 22 Aug 2026 12:01:01 +0200 Subject: [PATCH 1079/1082] implement suggestions from code review --- library/include/MiscUtils.h | 14 +++++++------- plugins/export-world-map.cpp | 28 ++++++++++++++++------------ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 47183d4ea7..2676bb3d49 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -415,14 +415,14 @@ inline bool static_add_to_map(CT *pmap, const typename CT::key_type key, const t */ template< std::ranges::input_range Range, - std::invocable> Callable> + std::invocable> Callable> void print_range( std::ostream &out, const Range &elements, Callable&& print_element, - const std::string &prefix = "[", - const std::string &separator = ", ", - const std::string &suffix = "]" + const std::string_view prefix = "[", + const std::string_view separator = ", ", + const std::string_view suffix = "]" ){ out << prefix; auto it = std::ranges::begin(elements); @@ -442,9 +442,9 @@ template void print_range( std::ostream &out, const Range& elements, - const std::string &prefix = "[", - const std::string &separator = ", ", - const std::string &suffix = "]" + const std::string_view prefix = "[", + const std::string_view separator = ", ", + const std::string_view suffix = "]" ){ auto print_element = [](std::ostream &out, auto& e) { out << e; }; print_range(out, elements, print_element, prefix, separator, suffix); diff --git a/plugins/export-world-map.cpp b/plugins/export-world-map.cpp index 867d11fe50..dc1532fdaf 100644 --- a/plugins/export-world-map.cpp +++ b/plugins/export-world-map.cpp @@ -69,9 +69,9 @@ constexpr int wdim = 768; // dimension of a world tile constexpr int rdim = 48; // dimension of a region tile /** - * Takes a vector of coordinates interpreted as global region tile coordinates + * Takes a range of coordinates interpreted as global region tile coordinates * (i.e. 16 region tiles per world tile) and emits a WKT path in GIS-compatible - * local tile coordinates (negative y-coordinates, 48 stepts per region tile) + * local tile coordinates (negative y-coordinates, 48 map units per region tile) */ static void print_path(std::ostream &out, const std::vector &path) { auto scale = rdim; @@ -81,6 +81,10 @@ static void print_path(std::ostream &out, const std::vector &path) { print_range(out, path, print_point, "(", ",", ")"); } +// format various values separated by the separator character (ends with a separator) +static auto print_csv_line(std::ostream &out, char sep, auto ...args) { + ([&]{ out << args << sep; }() ,...); +} static command_result do_command(color_ostream &out, vector ¶meters); static command_result export_regions(color_ostream &out); @@ -192,8 +196,8 @@ static command_result export_sites(color_ostream &out) } // If you change anything in this vector, don't forget to change the - // corresponding comments and arguments in the call to print_csv below - vector headings = { + // corresponding comments and arguments in the call to print_csv_line below + constexpr std::array headings = { "site_id", "civ_id", "created_year", "cur_owner_id", "type", "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" }; @@ -217,8 +221,7 @@ static command_result export_sites(color_ostream &out) } } - auto print_csv = [&out_file](auto ...args){ ([&]{ out_file << args << ";" ; }() ,...); }; - print_csv( + print_csv_line(out_file, ';', // "site_id", "civ_id", "created_year", "cur_owner_id", "type", site->id, site->civ_id, @@ -436,8 +439,8 @@ static command_result export_regions(color_ostream &out) } // If you change anything in this vector, don't forget to change the - // corresponding comments and arguments in the call to print_csv below - vector headings = { + // corresponding comments and arguments in the call to print_csv_line below + constexpr std::array headings = { "world_x", "world_y", "num_tiles", "num_components", "biome_type", "region_id", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df", "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "salinity", @@ -507,8 +510,7 @@ static command_result export_regions(color_ostream &out) auto world_region = df::world_region::find(region_map_entry.region_id); auto landmass = df::world_landmass::find(region_map_entry.landmass_id); - auto print_csv = [&out_line](auto ...args){ ([&]{ out_line << args << ";" ; }() ,...); }; - print_csv( + print_csv_line(out_line, ';', // "world_x", "world_y", "num_tiles", "num_components", "biome_type", biome_tile.x, biome_tile.y, @@ -805,8 +807,10 @@ static command_result export_rivers(color_ostream &out) for (auto& [r_idx, river_tiles] : tile_index) { auto river = world->world_data->rivers.at(r_idx); - out_file << DF2UTF(Translation::translateName(&river->name, false)) << ";"; - out_file << DF2UTF(Translation::translateName(&river->name, true)) << ";"; + print_csv_line(out_file, ';', + DF2UTF(Translation::translateName(&river->name, false)), + DF2UTF(Translation::translateName(&river->name, true)) + ); out_file << "MULTIPOLYGON("; bool first = true; for (auto &tile : river_tiles) { From cb8641c564b26e7119a96e27f31e4cc5e6ac781b Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 22 Aug 2026 13:30:09 +0200 Subject: [PATCH 1080/1082] use std::span for print_path --- plugins/export-world-map.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/export-world-map.cpp b/plugins/export-world-map.cpp index dc1532fdaf..20357caf68 100644 --- a/plugins/export-world-map.cpp +++ b/plugins/export-world-map.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -73,7 +74,7 @@ constexpr int rdim = 48; // dimension of a region tile * (i.e. 16 region tiles per world tile) and emits a WKT path in GIS-compatible * local tile coordinates (negative y-coordinates, 48 map units per region tile) */ -static void print_path(std::ostream &out, const std::vector &path) { +static void print_path(std::ostream &out, const std::span path) { auto scale = rdim; assert(path.size()); auto print_point = [scale](std::ostream &out, const coord &pos){ @@ -234,14 +235,14 @@ static command_result export_sites(color_ostream &out) TRANSLATE_DF_EN(owner, owner->name), race ? race->name[2] : "NONE" ); - const vector path{ + const std::array path{ coord(site->global_min_x, site->global_min_y), coord(site->global_max_x+1, site->global_min_y), coord(site->global_max_x+1, site->global_max_y+1), coord(site->global_min_x, site->global_max_y+1), coord(site->global_min_x, site->global_min_y) }; - print_range(out_file, std::vector>{path}, print_path , "POLYGON(", ",", ")\n" ); + print_range(out_file, std::array, 1>{path}, print_path , "POLYGON(", ",", ")\n" ); } const auto finish{std::chrono::steady_clock::now()}; From a0ad43f74a5140d08e12cfba764fe5f0b543803a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 23 Aug 2026 07:33:55 +0000 Subject: [PATCH 1081/1082] 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 5aee008756..a4353651b3 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 5aee008756c9f8514bfdbb1248deac77c8833262 +Subproject commit a4353651b35a43e69c14f2fed5c10e0b110c11b1 diff --git a/scripts b/scripts index de85988d55..e6dbde83c2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit de85988d550f352d1be16b4d411c48d53fdbe469 +Subproject commit e6dbde83c2311662b5aa811c71f34a19f1f0599e From ce23009e593f1de1dbc2f6a8d27d8c937720ff8e Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:58:07 +0000 Subject: [PATCH 1082/1082] 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 a4353651b3..eb8464f5f7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a4353651b35a43e69c14f2fed5c10e0b110c11b1 +Subproject commit eb8464f5f78c3de63782c0498051cd37c2862a8c