From 1b79147cce59e0288424ed2f5e438823e332185d Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 1 Jan 2025 11:34:58 -0600 Subject: [PATCH 001/272] Create helloSlider.lua Created helloSlider.lua (a prototype for a new single-slider widget) --- devel/helloSlider.lua | 212 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 devel/helloSlider.lua diff --git a/devel/helloSlider.lua b/devel/helloSlider.lua new file mode 100644 index 0000000000..ff850d0982 --- /dev/null +++ b/devel/helloSlider.lua @@ -0,0 +1,212 @@ +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 + + + + + + + + + + + +local gui = require('gui') +local widgets = require('gui.widgets') + +-- +-- RangerWindow +-- + +RangerWindow = defclass(RangerWindow, widgets.Window) +RangerWindow.ATTRS { + frame_title='Hello, Slider!', + frame={w=25, h=8}, + resizable=true, + resize_min={w=25, h=8}, +} + +function RangerWindow:init() + local LEVEL_OPTIONS = { + {label='Low', value=1}, + {label='Medium', value=2}, + {label='High', value=3}, + {label='Pro', value=4}, + {label='Insane', value=5}, + } + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='level', + frame={l=1, t=0, w=16}, + label='Level:', + label_below=true, + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options=LEVEL_OPTIONS, + initial_option=LEVEL_OPTIONS[1].value, + on_change=function(val) + self.subviews.level:setOption(val) + end, + }, + Slider{ + frame={l=1, t=3}, + num_stops=#LEVEL_OPTIONS, + get_idx_fn=function() + return self.subviews.level:getOptionValue() + end, + on_change=function(idx) self.subviews.level:setOption(idx) end, + }, + } +end + +-- +-- RangerScreen +-- + +RangerScreen = defclass(RangerScreen, gui.ZScreen) +RangerScreen.ATTRS { + focus_path='ranger', +} + +function RangerScreen:init() + self:addviews{RangerWindow{}} +end + +function RangerScreen:onDismiss() + view = nil +end + +-- +-- main logic +-- + +view = view and view:raise() or RangerScreen{}:show() \ No newline at end of file From 9d68acaef29eac40be4cbd85a7541d41a2e423e0 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 1 Jan 2025 11:37:53 -0600 Subject: [PATCH 002/272] Update helloSlider.lua to try and fix precommit EOF errors --- devel/helloSlider.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/helloSlider.lua b/devel/helloSlider.lua index ff850d0982..f22b7c7a6b 100644 --- a/devel/helloSlider.lua +++ b/devel/helloSlider.lua @@ -209,4 +209,4 @@ end -- main logic -- -view = view and view:raise() or RangerScreen{}:show() \ No newline at end of file +view = view and view:raise() or RangerScreen{}:show() From 81e5179d29ef12f8ea44d8efee35bd34157b9014 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:25:44 -0600 Subject: [PATCH 003/272] Move code to new files --- devel/helloSlider.lua | 143 +----------------------------------------- devel/slider.lua | 132 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 142 deletions(-) create mode 100644 devel/slider.lua diff --git a/devel/helloSlider.lua b/devel/helloSlider.lua index f22b7c7a6b..e2e250df6b 100644 --- a/devel/helloSlider.lua +++ b/devel/helloSlider.lua @@ -1,144 +1,3 @@ -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 - - - - - - - - - - - local gui = require('gui') local widgets = require('gui.widgets') @@ -177,7 +36,7 @@ function RangerWindow:init() self.subviews.level:setOption(val) end, }, - Slider{ + widgets.Slider{ frame={l=1, t=3}, num_stops=#LEVEL_OPTIONS, get_idx_fn=function() diff --git a/devel/slider.lua b/devel/slider.lua new file mode 100644 index 0000000000..92263d2924 --- /dev/null +++ b/devel/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 735f5d5a25d5cce7e6575d69aa6ed745bcad1ecd Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:15:32 -0600 Subject: [PATCH 004/272] Clean up code Move Slider example into the hello world move the Slider widget away to its proper place (DFHack Repo). --- devel/hello-world.lua | 33 ++++++++++- devel/helloSlider.lua | 71 ----------------------- devel/slider.lua | 132 ------------------------------------------ 3 files changed, 32 insertions(+), 204 deletions(-) delete mode 100644 devel/helloSlider.lua delete mode 100644 devel/slider.lua diff --git a/devel/hello-world.lua b/devel/hello-world.lua index 679bf1d52e..bf872f4baa 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -12,13 +12,23 @@ local HIGHLIGHT_PEN = dfhack.pen.parse{ HelloWorldWindow = defclass(HelloWorldWindow, widgets.Window) HelloWorldWindow.ATTRS{ - frame={w=20, h=14}, + frame={w=25, h=20}, frame_title='Hello World', autoarrange_subviews=true, autoarrange_gap=1, + resizable=true, + resize_min={w=25, h=20}, } function HelloWorldWindow:init() + local LEVEL_OPTIONS = { + {label='Low', value=1}, + {label='Medium', value=2}, + {label='High', value=3}, + {label='Pro', value=4}, + {label='Insane', value=5}, + } + self:addviews{ widgets.Label{text={{text='Hello, world!', pen=COLOR_LIGHTGREEN}}}, widgets.HotkeyLabel{ @@ -32,6 +42,27 @@ function HelloWorldWindow:init() frame={w=10, h=5}, frame_style=gui.INTERIOR_FRAME, }, + widgets.CycleHotkeyLabel{ + view_id='level', + frame={l=1, t=0, w=16}, + label='Level:', + label_below=true, + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options=LEVEL_OPTIONS, + initial_option=LEVEL_OPTIONS[1].value, + on_change=function(val) + self.subviews.level:setOption(val) + end, + }, + widgets.Slider{ + frame={l=1, t=3}, + num_stops=#LEVEL_OPTIONS, + get_idx_fn=function() + return self.subviews.level:getOptionValue() + end, + on_change=function(idx) self.subviews.level:setOption(idx) end, + }, } end diff --git a/devel/helloSlider.lua b/devel/helloSlider.lua deleted file mode 100644 index e2e250df6b..0000000000 --- a/devel/helloSlider.lua +++ /dev/null @@ -1,71 +0,0 @@ -local gui = require('gui') -local widgets = require('gui.widgets') - --- --- RangerWindow --- - -RangerWindow = defclass(RangerWindow, widgets.Window) -RangerWindow.ATTRS { - frame_title='Hello, Slider!', - frame={w=25, h=8}, - resizable=true, - resize_min={w=25, h=8}, -} - -function RangerWindow:init() - local LEVEL_OPTIONS = { - {label='Low', value=1}, - {label='Medium', value=2}, - {label='High', value=3}, - {label='Pro', value=4}, - {label='Insane', value=5}, - } - - self:addviews{ - widgets.CycleHotkeyLabel{ - view_id='level', - frame={l=1, t=0, w=16}, - label='Level:', - label_below=true, - key_back='CUSTOM_SHIFT_C', - key='CUSTOM_SHIFT_V', - options=LEVEL_OPTIONS, - initial_option=LEVEL_OPTIONS[1].value, - on_change=function(val) - self.subviews.level:setOption(val) - end, - }, - widgets.Slider{ - frame={l=1, t=3}, - num_stops=#LEVEL_OPTIONS, - get_idx_fn=function() - return self.subviews.level:getOptionValue() - end, - on_change=function(idx) self.subviews.level:setOption(idx) end, - }, - } -end - --- --- RangerScreen --- - -RangerScreen = defclass(RangerScreen, gui.ZScreen) -RangerScreen.ATTRS { - focus_path='ranger', -} - -function RangerScreen:init() - self:addviews{RangerWindow{}} -end - -function RangerScreen:onDismiss() - view = nil -end - --- --- main logic --- - -view = view and view:raise() or RangerScreen{}:show() diff --git a/devel/slider.lua b/devel/slider.lua deleted file mode 100644 index 92263d2924..0000000000 --- a/devel/slider.lua +++ /dev/null @@ -1,132 +0,0 @@ -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 9055dc8fe5a46e56786c7928493d7616593ee5ab Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:34:33 -0600 Subject: [PATCH 005/272] Clean up the code and add a divider --- devel/hello-world.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/devel/hello-world.lua b/devel/hello-world.lua index bf872f4baa..5e8b22ebb6 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -12,10 +12,10 @@ local HIGHLIGHT_PEN = dfhack.pen.parse{ HelloWorldWindow = defclass(HelloWorldWindow, widgets.Window) HelloWorldWindow.ATTRS{ - frame={w=25, h=20}, + frame={w=25, h=25}, frame_title='Hello World', autoarrange_subviews=true, - autoarrange_gap=1, + autoarrange_gap=2, resizable=true, resize_min={w=25, h=20}, } @@ -28,7 +28,7 @@ function HelloWorldWindow:init() {label='Pro', value=4}, {label='Insane', value=5}, } - + self:addviews{ widgets.Label{text={{text='Hello, world!', pen=COLOR_LIGHTGREEN}}}, widgets.HotkeyLabel{ @@ -42,9 +42,12 @@ function HelloWorldWindow:init() frame={w=10, h=5}, frame_style=gui.INTERIOR_FRAME, }, + widgets.Divider{ + frame={l=0,t=3} + }, widgets.CycleHotkeyLabel{ view_id='level', - frame={l=1, t=0, w=16}, + frame={l=0, t=3, w=16}, label='Level:', label_below=true, key_back='CUSTOM_SHIFT_C', From 45c1997ec1077abd50ddefed6003e22df22777c0 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 26 Jan 2025 14:42:16 -0600 Subject: [PATCH 006/272] Update hello-world.lua --- devel/hello-world.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/devel/hello-world.lua b/devel/hello-world.lua index 5e8b22ebb6..12b9eab7c3 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -17,7 +17,7 @@ HelloWorldWindow.ATTRS{ autoarrange_subviews=true, autoarrange_gap=2, resizable=true, - resize_min={w=25, h=20}, + resize_min={w=25, h=25}, } function HelloWorldWindow:init() @@ -32,7 +32,7 @@ function HelloWorldWindow:init() self:addviews{ widgets.Label{text={{text='Hello, world!', pen=COLOR_LIGHTGREEN}}}, widgets.HotkeyLabel{ - frame={l=0, t=0}, + frame={l=0}, label='Click me', key='CUSTOM_CTRL_A', on_activate=self:callback('toggleHighlight'), @@ -43,23 +43,25 @@ function HelloWorldWindow:init() frame_style=gui.INTERIOR_FRAME, }, widgets.Divider{ - frame={l=0,t=3} + frame={h=1}, + frame_style_l=false, + frame_style_r=false, }, widgets.CycleHotkeyLabel{ view_id='level', - frame={l=0, t=3, w=16}, + frame={l=0, w=20}, label='Level:', - label_below=true, + label_below=false, key_back='CUSTOM_SHIFT_C', key='CUSTOM_SHIFT_V', options=LEVEL_OPTIONS, initial_option=LEVEL_OPTIONS[1].value, on_change=function(val) - self.subviews.level:setOption(val) + self.callback{Slider.on_change(val)} end, }, widgets.Slider{ - frame={l=1, t=3}, + frame={l=1}, num_stops=#LEVEL_OPTIONS, get_idx_fn=function() return self.subviews.level:getOptionValue() From 1bc3501b5ee48ab70b24f96947314c4c3f4a89e7 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Tue, 11 Feb 2025 15:38:17 -0600 Subject: [PATCH 007/272] Update hello-world.lua --- devel/hello-world.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/devel/hello-world.lua b/devel/hello-world.lua index 12b9eab7c3..2af576f815 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -51,14 +51,10 @@ function HelloWorldWindow:init() view_id='level', frame={l=0, w=20}, label='Level:', - label_below=false, key_back='CUSTOM_SHIFT_C', key='CUSTOM_SHIFT_V', options=LEVEL_OPTIONS, initial_option=LEVEL_OPTIONS[1].value, - on_change=function(val) - self.callback{Slider.on_change(val)} - end, }, widgets.Slider{ frame={l=1}, From 2d6d854cb295f1eb16533a1528a31d28c42b7dae Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Tue, 11 Feb 2025 15:40:04 -0600 Subject: [PATCH 008/272] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 4965da0d76..31788f2d80 100644 --- a/changelog.txt +++ b/changelog.txt @@ -43,6 +43,7 @@ Template for new versions: - `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. +- `devel/hello-world`: updated to show off the new Slider widget ## Removed From de91116d8698f3568f60e10531c1423658d26041 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 6 Mar 2025 16:22:15 -0800 Subject: [PATCH 009/272] gui/adv-finder * Create adv-finder.lua * Create adv-finder.rst * Update changelog.txt * Update aquifer.rst - fix keybind label * Update stuckdoors.lua - isActive by default --- changelog.txt | 1 + docs/gui/adv-finder.rst | 112 ++++++ docs/gui/aquifer.rst | 2 +- fix/stuckdoors.lua | 2 +- gui/adv-finder.lua | 773 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 888 insertions(+), 2 deletions(-) create mode 100644 docs/gui/adv-finder.rst create mode 100644 gui/adv-finder.lua diff --git a/changelog.txt b/changelog.txt index 7b530fff3a..c28ff421b6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk - `gui/spectate`: interactive UI for configuring new `spectate` features - `gui/notes`: UI for adding and managing notes attached to tiles on the map +- `gui/adv-finder`: UI for tracking historical figures and artifacts in adventure mode ## New Features - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys diff --git a/docs/gui/adv-finder.rst b/docs/gui/adv-finder.rst new file mode 100644 index 0000000000..a1d12128e7 --- /dev/null +++ b/docs/gui/adv-finder.rst @@ -0,0 +1,112 @@ +gui/adv-finder +============== + +.. dfhack-tool:: + :summary: Find and track historical figures and artifacts + :tags: adventure armok inspection items units + +A real-time tracker for historical figures and artifacts. Select a target by +clicking the settings icon [☼] and selecting an entry from the list in the +relevant tab. The list can be filtered by search string, as well as by +excluding dead figures (displayed in red text). Artifacts can exclude books, +and the "dead" option excludes artifacts held by dead figures (which are +generally unrecoverable). Dismissing the screen (e.g., right-click) will +close the target search window first. A second dismissal will close the +finder window, but target settings will be preserved until the world is +unloaded. + +Your coordinates will be kept up to date alongside your target's. There are +two types of coordinates, and they will be displayed as long as they can be +determined. + +========== ========== +Coord Type Meaning +========== ========== +Global Distance in map blocks from the world origin (northwest corner). + The adventurer usually moves by 3 blocks during fast travel, but + slows to 1 when the zoomed site map is displayed. Equivalent to + 16 local tiles. Always available except for targets with an + indeterminate location. +Local Tile coordinates, available outside of fast travel and sleeping. + Your target's local coordinates are displayed when nearby and + loaded. Local coordinates will remain consistent within a site, but + may jump around in the wilderness as areas of the world are loaded. +========== ========== + +For global coordinates, the Z component will only be displayed if it can be +specifically determined by the location type. This represents an underground +layer depth, so the surface is indicated by ``Z0`` and the first cavern layer +is ``Z-1``. + +A compass and relative coordinates will be displayed. The relative coordinate +display uses the most precise coordinate type shared between you and your +target. + +There are six types of location types displayed for targets: + +============= ========== +Location Type Meaning +============= ========== +Nearby The target is loaded into the map area and the local + coordinates will be displayed. If you don't see this when you're + in the correct area and outside fast travel, then the target + isn't loading for some reason and you'll never be able to find + them. +Site The target is located within a site. The text displays + "At " and the global coords will represent the center + of the site if the target doesn't track its own precise + coordinates (e.g., worldgen being vague). +Traveling The target is traveling around the world map like an army. +Wilderness The target is somewhere on the surface not in a site. +Underground The target is somewhere in the caverns not in a site. +None The target's location isn't defined in the game world. + Maybe they're a deity. Maybe they got dropped off in limbo + after their army disbanded. If they're dead, the location + wasn't recorded properly in history. The text displays "Missing" + if they're dead or can die of old age, else "Transcendent" + because nothing can touch them. +============= ========== + +Dead figures generally can't be encountered at all, and they take their items +with them if they weren't separated properly by worldgen. The coord given is +usually a death or abstract burial location, but the corpse isn't guaranteed to +exist. Generally, wilderness and underground locations only have coords if you +left something there in adventure mode. Anything lost there during worldgen or a +fort mode mission likely can't be located. Anything in a site is usually a safe +bet, but sometimes items won't load. (Fort missions can be used to acquire these +for later retrieval, however.) Traveling targets are always valid. + +Usage +----- + +:: + + gui/adv-finder [] + +Examples +-------- + +``gui/adv-finder`` + Open the finder window (unless already open). Target will be blank on first + use, but maintained on future invocations. +``gui/adv-finder --histfig 1234`` + Track the historical figure with ID #1234. Finder will be opened if not + already. +``gui/adv-finder -h -1 -a -1`` + Clear any target so it's just the adventurer. Finder will be opened if not + already. +``gui/adv-finder --debug`` + Display selected target IDs in the finder window title bar. Finder will be + opened if not already. This setting isn't saved, so it can be disabled by + invoking ``gui/adv-finder`` again without the option. + +Options +------- + +``-h``, ``--histfig `` + Set the target to the historical figure with the given ID. +``-a``, ``--artifact `` + Set the target to the artifact record with the given ID. (Not an item ID!) +``-d``, ``--debug`` + Display selected target IDs in the finder window title bar. Doesn't persist + between invocations. diff --git a/docs/gui/aquifer.rst b/docs/gui/aquifer.rst index 52d47541b8..a595e197d1 100644 --- a/docs/gui/aquifer.rst +++ b/docs/gui/aquifer.rst @@ -12,7 +12,7 @@ tiles or warm tiles). Note that "just damp" tiles will still be highlighted if they are otherwise already visible. You can draw boxes around areas of tiles to alter their aquifer properties, or -you can use the :kbd:`Ctrl`:kbd:`A`` shortcut to affect entire layers at a time. +you can use the :kbd:`Ctrl`:kbd:`A` shortcut to affect entire layers at a time. If you want to see where the aquifer tiles are so you can designate digging, please run `gui/reveal`. If you only want to see the aquifer tiles and not diff --git a/fix/stuckdoors.lua b/fix/stuckdoors.lua index 8e1e1bc026..ec48a847b5 100644 --- a/fix/stuckdoors.lua +++ b/fix/stuckdoors.lua @@ -9,7 +9,7 @@ end -- Util function: find out if there are any units on the tile with coordinates x,y,z function unitOnTile(x, y, z) - local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z,dfhack.units.isActive) + local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z) return #(units) > 0 end diff --git a/gui/adv-finder.lua b/gui/adv-finder.lua new file mode 100644 index 0000000000..9831b0b04e --- /dev/null +++ b/gui/adv-finder.lua @@ -0,0 +1,773 @@ +-- Find and track historical figures and artifacts +--@module = true + +local argparse = require('argparse') +local gui = require('gui') +local widgets = require('gui.widgets') +local utils = require('utils') + +local world = df.global.world +local transName = dfhack.translation.translateName +local findHF = df.historical_figure.find +local toSearch = dfhack.toSearchNormalized + +LType = utils.invert{'None','Local','Site','Wild','Under','Army'} --Location type + +filter_text = filter_text --Stored filter between lists; for setting only! +-- Use AdvSelWindow:get_filter_text() instead for getting current filter +cur_tab = cur_tab or 1 -- 1: HF, 2: Artifact +show_dead = show_dead or false --Exclude dead HFs +show_books = show_books or false --Exclude books +sel_hf = sel_hf or -1 --Selected historical_figure.id +sel_art = sel_art or -1 --Selected artifact_record.id +debug_id = false --Show target ID in window title; reopening without -d option resets + +---- Fns for target names ---- + +local function get_race_name(hf) --E.g., 'Plump Helmet Man' + return dfhack.capitalizeStringWords(dfhack.units.getRaceReadableNameById(hf.race)) +end + +function get_hf_name(hf) --'Native Name "Translated Name", Race' + local full_name = transName(hf.name, false) + if full_name == '' then --Improve searchability + full_name = 'Anonymous' + else --Add the translation + local t_name = transName(hf.name, true) + if full_name ~= t_name then --Don't repeat + full_name = full_name..' "'..t_name..'"' + end + end + local race_name = get_race_name(hf) + if race_name == '' then --Elf deities don't have a race + full_name = full_name..', Force' + else --Add the race + full_name = full_name..', '..race_name + end + return full_name +end + +function get_art_name(ar) --'Native Name "Translated Name", Item' + local full_name = transName(ar.name, false) + if full_name == '' then --Improve searchability + full_name = 'Anonymous' + else --Add the translation + local t_name = transName(ar.name, true) + if full_name ~= t_name then --Don't repeat + full_name = full_name..' "'..t_name..'"' + end + end + return full_name..', '..dfhack.items.getDescription(ar.item, 1, true) +end + +local function build_hf_list() --Build alphabetized HF list + local t = {} + for _,hf in ipairs(world.history.figures) do + if show_dead or hf.died_year == -1 then --Filter dead + local name = get_hf_name(hf) + local str = toSearch(name) + + if hf.died_year ~= -1 then + name = {{text=name, pen=COLOR_RED}} --Dead + elseif not hf.info or not hf.info.whereabouts then + name = {{text=name, pen=COLOR_YELLOW}} --Deity (usually) + end + table.insert(t, {text=name, id=hf.id, search_key=str}) + end + end + table.sort(t, function(a, b) return a.search_key < b.search_key end) + return t +end + +local function get_id(first, second) --Try to get a numeric id or -1 + return (first >= 0 and first) or (second >= 0 and second) or -1 +end + +local function dead_holder(ar) --Return true if has holder and they're dead + local holder = df.historical_figure.find(get_id(ar.holder_hf, ar.owner_hf)) + return holder and holder.died_year ~= -1 +end + +local function is_book(ar) --Return true if codex/scroll/quire + local item = ar.item + return item._type == df.item_bookst or --We'll ignore slabs, despite legends mode behaviour + (item._type == df.item_toolst and item:hasToolUse(df.tool_uses.CONTAIN_WRITING)) +end + +local function build_art_list() --Build alphabetized artifact list + local t = {} + for _,ar in ipairs(world.artifacts.all) do + local dead = dead_holder(ar) + if (show_dead or not dead) and (show_books or not is_book(ar)) then + local name = get_art_name(ar) + local str = toSearch(name) + + if dead then + name = {{text=name, pen=COLOR_RED}} + end + table.insert(t, {text=name, id=ar.id, search_key=str}) + end + end + table.sort(t, function(a, b) return a.search_key < b.search_key end) + return t +end + +------------------ +-- AdvSelWindow -- +------------------ + +AdvSelWindow = defclass(AdvSelWindow, widgets.Window) +AdvSelWindow.ATTRS{ + frame_title = 'Find Target', + frame = {w=42, h=24, t=22, r=34}, + resizable = true, + visible = false, +} + +function AdvSelWindow:init() + self:addviews{ + widgets.TabBar{ + frame = {t=0}, + labels = { + 'Historical Figures', + 'Artifacts', + }, + on_select = self:callback('swap_tab'), + get_cur_page = function() return cur_tab end, + }, + widgets.FilteredList{ + view_id = 'sel_hf_list', + frame = {t=2, b=2}, + not_found_label = 'No results', + edit_key = 'CUSTOM_ALT_S', + on_submit = self:callback('select_entry'), + visible = false, --Handled in sel_list + }, + widgets.FilteredList{ --setChoices is too slow, don't reuse HF list + view_id = 'sel_art_list', + frame = {t=2, b=2}, + not_found_label = 'No results', + edit_key = 'CUSTOM_ALT_S', + on_submit = self:callback('select_entry'), + visible = false, + }, + widgets.ToggleHotkeyLabel + { + view_id = 'dead_toggle', + frame = {b=0, l=0, w=17, h=1}, + label = 'Show dead:', + key = 'CUSTOM_SHIFT_D', + initial_option = show_dead, + on_change = self:callback('set_show_dead'), + }, + widgets.ToggleHotkeyLabel + { + view_id = 'book_toggle', + frame = {b=0, r=0, w=18, h=1}, + label = 'Show books:', + key = 'CUSTOM_SHIFT_B', + initial_option = show_books, + on_change = self:callback('set_show_books'), + visible = function() return cur_tab ~= 1 end, + }, + } +end + +function AdvSelWindow:get_filter_text() --Get current filter from tab + if cur_tab == 1 then --HF + return self.subviews.sel_hf_list:getFilter() + else --Artifact + return self.subviews.sel_art_list:getFilter() + end +end + +function AdvSelWindow:swap_tab(idx) --Persist filter and swap list + if cur_tab ~= idx then + filter_text = self:get_filter_text() + cur_tab = idx + self:sel_list() + end +end + +function AdvSelWindow:sel_list() --Set correct list for tab + local new, old, build_fn + if cur_tab == 1 then --HF + new = self.subviews.sel_hf_list + old = self.subviews.sel_art_list + build_fn = build_hf_list + else --Artifact + new = self.subviews.sel_art_list + old = self.subviews.sel_hf_list + build_fn = build_art_list + end + + old.visible = false + new.visible = true + if not next(new:getChoices()) then --Empty, build list + new:setChoices(build_fn()) + end + new:setFilter(filter_text) --Restore filter + new.edit:setFocus(old.edit.focus) --Inherit search focus + old.edit:setFocus(false) +end + +function AdvSelWindow:select_entry(sel, obj) --Set correct target for tab + local id = obj and obj.id or -1 + if cur_tab == 1 then --HF + sel_hf, sel_art = id, -1 + else --Artifact + sel_hf, sel_art = -1, id + end +end + +function AdvSelWindow:set_show_dead(show) --Set filtering of dead HFs, rebuild list + show = not not show --To bool + if show == show_dead then + return --No change + end + show_dead = show + filter_text = self:get_filter_text() + self.subviews.sel_hf_list:setChoices() + self.subviews.sel_art_list:setChoices() --Held by HF + self:sel_list() +end + +function AdvSelWindow:set_show_books(show) --Set filtering of books, rebuild list + show = not not show + if show == show_books then + return + end + show_books = show + filter_text = self:get_filter_text() + self.subviews.sel_art_list:setChoices() + self:sel_list() +end + +function AdvSelWindow:onInput(keys) --Close only this window + if keys.LEAVESCREEN or keys._MOUSE_R then + self.visible = false + filter_text = self:get_filter_text() + self.subviews.sel_hf_list:setChoices() + self.subviews.sel_art_list:setChoices() + return true + end + return self.super.onInput(self, keys) +end + +---- Fns for getting adventurer data ---- + +function global_from_local(pos) --Calc global coords (blocks from world origin) from local map pos + return pos and {x = world.map.region_x*3 + pos.x//16, y = world.map.region_y*3 + pos.y//16} or nil +end + +function get_adv_data() --All the coords we can get + local adv = dfhack.world.getAdventurer() + if not adv then --Army exists when unit doesn't + local army = df.army.find(df.global.adventure.player_army_id) + if army then --Should always exist if unit doesn't + return {g_pos = army.pos} + end + return nil --Error + end + return {g_pos = global_from_local(adv.pos), pos = adv.pos} +end + +---- Fns for getting target data ---- + +local function div(n, d) return n//d, n%d end +--We can get the MLT coords of a CZ from its ID (e.g., hf.info.whereabouts.cz_id) +--The g_pos will represent the center of the 3x3 MLT +--In testing, the HF of interest remained in limbo, but it might be of use to someone +function cz_g_pos(cz_id) --Creation zone center in global coords + if not cz_id or cz_id < 0 then return nil end + local w, t, rem = world.world_data.world_width, {}, nil + t.reg_y, rem = div(cz_id, 16*16*w) + t.mlt_y, rem = div(rem, 16*w) + t.reg_x, t.mlt_x = div(rem, 16) + return {x = (t.reg_x*16 + t.mlt_x)*3+1, y = (t.reg_y*16 + t.mlt_y)*3+1} +end + +function site_g_pos(site) --Site center in global coords (blocks from world origin) + local x, y = site.global_min_x*3, site.global_min_y*3 + x, y = x + (site.global_max_x*3 - x)//2, y + (site.global_max_y*3 - y)//2 + return {x = x, y = y} +end + +local function apply_site_z(site, g_pos) --Improve Z coord using site + local pos = g_pos or site_g_pos(site) --Fall back on site center + pos.z = site.min_depth == site.max_depth and site.min_depth or nil --Single layer site + return pos --Return new table +end + +local function death_at_idx(idx) --Return death location data + if idx then --Dead + local event = world.history.events_death[idx] + return {site = event.site, sr = event.subregion, layer = event.feature_layer} + end + return {site = -1, sr = -1, layer = -1} --Alive +end + +local death_hfid, death_found_idx, death_last_idx --Cache history.events_death data +function get_death_data(hf) --Try to get death location data + if hf.died_year == -1 then --Alive (or undead) + return death_at_idx() + elseif hf.id ~= death_hfid then --Wrong HF, clear cache + death_hfid, death_found_idx, death_last_idx = hf.id, nil, nil + end + local deaths = world.history.events_death + local deaths_end = #deaths-1 + + if death_last_idx and death_last_idx == deaths_end then --No new entries + return death_at_idx(death_found_idx) --Use cached death + end + death_last_idx = death_last_idx or 0 --First time search entire vector + + for i=deaths_end, death_last_idx, -1 do --Iterate new entries backwards + local event = deaths[i] + if event._type == df.history_event_hist_figure_diedst then + if event.victim_hf == hf.id then + death_found_idx = i --Cache HF's most recent death + break + end + elseif event._type == df.history_event_hist_figure_revivest then + if event.histfig == hf.id then --Just in case died_year check failed somehow + death_found_idx = nil --Clear death state + break + end + end + end + death_last_idx = deaths_end --Cache latest index + return death_at_idx(death_found_idx) +end + +local function get_whereabouts(hf) --Return state profile data + local w = hf and hf.info and hf.info.whereabouts + if w then + local g_pos = w.abs_smm_x >= 0 and {x = w.abs_smm_x, y = w.abs_smm_y} or nil + return {site = w.site_id, sr = w.subregion_id, layer = w.feature_layer_id, army = w.army_id, g_pos = g_pos} + end + return {site = -1, sr = -1, layer = -1, army = -1} +end + +function get_hf_data(hf) --Locational data and coords + if not hf then --No target + return nil + end + + local where = get_whereabouts(hf) + for _,unit in ipairs(world.units.active) do + if unit.id == hf.unit_id then --Unit is loaded and active (i.e., player not traveling) + local pos = xyz2pos(dfhack.units.getPosition(unit)) + pos = pos.x >= 0 and pos or nil --Avoid bad coords + local g_pos = global_from_local(pos) or where.g_pos + return {loc_type = LType.Local, g_pos = g_pos, pos = pos} + end + end + local death = get_death_data(hf) + + local site = df.world_site.find(get_id(where.site, death.site)) + if site then --Site + return {loc_type = LType.Site, site = site, g_pos = apply_site_z(site, where.g_pos)} + end + + local sr = df.world_region.find(get_id(where.sr, death.sr)) + if sr then --Surface biome + if where.g_pos then + where.g_pos.z = 0 --Must be surface + end + return {loc_type = LType.Wild, sr = sr, g_pos = where.g_pos} + end + + local layer = df.world_underground_region.find(get_id(where.layer, death.layer)) + if layer then --Cavern layer + if where.g_pos then + where.g_pos.z = layer.layer_depth + end + return {loc_type = LType.Under, g_pos = where.g_pos} + end + + local army = df.army.find(where.army) + if army then --Traveling + return {loc_type = LType.Army, g_pos = army.pos} + end + + if #hf.site_links > 0 then --Try to grab site from links + local site = df.world_site.find(hf.site_links[#hf.site_links-1].site) --Only try last link + if site and utils.binsearch(site.populace.nemesis, hf.nemesis_id) then --HF is present + return {loc_type = LType.Site, site = site, g_pos = apply_site_z(site, where.g_pos)} + end + end + --We'd try cz_g_pos here if it actually helped + return {loc_type = LType.None, g_pos = where.g_pos} --Probably in limbo +end + +function get_art_data(ar) --Locational data and coords + if not ar then --No target + return nil + end + local holder = findHF(get_id(ar.holder_hf, ar.owner_hf)) + local data = get_hf_data(holder) or {loc_type = LType.None} + data.holder = holder + + local g_pos = ar.abs_tile_x >= 0 and {x = ar.abs_tile_x//16, y = ar.abs_tile_y//16} or nil + + for _,item in ipairs(world.items.other.ANY_ARTIFACT) do + if item == ar.item then --Item is nearby if categorized + local pos = xyz2pos(dfhack.items.getPosition(item)) + pos = pos.x >= 0 and pos or nil --Avoid bad coords + g_pos = global_from_local(pos) or g_pos + return {loc_type = LType.Local, holder = holder, g_pos = g_pos, pos = pos} + end + end + + local site = df.world_site.find(get_id(ar.site, ar.storage_site)) + if site then --Site + return {loc_type = LType.Site, site = site, holder = holder, g_pos = apply_site_z(site, g_pos)} + end + + if data.loc_type ~= LType.None then --Inherit from holder (seems lower priority than site) + return data + end + + local sr = df.world_region.find(get_id(ar.subregion, ar.loss_region)) + if sr then --Surface biome + if g_pos then + g_pos.z = 0 --Must be surface + end + return {loc_type = LType.Wild, holder = holder, sr = sr, g_pos = g_pos} + end + + local layer = df.world_underground_region.find(get_id(ar.feature_layer, ar.last_layer)) + if layer then --Cavern layer + if g_pos then + g_pos.z = layer.layer_depth + end + return {loc_type = LType.Under, holder = holder, g_pos = g_pos} + end + + data.g_pos = data.g_pos or g_pos or nil --Try our own if no holder g_pos + return data --Probably in limbo +end + +---- Fns for adventurer info panel ---- + +local compass_dir = { + 'E','ENE','NE','NNE', + 'N','NNW','NW','WNW', + 'W','WSW','SW','SSW', + 'S','SSE','SE','ESE', +} +local compass_pointer = { --Same chars as movement indicators + '>',string.char(191),string.char(191),string.char(191), + '^',string.char(218),string.char(218),string.char(218), + '<',string.char(192),string.char(192),string.char(192), + 'v',string.char(217),string.char(217),string.char(217), +} + +local idx_div_two_pi = 16/(2*math.pi) --16 indices / 2*Pi radians +function compass(dx, dy) --Handy compass strings + if dx*dx + dy*dy == 0 then --On target + return '***', string.char(249) --Char 249 is centered dot + end + local angle = math.atan(-dy, dx) --North is -Y + local index = math.floor(angle*idx_div_two_pi + 16.5)%16 --0.5 helps rounding + return compass_dir[index + 1], compass_pointer[index + 1] +end + +local function insert_text(t, text) --Insert newline before text + if text and text ~= '' then + table.insert(t, NEWLINE) + table.insert(t, text) + end +end + +local function relative_text(t, adv_data, target_data) --Add relative coords and compass + if not target_data then --No target + return + end + if target_data.pos and adv_data.pos then --Use local + local dx = target_data.pos.x - adv_data.pos.x + local dy = target_data.pos.y - adv_data.pos.y + local dir, point = compass(dx, dy) + table.insert(t, NEWLINE) --Improve visibility + insert_text(t, 'Target (local):') + insert_text(t, point..' '..dir) + insert_text(t, ('X%+d Y%+d Z%+d'):format(dx, dy, target_data.pos.z - adv_data.pos.z)) + elseif target_data.g_pos and adv_data.g_pos then --Use global + local dx = target_data.g_pos.x - adv_data.g_pos.x + local dy = target_data.g_pos.y - adv_data.g_pos.y + local dir, point = compass(dx, dy) + table.insert(t, NEWLINE) + insert_text(t, {text='Target (global):', pen=COLOR_GREY}) + insert_text(t, {text=point..' '..dir, pen=COLOR_GREY}) + + local str = ('X%+d Y%+d'):format(dx, dy) + if target_data.g_pos.z and adv_data.g_pos.z then --Use Z if we have it + str = str..(' Z%+d'):format(adv_data.g_pos.z - target_data.g_pos.z) --Negate because it's depth + end + insert_text(t, {text=str, pen=COLOR_GREY}) + end --else insufficient data +end + +local function pos_text(t, g_pos, pos) --Add available coords + if g_pos then + local str = g_pos.z and (' Z'..-g_pos.z) or '' --Use Z if we have it, negate because it's depth + insert_text(t, {text='Global: X'..g_pos.x..' Y'..g_pos.y..str, pen=COLOR_GREY}) + else --Keep compass in consistent spot + table.insert(t, NEWLINE) + end + if pos then + insert_text(t, ('Local: X%d Y%d Z%d'):format(pos.x, pos.y, pos.z)) + else + table.insert(t, NEWLINE) + end +end + +local function adv_text(adv_data, target_data) --Text for adv info panel + if not adv_data then + return 'Error' + end + local t = {'You'} --You, global, local, relative + pos_text(t, adv_data.g_pos, adv_data.pos) + + relative_text(t, adv_data, target_data) + return t +end + +---- Fns for target info panel ---- + +local function insert_name_text(t, name) --HF or artifact name; Return true if both lines + local str = transName(name, false) + if str == '' then + table.insert(t, 'Anonymous') + else --Both native and translation + table.insert(t, str) --Native + local t_name = transName(name, true) + if str ~= t_name then --Don't repeat + insert_text(t, '"'..t_name..'"') + return true + end + end +end + +local function hf_text(hf, target_data) --HF text for target info panel + if not hf or not target_data then --No target + return '' + end + local t = {} --Native, [translated], race, alive, location, global, local + + local both_lines = insert_name_text(t, hf.name) + local str = get_race_name(hf) + insert_text(t, str ~= '' and str or 'Force') + if not both_lines then --Consistent spacing + table.insert(t, NEWLINE) + end + + local eternal --Can't reasonably die + if hf.died_year ~= -1 then + insert_text(t, {text='DEAD', pen=COLOR_RED}) + elseif hf.old_year == -1 and target_data.loc_type == LType.None then + eternal = true --In limbo and can't reasonably die + insert_text(t, {text='ETERNAL', pen=COLOR_LIGHTBLUE}) + else + insert_text(t, {text='ALIVE', pen=COLOR_LIGHTGREEN}) + end + + if target_data.loc_type == LType.None then --Everywhere or nowhere + if eternal then + insert_text(t, {text='Transcendent', pen=COLOR_YELLOW}) + else + insert_text(t, {text='Missing', pen=COLOR_MAGENTA}) + end + else --Physical location + if target_data.loc_type == LType.Local then + insert_text(t, 'Nearby') + elseif target_data.loc_type == LType.Site then + insert_text(t, {text='At '..transName(target_data.site.name, true), pen=COLOR_LIGHTBLUE}) + elseif target_data.loc_type == LType.Army then + insert_text(t, {text='Traveling', pen=COLOR_LIGHTBLUE}) + elseif target_data.loc_type == LType.Wild then + insert_text(t, {text='Wilderness ('..transName(target_data.sr.name, true)..')', pen=COLOR_LIGHTRED}) + elseif target_data.loc_type == LType.Under then + insert_text(t, {text='Underground', pen=COLOR_LIGHTRED}) + else --Undefined loc_type + insert_text(t, {text='Error', pen=COLOR_MAGENTA}) + end + end + pos_text(t, target_data.g_pos, target_data.pos) + return t +end + +local function art_text(art, target_data) --Artifact text for target info panel + if not art or not target_data then --No target + return '' + end + local t = {} --Native, [translated], item_type, [held,] location, global, local + + local both_lines = insert_name_text(t, art.name) + insert_text(t, dfhack.items.getDescription(art.item, 1, true)) + if not both_lines then --Consistent spacing + table.insert(t, NEWLINE) + end + + if target_data.holder then + local str = 'Held by '..transName(target_data.holder.name, false) + insert_text(t, {text=str, pen=(target_data.holder.died_year == -1 and COLOR_LIGHTGREEN or COLOR_RED)}) + else --Consistent spacing + table.insert(t, NEWLINE) + end + + if target_data.loc_type == LType.None then + insert_text(t, {text='Missing', pen=COLOR_MAGENTA}) + elseif target_data.loc_type == LType.Local then + insert_text(t, 'Nearby') + elseif target_data.loc_type == LType.Site then + insert_text(t, {text='At '..transName(target_data.site.name, true), pen=COLOR_LIGHTBLUE}) + elseif target_data.loc_type == LType.Army then + insert_text(t, {text='Traveling', pen=COLOR_LIGHTBLUE}) + elseif target_data.loc_type == LType.Wild then + insert_text(t, {text='Wilderness ('..transName(target_data.sr.name, true)..')', pen=COLOR_LIGHTRED}) + elseif target_data.loc_type == LType.Under then + insert_text(t, {text='Underground', pen=COLOR_LIGHTRED}) + else --Undefined loc_type + insert_text(t, {text='Error', pen=COLOR_MAGENTA}) + end + pos_text(t, target_data.g_pos, target_data.pos) + return t +end + +------------------- +-- AdvFindWindow -- +------------------- + +AdvFindWindow = defclass(AdvFindWindow, widgets.Window) +AdvFindWindow.ATTRS{ + frame_title = 'Finder', + frame = {w=30, h=24, t=22, r=2}, + resizable = true, +} + +function AdvFindWindow:init() + self:addviews{ + widgets.Panel{ + view_id = 'adv_panel', + frame = {t=1, h=9}, + frame_style = gui.FRAME_INTERIOR, + subviews = { + widgets.Label{ + view_id = 'adv_label', + text = '', + frame = {t=0}, + }, + }, + }, + widgets.Panel{ + view_id = 'target_panel', + frame = {t=11}, + frame_style = gui.FRAME_INTERIOR, + subviews = { + widgets.Label{ + view_id = 'target_label', + text = '', + frame = {t=0}, + }, + }, + }, + widgets.ConfigureButton{ + frame = {t=0, r=0}, + on_click = function() + local sel_window = view.subviews[2] --AdvSelWindow + sel_window.visible = true + sel_window:sel_list() + end, + } + } +end + +local function set_title(self) --Display target ID in title + if debug_id then + local id = get_id(sel_hf, sel_art) + self.frame_title = 'Finder'..(id ~= -1 and ' (#'..id..')' or '') + else + self.frame_title = 'Finder' + end +end + +function AdvFindWindow:onRenderFrame(dc, rect) + if not dfhack.world.isAdventureMode() then --Could be advfort, etc. + view:dismiss() + print('gui/adv-finder: lost adv mode, dismissing view') + end + self.super.onRenderFrame(self, dc, rect) + + local adv_panel = self.subviews.adv_panel + local target_panel = self.subviews.target_panel + + local target_data + if sel_hf >= 0 then --HF + local target_hf = findHF(sel_hf) + target_data = get_hf_data(target_hf) + target_panel.subviews.target_label:setText(hf_text(target_hf, target_data)) + elseif sel_art >= 0 then --Artifact + local target_art = df.artifact_record.find(sel_art) + target_data = get_art_data(target_art) + target_panel.subviews.target_label:setText(art_text(target_art, target_data)) + else --None + target_panel.subviews.target_label:setText() + end + adv_panel.subviews.adv_label:setText(adv_text(get_adv_data(), target_data)) + + adv_panel:updateLayout() + target_panel:updateLayout() + set_title(self) +end + +------------------- +-- AdvFindScreen -- +------------------- + +AdvFindScreen = defclass(AdvFindScreen, gui.ZScreen) +AdvFindScreen.ATTRS{ + focus_path = 'advfinder', +} + +function AdvFindScreen:init() + self:addviews{AdvFindWindow{}, AdvSelWindow{}} +end + +function AdvFindScreen:onDismiss() + view = nil +end + +if dfhack_flags.module then + return +end + +if not dfhack.world.isAdventureMode() then + qerror('Adventure mode only!') +end + +dfhack.onStateChange['adv-finder'] = function(sc) + if sc == SC_WORLD_UNLOADED then --Data is world-specific + sel_hf = -1 --Invalidate IDs + sel_art = -1 + filter_text = nil --Probably unwanted + cur_tab = 1 --Reset to first tab, but keep other settings + print('gui/adv-finder: cleared target') + dfhack.onStateChange['adv-finder'] = nil --Do once + end +end + +argparse.processArgsGetopt({...}, { + {'h', 'histfig', handler = function(arg) + sel_hf = math.tointeger(arg) or -1 + sel_art = -1 + end, hasArg = true}, + {'a', 'artifact', handler = function(arg) + sel_art = math.tointeger(arg) or -1 + sel_hf = -1 + end, hasArg = true}, + {'d', 'debug', handler = function() debug_id = true end}, +}) + +view = view and view:raise() or AdvFindScreen{}:show() From 4cacf13427f3a7a69cdb3920a99041b4821f3fda Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:09:00 -0500 Subject: [PATCH 010/272] Update deathcause.lua make it more api-like --- deathcause.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index 953f36bd22..a735aee249 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -29,8 +29,7 @@ function displayDeathUnit(unit) str = str .. dfhack.units.getReadableName(unit) if not dfhack.units.isDead(unit) then - print(dfhack.df2console(str) .. " is not dead yet!") - return + return(dfhack.df2console(str) .. " is not dead yet!") end str = str .. (" %s"):format(getDeathStringFromCause(unit.counters.death_cause)) @@ -50,7 +49,7 @@ function displayDeathUnit(unit) end end - print(dfhack.df2console(str) .. '.') + return(dfhack.df2console(str) .. '.') end -- returns the item description if the item still exists; otherwise @@ -87,7 +86,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) end end - print(dfhack.df2console(str) .. '.') + return(dfhack.df2console(str) .. '.') end -- Returns the death event for the given histfig or nil if not found @@ -109,10 +108,10 @@ function displayDeathHistFig(histfig) end if not dfhack.units.isDead(histfig_unit) then - print(("%s is not dead yet!"):format(dfhack.df2console(dfhack.units.getReadableName(histfig_unit)))) + return(("%s is not dead yet!"):format(dfhack.df2console(dfhack.units.getReadableName(histfig_unit)))) else local death_event = getDeathEventForHistFig(histfig.id) - displayDeathEventHistFigUnit(histfig_unit, death_event) + return displayDeathEventHistFigUnit(histfig_unit, death_event) end end @@ -155,7 +154,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - displayDeathUnit(selected_unit) + print(displayDeathUnit(selected_unit)) else - displayDeathHistFig(df.historical_figure.find(hist_figure_id)) + print(displayDeathHistFig(df.historical_figure.find(hist_figure_id))) end From c4483b557f10c4c7c4a625badd4cb02cd572d1c3 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:20:03 -0500 Subject: [PATCH 011/272] Update deathcause.lua Remove the console formatting Signed-off-by: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> --- deathcause.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index a735aee249..d07d62d866 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -29,7 +29,7 @@ function displayDeathUnit(unit) str = str .. dfhack.units.getReadableName(unit) if not dfhack.units.isDead(unit) then - return(dfhack.df2console(str) .. " is not dead yet!") + return(str .. " is not dead yet!") end str = str .. (" %s"):format(getDeathStringFromCause(unit.counters.death_cause)) @@ -49,7 +49,7 @@ function displayDeathUnit(unit) end end - return(dfhack.df2console(str) .. '.') + return(str .. '.') end -- returns the item description if the item still exists; otherwise @@ -86,7 +86,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) end end - return(dfhack.df2console(str) .. '.') + return( str .. '.') end -- Returns the death event for the given histfig or nil if not found @@ -108,7 +108,7 @@ function displayDeathHistFig(histfig) end if not dfhack.units.isDead(histfig_unit) then - return(("%s is not dead yet!"):format(dfhack.df2console(dfhack.units.getReadableName(histfig_unit)))) + return(("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit))) else local death_event = getDeathEventForHistFig(histfig.id) return displayDeathEventHistFigUnit(histfig_unit, death_event) From 2fb14b623074f9d4aeba73b2fb31feecc4e92fe8 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:23:48 -0500 Subject: [PATCH 012/272] Update deathcause.lua Format to keep old usage working Signed-off-by: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> --- deathcause.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index d07d62d866..9ba2d73b34 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -154,7 +154,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - print(displayDeathUnit(selected_unit)) + print(dfhack.df2console(displayDeathUnit(selected_unit))) else - print(displayDeathHistFig(df.historical_figure.find(hist_figure_id))) + print(dfhack.df2console(displayDeathHistFig(df.historical_figure.find(hist_figure_id)))) end From cf4af78cc62114bdd478828d4573a0d05cabaf2b Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 17:25:10 -0500 Subject: [PATCH 013/272] Update deathcause.lua final fixes --- deathcause.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index 9ba2d73b34..d0a90dd6c9 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -1,4 +1,5 @@ -- show death cause of a creature +--@ module = true local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES @@ -29,7 +30,7 @@ function displayDeathUnit(unit) str = str .. dfhack.units.getReadableName(unit) if not dfhack.units.isDead(unit) then - return(str .. " is not dead yet!") + return str .. " is not dead yet!" end str = str .. (" %s"):format(getDeathStringFromCause(unit.counters.death_cause)) @@ -49,7 +50,7 @@ function displayDeathUnit(unit) end end - return(str .. '.') + return str .. '.' end -- returns the item description if the item still exists; otherwise @@ -86,7 +87,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) end end - return( str .. '.') + return str .. '.' end -- Returns the death event for the given histfig or nil if not found @@ -108,7 +109,7 @@ function displayDeathHistFig(histfig) end if not dfhack.units.isDead(histfig_unit) then - return(("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit))) + return ("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit)) else local death_event = getDeathEventForHistFig(histfig.id) return displayDeathEventHistFigUnit(histfig_unit, death_event) @@ -146,6 +147,10 @@ local function get_target() return selected_item.hist_figure_id, df.unit.find(selected_item.unit_id) end +if dfhack_flags.module then + return +end + local hist_figure_id, selected_unit = get_target() if not hist_figure_id then From add5d172d82b3d136529509a4963911694488975 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 13 Apr 2025 14:15:09 -0500 Subject: [PATCH 014/272] Update deathcause.lua --- deathcause.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index d0a90dd6c9..0ea44d24e7 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -4,7 +4,7 @@ local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES -- Gets the first corpse item at the given location -function getItemAtPosition(pos) +local function getItemAtPosition(pos) for _, item in ipairs(df.global.world.items.other.ANY_CORPSE) do if item.pos.x == pos.x and item.pos.y == pos.y and item.pos.z == pos.z then print("Automatically chose first corpse at the selected location.") @@ -13,11 +13,11 @@ function getItemAtPosition(pos) end end -function getRaceNameSingular(race_id) +local function getRaceNameSingular(race_id) return df.creature_raw.find(race_id).name[0] end -function getDeathStringFromCause(cause) +local function getDeathStringFromCause(cause) if cause == -1 then return "died" else @@ -25,7 +25,7 @@ function getDeathStringFromCause(cause) end end -function displayDeathUnit(unit) +function getDeathUnit(unit) local str = unit.name.has_name and '' or 'The ' str = str .. dfhack.units.getReadableName(unit) @@ -63,7 +63,7 @@ function getWeaponName(item_id, subtype) return dfhack.items.getDescription(item, 0, false) end -function displayDeathEventHistFigUnit(histfig_unit, event) +function getDeathEventHistFigUnit(histfig_unit, event) local str = ("The %s %s %s in year %d"):format( getRaceNameSingular(histfig_unit.race), dfhack.translation.translateName(dfhack.units.getVisibleName(histfig_unit)), @@ -102,7 +102,7 @@ function getDeathEventForHistFig(histfig_id) end end -function displayDeathHistFig(histfig) +function getDeathHistFig(histfig) local histfig_unit = df.unit.find(histfig.unit_id) if not histfig_unit then qerror("Cause of death not available") @@ -112,7 +112,7 @@ function displayDeathHistFig(histfig) return ("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit)) else local death_event = getDeathEventForHistFig(histfig.id) - return displayDeathEventHistFigUnit(histfig_unit, death_event) + return getDeathEventHistFigUnit(histfig_unit, death_event) end end @@ -159,7 +159,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - print(dfhack.df2console(displayDeathUnit(selected_unit))) + print(dfhack.df2console(getDeathUnit(selected_unit))) else - print(dfhack.df2console(displayDeathHistFig(df.historical_figure.find(hist_figure_id)))) + print(dfhack.df2console(getDeathHistFig(df.historical_figure.find(hist_figure_id)))) end From 3977dd1b9829bc64d23f158bb65cb0d3e996f2f6 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 15 Apr 2025 19:12:23 -0700 Subject: [PATCH 015/272] Fix changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index ef4d58e27d..3274dd97b6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `gui/adv-finder`: UI for tracking historical figures and artifacts in adventure mode ## New Features @@ -60,7 +61,6 @@ Template for new versions: - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk - `gui/spectate`: interactive UI for configuring `spectate` - `gui/notes`: UI for adding and managing notes attached to tiles on the map -- `gui/adv-finder`: UI for tracking historical figures and artifacts in adventure mode - `launch`: (reinstated) new adventurer fighting move: thrash your enemies with a flying suplex - `putontable`: (reinstated) make an item appear on a table From bedf4266c7b969c5d8031749f70b380ed9a0e719 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 15 Apr 2025 19:15:28 -0700 Subject: [PATCH 016/272] adv-finder.lua - Improve site center pos --- gui/adv-finder.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/adv-finder.lua b/gui/adv-finder.lua index 9831b0b04e..cfd17b7646 100644 --- a/gui/adv-finder.lua +++ b/gui/adv-finder.lua @@ -288,8 +288,8 @@ function cz_g_pos(cz_id) --Creation zone center in global coords end function site_g_pos(site) --Site center in global coords (blocks from world origin) - local x, y = site.global_min_x*3, site.global_min_y*3 - x, y = x + (site.global_max_x*3 - x)//2, y + (site.global_max_y*3 - y)//2 + local x, y = site.global_min_x, site.global_min_y + x, y = (x + (site.global_max_x - x)//2)*3+1, (y + (site.global_max_y - y)//2)*3+1 return {x = x, y = y} end From b86f9f03bdb8ca00afa17e6362ad18b4d7bd5ed4 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 18 Apr 2025 12:28:19 -0500 Subject: [PATCH 017/272] Update deathcause.lua localize some more functions and then rename the APIs and add comments --- deathcause.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index 0ea44d24e7..6c212821ac 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -25,7 +25,8 @@ local function getDeathStringFromCause(cause) end end -function getDeathUnit(unit) +-- Returns a cause of death given a unit +function getDeathCauseFromUnit(unit) local str = unit.name.has_name and '' or 'The ' str = str .. dfhack.units.getReadableName(unit) @@ -55,7 +56,7 @@ end -- returns the item description if the item still exists; otherwise -- returns the weapon name -function getWeaponName(item_id, subtype) +local function getWeaponName(item_id, subtype) local item = df.item.find(item_id) if not item then return df.global.world.raws.itemdefs.weapons[subtype].name @@ -63,7 +64,7 @@ function getWeaponName(item_id, subtype) return dfhack.items.getDescription(item, 0, false) end -function getDeathEventHistFigUnit(histfig_unit, event) +local function getDeathEventHistFigUnit(histfig_unit, event) local str = ("The %s %s %s in year %d"):format( getRaceNameSingular(histfig_unit.race), dfhack.translation.translateName(dfhack.units.getVisibleName(histfig_unit)), @@ -91,7 +92,7 @@ function getDeathEventHistFigUnit(histfig_unit, event) end -- Returns the death event for the given histfig or nil if not found -function getDeathEventForHistFig(histfig_id) +local function getDeathEventForHistFig(histfig_id) for i = #df.global.world.history.events - 1, 0, -1 do local event = df.global.world.history.events[i] if event:getType() == df.history_event_type.HIST_FIGURE_DIED then @@ -102,7 +103,8 @@ function getDeathEventForHistFig(histfig_id) end end -function getDeathHistFig(histfig) +-- Returns the cause of death given a histfig +function getDeathCauseFromHistFig(histfig) local histfig_unit = df.unit.find(histfig.unit_id) if not histfig_unit then qerror("Cause of death not available") @@ -159,7 +161,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - print(dfhack.df2console(getDeathUnit(selected_unit))) + print(dfhack.df2console(getDeathCauseFromUnit(selected_unit))) else - print(dfhack.df2console(getDeathHistFig(df.historical_figure.find(hist_figure_id)))) + print(dfhack.df2console(getDeathCauseFromHistFig(df.historical_figure.find(hist_figure_id)))) end From 435fcd066dcd6ee0e271a67fbd2e4d1dbfec376c Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Mar 2025 05:35:01 -0500 Subject: [PATCH 018/272] confirm: only show pause option for pausable confirmations Prevent dialogs.showYesNoPrompt from showing the pause option by passing a nil on_pause argument when a confirmation is not pausable (e.g., trade-cancel, depot-remove). --- changelog.txt | 1 + confirm.lua | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 49a241e338..c9b0c8ac22 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: ## Fixes - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game +- `confirm`: only show pause option for pausable confirmations ## Misc Improvements diff --git a/confirm.lua b/confirm.lua index fb0a108ed6..9fb114e9b3 100644 --- a/confirm.lua +++ b/confirm.lua @@ -131,8 +131,9 @@ function ConfirmOverlay:onInput(keys) gui.simulateInput(scr, keys) self.simulating = false end + local pause_fn = conf.pausable and curry(propagate_fn, true) or nil dialogs.showYesNoPrompt(conf.title, utils.getval(conf.message):wrap(45), COLOR_YELLOW, - propagate_fn, nil, curry(propagate_fn, true), curry(dfhack.run_script, 'gui/confirm', tostring(conf.id))) + propagate_fn, nil, pause_fn, curry(dfhack.run_script, 'gui/confirm', tostring(conf.id))) return true end end From 2a73f93541404ab181abaf06fb9f392c02c65a7a Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Mar 2025 05:58:55 -0500 Subject: [PATCH 019/272] confirm: handle LEAVESCREEN in uniform-discard-changes Match the _MOUSE_R handling: prompt if there are uniform changes. --- changelog.txt | 1 + internal/confirm/specs.lua | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index c9b0c8ac22..ad4e2676d7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,7 @@ Template for new versions: - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game - `confirm`: only show pause option for pausable confirmations +- `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape ## Misc Improvements diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 73b9179e41..03d55fae82 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -325,13 +325,13 @@ ConfirmSpec{ id='uniform-discard-changes', title='Discard uniform changes', message='Are you sure you want to discard changes to this uniform?', - intercept_keys={'_MOUSE_L', '_MOUSE_R'}, + intercept_keys={'LEAVESCREEN', '_MOUSE_L', '_MOUSE_R'}, -- sticks out the left side so it can move with the panel -- when the screen is resized too narrow intercept_frame={r=32, t=19, w=101, b=3}, context='dwarfmode/Squads/Equipment/Customizing/Default', predicate=function(keys, mouse_offset) - if keys._MOUSE_R then + if keys.LEAVESCREEN or keys._MOUSE_R then return uniform_has_changes() end if clicked_on_confirm_button(mouse_offset) then From fe09c126e4811ab43a5e7e1eaf1b2a954d3c11a8 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Wed, 9 Apr 2025 06:52:09 -0500 Subject: [PATCH 020/272] confirm: use interface rect for order-remove calculations When the DF interface percentage is not 100, the interface width can be smaller than the window width. For certain sizes (depending on the interface percentage), the following condition can hold: - interface width <= 154 < window width In this situation, the info window tab row will not have "unwrapped" (from four to two UI rows), but the previous order index calculation code would assume that it had (due to using the window width). This two UI row discrepancy caused the calculated order index to be one too high (and possibly out of bounds) when clicking on either of the bottom two UI rows of an order remove button. The incorrect index caused the confirmation to display the description of the following order. --- changelog.txt | 1 + internal/confirm/specs.lua | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index ad4e2676d7..02bd55ccd1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -39,6 +39,7 @@ Template for new versions: - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game - `confirm`: only show pause option for pausable confirmations - `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape +- `confirm`: when removing a manager order, show correct order description when using non-100% interface setting ## Misc Improvements diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 03d55fae82..9147674330 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -7,6 +7,7 @@ local json = require('json') local trade_internal = reqscript('internal/caravan/trade') +local gui = require('gui') local CONFIG_FILE = 'dfhack-config/confirm.json' @@ -457,7 +458,7 @@ ConfirmSpec{ message=function() local order_desc = '' local scroll_pos = mi.info.work_orders.scroll_position_work_orders - local y_offset = dfhack.screen.getWindowSize() > 154 and 8 or 10 + local y_offset = gui.get_interface_rect().width > 154 and 8 or 10 local _, y = dfhack.screen.getMousePos() if y then local order_idx = scroll_pos + (y - y_offset) // 3 From ded125a055f0d7b452b7628156ca88ce68c44f03 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Mon, 24 Mar 2025 02:38:37 -0500 Subject: [PATCH 021/272] confirm: order-remove: work around stale scroll position The scroll_position_work_orders value is being used here as the index of the first displayed order. DF seems to maintain this when the list view is updated via scrolling, but DF does not update it in at least two important situations: - when an order is removed, and - when the order list view grows enough to display additional orders (e.g., by increasing the height of the DF window). When the order list view is not scrolled all the way to the bottom, DF handles these actions by pushing orders into view at the bottom of the list view. Since the same order is still at the top of the list view, this does not require an update of the reported scroll position value. However, when the order list view is scrolled all the way to the bottom, DF handles these actions by pushing orders into view at the *top* of the list view. Since a new order is now at the top of the list view, we would expect that DF should have updated the reported scroll position, but it does not. The lack of scroll position update breaks our expectation that the reported scroll position should match the index of the first displayed order. If N orders have been removed and M order rows have been added to the order list view (after having scrolled to the bottom of the order list), our calculated order_idx will be N+M too high (causing mismatched descriptions in the confirmation dialogs, and out-of-bounds errors that entirely prevent confirmation when acting on the last N+M orders!). When there are at least as many orders as the height of the orders list view, DF seems to always keep the bottom of the list view populated. Assume this is will be case and adjust our order_idx calculation when the reported scroll position would put the effective index of the last order out of bounds. If DF's order list view ever changes to displaying empty order rows even when there are orders that could be "pushed in" from the top, this will need to be revisited. --- changelog.txt | 1 + internal/confirm/specs.lua | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 02bd55ccd1..d373953650 100644 --- a/changelog.txt +++ b/changelog.txt @@ -40,6 +40,7 @@ Template for new versions: - `confirm`: only show pause option for pausable confirmations - `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape - `confirm`: when removing a manager order, show correct order description when using non-100% interface setting +- `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) ## Misc Improvements diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 9147674330..4526daa6bb 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -458,7 +458,16 @@ ConfirmSpec{ message=function() local order_desc = '' local scroll_pos = mi.info.work_orders.scroll_position_work_orders - local y_offset = gui.get_interface_rect().width > 154 and 8 or 10 + local ir = gui.get_interface_rect() + local y_offset = ir.width > 154 and 8 or 10 + local order_rows = (ir.height - y_offset - 9) // 3 + local max_scroll_pos = math.max(0, #orders - order_rows) -- DF keeps list view "full" (no empty rows at bottom), if possible + if scroll_pos > max_scroll_pos then + -- sometimes, DF does not adjust scroll_position_work_orders (when + -- scrolled to bottom: order removed, or list view height grew); + -- compensate to keep order_idx in sync (and in bounds) + scroll_pos = max_scroll_pos + end local _, y = dfhack.screen.getMousePos() if y then local order_idx = scroll_pos + (y - y_offset) // 3 From fc3e30038ffd16d5a9c6a67b30ae57371cfdb607 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 25 Mar 2025 05:36:01 -0500 Subject: [PATCH 022/272] confirm: rework order-remove description generation Handle possibly out-of-bounds order index. Factor out call to material description generation. --- internal/confirm/specs.lua | 52 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 4526daa6bb..5abd2aa58a 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -431,7 +431,7 @@ ConfirmSpec{ end, } -local function make_order_desc(order, noun) +local function make_order_material_desc(order, noun) local desc = '' if order.mat_type >= 0 then local matinfo = dfhack.matinfo.decode(order.mat_type, order.mat_index) @@ -452,6 +452,34 @@ end local orders = df.global.world.manager_orders.all local itemdefs = df.global.world.raws.itemdefs local reactions = df.global.world.raws.reactions.reactions + +local function make_order_desc(order) + if order.job_type == df.job_type.CustomReaction then + for _, reaction in ipairs(reactions) do + if reaction.code == order.reaction_name then + return reaction.name + end + end + return '' + end + local noun + if order.job_type == df.job_type.MakeArmor then + noun = itemdefs.armor[order.item_subtype].name + elseif order.job_type == df.job_type.MakeWeapon then + noun = itemdefs.weapons[order.item_subtype].name + elseif order.job_type == df.job_type.MakePants then + noun = itemdefs.pants[order.item_subtype].name + elseif order.job_type == df.job_type.MakeTool then + noun = itemdefs.tools[order.item_subtype].name + elseif order.job_type == df.job_type.SmeltOre then + noun = 'ore' + else + -- caption is usually "verb noun(-phrase)" + noun = df.job_type.attrs[order.job_type].caption + end + return make_order_material_desc(order, noun) +end + ConfirmSpec{ id='order-remove', title='Remove manger order', @@ -471,25 +499,9 @@ ConfirmSpec{ local _, y = dfhack.screen.getMousePos() if y then local order_idx = scroll_pos + (y - y_offset) // 3 - local order = orders[order_idx] - if order.job_type == df.job_type.CustomReaction then - for _, reaction in ipairs(reactions) do - if reaction.code == order.reaction_name then - order_desc = reaction.name - end - end - elseif order.job_type == df.job_type.MakeArmor then - order_desc = make_order_desc(order, itemdefs.armor[order.item_subtype].name) - elseif order.job_type == df.job_type.MakeWeapon then - order_desc = make_order_desc(order, itemdefs.weapons[order.item_subtype].name) - elseif order.job_type == df.job_type.MakePants then - order_desc = make_order_desc(order, itemdefs.pants[order.item_subtype].name) - elseif order.job_type == df.job_type.SmeltOre then - order_desc = make_order_desc(order, 'ore') - elseif order.job_type == df.job_type.MakeTool then - order_desc = make_order_desc(order, itemdefs.tools[order.item_subtype].name) - else - order_desc = make_order_desc(order, df.job_type.attrs[order.job_type].caption) + local order = safe_index(orders, order_idx) + if order then + order_desc = make_order_desc(order) end end return ('Are you sure you want to remove this manager order?\n\n%s'):format(dfhack.capitalizeStringWords(order_desc)) From 4bce233fb8f11664c7fb5f69d4ba571e6a90b9d1 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 25 Mar 2025 05:44:44 -0500 Subject: [PATCH 023/272] confirm: more specific order-remove descriptions Let order removal confirmation show the specific variety of - shield (shield, buckler), and - helm (helm, cap, hood), - gloves (gauntlets, gloves, mittens), - shoes (shoes, high boots, low boots, socks), - ammo (bolt), - trap component (axe blade, corkscrew, ball, disc, spike), and - meal (easy, fine, lavish). Most of these were previously described using the "generic" variety ("Gloves" for gauntlets, "Shoes" for socks, etc.), but meals looked particularly odd since they were previously described as "Coral", "Green Glass", and "Clear Glass" "Prepare Meal". `itemdefs.food` is not used because those list the final item names (biscuits, stew, roast), not the meal "type" (easy, fine, lavish). --- changelog.txt | 1 + internal/confirm/specs.lua | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/changelog.txt b/changelog.txt index d373953650..603dac8b5d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -41,6 +41,7 @@ Template for new versions: - `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape - `confirm`: when removing a manager order, show correct order description when using non-100% interface setting - `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) +- `confirm`: when removing a manager order, show specific item/job type for ammo, shield, helm, gloves, shoes, trap component, and meal orders ## Misc Improvements diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 5abd2aa58a..8daffa92d4 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -453,6 +453,12 @@ local orders = df.global.world.manager_orders.all local itemdefs = df.global.world.raws.itemdefs local reactions = df.global.world.raws.reactions.reactions +local meal_type_by_ingredient_count = { + [2] = 'easy', + [3] = 'fine', + [4] = 'lavish', +} + local function make_order_desc(order) if order.job_type == df.job_type.CustomReaction then for _, reaction in ipairs(reactions) do @@ -461,16 +467,35 @@ local function make_order_desc(order) end end return '' + elseif order.job_type == df.job_type.PrepareMeal then + -- DF uses mat_type as ingredient count? + local meal_type = meal_type_by_ingredient_count[order.mat_type] + if meal_type then + return 'prepare ' .. meal_type .. ' meal' + end + return 'prepare meal' end local noun if order.job_type == df.job_type.MakeArmor then noun = itemdefs.armor[order.item_subtype].name elseif order.job_type == df.job_type.MakeWeapon then noun = itemdefs.weapons[order.item_subtype].name + elseif order.job_type == df.job_type.MakeShield then + noun = itemdefs.shields[order.item_subtype].name + elseif order.job_type == df.job_type.MakeAmmo then + noun = itemdefs.ammo[order.item_subtype].name + elseif order.job_type == df.job_type.MakeHelm then + noun = itemdefs.helms[order.item_subtype].name + elseif order.job_type == df.job_type.MakeGloves then + noun = itemdefs.gloves[order.item_subtype].name elseif order.job_type == df.job_type.MakePants then noun = itemdefs.pants[order.item_subtype].name + elseif order.job_type == df.job_type.MakeShoes then + noun = itemdefs.shoes[order.item_subtype].name elseif order.job_type == df.job_type.MakeTool then noun = itemdefs.tools[order.item_subtype].name + elseif order.job_type == df.job_type.MakeTrapComponent then + noun = itemdefs.trapcomps[order.item_subtype].name elseif order.job_type == df.job_type.SmeltOre then noun = 'ore' else From fb25d4d2e7d0b34cbdbb9c1f175573729cb5ae02 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Mar 2025 06:13:55 -0500 Subject: [PATCH 024/272] confirm: only pause specific confirmations Pausing a confirmation currently pauses all confirmations, not just other instances of the current confirmation. For example, when trading, pausing a Mark All confirmation will also skip confirmation of the Seize action. The prompt in showYesNoPrompt is "Pause this confirmation". To better match that description, only pause new occurrences of the current confirmation. --- changelog.txt | 1 + confirm.lua | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 603dac8b5d..6b3d279ae1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,7 @@ Template for new versions: - `confirm`: when removing a manager order, show correct order description when using non-100% interface setting - `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) - `confirm`: when removing a manager order, show specific item/job type for ammo, shield, helm, gloves, shoes, trap component, and meal orders +- `confirm`: the pause option now only pauses future instances of the current confirmation instead of all confirmations in the current context ## Misc Improvements diff --git a/confirm.lua b/confirm.lua index 9fb114e9b3..5d1944d29b 100644 --- a/confirm.lua +++ b/confirm.lua @@ -108,12 +108,15 @@ function ConfirmOverlay:matches_conf(conf, keys, scr) end function ConfirmOverlay:onInput(keys) - if self.paused_conf or self.simulating then + if self.simulating then return false end local scr = dfhack.gui.getDFViewscreen(true) for id, conf in pairs(specs.REGISTRY) do if specs.config.data[id].enabled and self:matches_conf(conf, keys, scr) then + if conf == self.paused_conf then + return false + end local mouse_pos = xy2pos(dfhack.screen.getMousePos()) local propagate_fn = function(pause) if conf.on_propagate then From d73e2dc1afd205272d5ade3a47b72d9bf5998e6d Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 3 May 2025 21:23:04 -0500 Subject: [PATCH 025/272] confirm: allow multiple confirmations to be paused Allowing only a single, mutually exclusive, paused confirmation was likely to be confusing. Per review from lethosor. --- changelog.txt | 2 +- confirm.lua | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6b3d279ae1..6d14f32e86 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,7 +42,7 @@ Template for new versions: - `confirm`: when removing a manager order, show correct order description when using non-100% interface setting - `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) - `confirm`: when removing a manager order, show specific item/job type for ammo, shield, helm, gloves, shoes, trap component, and meal orders -- `confirm`: the pause option now only pauses future instances of the current confirmation instead of all confirmations in the current context +- `confirm`: the pause option now pauses individual confirmation types, allowing multiple different confirmations to be paused independently ## Misc Improvements diff --git a/confirm.lua b/confirm.lua index 5d1944d29b..5142013d1f 100644 --- a/confirm.lua +++ b/confirm.lua @@ -63,6 +63,7 @@ function ConfirmOverlay:init() } end end + self.paused_confs = {} end function ConfirmOverlay:preUpdateLayout() @@ -77,11 +78,14 @@ function ConfirmOverlay:preUpdateLayout() end function ConfirmOverlay:overlay_onupdate() - if self.paused_conf and - not dfhack.gui.matchFocusString(self.paused_conf.context, + for conf in pairs(self.paused_confs) do + if not dfhack.gui.matchFocusString(conf.context, dfhack.gui.getDFViewscreen(true)) - then - self.paused_conf = nil + then + self.paused_confs[conf] = nil + end + end + if not next(self.paused_confs) then self.overlay_onupdate_max_freq_seconds = 300 end end @@ -114,7 +118,7 @@ function ConfirmOverlay:onInput(keys) local scr = dfhack.gui.getDFViewscreen(true) for id, conf in pairs(specs.REGISTRY) do if specs.config.data[id].enabled and self:matches_conf(conf, keys, scr) then - if conf == self.paused_conf then + if self.paused_confs[conf] then return false end local mouse_pos = xy2pos(dfhack.screen.getMousePos()) @@ -123,7 +127,7 @@ function ConfirmOverlay:onInput(keys) conf.on_propagate() end if pause then - self.paused_conf = conf + self.paused_confs[conf] = true self.overlay_onupdate_max_freq_seconds = 0 end if keys._MOUSE_L then From a193969f27050581daecd49df63f2df8f2843f1f Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Wed, 7 May 2025 22:22:23 -0700 Subject: [PATCH 026/272] Update adv-finder.lua - "Unnamed" fits DF better --- gui/adv-finder.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/adv-finder.lua b/gui/adv-finder.lua index cfd17b7646..ccb3cd664c 100644 --- a/gui/adv-finder.lua +++ b/gui/adv-finder.lua @@ -31,7 +31,7 @@ end function get_hf_name(hf) --'Native Name "Translated Name", Race' local full_name = transName(hf.name, false) if full_name == '' then --Improve searchability - full_name = 'Anonymous' + full_name = 'Unnamed' else --Add the translation local t_name = transName(hf.name, true) if full_name ~= t_name then --Don't repeat @@ -50,7 +50,7 @@ end function get_art_name(ar) --'Native Name "Translated Name", Item' local full_name = transName(ar.name, false) if full_name == '' then --Improve searchability - full_name = 'Anonymous' + full_name = 'Unnamed' else --Add the translation local t_name = transName(ar.name, true) if full_name ~= t_name then --Don't repeat @@ -539,7 +539,7 @@ end local function insert_name_text(t, name) --HF or artifact name; Return true if both lines local str = transName(name, false) if str == '' then - table.insert(t, 'Anonymous') + table.insert(t, 'Unnamed') else --Both native and translation table.insert(t, str) --Native local t_name = transName(name, true) From 8a2c8e7b13d703da672532d408e4250dd11a86c9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:30:38 -0700 Subject: [PATCH 027/272] update changelog for 51.12 --- changelog.txt | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0678eabe68..9f0324d3aa 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,12 +16,8 @@ Template for new versions: ## New Features -- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction - ## Fixes -- `gui/design`: prevent line thickness from extending outside the map boundary - ## Misc Improvements ## Removed @@ -30,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 51.12-r1 + ## New Tools - `deteriorate`: (reinstated) allow corpses, body parts, food, and/or damaged clothes to rot away - `modtools/moddable-gods`: (reinstated) create new deities from scratch @@ -38,16 +46,16 @@ Template for new versions: - `gui/spectate`: added "Prefer nicknamed" to the list of options - `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting - `gui/blueprint`: now records zone designations +- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction ## Fixes - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game +- `gui/design`: prevent line thickness from extending outside the map boundary ## Misc Improvements - `remove-stress`: also applied to long-term stress, immediately removing stressed and haggard statuses -## Removed - # 51.11-r1 ## Fixes From a719636f8273f5d646058edac6ee7976f1359a70 Mon Sep 17 00:00:00 2001 From: Louis Hong Date: Thu, 26 Jun 2025 14:07:23 -0700 Subject: [PATCH 028/272] bug fix: journal.lua minor performance lost due to typo "colllapsed" obvious typo causing unintended performance lost --- gui/journal.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/journal.lua b/gui/journal.lua index 4faacb2183..03cf34b022 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -72,7 +72,7 @@ function JournalWindow:init() self.subviews.table_of_contents_panel.visible = not collapsed self.subviews.table_of_contents_divider.visible = not collapsed - if not colllapsed then + if not collapsed then self:reloadTableOfContents() end From 2511a57f2702e99ea945e2005c4d6e3ad8c0aade Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 27 Jun 2025 12:49:31 -0500 Subject: [PATCH 029/272] remove `think_counter` from `gui/gm-unit` for 51.12 compatibility --- internal/gm-unit/editor_counters.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/gm-unit/editor_counters.lua b/internal/gm-unit/editor_counters.lua index 0bf258df1e..c740bcfb32 100644 --- a/internal/gm-unit/editor_counters.lua +++ b/internal/gm-unit/editor_counters.lua @@ -9,7 +9,6 @@ Editor_Counters=defclass(Editor_Counters, base_editor.Editor) Editor_Counters.ATTRS{ frame_title = "Counters editor", counters1={ - "think_counter", "job_counter", "swap_counter", "winded", From e8267fcbd4892d8120332ca466b77dd71c0970ac Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 28 Jun 2025 17:27:21 -0500 Subject: [PATCH 030/272] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 9f0324d3aa..d669e7230b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `deathcause`: added functionality to this script to fetch cause of death programatically ## Fixes From 7ec2c9ec8581259266c16f2a7de7b0fbc58ed565 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 29 Jun 2025 09:15:59 -0500 Subject: [PATCH 031/272] mod-manager.lua: do not except on missing mod metadata fixes dfhack/dfhack#5489 --- gui/mod-manager.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 792715c6d1..7e6b6efe72 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -524,7 +524,8 @@ function ModlistWindow:refresh_list() local choices = {} for idx,mod in ipairs(scriptmanager.get_active_mods()) do if not include_vanilla and mod.vanilla then goto continue end - local steam_id = scriptmanager.get_mod_info_metadata(mod.path, 'STEAM_FILE_ID').STEAM_FILE_ID + local metadata = scriptmanager.get_mod_info_metadata(mod.path, 'STEAM_FILE_ID') + local steam_id = metadata and metadata.STEAM_FILE_ID or nil local url = steam_id and (': https://steamcommunity.com/sharedfiles/filedetails/?id=%s'):format(steam_id) or '' table.insert(choices, { text={ From 71194edf327dc9f725a931b19227249e04b0e03e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 29 Jun 2025 09:27:50 -0500 Subject: [PATCH 032/272] add changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 9f0324d3aa..c6dbbe078c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files ## Misc Improvements From 4992aa329942d91507209e65368aae3dd35cfc49 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 29 Jun 2025 10:12:31 -0500 Subject: [PATCH 033/272] add changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 9f0324d3aa..f6a2fda3f7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 ## Misc Improvements From 9f9ae7085b4d5d545e63d49ab26f683d97e9aa86 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Mon, 30 Jun 2025 12:19:30 -0500 Subject: [PATCH 034/272] add api docs --- docs/deathcause.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/deathcause.rst b/docs/deathcause.rst index c9a2ae0a06..20dddb11e6 100644 --- a/docs/deathcause.rst +++ b/docs/deathcause.rst @@ -14,3 +14,29 @@ Usage :: deathcause + +API +--- + +The ``deathcause`` script can be called programmatically by other scripts, either via the +commandline interface with ``dfhack.run_script()`` or via the API functions +defined in :source-scripts:`deathcause.lua`, available from the return value of +``reqscript('deathcause')``: + +* ``getDeathCauseFromHistFig(histfig)`` + +Returns a string with the historical figure's cause of death, sometimes with more information +than with a unit. + +* ``getDeathCauseFromUnit(unit)`` + +Returns a string with the unit's cause of death. + + API usage example:: + + local dc = reqscript('deathcause') + + -- Note: this is an arguably bad example because this is the same as running deathcause + -- from the launcher, but this would theoretically still work. + local deathReason = dc.getDeathCauseFromUnit(dfhack.gui.getSelectedUnit()) + print(deathReason) From 3c599e263ba9340bb48b3311b604df60cc24c350 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:24:59 +0200 Subject: [PATCH 035/272] resolve overlap with new buttons in 51.13 --- changelog.txt | 1 + uniform-unstick.lua | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index abb2dbbc4f..561f4e3c48 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files +- `uniform-unstick`: resolve overlap with new buttons in 51.13 ## Misc Improvements diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 73fd78b9aa..0fb501fd2d 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -343,10 +343,11 @@ local MIN_WIDTH = 26 EquipOverlay = defclass(EquipOverlay, overlay.OverlayWidget) EquipOverlay.ATTRS{ desc='Adds a link to the equip screen to fix equipment conflicts.', - default_pos={x=7,y=21}, + default_pos={x=7,y=23}, default_enabled=true, viewscreens='dwarfmode/Squads/Equipment/Default', frame={w=MIN_WIDTH, h=1}, + version=1 } function EquipOverlay:init() From 28cef1686412a0d2c933485b694c15ba53963988 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 2 Jul 2025 13:47:10 -0500 Subject: [PATCH 036/272] add changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 9f0324d3aa..9f267bc5d9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed ## Misc Improvements From 800ae8321b81558ff9522a51c444287126389bb6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 2 Jul 2025 13:48:54 -0500 Subject: [PATCH 037/272] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c794f6d312..ba51e4f81d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -32,7 +32,7 @@ Template for new versions: ## New Features ## Fixes -- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed +- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 From 1b95334b2ee7d40a686be17bd343ecbf55504e4c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 3 Jul 2025 16:56:16 -0500 Subject: [PATCH 038/272] remove fake `curse` compound in `unitst` resolves an alignment issue in `unitst` --- changelog.txt | 1 + devel/export-dt-ini.lua | 6 +++--- devel/make-dt.pl | 6 +++--- dwarf-op.lua | 2 +- fix/noexert-exhaustion.lua | 4 ++-- immortal-cravings.lua | 4 ++-- starvingdead.lua | 2 +- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index ba51e4f81d..a937e2fb20 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ Template for new versions: ## Fixes - `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 +- fixed references to removed ``unit.curse`` compound ## Misc Improvements diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index ad4d5bfbde..84e1da5cf6 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -318,9 +318,9 @@ address('physical_attrs',df.unit,'body','physical_attrs') address('body_size',df.unit,'appearance','body_modifiers') address('size_info',df.unit,'body','size_info','size_cur') address('size_base',df.unit,'body','size_info','size_base') -address('curse',df.unit,'curse','name') -address('curse_add_flags1',df.unit,'curse','add_tags1') -address('turn_count',df.unit,'curse','interaction','time_on_site') +address('curse',df.unit,'uwss_display_name_string') +address('curse_add_flags1',df.unit,'uwss_add_caste_flag') +address('turn_count',df.unit,'usable_interaction','time_on_site') address('souls',df.unit,'status','souls') address('states',df.unit,'status','misc_traits') address('labors',df.unit,'status','labors') diff --git a/devel/make-dt.pl b/devel/make-dt.pl index 4bffeb0d55..9d8ba2c433 100755 --- a/devel/make-dt.pl +++ b/devel/make-dt.pl @@ -325,9 +325,9 @@ ($$$$) emit_addr 'physical_attrs',%all,'unit','body.physical_attrs'; emit_addr 'body_size',%all,'unit','appearance.body_modifiers'; emit_addr 'size_info',%all,'unit','body.size_info'; - emit_addr 'curse',%all,'unit','curse.name'; - emit_addr 'curse_add_flags1',%all,'unit','curse.add_tags1'; - emit_addr 'turn_count',%all,'unit','curse.time_on_site'; + emit_addr 'curse',%all,'unit','uwss_display_name_sing'; + emit_addr 'curse_add_flags1',%all,'unit','uwss_add_caste_flag'; + emit_addr 'turn_count',%all,'unit','usable_interaction.time_on_site'; emit_addr 'souls',%all,'unit','status.souls'; emit_addr 'states',%all,'unit','status.misc_traits'; emit_addr 'labors',%all,'unit','status.labors'; diff --git a/dwarf-op.lua b/dwarf-op.lua index 3581e3004c..45b7233263 100644 --- a/dwarf-op.lua +++ b/dwarf-op.lua @@ -733,7 +733,7 @@ local seasons = { 'winter', } function GetWave(dwf) - arrival_time = current_tick - dwf.curse.interaction.time_on_site; + arrival_time = current_tick - dwf.useable_interaction.time_on_site; --print(string.format("Current year %s, arrival_time = %s, ticks_per_year = %s", df.global.cur_year, arrival_time, ticks_per_year)) arrival_year = df.global.cur_year + (arrival_time // ticks_per_year); arrival_season = 1 + (arrival_time % ticks_per_year) // ticks_per_season; diff --git a/fix/noexert-exhaustion.lua b/fix/noexert-exhaustion.lua index 44ea9d506f..eabfd965ef 100644 --- a/fix/noexert-exhaustion.lua +++ b/fix/noexert-exhaustion.lua @@ -5,10 +5,10 @@ --Running this script on repeat approximately at least every 350 ticks should prevent NOEXERT units from becoming Tired as a result of Individual Combat Drill. function isNoExert(u) - if(u.curse.rem_tags1.NOEXERT) then --tag removal overrides tag addition, so if the NOEXERT tag is removed the unit cannot be NOEXERT. + if(u.uwss_remove_caste_flag.NOEXERT) then --tag removal overrides tag addition, so if the NOEXERT tag is removed the unit cannot be NOEXERT. return false end - if(u.curse.add_tags1.NOEXERT) then--if the tag hasn't been removed, and the unit has a curse that adds it, they must be NOEXERT. + if(u.uwss_add_caste_flag.NOEXERT) then--if the tag hasn't been removed, and the unit has a curse that adds it, they must be NOEXERT. return true end if(dfhack.units.casteFlagSet(u.race,u.caste, df.caste_raw_flags.NOEXERT)) then --if the tag hasn't been added or removed, but their race and caste has the tag, they're NOEXERT. diff --git a/immortal-cravings.lua b/immortal-cravings.lua index 2b76ee4646..2162045fd2 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -172,8 +172,8 @@ local function unit_loop() end local function is_active_caste_flag(unit, flag_name) - return not unit.curse.rem_tags1[flag_name] and - (unit.curse.add_tags1[flag_name] or dfhack.units.casteFlagSet(unit.race, unit.caste, df.caste_raw_flags[flag_name])) + return not unit.uwss_remove_caste_flag[flag_name] and + (unit.uwss_add_caste_flag[flag_name] or dfhack.units.casteFlagSet(unit.race, unit.caste, df.caste_raw_flags[flag_name])) end ---main loop: look for citizens with personality needs for food/drink but w/o physiological need diff --git a/starvingdead.lua b/starvingdead.lua index 5518676ad1..b61db20de0 100644 --- a/starvingdead.lua +++ b/starvingdead.lua @@ -43,7 +43,7 @@ local function do_decay() attribute.value = math.floor(attribute.value - (attribute.value * attribute_decay)) end - if unit.curse.interaction.time_on_site > (state.death_threshold * TICKS_PER_MONTH) then + if unit.usable_interaction.time_on_site > (state.death_threshold * TICKS_PER_MONTH) then unit.animal.vanish_countdown = 1 end end From a8b2f21cd78a417cc2227a69d7eb529bdc2a3302 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:08:18 +0200 Subject: [PATCH 039/272] prioritize high-value meals and don't go eating or drinking on a full stomach --- changelog.txt | 1 + immortal-cravings.lua | 49 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index a937e2fb20..142f3f5452 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 +- `immortal-cravings`: prioritize high-value meals and don't go eating or drinking on a full stomach ## Misc Improvements diff --git a/immortal-cravings.lua b/immortal-cravings.lua index 2162045fd2..de251f3fc8 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -13,6 +13,27 @@ function distance(p1, p2) return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + math.abs(p1.z - p2.z) end +---find best item in an item vector (according to some metric) +---@generic T : df.item +---@param item_vector T[] +---@param metric fun(item: T): number +---@param is_good? fun(item: T): boolean +---@return T? +function findBest(item_vector, metric, is_good) + local best = nil + local mbest = -1 + for _,item in ipairs(item_vector) do + if not item.flags.in_job and (not is_good or is_good(item)) then + mitem = metric(item) + if not best or mitem > mbest then + best = item + mbest = mitem + end + end + end + return best +end + ---find closest accessible item in an item vector ---@generic T : df.item ---@param pos df.coord @@ -46,19 +67,28 @@ local function get_closest_drink(pos) return findClosest(pos, df.global.world.items.other.DRINK, is_good) end ----find some prepared meal +---find highest-value accessible meal ---@return df.item_foodst? -local function get_closest_meal(pos) +local function get_best_meal(pos) + ---@param meal df.item_foodst local function is_good(meal) - if meal.flags.rotten then + local accessible = dfhack.maps.canWalkBetween(pos,xyz2pos(dfhack.items.getPosition(meal))) + if meal.flags.rotten or not accessible then return false else + -- check that meal is either on the ground or in food storage (and not in a backpack) local container = dfhack.items.getContainer(meal) return not container or container:isFoodStorage() end end - return findClosest(pos, df.global.world.items.other.FOOD, is_good) + + ---@param meal df.item_foodst + local function portion_value(meal) + return dfhack.items.getValue(meal) / meal.stack_size + end + + return findBest(df.global.world.items.other.FOOD, portion_value, is_good) end ---create a Drink job for the given unit @@ -86,7 +116,7 @@ end ---create Eat job for the given unit ---@param unit df.unit local function goEat(unit) - local meal = get_closest_meal(unit.pos) + local meal = get_best_meal(unit.pos) if not meal then -- print('no accessible meals found') return @@ -181,12 +211,15 @@ local function main_loop() -- print('immortal-cravings watching:') watched = {} for _, unit in ipairs(dfhack.units.getCitizens()) do - if not is_active_caste_flag(unit, 'NO_DRINK') and not is_active_caste_flag(unit, 'NO_EAT') then + if + not (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) or + unit.counters2.stomach_content > 0 + then goto next_unit end for _, need in ipairs(unit.status.current_soul.personality.needs) do - if need.id == DrinkAlcohol and need.focus_level < threshold or - need.id == EatGoodMeal and need.focus_level < threshold + if need.id == DrinkAlcohol and need.focus_level < threshold or + need.id == EatGoodMeal and need.focus_level < threshold then table.insert(watched, unit.id) -- print(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) From be6f620abf453a55f5ded1a5756c69f736b7390b 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:42 +0000 Subject: [PATCH 040/272] [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 afa4a6dee2..2ec6f9ff9f 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 df9d85a0d1c703d9ec2cbed9d05baebaffc9c2a9 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 03:25:28 +0800 Subject: [PATCH 041/272] Add flexibility for changing vanilla module versions --- gui/mod-manager.lua | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 7e6b6efe72..183962cdf1 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,11 +12,45 @@ local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' +local vanilla_modules = { + ['vanilla_text'] = true, + ['vanilla_languages'] = true, + ['vanilla_descriptors'] = true, + ['vanilla_materials'] = true, + ['vanilla_environment'] = true, + ['vanilla_plants'] = true, + ['vanilla_items'] = true, + ['vanilla_buildings'] = true, + ['vanilla_bodies'] = true, + ['vanilla_creatures'] = true, + ['vanilla_entities'] = true, + ['vanilla_reactions'] = true, + ['vanilla_interactions'] = true, + ['vanilla_descriptors_graphics'] = true, + ['vanilla_plants_graphics'] = true, + ['vanilla_items_graphics'] = true, + ['vanilla_buildings_graphics'] = true, + ['vanilla_creatures_graphics'] = true, + ['vanilla_interactions_graphics'] = true, + ['vanilla_world_map'] = true, + ['vanilla_interface'] = true, + ['vanilla_music'] = true, +} + +function get_moddable_viewscreen(type) + local vs = nil + if type == 'region' then + vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) + elseif type == 'arena' then + vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_arenast, 0) + end + return vs +end + -- get_newregion_viewscreen and get_modlist_fields are declared as global functions -- so external tools can call them to get the DF mod list function get_newregion_viewscreen() - local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) - return vs + return get_moddable_viewscreen('region') end function get_modlist_fields(kind, viewscreen) @@ -62,7 +96,9 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local mod_index = nil for i, v in ipairs(from_fields.id) do local version = from_fields.numeric_version[i] - if v.value == mod_id and version == mod_version then + local vanilla = vanilla_modules[mod_id] + -- assuming that vanilla mods will not have multiple possible indices + if v.value == mod_id and (vanilla or version == mod_version) then mod_index = i break end From 3b49b24c6ddedf305d18f5575574504fdee263f6 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 04:30:15 +0800 Subject: [PATCH 042/272] Add comments --- gui/mod-manager.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 183962cdf1..784055c4a3 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,6 +12,9 @@ local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' +-- hardly an elegant solution, but mysteriously, +-- using from_fields.src_dir[i].startswith('data/vanilla') in move_mod_entry() +-- leads to lua complaining that it 'cannot read field string.startswith: not found' local vanilla_modules = { ['vanilla_text'] = true, ['vanilla_languages'] = true, @@ -97,7 +100,7 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) for i, v in ipairs(from_fields.id) do local version = from_fields.numeric_version[i] local vanilla = vanilla_modules[mod_id] - -- assuming that vanilla mods will not have multiple possible indices + -- assumes that vanilla mods will not have multiple possible indices. if v.value == mod_id and (vanilla or version == mod_version) then mod_index = i break From cadf06b017a91d316343bac0a5ef3f50ef26116a Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 16:29:31 +0800 Subject: [PATCH 043/272] Edit vanilla mod identification logic --- gui/mod-manager.lua | 39 ++++++++++----------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 784055c4a3..832a2c1482 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,34 +12,15 @@ local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' --- hardly an elegant solution, but mysteriously, --- using from_fields.src_dir[i].startswith('data/vanilla') in move_mod_entry() --- leads to lua complaining that it 'cannot read field string.startswith: not found' -local vanilla_modules = { - ['vanilla_text'] = true, - ['vanilla_languages'] = true, - ['vanilla_descriptors'] = true, - ['vanilla_materials'] = true, - ['vanilla_environment'] = true, - ['vanilla_plants'] = true, - ['vanilla_items'] = true, - ['vanilla_buildings'] = true, - ['vanilla_bodies'] = true, - ['vanilla_creatures'] = true, - ['vanilla_entities'] = true, - ['vanilla_reactions'] = true, - ['vanilla_interactions'] = true, - ['vanilla_descriptors_graphics'] = true, - ['vanilla_plants_graphics'] = true, - ['vanilla_items_graphics'] = true, - ['vanilla_buildings_graphics'] = true, - ['vanilla_creatures_graphics'] = true, - ['vanilla_interactions_graphics'] = true, - ['vanilla_world_map'] = true, - ['vanilla_interface'] = true, - ['vanilla_music'] = true, -} +-- Shamelessly taken from hack/library/lua/script-manager.lua +function vanilla(dir) + dir = dir.value + dir = dir -- better safe than sorry i guess + return dir:startswith('data/vanilla') +end +-- get_moddable_viewscreen(), get_any_moddable_viewscreen() and get_modlist_fields are declared +-- as global functions so external tools can call them to get the DF mod list function get_moddable_viewscreen(type) local vs = nil if type == 'region' then @@ -99,9 +80,9 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local mod_index = nil for i, v in ipairs(from_fields.id) do local version = from_fields.numeric_version[i] - local vanilla = vanilla_modules[mod_id] + local src_dir = from_fields.src_dir[i] -- assumes that vanilla mods will not have multiple possible indices. - if v.value == mod_id and (vanilla or version == mod_version) then + if v.value == mod_id and (vanilla(src_dir) or version == mod_version) then mod_index = i break end From f194ffd6c82f144447ceef2ec62b078c27b81fe7 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 16:29:59 +0800 Subject: [PATCH 044/272] Add support for arena mode --- gui/mod-manager.lua | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 832a2c1482..7783fbca4a 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -19,7 +19,7 @@ function vanilla(dir) return dir:startswith('data/vanilla') end --- get_moddable_viewscreen(), get_any_moddable_viewscreen() and get_modlist_fields are declared +-- get_moddable_viewscreen(), get_any_moddable_viewscreen() and get_modlist_fields are declared -- as global functions so external tools can call them to get the DF mod list function get_moddable_viewscreen(type) local vs = nil @@ -31,10 +31,12 @@ function get_moddable_viewscreen(type) return vs end --- get_newregion_viewscreen and get_modlist_fields are declared as global functions --- so external tools can call them to get the DF mod list -function get_newregion_viewscreen() - return get_moddable_viewscreen('region') +function get_any_moddable_viewscreen() + local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) + if not vs then + vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_arenast, 0) + end + return vs end function get_modlist_fields(kind, viewscreen) @@ -157,7 +159,7 @@ ModmanageMenu.ATTRS { } local function save_new_preset(preset_name) - local viewscreen = get_newregion_viewscreen() + local viewscreen = get_any_moddable_viewscreen() local modlist = get_active_modlist(viewscreen) table.insert(presets_file.data, { name = preset_name, modlist = modlist }) presets_file:write() @@ -177,7 +179,7 @@ local function overwrite_preset(idx) return end - local viewscreen = get_newregion_viewscreen() + local viewscreen = get_any_moddable_viewscreen() local modlist = get_active_modlist(viewscreen) presets_file.data[idx].modlist = modlist presets_file:write() @@ -188,7 +190,7 @@ local function load_preset(idx, unset_default_on_failure) return end - local viewscreen = get_newregion_viewscreen() + local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist local failures = swap_modlist(viewscreen, modlist) @@ -225,7 +227,7 @@ local function load_preset(idx, unset_default_on_failure) table.insert(text, NEWLINE) end dialogs.showMessage("Warning", text) -end + end end local function find_preset_by_name(name) @@ -593,7 +595,7 @@ ModmanageOverlay.ATTRS { desc = "Adds a link to the mod selection screen for accessing the mod manager.", default_pos = { x=5, y=-6 }, version = 2, - viewscreens = { "new_region/Mods" }, + viewscreens = { "new_region/Mods", "new_arena/Mods" }, default_enabled=true, } @@ -656,7 +658,7 @@ notification_timer_fn() local default_applied = false dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_VIEWSCREEN_CHANGED then - local vs = get_newregion_viewscreen() + local vs = get_any_moddable_viewscreen() if vs and not default_applied then default_applied = true for i, v in ipairs(presets_file.data) do From d6e12541a4c88b39287f3c325d991aaae92cace6 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 18:12:00 +0800 Subject: [PATCH 045/272] Add notifications for updated mods --- gui/mod-manager.lua | 99 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 7783fbca4a..16f6aa037d 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -13,7 +13,7 @@ local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' -- Shamelessly taken from hack/library/lua/script-manager.lua -function vanilla(dir) +local function vanilla(dir) dir = dir.value dir = dir -- better safe than sorry i guess return dir:startswith('data/vanilla') @@ -75,23 +75,29 @@ function get_modlist_fields(kind, viewscreen) end end +--- @return { success: boolean, version: string } local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) local mod_index = nil + local loaded_version = nil for i, v in ipairs(from_fields.id) do local version = from_fields.numeric_version[i] local src_dir = from_fields.src_dir[i] + local displayed_version = from_fields.displayed_version[i].value -- assumes that vanilla mods will not have multiple possible indices. if v.value == mod_id and (vanilla(src_dir) or version == mod_version) then + if version ~= mod_version then + loaded_version = displayed_version + end mod_index = i break end end if mod_index == nil then - return false + return { success= false, version= nil } end for k, v in pairs(to_fields) do @@ -106,13 +112,15 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) v:erase(mod_index) end - return true + return { success= true, version= loaded_version } end +--- @return { success: boolean, version: string } local function enable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) end +--- @return { success: boolean, version: string } local function disable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) end @@ -127,6 +135,7 @@ local function get_active_modlist(viewscreen) return t end +--- @return { failures: [string], changed: [{ id: string, new: string }] } local function swap_modlist(viewscreen, modlist) local current = get_active_modlist(viewscreen) for _, v in ipairs(current) do @@ -134,12 +143,17 @@ local function swap_modlist(viewscreen, modlist) end local failures = {} + local changed = {} for _, v in ipairs(modlist) do - if not enable_mod(viewscreen, v.id, v.version) then + res = enable_mod(viewscreen, v.id, v.version) + if not res.success then table.insert(failures, v.id) end + if res.version then + table.insert(changed, { id= v.id, new= res.version }) + end end - return failures + return { failures= failures, changed= changed } end -------------------- @@ -192,33 +206,53 @@ local function load_preset(idx, unset_default_on_failure) local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist - local failures = swap_modlist(viewscreen, modlist) - - if #failures > 0 then - local text = {} - if unset_default_on_failure then - presets_file.data[idx].default = false - presets_file:write() - - table.insert(text, { - text='Failed to load some mods from your default preset.', - pen=COLOR_LIGHTRED, - }) + local results = swap_modlist(viewscreen, modlist) + local failures = results.failures + local changes = results.changed + local text = {} + + local failed = #failures > 0 + local changed = #changes > 0 + local should_warn = failed or changed + + if should_warn then + if failed then + if unset_default_on_failure then + presets_file.data[idx].default = false + presets_file:write() + + table.insert(text, { + text='Failed to load some mods from your default preset.', + pen=COLOR_LIGHTRED, + }) + table.insert(text, NEWLINE) + table.insert(text, { + text='Preset is being unmarked as the default for safety.', + pen=COLOR_LIGHTRED, + }) + else + table.insert(text, { + text='Failed to load some mods from the preset.', + pen=COLOR_LIGHTRED, + }) + end + end + if failed and changed then table.insert(text, NEWLINE) + end + if changed then table.insert(text, { - text='Preset is being unmarked as the default for safety.', - pen=COLOR_LIGHTRED, - }) - else - table.insert(text, { - text='Failed to load some mods from the preset.', + text='Some vanilla mods have been updated.', pen=COLOR_LIGHTRED, }) end table.insert(text, NEWLINE) - table.insert(text, NEWLINE) table.insert(text, 'Please re-create your preset with mods you currently have installed.') table.insert(text, NEWLINE) + table.insert(text, NEWLINE) + end + + if failed then table.insert(text, 'Here are the mods that failed to load:') table.insert(text, NEWLINE) table.insert(text, NEWLINE) @@ -226,6 +260,23 @@ local function load_preset(idx, unset_default_on_failure) table.insert(text, ('- %s'):format(v)) table.insert(text, NEWLINE) end + end + + if failed and changed then + table.insert(text, NEWLINE) -- just to separate the sections + end + + if changed then + table.insert(text, 'Here are the vanilla mods that have been updated:') + table.insert(text, NEWLINE) + table.insert(text, NEWLINE) + for _, v in ipairs(changes) do + table.insert(text, ('- %s to %s'):format(v.id, v.new)) + table.insert(text, NEWLINE) + end + end + + if should_warn then dialogs.showMessage("Warning", text) end end From a292f126b17d133c97e66b6ccd143b90f924511e Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 18:25:48 +0800 Subject: [PATCH 046/272] Update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index a937e2fb20..afab6089d7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Fixes - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files +- `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset - `uniform-unstick`: resolve overlap with new buttons in 51.13 ## Misc Improvements From 90d437e9a7adad61ec8139df3e730345037d6bdd Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 19:56:56 +0800 Subject: [PATCH 047/272] Refactor warning logic --- gui/mod-manager.lua | 82 ++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 16f6aa037d..0c53078ec8 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -199,6 +199,44 @@ local function overwrite_preset(idx) presets_file:write() end +local function prepare_warning(text, failed, changed, unset_default_on_failure) + if not failed and not changed then return end + + if failed then + if unset_default_on_failure then + table.insert(text, { + text='Failed to load some mods from your default preset.', + pen=COLOR_LIGHTRED, + }) + table.insert(text, NEWLINE) + table.insert(text, { + text='Preset is being unmarked as the default for safety.', + pen=COLOR_LIGHTRED, + }) + else + table.insert(text, { + text='Failed to load some mods from the preset.', + pen=COLOR_LIGHTRED, + }) + end + end + + if failed and changed then + table.insert(text, NEWLINE) + end + + if changed then + table.insert(text, { + text='Some vanilla mods have been updated.', + pen=COLOR_LIGHTRED, + }) + end + table.insert(text, NEWLINE) + table.insert(text, 'Please re-create your preset with mods you currently have installed.') + table.insert(text, NEWLINE) + table.insert(text, NEWLINE) +end + local function load_preset(idx, unset_default_on_failure) if idx > #presets_file.data then return @@ -213,43 +251,11 @@ local function load_preset(idx, unset_default_on_failure) local failed = #failures > 0 local changed = #changes > 0 - local should_warn = failed or changed - - if should_warn then - if failed then - if unset_default_on_failure then - presets_file.data[idx].default = false - presets_file:write() - - table.insert(text, { - text='Failed to load some mods from your default preset.', - pen=COLOR_LIGHTRED, - }) - table.insert(text, NEWLINE) - table.insert(text, { - text='Preset is being unmarked as the default for safety.', - pen=COLOR_LIGHTRED, - }) - else - table.insert(text, { - text='Failed to load some mods from the preset.', - pen=COLOR_LIGHTRED, - }) - end - end - if failed and changed then - table.insert(text, NEWLINE) - end - if changed then - table.insert(text, { - text='Some vanilla mods have been updated.', - pen=COLOR_LIGHTRED, - }) - end - table.insert(text, NEWLINE) - table.insert(text, 'Please re-create your preset with mods you currently have installed.') - table.insert(text, NEWLINE) - table.insert(text, NEWLINE) + + prepare_warning(text, failed, changed) + if failed and unset_default_on_failure then + presets_file.data[idx].default = false + presets_file:write() end if failed then @@ -276,7 +282,7 @@ local function load_preset(idx, unset_default_on_failure) end end - if should_warn then + if failed or changed then dialogs.showMessage("Warning", text) end end From 3655e85492b9e0fd7a0ae27814d5861a6f6788dd Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 23:11:49 +0800 Subject: [PATCH 048/272] Remove excess locals assignment --- gui/mod-manager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 0c53078ec8..686fc31f29 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -15,7 +15,6 @@ local GLOBAL_KEY = 'mod-manager' -- Shamelessly taken from hack/library/lua/script-manager.lua local function vanilla(dir) dir = dir.value - dir = dir -- better safe than sorry i guess return dir:startswith('data/vanilla') end From 3612f48ed1f749f50aa462d196c940b7982eeb4e Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 23:22:54 +0800 Subject: [PATCH 049/272] Add missing local assignment --- gui/mod-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 686fc31f29..350ac3143c 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -144,7 +144,7 @@ local function swap_modlist(viewscreen, modlist) local failures = {} local changed = {} for _, v in ipairs(modlist) do - res = enable_mod(viewscreen, v.id, v.version) + local res = enable_mod(viewscreen, v.id, v.version) if not res.success then table.insert(failures, v.id) end From 71c5b15fea8979aa1937c6eaff36c50c66d54070 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 23:58:29 +0800 Subject: [PATCH 050/272] Add missing changelog entry --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index afab6089d7..e5bd22e76e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset +- `gui/mod-manager`: now supports arena mode - `uniform-unstick`: resolve overlap with new buttons in 51.13 ## Misc Improvements From 86e636f1b4327ab606c27cc36e306258ee809461 Mon Sep 17 00:00:00 2001 From: git--amade Date: Tue, 15 Jul 2025 19:08:38 +0800 Subject: [PATCH 051/272] Add entomb.lua and docs/entomb.rst --- docs/entomb.rst | 61 +++++++++++++++ entomb.lua | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 docs/entomb.rst create mode 100644 entomb.lua diff --git a/docs/entomb.rst b/docs/entomb.rst new file mode 100644 index 0000000000..c519ff7bcf --- /dev/null +++ b/docs/entomb.rst @@ -0,0 +1,61 @@ +entomb +====== + +.. dfhack-tool:: + :summary: Entomb any corpse into tomb zones. + :tags: fort items buildings + +Assign any corpse regardless of citizenship, residency, pet status, +or affiliation to an unassigned tomb zone for burial. + +Usage +----- + +``entomb []`` + +This script must be executed with either a unit's corpse or body part +selected or with a unit ID specified. An unassigned tomb zone will then +be assigned to the unit for burial and all its corpse and/or body parts +will become valid items for interment. + +Optionally, the zone ID may also be specified to assign a specific tomb +zone to the unit. + +A non-citizen, non-resident, or non-pet unit that is still alive may +even be assigned a tomb zone if they have lost any body part that can +be placed inside a tomb, e.g. teeth or severed limbs. New corpse items +after a tomb has already been assigned will not be properly interred +until the script is executed again on either the unit, its corpse, or +any of its body parts. + +If executed on slaughtered animals, all its butchering returns will +become valid burial items and no longer usable for cooking or crafting. + +Examples +-------- + +``entomb unit `` + Assign an unassigned tomb zone to the unit with the specified ID. + +``entomb tomb `` + Assign a tomb zone with the specified ID to the selected corpse + item's unit. + +``entomb unit tomb now`` + Assign a tomb zone with the specified ID to the unit with the + specified ID and teleport its corpse and/or body parts into the + coffin in the tomb zone. + +Options +------- + +``unit `` + Specify the ID of the unit to be assigned to a tomb zone. + +``tomb `` + Specify the ID of the zone into which a unit will be interred. + +``now`` + Instantly teleport the unit's corpse and/or body parts into the + coffin of its assigned tomb zone. This option can be called on + corpse items or units that are already assigned a tomb zone. diff --git a/entomb.lua b/entomb.lua new file mode 100644 index 0000000000..2780232d25 --- /dev/null +++ b/entomb.lua @@ -0,0 +1,193 @@ +-- Entomb corpse items of any dead unit. +--@module = true + +local utils = require('utils') + +local unit_id +local unit +local building_id +local tomb +local forceBurial + +local args = {...} + +-- Get unit from selected corpse or corpse piece item. +local function GetUnitFromCorpse() + local item = dfhack.gui.getSelectedItem(true) + if item then + if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then + unit_id = item.unit_id + unit = df.unit.find(unit_id) + else + qerror('Selected item is not a corpse or body part.') + end + else + qerror('No item selected or unit specified.') + end +end + +-- Validate tomb zone assignment. +local function CheckTombZone(building, id) + if df.building_civzonest:is_instance(building) then + if building.type == 97 then + if building.assigned_unit_id == id then + return true + end + end + end +end + +-- Iterate through all available tomb zones. +local function IterateTombZone(id) + for _, building in pairs(df.global.world.buildings.all) do + if CheckTombZone(building, id) then return building end + end +end + +-- Check if any of the unit's corpse items are still not in a coffin. +local function isNotBuried() + for _, item_id in pairs(unit.corpse_parts) do + local item = df.item.find(item_id) + if item then + local inCoffin = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local coffinBuilding_id = inCoffin and inCoffin.building_id or nil + local coffin = coffinBuilding_id and df.building.find(coffinBuilding_id) or nil + local isCoffin = coffin and df.building_coffinst:is_instance(coffin) or nil + -- Return TRUE if even one item is not interred. + if not isCoffin then + return true + end + end + end +end + +local function GetEmptyTombZone() + -- Check if unit is already assigned to a tomb zone. + local isAlreadyAssigned = IterateTombZone(unit_id) + if isAlreadyAssigned then + if isNotBuried() or forceBurial then + tomb = isAlreadyAssigned + print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') + else + qerror('Unit is already interred in a tomb zone.') + end + else + -- Find an unassigned tomb zone. + tomb = IterateTombZone(-1) + end + if not tomb then + qerror('No unassigned tomb zones are available.') + end +end + +-- Set corpse items to be valid for burial. +local function FlagForBurial(corpseParts) + -- Undead units have empty corpse_parts vector. + if unit.enemy.undead then + for _, item in pairs(df.global.world.items.other.IN_PLAY) do + if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then + if item.unit_id == unit_id then + corpseParts:insert(#corpseParts, item.id) + end + end + end + utils.sort_vector(corpseParts) + end + local burialItemCount = 0 + for _, item_id in pairs(corpseParts) do + local item = df.item.find(item_id) + if item then + item.flags.dead_dwarf = true + -- Some corpse items may be lost/destroyed before burial. + burialItemCount = burialItemCount + 1 + end + end + if burialItemCount == 0 then + qerror('Unit has no corpse or body parts available for burial.') + end + tomb.assigned_unit_id = unit_id + return burialItemCount +end + +local function PutInCoffin(corpseParts) + local coffin + for _, building in pairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + if coffin then + -- Set df.building_item_role_type.PERM first before changing + -- it to TEMP to turn it into an interred corpse item. + for _, item_id in pairs(corpseParts) do + local item = df.item.find(item_id) + if item then + dfhack.items.moveToBuilding(item, coffin, 2) + end + end + for _, buildingItem in pairs(coffin.contained_items) do + local item = buildingItem.item + if not df.item_coffinst:is_instance(item) then + buildingItem.use_mode = 0 + end + end + print('Corpse items have been teleported into a coffin.') + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + end +end + +local function AssignToTomb() + local corpseParts = unit.corpse_parts + local strBurial = '%s assigned to a tomb zone for burial.' + local strCorpseItems = '(%d corpse or body part%s)' + local strUnitName = unit and dfhack.units.getReadableName(unit) + local strPlural = '' + local incident_id = unit.counters.death_id + if incident_id ~= -1 then + local incident = df.incident.find(incident_id) + -- Corpse will not be interred if not yet discovered. + incident.flags.discovered = true + end + local burialItemCount = FlagForBurial(corpseParts) + print(string.format(strBurial, strUnitName)) + if forceBurial then PutInCoffin(corpseParts) end + if burialItemCount > 1 then strPlural = 's' end + print(string.format(strCorpseItems, burialItemCount, strPlural)) +end + +local function parseArgs() + local building + if #args > 0 then + for i, v in ipairs(args) do + if v == 'unit' then + unit_id = tonumber(args[i+1]) or nil + unit = unit_id and df.unit.find(unit_id) + if not unit then qerror('Invalid unit ID.') end + end + if v == 'tomb' then + building_id = tonumber(args[i+1]) or nil + building = building_id and df.building.find(building_id) + if not building then qerror('Invalid zone ID.') end + -- Check if tomb zone is unassigned. + if CheckTombZone(building, -1) then + tomb = building + else + qerror('Specified zone ID does not point to an unassigned tomb zone.') + end + end + if v == 'now' then forceBurial = true end + end + end +end + +local function Main() + parseArgs() + if not unit then GetUnitFromCorpse() end + if unit then + if not tomb then GetEmptyTombZone() end + if tomb then AssignToTomb() end + end +end + +if not dfhack_flags.module then + Main() +end From f684542ccc42068a4785d9c9f296467be87d0521 Mon Sep 17 00:00:00 2001 From: git--amade Date: Tue, 15 Jul 2025 19:26:31 +0800 Subject: [PATCH 052/272] Update changelog.txt to add new tool: entomb --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index a937e2fb20..624dc7723f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: # Future ## New Tools +- `entomb`: allow any unit that has a corpse or body parts to be assigned a tomb zone ## New Features From 07594b542d594c9201c2332167cabeb087865a32 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 16 Jul 2025 21:31:53 +0800 Subject: [PATCH 053/272] Apply revisions to entomb.lua according to PR comments --- entomb.lua | 206 ++++++++++++++++++++++++++++------------------------- 1 file changed, 109 insertions(+), 97 deletions(-) diff --git a/entomb.lua b/entomb.lua index 2780232d25..e462456d4c 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,100 +1,85 @@ -- Entomb corpse items of any dead unit. --@module = true -local utils = require('utils') - -local unit_id -local unit -local building_id -local tomb -local forceBurial - -local args = {...} - -- Get unit from selected corpse or corpse piece item. -local function GetUnitFromCorpse() - local item = dfhack.gui.getSelectedItem(true) +function GetUnitFromCorpse(item) + if math.type(item) == "integer" then item = df.item.find(item) + elseif not item then item = dfhack.gui.getSelectedItem(true) end if item then if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then - unit_id = item.unit_id - unit = df.unit.find(unit_id) + return df.unit.find(item.unit_id) else - qerror('Selected item is not a corpse or body part.') + qerror('Item is not a corpse or body part.') end - else - qerror('No item selected or unit specified.') end end -- Validate tomb zone assignment. -local function CheckTombZone(building, id) - if df.building_civzonest:is_instance(building) then - if building.type == 97 then - if building.assigned_unit_id == id then - return true - end +local function CheckTombZone(building, unit_id) + if building.type == df.civzone_type.Tomb then + if building.assigned_unit_id == unit_id then + return true end end end -- Iterate through all available tomb zones. -local function IterateTombZone(id) - for _, building in pairs(df.global.world.buildings.all) do - if CheckTombZone(building, id) then return building end +local function IterateTombZones(unit_id) + for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do + if CheckTombZone(building, unit_id) then return building end end end --- Check if any of the unit's corpse items are still not in a coffin. -local function isNotBuried() - for _, item_id in pairs(unit.corpse_parts) do +-- Check if any of the unit's corpse items are not yet placed in a coffin. +function isEntombed(unit) + -- Return FALSE for still living or undead units with empty corpse_parts vector. + if #unit.corpse_parts == 0 then return false end + for _, item_id in ipairs(unit.corpse_parts) do local item = df.item.find(item_id) if item then - local inCoffin = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) - local coffinBuilding_id = inCoffin and inCoffin.building_id or nil - local coffin = coffinBuilding_id and df.building.find(coffinBuilding_id) or nil - local isCoffin = coffin and df.building_coffinst:is_instance(coffin) or nil - -- Return TRUE if even one item is not interred. + local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local building_id = inBuilding and inBuilding.building_id or -1 + local building = df.building.find(building_id) + local isCoffin = (building and df.building_coffinst:is_instance(building)) or false + -- Return FALSE if even one item is not interred. if not isCoffin then - return true + return false end end end + return true end -local function GetEmptyTombZone() +local function GetTombZone(unit) + local unit_id = unit.id + local tomb + local entombed = false -- Check if unit is already assigned to a tomb zone. - local isAlreadyAssigned = IterateTombZone(unit_id) + local isAlreadyAssigned = IterateTombZones(unit_id) if isAlreadyAssigned then - if isNotBuried() or forceBurial then - tomb = isAlreadyAssigned - print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') - else - qerror('Unit is already interred in a tomb zone.') - end + tomb = isAlreadyAssigned + entombed = isEntombed(unit) else -- Find an unassigned tomb zone. - tomb = IterateTombZone(-1) - end - if not tomb then - qerror('No unassigned tomb zones are available.') + tomb = IterateTombZones(-1) end + return tomb, entombed end -- Set corpse items to be valid for burial. -local function FlagForBurial(corpseParts) +local function FlagForBurial(unit, corpseParts) -- Undead units have empty corpse_parts vector. if unit.enemy.undead then - for _, item in pairs(df.global.world.items.other.IN_PLAY) do + for _, item in ipairs(df.global.world.items.other.ANY_CORPSE) do if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then - if item.unit_id == unit_id then - corpseParts:insert(#corpseParts, item.id) + if item.unit_id == unit.id then + corpseParts:insert('#', item.id) end end end - utils.sort_vector(corpseParts) end local burialItemCount = 0 - for _, item_id in pairs(corpseParts) do + for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) if item then item.flags.dead_dwarf = true @@ -102,70 +87,87 @@ local function FlagForBurial(corpseParts) burialItemCount = burialItemCount + 1 end end - if burialItemCount == 0 then - qerror('Unit has no corpse or body parts available for burial.') - end - tomb.assigned_unit_id = unit_id return burialItemCount end -local function PutInCoffin(corpseParts) - local coffin - for _, building in pairs(tomb.contained_buildings) do - if df.building_coffinst:is_instance(building) then coffin = building end - end - if coffin then - -- Set df.building_item_role_type.PERM first before changing - -- it to TEMP to turn it into an interred corpse item. - for _, item_id in pairs(corpseParts) do - local item = df.item.find(item_id) - if item then - dfhack.items.moveToBuilding(item, coffin, 2) - end +function PutInCoffin(coffin, item) + if item then + -- Set df.building_item_role_type.PERM first before changing it to TEMP to turn the items + -- into interred burial items, otherwise the items will be hauled back to stockpiles. + -- https://discord.com/channels/793331351645323264/873014631315148840/1394242351345434654 + dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.PERM) end - for _, buildingItem in pairs(coffin.contained_items) do - local item = buildingItem.item - if not df.item_coffinst:is_instance(item) then - buildingItem.use_mode = 0 - end + for _, buildingItem in ipairs(coffin.contained_items) do + local item = buildingItem.item + if not df.item_coffinst:is_instance(item) then + buildingItem.use_mode = df.building_item_role_type.TEMP end - print('Corpse items have been teleported into a coffin.') - else - print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') end end -local function AssignToTomb() +local function GetCoffin(tomb) + local coffin + if tomb.type == df.civzone_type.Tomb then + for _, building in ipairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + -- Allow other scripts to call this function and pass the actual coffin building instead. + elseif df.building_coffinst:is_instance(tomb) then + coffin = tomb + end + return coffin +end + +function AssignToTomb(unit, tomb, forceBurial) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to a tomb zone for burial.' local strCorpseItems = '(%d corpse or body part%s)' + local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) local strPlural = '' local incident_id = unit.counters.death_id if incident_id ~= -1 then local incident = df.incident.find(incident_id) - -- Corpse will not be interred if not yet discovered. + -- Corpse will not be interred if not yet discovered, + -- which never happens for units not belonging to player's civ. incident.flags.discovered = true end - local burialItemCount = FlagForBurial(corpseParts) - print(string.format(strBurial, strUnitName)) - if forceBurial then PutInCoffin(corpseParts) end - if burialItemCount > 1 then strPlural = 's' end - print(string.format(strCorpseItems, burialItemCount, strPlural)) + local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount == 0 then + print(string.format(strNoCorpse, strUnitName)) + else + tomb.assigned_unit_id = unit.id + print(string.format(strBurial, strUnitName)) + if forceBurial then + local coffin = GetCoffin(tomb) + print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') + if coffin then + for _, item_id in ipairs(corpseParts) do + local item = df.item.find(item_id) + PutInCoffin(coffin, item) + end + print('Corpse items have been teleported into a coffin.') + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + end + end + if burialItemCount > 1 then strPlural = 's' end + print(string.format(strCorpseItems, burialItemCount, strPlural)) + end end -local function parseArgs() - local building - if #args > 0 then +local function parseArgs(args) + local unit, tomb, forceBurial + if args and #args > 0 then for i, v in ipairs(args) do if v == 'unit' then - unit_id = tonumber(args[i+1]) or nil + local unit_id = tonumber(args[i+1]) or nil unit = unit_id and df.unit.find(unit_id) if not unit then qerror('Invalid unit ID.') end end if v == 'tomb' then - building_id = tonumber(args[i+1]) or nil - building = building_id and df.building.find(building_id) + local building_id = tonumber(args[i+1]) or nil + local building = building_id and df.building.find(building_id) if not building then qerror('Invalid zone ID.') end -- Check if tomb zone is unassigned. if CheckTombZone(building, -1) then @@ -177,17 +179,27 @@ local function parseArgs() if v == 'now' then forceBurial = true end end end + return unit, tomb, forceBurial end -local function Main() - parseArgs() - if not unit then GetUnitFromCorpse() end +local function Main(args) + local unit, tomb, forceBurial = parseArgs(args) + local entombed + if not unit then unit = GetUnitFromCorpse() end if unit then - if not tomb then GetEmptyTombZone() end - if tomb then AssignToTomb() end + if not tomb then tomb, entombed = GetTombZone(unit) end + if entombed then + print('Unit is already completely interred in a tomb zone.') + elseif tomb then + AssignToTomb(unit, tomb, forceBurial) + else + print('No unassigned tomb zones are available.') + end + else + qerror('No item selected or unit specified.') end end if not dfhack_flags.module then - Main() + Main({...}) end From 0f75b5622740e2cfb40ab70e826748598012d1af Mon Sep 17 00:00:00 2001 From: git--amade Date: Thu, 17 Jul 2025 03:31:32 +0800 Subject: [PATCH 054/272] Apply 2nd revision to entomb.lua: improve PutInCoffin() --- entomb.lua | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/entomb.lua b/entomb.lua index e462456d4c..88483d4609 100644 --- a/entomb.lua +++ b/entomb.lua @@ -21,6 +21,7 @@ local function CheckTombZone(building, unit_id) return true end end + return false end -- Iterate through all available tomb zones. @@ -28,6 +29,7 @@ local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do if CheckTombZone(building, unit_id) then return building end end + return nil end -- Check if any of the unit's corpse items are not yet placed in a coffin. @@ -92,15 +94,17 @@ end function PutInCoffin(coffin, item) if item then - -- Set df.building_item_role_type.PERM first before changing it to TEMP to turn the items - -- into interred burial items, otherwise the items will be hauled back to stockpiles. - -- https://discord.com/channels/793331351645323264/873014631315148840/1394242351345434654 - dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.PERM) + -- Remove job from item to allow it to be teleported. + if item.flags.in_job then + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job + if job then + dfhack.job.removeJob(job) + end end - for _, buildingItem in ipairs(coffin.contained_items) do - local item = buildingItem.item - if not df.item_coffinst:is_instance(item) then - buildingItem.use_mode = df.building_item_role_type.TEMP + if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then + -- Flag the item become an interred item, otherwise it will be hauled back to stockpiles. + item.flags.in_building = true end end end @@ -120,7 +124,9 @@ end function AssignToTomb(unit, tomb, forceBurial) local corpseParts = unit.corpse_parts - local strBurial = '%s assigned to a tomb zone for burial.' + local strBurial = '%s assigned to %s for burial.' + local strTomb = 'a tomb zone' + if #tomb.name > 0 then strTomb = tomb.name end local strCorpseItems = '(%d corpse or body part%s)' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) @@ -137,10 +143,9 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id - print(string.format(strBurial, strUnitName)) + print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) - print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) From 16ce689f74a5a58d67391e7516ca42664b7914d6 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Thu, 17 Jul 2025 22:53:03 +0800 Subject: [PATCH 055/272] Fix possible fallthrough in swap_modlist --- gui/mod-manager.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 350ac3143c..87928b5eb1 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -147,8 +147,7 @@ local function swap_modlist(viewscreen, modlist) local res = enable_mod(viewscreen, v.id, v.version) if not res.success then table.insert(failures, v.id) - end - if res.version then + elseif res.version then table.insert(changed, { id= v.id, new= res.version }) end end From 349321c9f166670e317d53b7bcdfe5541493da1c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 17 Jul 2025 14:10:39 -0500 Subject: [PATCH 056/272] correct typo in export-dt-ini.lua --- devel/export-dt-ini.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index 84e1da5cf6..5bf5eadd6c 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -318,7 +318,7 @@ address('physical_attrs',df.unit,'body','physical_attrs') address('body_size',df.unit,'appearance','body_modifiers') address('size_info',df.unit,'body','size_info','size_cur') address('size_base',df.unit,'body','size_info','size_base') -address('curse',df.unit,'uwss_display_name_string') +address('curse',df.unit,'uwss_display_name_sing') address('curse_add_flags1',df.unit,'uwss_add_caste_flag') address('turn_count',df.unit,'usable_interaction','time_on_site') address('souls',df.unit,'status','souls') From 36b7771fab7892e5ed74ef39ee8438657e84904c Mon Sep 17 00:00:00 2001 From: SilasD Date: Fri, 18 Jul 2025 04:24:29 -0700 Subject: [PATCH 057/272] entomb.lua Implement a function that creates a job to haul an item to a coffin. Only the function has been created; the program's UI has not been updated. --- entomb.lua | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/entomb.lua b/entomb.lua index 88483d4609..8d4db14337 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,5 +1,6 @@ -- Entomb corpse items of any dead unit. --@module = true +local utils = require('utils') -- Get unit from selected corpse or corpse piece item. function GetUnitFromCorpse(item) @@ -109,6 +110,51 @@ function PutInCoffin(coffin, item) end end +function HaulToCoffin(tomb, coffin, item) + if not tomb or not coffin or not item then return end + + if dfhack.items.getHolderBuilding(item) == coffin and item.flags.in_building == true then +print("DEBUG: item is already properly interred, skipping", tomb.id, dfhack.buildings.getName(tomb), +coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item)) + return -- already interred in this coffin, skip + end + + -- TODO Consider what should happen when certain item.flags are set, particularly .forbid and .dump. + -- TODO Consider copy-paste-modify scripts/internal/caravan/pedestal.lua::is_displayable_item() + + -- Remove current job from item to allow it to be moved to the tomb. + if item.flags.in_job then + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job or nil + if job + and job.job_type == df.job_type.PlaceItemInTomb + and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) ~= nil + and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER).building_id == tomb.id + then +print("DEBUG: desired job already exists, skipping", tomb.id, dfhack.buildings.getName(tomb), +coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item), job.id) + return -- desired job already exists, skip + end + if job then +print("DEBUG: removing current job from this item", item.id, dfhack.items.getReadableDescription(item), +job.id, df.job_type[job.job_type]) + dfhack.job.removeJob(job) + end + end + + local pos = utils.getBuildingCenter(coffin) + + local job = df.job:new() + job.job_type = df.job_type.PlaceItemInTomb + job.pos = pos + + dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, -1, -1) + dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, tomb.id) + tomb.jobs:insert('#', job) + + dfhack.job.linkIntoWorld(job, true) +end + local function GetCoffin(tomb) local coffin if tomb.type == df.civzone_type.Tomb then @@ -149,7 +195,8 @@ function AssignToTomb(unit, tomb, forceBurial) if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) - PutInCoffin(coffin, item) + -- PutInCoffin(coffin, item) + HaulToCoffin(tomb, coffin, item) end print('Corpse items have been teleported into a coffin.') else From b359cbaa4c7ea3b62da9ccea24ca3fdf6e025581 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 19 Jul 2025 10:31:32 -0700 Subject: [PATCH 058/272] entomb.lua Very Important Bugfix completely assign unit to tomb. --- entomb.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/entomb.lua b/entomb.lua index 8d4db14337..f5e1895d57 100644 --- a/entomb.lua +++ b/entomb.lua @@ -189,6 +189,7 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id + tomb.assigned_unit = unit print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) From 6a185fd92433b03c8e912597b3fdc9814f6c92ee Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 19 Jul 2025 21:09:30 -0700 Subject: [PATCH 059/272] entomb.lua Update the unit's owned buildings with the newly-assigned tomb. --- entomb.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/entomb.lua b/entomb.lua index f5e1895d57..0f0e084282 100644 --- a/entomb.lua +++ b/entomb.lua @@ -190,6 +190,9 @@ function AssignToTomb(unit, tomb, forceBurial) else tomb.assigned_unit_id = unit.id tomb.assigned_unit = unit + if not utils.linear_index(unit.owned_buildings, tomb) then + unit.owned_buildings:insert('#', tomb) + end print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) From 9a3cf1a1c13c0def58979f0c6b59aa3939af7099 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 22 Jul 2025 12:26:13 -0500 Subject: [PATCH 060/272] Update changelog for 52.01-r1 --- changelog.txt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index a937e2fb20..71e2a281fd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,8 +17,6 @@ Template for new versions: ## New Features ## Fixes -- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 -- fixed references to removed ``unit.curse`` compound ## Misc Improvements @@ -33,6 +31,20 @@ Template for new versions: ## New Features ## Fixes + +## Misc Improvements + +## Removed + +# 52.01-r1 + +## New Tools + +## New Features + +## Fixes +- fixed references to removed ``unit.curse`` compound +- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 From a2f45484b58a7ada57e3c7ea3f436424a873b744 Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 23 Jul 2025 11:20:39 -0700 Subject: [PATCH 061/272] Undo commit b359cbaa4c7ea3b62da9ccea24ca3fdf6e025581 entomb.lua Very Important Bugfix completely assign unit to tomb. Because this field was removed in DF 0.51.11. --- entomb.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/entomb.lua b/entomb.lua index 0f0e084282..182981042f 100644 --- a/entomb.lua +++ b/entomb.lua @@ -189,7 +189,6 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id - tomb.assigned_unit = unit if not utils.linear_index(unit.owned_buildings, tomb) then unit.owned_buildings:insert('#', tomb) end From 9d10f0fb11689eba6f0853c01e2c41428c02c1e4 Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 23 Jul 2025 15:19:26 -0700 Subject: [PATCH 062/272] embark-anyone.lua Test the current viewscreen to ensure that it is the choose_start_site viewscreen, before trying to use it. This was found while diagnosing Issue #5509, but is not related. Minimal changes to the script. --- embark-anyone.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/embark-anyone.lua b/embark-anyone.lua index 10772e46e1..156fcaaf0a 100644 --- a/embark-anyone.lua +++ b/embark-anyone.lua @@ -3,6 +3,9 @@ local utils = require('utils') function addCivToEmbarkList(info) local viewscreen = dfhack.gui.getDFViewscreen(true) + if viewscreen._type ~= df.viewscreen_choose_start_sitest then + qerror("This script can only be used on the embark screen!") + end viewscreen.start_civ:insert ('#', info.civ) viewscreen.start_civ_nem_num:insert ('#', info.nemeses) @@ -12,16 +15,16 @@ end function embarkAnyone() local viewscreen = dfhack.gui.getDFViewscreen(true) + if viewscreen._type ~= df.viewscreen_choose_start_sitest then + qerror("This script can only be used on the embark screen!") + end + local choices, existing_civs = {}, {} for _,existing_civ in ipairs(viewscreen.start_civ) do existing_civs[existing_civ.id] = true end - if viewscreen._type ~= df.viewscreen_choose_start_sitest then - qerror("This script can only be used on the embark screen!") - end - for i, civ in ipairs (df.global.world.entities.all) do -- Test if entity is a civ if civ.type ~= df.historical_entity_type.Civilization then goto continue end From 5295834dc6625da9650ab65cc32ab47318733b55 Mon Sep 17 00:00:00 2001 From: SilasD Date: Thu, 24 Jul 2025 07:46:04 -0700 Subject: [PATCH 063/272] Test the current viewscreen The bug: if the embark-anyone script is executed on any viewscreen other than the embark viewscreen, it aborts with a stack trace. This bugfix makes it cleanly abort with a reasonably-descriptive error message. This was found while diagnosing Issue #5509, but is not related. Minimal changes to the script. Note on the code change: by moving the function addCivToEmbarkList() inside the function embarkAnyone(), addCivToEmbarkList() cannot execute unless embarkAnyone() has at least passed its safety check. --- embark-anyone.lua | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/embark-anyone.lua b/embark-anyone.lua index 156fcaaf0a..8239cd332b 100644 --- a/embark-anyone.lua +++ b/embark-anyone.lua @@ -1,19 +1,17 @@ local dialogs = require('gui.dialogs') local utils = require('utils') -function addCivToEmbarkList(info) - local viewscreen = dfhack.gui.getDFViewscreen(true) - if viewscreen._type ~= df.viewscreen_choose_start_sitest then - qerror("This script can only be used on the embark screen!") - end +function embarkAnyone() - viewscreen.start_civ:insert ('#', info.civ) - viewscreen.start_civ_nem_num:insert ('#', info.nemeses) - viewscreen.start_civ_entpop_num:insert ('#', info.pops) - viewscreen.start_civ_site_num:insert ('#', info.sites) -end + function addCivToEmbarkList(info) + local viewscreen = dfhack.gui.getDFViewscreen(true) + + viewscreen.start_civ:insert ('#', info.civ) + viewscreen.start_civ_nem_num:insert ('#', info.nemeses) + viewscreen.start_civ_entpop_num:insert ('#', info.pops) + viewscreen.start_civ_site_num:insert ('#', info.sites) + end -function embarkAnyone() local viewscreen = dfhack.gui.getDFViewscreen(true) if viewscreen._type ~= df.viewscreen_choose_start_sitest then qerror("This script can only be used on the embark screen!") From d7e20aa29402b8321bce89f06b3e239a296350df Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 11:58:42 -0500 Subject: [PATCH 064/272] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 71e2a281fd..b0ca7040df 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ Template for new versions: ## New Features ## Fixes +- ``embark-anyone``: validate viewscreen before using, avoids a crash ## Misc Improvements From 166b2378a95721f5bdf6d7575541772ac420b48a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 13:30:39 -0500 Subject: [PATCH 065/272] Update changelog for 52.02-r1 --- changelog.txt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index b0ca7040df..9be1e38bbe 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,7 +17,6 @@ Template for new versions: ## New Features ## Fixes -- ``embark-anyone``: validate viewscreen before using, avoids a crash ## Misc Improvements @@ -37,6 +36,19 @@ Template for new versions: ## Removed +# 52.02-r1 + +## New Tools + +## New Features + +## Fixes +- ``embark-anyone``: validate viewscreen before using, avoids a crash + +## Misc Improvements + +## Removed + # 52.01-r1 ## New Tools From 25714bf78b8b54705c50ad47c0f13c6b13207861 Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 10:20:28 +0800 Subject: [PATCH 066/272] Add function to validate moving items, separate job removal into own function --- entomb.lua | 158 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 58 deletions(-) diff --git a/entomb.lua b/entomb.lua index 182981042f..a1e1096618 100644 --- a/entomb.lua +++ b/entomb.lua @@ -17,7 +17,7 @@ end -- Validate tomb zone assignment. local function CheckTombZone(building, unit_id) - if building.type == df.civzone_type.Tomb then + if df.building_civzonest:is_instance(building) and building.type == df.civzone_type.Tomb then if building.assigned_unit_id == unit_id then return true end @@ -93,36 +93,21 @@ local function FlagForBurial(unit, corpseParts) return burialItemCount end -function PutInCoffin(coffin, item) - if item then - -- Remove job from item to allow it to be teleported. - if item.flags.in_job then - local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) - local job = inJob and inJob.data.job - if job then - dfhack.job.removeJob(job) - end - end - if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then - -- Flag the item become an interred item, otherwise it will be hauled back to stockpiles. - item.flags.in_building = true - end - end -end - -function HaulToCoffin(tomb, coffin, item) - if not tomb or not coffin or not item then return end - - if dfhack.items.getHolderBuilding(item) == coffin and item.flags.in_building == true then -print("DEBUG: item is already properly interred, skipping", tomb.id, dfhack.buildings.getName(tomb), -coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item)) - return -- already interred in this coffin, skip +-- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() +-- Allow checks for possible use case of interring of non-corpse items. +local function isMoveableItem(tomb, coffin, item, options) + if not item or + item.flags.hostile or + item.flags.removed or + item.flags.spider_web or + item.flags.construction or + item.flags.encased or + item.flags.trader or + item.flags.owned or + item.flags.on_fire + then + return false end - - -- TODO Consider what should happen when certain item.flags are set, particularly .forbid and .dump. - -- TODO Consider copy-paste-modify scripts/internal/caravan/pedestal.lua::is_displayable_item() - - -- Remove current job from item to allow it to be moved to the tomb. if item.flags.in_job then local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) local job = inJob and inJob.data.job or nil @@ -130,34 +115,68 @@ coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDe and job.job_type == df.job_type.PlaceItemInTomb and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) ~= nil and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER).building_id == tomb.id + -- Allow task to be cancelled if teleporting. + and not options.teleport then -print("DEBUG: desired job already exists, skipping", tomb.id, dfhack.buildings.getName(tomb), -coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item), job.id) - return -- desired job already exists, skip + return false end - if job then -print("DEBUG: removing current job from this item", item.id, dfhack.items.getReadableDescription(item), -job.id, df.job_type[job.job_type]) - dfhack.job.removeJob(job) + elseif item.flags.in_inventory then + local inContainer = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) + if not inContainer then return false end + end + if not dfhack.maps.isTileVisible(xyz2pos(dfhack.items.getPosition(item))) then + return false + end + if item.flags.in_building then + local building = dfhack.items.getHolderBuilding(item) + -- Item is already interred. + if building and building == coffin then return false end + for _, containedItem in ipairs(building.contained_items) do + -- Item is part of a building. + if item == contained_item.item then return false end end end + return true +end - local pos = utils.getBuildingCenter(coffin) +-- Remove job from item to allow for hauling or teleportation. +local function RemoveJob(item) + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job + if job then dfhack.job.removeJob(job) end +end + +function TeleportToCoffin(tomb, coffin, item) + if not tomb or not coffin then return end + local itemName = item and dfhack.items.getReadableDescription(item) or nil + if item.flags.in_job then RemoveJob(item) end + if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then + -- Flag the item to become an interred item, otherwise it will be hauled back to stockpiles. + item.flags.in_building = true + local strMove = 'Teleporting %d %s into a coffin.' + print(string.format(strMove, item.id, itemName)) + end +end +function HaulToCoffin(tomb, coffin, item) + if not tomb or not coffin then return end + local itemName = item and dfhack.items.getReadableDescription(item) or nil + if item.flags.in_job then RemoveJob(item) end + local pos = utils.getBuildingCenter(coffin) local job = df.job:new() job.job_type = df.job_type.PlaceItemInTomb job.pos = pos - dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, -1, -1) dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, tomb.id) tomb.jobs:insert('#', job) - dfhack.job.linkIntoWorld(job, true) + local strMove = 'Tasking %d %s for immediate burial.' + print(string.format(strMove, item.id, itemName)) end -local function GetCoffin(tomb) +function GetCoffin(tomb) local coffin - if tomb.type == df.civzone_type.Tomb then + if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then for _, building in ipairs(tomb.contained_buildings) do if df.building_coffinst:is_instance(building) then coffin = building end end @@ -168,15 +187,15 @@ local function GetCoffin(tomb) return coffin end -function AssignToTomb(unit, tomb, forceBurial) +function AssignToTomb(unit, tomb, options) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' local strTomb = 'a tomb zone' if #tomb.name > 0 then strTomb = tomb.name end local strCorpseItems = '(%d corpse or body part%s)' + local strPlural = '' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) - local strPlural = '' local incident_id = unit.counters.death_id if incident_id ~= -1 then local incident = df.incident.find(incident_id) @@ -185,6 +204,7 @@ function AssignToTomb(unit, tomb, forceBurial) incident.flags.discovered = true end local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount > 1 then strPlural = 's' end if burialItemCount == 0 then print(string.format(strNoCorpse, strUnitName)) else @@ -193,28 +213,36 @@ function AssignToTomb(unit, tomb, forceBurial) unit.owned_buildings:insert('#', tomb) end print(string.format(strBurial, strUnitName, strTomb)) - if forceBurial then + print(string.format(strCorpseItems, burialItemCount, strPlural)) + if options.haulNow or options.teleport then local coffin = GetCoffin(tomb) if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) - -- PutInCoffin(coffin, item) - HaulToCoffin(tomb, coffin, item) + if isMoveableItem(tomb, coffin, item, options) then + if options.teleport then + TeleportToCoffin(tomb, coffin, item) + elseif options.haulNow then + HaulToCoffin(tomb, coffin, item) + end + end end - print('Corpse items have been teleported into a coffin.') else - print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end - if burialItemCount > 1 then strPlural = 's' end - print(string.format(strCorpseItems, burialItemCount, strPlural)) end end -local function parseArgs(args) - local unit, tomb, forceBurial +local function ParseArgs(args) + local unit, tomb + local options = { + haulNow = false, + teleport = false + } if args and #args > 0 then for i, v in ipairs(args) do + if v == 'help' then print(dfhack.script_help()) return end if v == 'unit' then local unit_id = tonumber(args[i+1]) or nil unit = unit_id and df.unit.find(unit_id) @@ -231,22 +259,36 @@ local function parseArgs(args) qerror('Specified zone ID does not point to an unassigned tomb zone.') end end - if v == 'now' then forceBurial = true end + if v == 'now' then options.haulNow = true end + if v == 'teleport' then options.teleport = true end + if options.haulNow and options.teleport then + qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + end end end - return unit, tomb, forceBurial + return unit, tomb, options end local function Main(args) - local unit, tomb, forceBurial = parseArgs(args) - local entombed + if not dfhack.isSiteLoaded() and not dfhack.world.isFortressMode() then + qerror('This script requires the game to be in fortress mode.') + end + local unit, tomb, options = ParseArgs(args) if not unit then unit = GetUnitFromCorpse() end if unit then + local entombed if not tomb then tomb, entombed = GetTombZone(unit) end if entombed then print('Unit is already completely interred in a tomb zone.') elseif tomb then - AssignToTomb(unit, tomb, forceBurial) + -- Prevent multiple tomb zone assignments when tomb ID is specified in the command line. + -- Iterating through building.assigned_unit_id is probably safer than checking in + -- unit.owned_buildings, as a reference in one does not guarantee a reference in the other. + building = IterateTombZones(unit.id) + if building and tomb ~= building then + qerror('Unit already has an assigned tomb zone.') + end + AssignToTomb(unit, tomb, options) else print('No unassigned tomb zones are available.') end From 7952e132191f312996cbd0662720220622d3703c Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 15:51:26 +0800 Subject: [PATCH 067/272] Assign new name to unnamed tombs during assignment --- entomb.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/entomb.lua b/entomb.lua index a1e1096618..f5a00e2680 100644 --- a/entomb.lua +++ b/entomb.lua @@ -190,9 +190,16 @@ end function AssignToTomb(unit, tomb, options) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' - local strTomb = 'a tomb zone' - if #tomb.name > 0 then strTomb = tomb.name end - local strCorpseItems = '(%d corpse or body part%s)' + local strTomb = 'Tomb %d' + -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + strTomb = string.format(strTomb, tomb.id) + if #tomb.name > 0 then + strTomb = tomb.name + else + -- Assign name to unnamed tombs for easier search/reference. + tomb.name = strTomb + end + local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' local strPlural = '' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) @@ -213,7 +220,7 @@ function AssignToTomb(unit, tomb, options) unit.owned_buildings:insert('#', tomb) end print(string.format(strBurial, strUnitName, strTomb)) - print(string.format(strCorpseItems, burialItemCount, strPlural)) + print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) if options.haulNow or options.teleport then local coffin = GetCoffin(tomb) if coffin then From f484912b8efe47aa733b553186f4e7c3a108745d Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 17:20:08 +0800 Subject: [PATCH 068/272] Move logic to call HaulToCoffin() and TeleportToCoffin() into new function --- entomb.lua | 127 ++++++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/entomb.lua b/entomb.lua index f5a00e2680..f9af943936 100644 --- a/entomb.lua +++ b/entomb.lua @@ -93,6 +93,56 @@ local function FlagForBurial(unit, corpseParts) return burialItemCount end +function AssignToTomb(unit, tomb) + local corpseParts = unit.corpse_parts + local strBurial = '%s assigned to %s for burial.' + local strTomb = 'Tomb %d' + -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + strTomb = string.format(strTomb, tomb.id) + if #tomb.name > 0 then + strTomb = tomb.name + else + -- Assign name to unnamed tombs for easier search/reference. + tomb.name = strTomb + end + local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' + local strPlural = '' + local strNoCorpse = '%s has no corpse or body parts available for burial.' + local strUnitName = unit and dfhack.units.getReadableName(unit) + local incident_id = unit.counters.death_id + if incident_id ~= -1 then + local incident = df.incident.find(incident_id) + -- Corpse will not be interred if not yet discovered, + -- which never happens for units not belonging to player's civ. + incident.flags.discovered = true + end + local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount > 1 then strPlural = 's' end + if burialItemCount == 0 then + print(string.format(strNoCorpse, strUnitName)) + else + tomb.assigned_unit_id = unit.id + if not utils.linear_index(unit.owned_buildings, tomb) then + unit.owned_buildings:insert('#', tomb) + end + print(string.format(strBurial, strUnitName, strTomb)) + print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) + end +end + +function GetCoffin(tomb) + local coffin + if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then + for _, building in ipairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + -- Allow other scripts to call this function and pass the actual coffin building instead. + elseif df.building_coffinst:is_instance(tomb) then + coffin = tomb + end + return coffin +end + -- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() -- Allow checks for possible use case of interring of non-corpse items. local function isMoveableItem(tomb, coffin, item, options) @@ -174,70 +224,22 @@ function HaulToCoffin(tomb, coffin, item) print(string.format(strMove, item.id, itemName)) end -function GetCoffin(tomb) - local coffin - if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then - for _, building in ipairs(tomb.contained_buildings) do - if df.building_coffinst:is_instance(building) then coffin = building end - end - -- Allow other scripts to call this function and pass the actual coffin building instead. - elseif df.building_coffinst:is_instance(tomb) then - coffin = tomb - end - return coffin -end - -function AssignToTomb(unit, tomb, options) +local function InterItems(tomb, unit, options) local corpseParts = unit.corpse_parts - local strBurial = '%s assigned to %s for burial.' - local strTomb = 'Tomb %d' - -- Provide the tomb's ID so users can invoke it when interring arbitrary items. - strTomb = string.format(strTomb, tomb.id) - if #tomb.name > 0 then - strTomb = tomb.name - else - -- Assign name to unnamed tombs for easier search/reference. - tomb.name = strTomb - end - local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' - local strPlural = '' - local strNoCorpse = '%s has no corpse or body parts available for burial.' - local strUnitName = unit and dfhack.units.getReadableName(unit) - local incident_id = unit.counters.death_id - if incident_id ~= -1 then - local incident = df.incident.find(incident_id) - -- Corpse will not be interred if not yet discovered, - -- which never happens for units not belonging to player's civ. - incident.flags.discovered = true - end - local burialItemCount = FlagForBurial(unit, corpseParts) - if burialItemCount > 1 then strPlural = 's' end - if burialItemCount == 0 then - print(string.format(strNoCorpse, strUnitName)) - else - tomb.assigned_unit_id = unit.id - if not utils.linear_index(unit.owned_buildings, tomb) then - unit.owned_buildings:insert('#', tomb) - end - print(string.format(strBurial, strUnitName, strTomb)) - print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) - if options.haulNow or options.teleport then - local coffin = GetCoffin(tomb) - if coffin then - for _, item_id in ipairs(corpseParts) do - local item = df.item.find(item_id) - if isMoveableItem(tomb, coffin, item, options) then - if options.teleport then - TeleportToCoffin(tomb, coffin, item) - elseif options.haulNow then - HaulToCoffin(tomb, coffin, item) - end - end + local coffin = GetCoffin(tomb) + if coffin then + for _, item_id in ipairs(corpseParts) do + local item = df.item.find(item_id) + if isMoveableItem(tomb, coffin, item, options) then + if options.teleport then + TeleportToCoffin(tomb, coffin, item) + elseif options.haulNow then + HaulToCoffin(tomb, coffin, item) end - else - print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end @@ -295,10 +297,13 @@ local function Main(args) if building and tomb ~= building then qerror('Unit already has an assigned tomb zone.') end - AssignToTomb(unit, tomb, options) + AssignToTomb(unit, tomb) else print('No unassigned tomb zones are available.') end + if options.haulNow or options.teleport then + InterItems(tomb, unit, options) + end else qerror('No item selected or unit specified.') end From 821fef9d916a8bcdd4864a163931749b431114f0 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:28:42 +0800 Subject: [PATCH 069/272] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 87928b5eb1..3a53c19d6e 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -134,7 +134,8 @@ local function get_active_modlist(viewscreen) return t end ---- @return { failures: [string], changed: [{ id: string, new: string }] } +--- @return string[] +--- @return { id: string, new: string }[] local function swap_modlist(viewscreen, modlist) local current = get_active_modlist(viewscreen) for _, v in ipairs(current) do From 466c058a71c2d8d45825a87a8137f9b6dba3d80a Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:30:01 +0800 Subject: [PATCH 070/272] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 3a53c19d6e..48d9606a8b 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -152,7 +152,7 @@ local function swap_modlist(viewscreen, modlist) table.insert(changed, { id= v.id, new= res.version }) end end - return { failures= failures, changed= changed } + return failures, changed end -------------------- From 077e3c9c045c3a29d8432ce506090cdc1c55f011 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:30:28 +0800 Subject: [PATCH 071/272] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 48d9606a8b..645df30c7d 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -243,7 +243,7 @@ local function load_preset(idx, unset_default_on_failure) local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist - local results = swap_modlist(viewscreen, modlist) + local failures, changed = swap_modlist(viewscreen, modlist) local failures = results.failures local changes = results.changed local text = {} From 0fb8e3a2d28237f8fb274f147933f891036d8fca Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:30:39 +0800 Subject: [PATCH 072/272] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 645df30c7d..8850e40d43 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -245,7 +245,6 @@ local function load_preset(idx, unset_default_on_failure) local modlist = presets_file.data[idx].modlist local failures, changed = swap_modlist(viewscreen, modlist) local failures = results.failures - local changes = results.changed local text = {} local failed = #failures > 0 From 7127f4b10c1b8315a2f44caf36d431bd456d2bd6 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:31:34 +0800 Subject: [PATCH 073/272] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 8850e40d43..da844c0252 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -74,7 +74,8 @@ function get_modlist_fields(kind, viewscreen) end end ---- @return { success: boolean, version: string } +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) From fbb2ae021edc0c8017d016edf83198901f1a4fcc Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:32:00 +0800 Subject: [PATCH 074/272] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index da844c0252..ac3ce61484 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -112,7 +112,7 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) v:erase(mod_index) end - return { success= true, version= loaded_version } + return true, loaded_version end --- @return { success: boolean, version: string } From ef7986a68e68f900dfdcc2a2dcb2dba5ade391fe Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:32:45 +0800 Subject: [PATCH 075/272] Apply suggestions from code review Co-authored-by: SilasD --- gui/mod-manager.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index ac3ce61484..9bd1778c4a 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -115,12 +115,14 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) return true, loaded_version end ---- @return { success: boolean, version: string } +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) end ---- @return { success: boolean, version: string } +---@return boolean # returns true if the mod entry was moved; returns false if the mod or mod version was not found. +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function disable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) end @@ -146,11 +148,11 @@ local function swap_modlist(viewscreen, modlist) local failures = {} local changed = {} for _, v in ipairs(modlist) do - local res = enable_mod(viewscreen, v.id, v.version) - if not res.success then + local success, version = enable_mod(viewscreen, v.id, v.version) + if not success then table.insert(failures, v.id) - elseif res.version then - table.insert(changed, { id= v.id, new= res.version }) + elseif version then + table.insert(changed, { id= v.id, new= version }) end end return failures, changed @@ -245,7 +247,6 @@ local function load_preset(idx, unset_default_on_failure) local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist local failures, changed = swap_modlist(viewscreen, modlist) - local failures = results.failures local text = {} local failed = #failures > 0 From 68f87f6db80ac224218e78b2317bd9820eab6ee5 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 26 Jul 2025 04:39:04 +0800 Subject: [PATCH 076/272] Remove comment --- gui/mod-manager.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 9bd1778c4a..d78f0bc8f2 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,7 +12,6 @@ local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' --- Shamelessly taken from hack/library/lua/script-manager.lua local function vanilla(dir) dir = dir.value return dir:startswith('data/vanilla') @@ -75,7 +74,7 @@ function get_modlist_fields(kind, viewscreen) end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) @@ -116,13 +115,13 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) end ---@return boolean # returns true if the mod entry was moved; returns false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function disable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) end From 72b13db868a280e87da4755e6092911e864bec5d Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 26 Jul 2025 05:39:08 +0800 Subject: [PATCH 077/272] Update for 52.02 paths --- gui/mod-manager.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index d78f0bc8f2..5e258b470c 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -13,7 +13,6 @@ local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' local function vanilla(dir) - dir = dir.value return dir:startswith('data/vanilla') end @@ -245,7 +244,7 @@ local function load_preset(idx, unset_default_on_failure) local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist - local failures, changed = swap_modlist(viewscreen, modlist) + local failures, changes = swap_modlist(viewscreen, modlist) local text = {} local failed = #failures > 0 From 63a64fd83ea96e0233c45f4bc9f88333c0a02d11 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 26 Jul 2025 18:03:49 +0800 Subject: [PATCH 078/272] Edit docstrings --- gui/mod-manager.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 5e258b470c..12029661b6 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -72,8 +72,8 @@ function get_modlist_fields(kind, viewscreen) end end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) @@ -95,7 +95,7 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) end if mod_index == nil then - return { success= false, version= nil } + return false, nil end for k, v in pairs(to_fields) do @@ -106,21 +106,21 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) end end - for k, v in pairs(from_fields) do + for _, v in pairs(from_fields) do v:erase(mod_index) end return true, loaded_version end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) end ----@return boolean # returns true if the mod entry was moved; returns false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function disable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) end From 3cec9d6f116a7e4246723f0aaeb2dfe6b124a697 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 26 Jul 2025 11:52:50 -0500 Subject: [PATCH 079/272] correct changelog for #1481 --- changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1094ea67de..2b0db42d71 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,8 +29,10 @@ Template for new versions: ## New Tools ## New Features +- `gui/mod-manager`: now supports arena mode ## Fixes +- `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset ## Misc Improvements @@ -60,8 +62,6 @@ Template for new versions: - `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files -- `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset -- `gui/mod-manager`: now supports arena mode - `uniform-unstick`: resolve overlap with new buttons in 51.13 ## Misc Improvements From 699d0f3ad174fde5e7f09955960cd6c9ee201748 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 16:08:24 +0800 Subject: [PATCH 080/272] Add deduplication logic for gui/mod-manager --- gui/mod-manager.lua | 60 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 12029661b6..7a52ed8adb 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -74,7 +74,7 @@ end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt -local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) +local function copy_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) @@ -106,23 +106,51 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) end end - for _, v in pairs(from_fields) do - v:erase(mod_index) - end - return true, loaded_version end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) - return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) + return copy_mod_entry(viewscreen, "object_load_order", "base_available", mod_id, mod_version) end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt -local function disable_mod(viewscreen, mod_id, mod_version) - return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) +local function make_available_mod(viewscreen, mod_id, mod_version) + return copy_mod_entry(viewscreen, "available", "base_available", mod_id, mod_version) +end + +local function clear_mods(viewscreen) + local active_modlist = get_modlist_fields('object_load_order', viewscreen) + local avail_modlist = get_modlist_fields('available', viewscreen) + for _, modlist in ipairs({active_modlist, avail_modlist}) do + for _, v in pairs(modlist) do + for i = #v - 1, 0, -1 do + v:erase(i) + end + end + end +end + +local function set_available_mods(viewscreen, loaded) + local base_avail = get_modlist_fields('base_available', viewscreen) + local unused = {} + for i, id in ipairs(base_avail.id) do + local j = utils.linear_index(loaded, id.value) + if j then goto continue end + + local version = base_avail.numeric_version[i] + table.insert(unused, { id= id.value, version= version }) + ::continue:: + end + + for _, v in ipairs(unused) do + local success, _ = make_available_mod(viewscreen, v.id, v.version) + if not success then + dfhack.printerr('failed to show '..v.id..' in available list') + end + end end local function get_active_modlist(viewscreen) @@ -138,21 +166,27 @@ end --- @return string[] --- @return { id: string, new: string }[] local function swap_modlist(viewscreen, modlist) - local current = get_active_modlist(viewscreen) - for _, v in ipairs(current) do - disable_mod(viewscreen, v.id, v.version) - end + clear_mods(viewscreen) local failures = {} local changed = {} + local loaded = {} for _, v in ipairs(modlist) do local success, version = enable_mod(viewscreen, v.id, v.version) if not success then table.insert(failures, v.id) - elseif version then + goto continue + end + + table.insert(loaded, v.id) + if version then table.insert(changed, { id= v.id, new= version }) end + + ::continue:: end + + set_available_mods(viewscreen, loaded) return failures, changed end From f0d1be15f41a4317f69ae0911ef1e63d69a1cc21 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 16:12:34 +0800 Subject: [PATCH 081/272] Update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 1094ea67de..8a95feac9c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded ## Misc Improvements From 8321e37d6e9b1e551678f50b7c8b3ad1033689e6 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 16:17:32 +0800 Subject: [PATCH 082/272] Update docstrings in gui/mod-manager --- gui/mod-manager.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 7a52ed8adb..5eac49d0f5 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -72,7 +72,7 @@ function get_modlist_fields(kind, viewscreen) end end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return boolean # true if the mod entry was copied over; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function copy_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) @@ -109,13 +109,13 @@ local function copy_mod_entry(viewscreen, to, from, mod_id, mod_version) return true, loaded_version end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return boolean # true if the mod entry was copied over; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) return copy_mod_entry(viewscreen, "object_load_order", "base_available", mod_id, mod_version) end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return boolean # true if the mod entry was copied over; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function make_available_mod(viewscreen, mod_id, mod_version) return copy_mod_entry(viewscreen, "available", "base_available", mod_id, mod_version) From aae344e11df950b050e40aa2d857e3b3e9f09a38 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 21:37:32 +0800 Subject: [PATCH 083/272] Update gui/mod-manager --- gui/mod-manager.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 5eac49d0f5..476d8afc7a 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -137,8 +137,7 @@ local function set_available_mods(viewscreen, loaded) local base_avail = get_modlist_fields('base_available', viewscreen) local unused = {} for i, id in ipairs(base_avail.id) do - local j = utils.linear_index(loaded, id.value) - if j then goto continue end + if loaded[id.value] then goto continue end local version = base_avail.numeric_version[i] table.insert(unused, { id= id.value, version= version }) @@ -178,7 +177,7 @@ local function swap_modlist(viewscreen, modlist) goto continue end - table.insert(loaded, v.id) + loaded[v.id] = true if version then table.insert(changed, { id= v.id, new= version }) end From b021bc08626e85eceae8ed7f829c61a683e1ee08 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 23:35:26 +0800 Subject: [PATCH 084/272] Apply code review suggestions --- changelog.txt | 2 +- gui/mod-manager.lua | 22 +++++++++------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8a95feac9c..6139a0d000 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,7 +17,6 @@ Template for new versions: ## New Features ## Fixes -- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded ## Misc Improvements @@ -32,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded ## Misc Improvements diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 476d8afc7a..a3a745963d 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -137,11 +137,10 @@ local function set_available_mods(viewscreen, loaded) local base_avail = get_modlist_fields('base_available', viewscreen) local unused = {} for i, id in ipairs(base_avail.id) do - if loaded[id.value] then goto continue end - - local version = base_avail.numeric_version[i] - table.insert(unused, { id= id.value, version= version }) - ::continue:: + if not loaded[id.value] then + local version = base_avail.numeric_version[i] + table.insert(unused, { id= id.value, version= version }) + end end for _, v in ipairs(unused) do @@ -174,15 +173,12 @@ local function swap_modlist(viewscreen, modlist) local success, version = enable_mod(viewscreen, v.id, v.version) if not success then table.insert(failures, v.id) - goto continue - end - - loaded[v.id] = true - if version then - table.insert(changed, { id= v.id, new= version }) + else + if version then + table.insert(changed, { id= v.id, new= version }) + end + loaded[v.id] = true end - - ::continue:: end set_available_mods(viewscreen, loaded) From 30993eef1c6ad88279886b6863ad56bbc82f669b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 27 Jul 2025 12:22:31 -0500 Subject: [PATCH 085/272] Update changelog for 52.02-r2 --- changelog.txt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index bcf188da2d..8b79bd7b2d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,12 +28,24 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 52.02-r2 + +## New Tools + ## New Features - `gui/mod-manager`: now supports arena mode ## Fixes -- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded - `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset +- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded ## Misc Improvements From 514da4a2aecdbd5e033ce90c78e9dba0847f9be7 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 27 Jul 2025 10:45:41 -0700 Subject: [PATCH 086/272] pedestal.lua bad handling of .displayed_items. --- internal/caravan/pedestal.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index 363160093a..5e469c6815 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -599,8 +599,8 @@ end local function unassign_item(bld, item) if not bld then return end - local _, found, idx = utils.binsearch(bld.displayed_items, item.id) - if found then + local idx, _ = utils.linear_index(bld.displayed_items, item.id) + if idx then bld.displayed_items:erase(idx) end end @@ -628,7 +628,7 @@ local function attach_item(item, display_bld) local ref = df.new(df.general_ref_building_display_furniturest) ref.building_id = display_bld.id item.general_refs:insert('#', ref) - utils.insert_sorted(display_bld.displayed_items, item.id) + display_bld.displayed_items:insert('#', item.id) item.flags.forbid = false item.flags.in_building = false end From 842e82e261d47f881e32405d1b087a03577fa121 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 27 Jul 2025 12:00:03 -0700 Subject: [PATCH 087/272] pedestal.lua clear .in_building flag on unassign --- internal/caravan/pedestal.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index 5e469c6815..dab100ef48 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -602,6 +602,7 @@ local function unassign_item(bld, item) local idx, _ = utils.linear_index(bld.displayed_items, item.id) if idx then bld.displayed_items:erase(idx) + item.flags.in_building = false end end From ba5e5d151555d51599d741a12f29efb4a6673ef4 Mon Sep 17 00:00:00 2001 From: git--amade Date: Mon, 28 Jul 2025 18:21:45 +0800 Subject: [PATCH 088/272] Implement add-item option, revise Main() logic, switch to use argparse module --- entomb.lua | 313 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 232 insertions(+), 81 deletions(-) diff --git a/entomb.lua b/entomb.lua index f9af943936..fa40d7960c 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,8 +1,31 @@ -- Entomb corpse items of any dead unit. --@module = true + +local argparse = require('argparse') local utils = require('utils') +local guidm = require('gui.dwarfmode') --- Get unit from selected corpse or corpse piece item. +-- Check if any of the unit's corpse items are not yet placed in a coffin. +function isEntombed(unit) + -- Return FALSE for still living or undead units with empty corpse_parts vector. + if #unit.corpse_parts == 0 then return false end + for _, item_id in ipairs(unit.corpse_parts) do + local item = df.item.find(item_id) + if item then + local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local building_id = inBuilding and inBuilding.building_id or -1 + local building = df.building.find(building_id) + local isCoffin = (building and df.building_coffinst:is_instance(building)) or false + -- Return FALSE if even one item is not interred. + if not isCoffin then + return false + end + end + end + return true +end + +-- Get unit from selected corpse or body part item. function GetUnitFromCorpse(item) if math.type(item) == "integer" then item = df.item.find(item) elseif not item then item = dfhack.gui.getSelectedItem(true) end @@ -10,9 +33,10 @@ function GetUnitFromCorpse(item) if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then return df.unit.find(item.unit_id) else - qerror('Item is not a corpse or body part.') + qerror('Selected item is not a corpse or body part.') end end + return nil end -- Validate tomb zone assignment. @@ -33,43 +57,39 @@ local function IterateTombZones(unit_id) return nil end --- Check if any of the unit's corpse items are not yet placed in a coffin. -function isEntombed(unit) - -- Return FALSE for still living or undead units with empty corpse_parts vector. - if #unit.corpse_parts == 0 then return false end - for _, item_id in ipairs(unit.corpse_parts) do - local item = df.item.find(item_id) - if item then - local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) - local building_id = inBuilding and inBuilding.building_id or -1 - local building = df.building.find(building_id) - local isCoffin = (building and df.building_coffinst:is_instance(building)) or false - -- Return FALSE if even one item is not interred. - if not isCoffin then - return false +-- Use when user inputs coffin building ID instead of tomb zone ID. +function GetTombFromCoffin(building) + if #building.relations > 0 then + for _, v in ipairs(building.relations) do + if df.building_civzonest:is_instance(v) and v.type == df.civzone_type.Tomb then + return v end end end - return true + return nil end -local function GetTombZone(unit) - local unit_id = unit.id - local tomb - local entombed = false - -- Check if unit is already assigned to a tomb zone. - local isAlreadyAssigned = IterateTombZones(unit_id) - if isAlreadyAssigned then - tomb = isAlreadyAssigned - entombed = isEntombed(unit) +function GetTombFromZone(building) + if df.building_civzonest:is_instance(building) and building.type == df.civzone_type.Tomb then + return building + elseif df.building_coffinst:is_instance(building) then + return GetTombFromCoffin(building) + end + return nil +end + +function GetTombFromUnit(unit) + -- Check if unit already has a tomb zone assigned. + local alreadyAssignedTomb = unit and IterateTombZones(unit.id) + if alreadyAssignedTomb then + return alreadyAssignedTomb else - -- Find an unassigned tomb zone. - tomb = IterateTombZones(-1) + -- Get an unassigned tomb zone. + return IterateTombZones(-1) end - return tomb, entombed end --- Set corpse items to be valid for burial. +-- Set unit's corpse items to be valid for burial. local function FlagForBurial(unit, corpseParts) -- Undead units have empty corpse_parts vector. if unit.enemy.undead then @@ -97,7 +117,7 @@ function AssignToTomb(unit, tomb) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' local strTomb = 'Tomb %d' - -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + -- Provide the tomb's ID so the user can invoke it when interring arbitrary items. strTomb = string.format(strTomb, tomb.id) if #tomb.name > 0 then strTomb = tomb.name @@ -144,9 +164,10 @@ function GetCoffin(tomb) end -- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() --- Allow checks for possible use case of interring of non-corpse items. +-- Allow checks for possible use case of interring arbitrary items. local function isMoveableItem(tomb, coffin, item, options) if not item or + -- Allow forbid/dump/melt designated items to be valid. item.flags.hostile or item.flags.removed or item.flags.spider_web or @@ -154,10 +175,15 @@ local function isMoveableItem(tomb, coffin, item, options) item.flags.encased or item.flags.trader or item.flags.owned or + item.flags.garbage_collect or item.flags.on_fire then return false end + -- Allow user to exclude items by forbidding when adding arbitrary items. + if options.addItem and item.flags.forbid then + return false + end if item.flags.in_job then local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) local job = inJob and inJob.data.job or nil @@ -189,6 +215,81 @@ local function isMoveableItem(tomb, coffin, item, options) return true end +function isAlreadyBurialItem(unit, item) + -- Prevent duplicating unit's own corpse parts in corpse_parts. + for _, v in ipairs(unit.corpse_parts) do + if item.id == v then return true end + end + -- Prevent adding burial items belonging to other units with an assigned tomb. + for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do + if not CheckTombZone(building, -1) then + local otherUnit = df.unit.find(building.assigned_unit_id) + for _, v in ipairs(otherUnit.corpse_parts) do + if item.id == v then return true end + end + end + end + return false +end + +-- Set additional arbitrary items to be valid for burial. +function AddBurialItems(unit, tomb, options) + local coffin = GetCoffin(tomb) + local item = dfhack.gui.getSelectedItem(true) + local cursor = guidm.getCursorPos() + local burialItems = {} + local strAddItem = 'Adding %s for burial with unit.' + local strItemName + local strCannotInter = 'Unable to inter additional item(s);\n ...%s.' + local strNoCoffin = 'no coffin in assigned tomb zone' + local strNotValidItem = 'selected item is not valid for burial' + local strNoCursorItems = 'no items at cursor are valid for burial' + local strNoSelect = 'no item selected and keyboard cursor not enabled' + if not coffin then + print(string.format(strCannotInter, strNoCoffin)) + elseif item then + if isMoveableItem(tomb, coffin, item, options) and + not isAlreadyBurialItem(unit, item) + then + strItemName = item and dfhack.items.getReadableDescription(item) or nil + print(string.format(strAddItem, strItemName)) + table.insert(burialItems, item) + else + print(string.format(strCannotInter, strNotValidItem)) + end + -- Use keyboard cursor to set multiple items for burial. + elseif cursor then + -- Filter items to iterate according to tile block at cursor. + local block = dfhack.maps.getTileBlock(cursor) + for _, blockItem_id in ipairs(block.items) do + local blockItem = df.item.find(blockItem_id) + local x, y, _ = dfhack.items.getPosition(blockItem) + if x == cursor.x and y == cursor.y then + item = blockItem + if isMoveableItem(tomb, coffin, item, options) and + not isAlreadyBurialItem(unit, item) + then + strItemName = item and dfhack.items.getReadableDescription(item) or nil + print(string.format(strAddItem, strItemName)) + table.insert(burialItems, item) + end + end + end + if #burialItems == 0 then + print(string.format(strCannotInter, strNoCursorItems)) + end + else + print(string.format(strCannotInter, strNoSelect)) + end + if #burialItems > 0 then + local corpseParts = unit.corpse_parts + for _, burialItem in ipairs(burialItems) do + burialItem.flags.dead_dwarf = true + corpseParts:insert('#', burialItem.id) + end + end +end + -- Remove job from item to allow for hauling or teleportation. local function RemoveJob(item) local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) @@ -239,73 +340,123 @@ local function InterItems(tomb, unit, options) end end else - print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') + print('Unable to move burial item(s);\n ...no coffin in assigned tomb zone.') end end -local function ParseArgs(args) - local unit, tomb - local options = { - haulNow = false, - teleport = false - } - if args and #args > 0 then - for i, v in ipairs(args) do - if v == 'help' then print(dfhack.script_help()) return end - if v == 'unit' then - local unit_id = tonumber(args[i+1]) or nil - unit = unit_id and df.unit.find(unit_id) - if not unit then qerror('Invalid unit ID.') end +-- Process unit and tomb before executing operations. +local function PreOpProcess(unit, building, options) + local tomb = building and GetTombFromZone(building) + local entombed = false + if not options.addItem then + if not unit then + unit = GetUnitFromCorpse() + end + if not tomb then + tomb = GetTombFromUnit(unit) + end + if unit and tomb then + -- Unit has a tomb, but it's not the specified tomb. + if IterateTombZones(unit.id) and tomb ~= IterateTombZones(unit.id) then + qerror('Unit already has an assigned tomb zone.') + -- Specified tomb is not assigned to unit, and specified tomb is not unassigned. + elseif not CheckTombZone(tomb, unit.id) and not CheckTombZone(tomb, -1) then + qerror('Specified tomb zone is already assigned to a different unit.') end - if v == 'tomb' then - local building_id = tonumber(args[i+1]) or nil - local building = building_id and df.building.find(building_id) - if not building then qerror('Invalid zone ID.') end - -- Check if tomb zone is unassigned. - if CheckTombZone(building, -1) then - tomb = building - else - qerror('Specified zone ID does not point to an unassigned tomb zone.') - end + end + if unit then + if not tomb then + qerror('No unassigned tomb zones are available.') + end + entombed = isEntombed(unit) + else + qerror('No item selected or unit specified.') + end + else + -- Either a unit or an assigned tomb zone must be specified when add-item is called, + -- as corpse/body part items cannot be used to assign tomb zones with this option. + local strCannotInter = 'Unable to inter additional item(s);\n ...%s.' + local strNoUnit = 'specified tomb zone is not assigned to a unit' + local strNoTomb = 'specified unit has no assigned tomb zone' + local strWrongPair = 'specified tomb zone is not assigned to specified unit' + local strNotSpecified = 'no assigned tomb zone or unit with assigned tomb zone specified' + if tomb and not unit then + if tomb.assigned_unit_id == -1 then + qerror(string.format(strCannotInter, strNoUnit)) end - if v == 'now' then options.haulNow = true end - if v == 'teleport' then options.teleport = true end - if options.haulNow and options.teleport then - qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + unit = df.unit.find(tomb.assigned_unit_id) + if not unit then + qerror(string.format(strCannotInter, strNoUnit)) end + elseif unit and not tomb then + tomb = GetTombFromUnit(unit) + if not tomb then + -- Equivalent to having no available unassigned tomb zones, + -- but emphasize on unit having no assigned tomb. + qerror(string.format(strCannotInter, strNoTomb)) + end + elseif tomb and unit then + if not CheckTombZone(tomb, unit.id) and not CheckTombZone(tomb, -1) then + qerror(string.format(strCannotInter, strWrongPair)) + end + else + qerror(string.format(strCannotInter, strNotSpecified)) end end - return unit, tomb, options + return unit, tomb, entombed +end + +local function ParseCommandLine(args) + local unit, building + local options = { + help = false, + addItem = false, + haulNow = false, + teleport = false + } + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler = function() options.help = true end}, + {'u', 'unit', hasArg = true, handler = function(arg) + local unit_id = argparse.positiveInt(arg, 'unit') + unit = unit_id and df.unit.find(unit_id) + if not unit then qerror('Invalid unit ID.') end end + }, + {'t', 'tomb', hasArg = true, handler = function(arg) + local building_id = argparse.positiveInt(arg, 'tomb') + building = building_id and df.building.find(building_id) + if not building then qerror('Invalid zone ID.') end end + }, + {'a', 'add-item', handler = function() options.addItem = true end}, + {'h', 'haul-now', handler = function() options.haulNow = true end}, + {'', 'teleport', handler = function() options.teleport = true end} + }) + return unit, building, options end local function Main(args) if not dfhack.isSiteLoaded() and not dfhack.world.isFortressMode() then qerror('This script requires the game to be in fortress mode.') end - local unit, tomb, options = ParseArgs(args) - if not unit then unit = GetUnitFromCorpse() end - if unit then - local entombed - if not tomb then tomb, entombed = GetTombZone(unit) end - if entombed then - print('Unit is already completely interred in a tomb zone.') - elseif tomb then - -- Prevent multiple tomb zone assignments when tomb ID is specified in the command line. - -- Iterating through building.assigned_unit_id is probably safer than checking in - -- unit.owned_buildings, as a reference in one does not guarantee a reference in the other. - building = IterateTombZones(unit.id) - if building and tomb ~= building then - qerror('Unit already has an assigned tomb zone.') - end - AssignToTomb(unit, tomb) - else - print('No unassigned tomb zones are available.') + local unit, building, options = ParseCommandLine(args) + if args == 'help' or options.help then + print(dfhack.script_help()) + return + end + if options.haulNow and options.teleport then + qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + end + local tomb, entombed + unit, tomb, entombed = PreOpProcess(unit, building, options) + if entombed then + print('Unit is already completely interred in a tomb zone.') + elseif unit and tomb then + AssignToTomb(unit, tomb) + if options.addItem then + AddBurialItems(unit, tomb, options) end if options.haulNow or options.teleport then InterItems(tomb, unit, options) end - else - qerror('No item selected or unit specified.') end end From e11d9680a59f761224ccf5787fc2cd258ddb08de Mon Sep 17 00:00:00 2001 From: git--amade Date: Mon, 28 Jul 2025 20:14:04 +0800 Subject: [PATCH 089/272] Disable teleport function, update documentation --- docs/entomb.rst | 51 +++++++++++++++++++++++++++---------------------- entomb.lua | 3 ++- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index c519ff7bcf..64f708a3d0 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -5,7 +5,7 @@ entomb :summary: Entomb any corpse into tomb zones. :tags: fort items buildings -Assign any corpse regardless of citizenship, residency, pet status, +Assign any unit regardless of citizenship, residency, pet status, or affiliation to an unassigned tomb zone for burial. Usage @@ -13,20 +13,20 @@ Usage ``entomb []`` -This script must be executed with either a unit's corpse or body part -selected or with a unit ID specified. An unassigned tomb zone will then -be assigned to the unit for burial and all its corpse and/or body parts -will become valid items for interment. +Select a unit's corpse or body part, or specify the unit's ID +when executing this script to assign an unassigned tomb zone to +the unit, and flag the unit's corpse as well as any severed body +parts to become valid items for interment. -Optionally, the zone ID may also be specified to assign a specific tomb +Optionally, specify the tomb zone's ID to assign a specific tomb zone to the unit. -A non-citizen, non-resident, or non-pet unit that is still alive may -even be assigned a tomb zone if they have lost any body part that can -be placed inside a tomb, e.g. teeth or severed limbs. New corpse items -after a tomb has already been assigned will not be properly interred -until the script is executed again on either the unit, its corpse, or -any of its body parts. +A non-citizen, non-resident, or non-pet unit that is still alive +may even be assigned a tomb zone if they have lost any body part +that can be placed inside a tomb, e.g. teeth or severed limbs. +New corpse items after a tomb has already been assigned will not +be properly interred until the script is executed again with the +unit ID specified, or the unit's corpse or any body part selected. If executed on slaughtered animals, all its butchering returns will become valid burial items and no longer usable for cooking or crafting. @@ -34,28 +34,33 @@ become valid burial items and no longer usable for cooking or crafting. Examples -------- -``entomb unit `` +``entomb --unit `` Assign an unassigned tomb zone to the unit with the specified ID. -``entomb tomb `` +``entomb --tomb `` Assign a tomb zone with the specified ID to the selected corpse item's unit. -``entomb unit tomb now`` +``entomb -u -t -h`` Assign a tomb zone with the specified ID to the unit with the - specified ID and teleport its corpse and/or body parts into the - coffin in the tomb zone. + specified ID and task all its burial items for simultaneous + hauling into the coffin in the tomb zone. Options ------- -``unit `` +``-u``, ``--unit `` Specify the ID of the unit to be assigned to a tomb zone. -``tomb `` +``-t``, ``--tomb `` Specify the ID of the zone into which a unit will be interred. -``now`` - Instantly teleport the unit's corpse and/or body parts into the - coffin of its assigned tomb zone. This option can be called on - corpse items or units that are already assigned a tomb zone. +``-a``, ``add-item`` + Add a selected item, or multiple items at the keyboard cursor's + position to be interred together with a unit. A unit or tomb + zone ID must be specified when calling this option. + +``-h``, ``haul-now`` + Task all of the unit's burial items for simultaneous hauling + into the coffin of its assigned tomb zone. This option can be + called even after a tomb zone is already assigned to the unit. diff --git a/entomb.lua b/entomb.lua index fa40d7960c..2160a77fde 100644 --- a/entomb.lua +++ b/entomb.lua @@ -428,7 +428,8 @@ local function ParseCommandLine(args) }, {'a', 'add-item', handler = function() options.addItem = true end}, {'h', 'haul-now', handler = function() options.haulNow = true end}, - {'', 'teleport', handler = function() options.teleport = true end} + -- Commenting out to make this script a non-Armok tool. + -- {'', 'teleport', handler = function() options.teleport = true end} }) return unit, building, options end From 8f4feca6f6fce604fb8d1ea5baaac91ce2225bde Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 30 Jul 2025 03:43:57 +0800 Subject: [PATCH 090/272] Fix argparse short-form conflict --- docs/entomb.rst | 2 +- entomb.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index 64f708a3d0..cef6025199 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -60,7 +60,7 @@ Options position to be interred together with a unit. A unit or tomb zone ID must be specified when calling this option. -``-h``, ``haul-now`` +``-n``, ``haul-now`` Task all of the unit's burial items for simultaneous hauling into the coffin of its assigned tomb zone. This option can be called even after a tomb zone is already assigned to the unit. diff --git a/entomb.lua b/entomb.lua index 2160a77fde..dd4f22ca97 100644 --- a/entomb.lua +++ b/entomb.lua @@ -427,7 +427,7 @@ local function ParseCommandLine(args) if not building then qerror('Invalid zone ID.') end end }, {'a', 'add-item', handler = function() options.addItem = true end}, - {'h', 'haul-now', handler = function() options.haulNow = true end}, + {'n', 'haul-now', handler = function() options.haulNow = true end}, -- Commenting out to make this script a non-Armok tool. -- {'', 'teleport', handler = function() options.teleport = true end} }) From 785741bf8043d1148eac8d02ac4b4ca04fe7afd6 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 30 Jul 2025 03:46:38 +0800 Subject: [PATCH 091/272] Fix documentation --- docs/entomb.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index cef6025199..1352b990e4 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -55,12 +55,12 @@ Options ``-t``, ``--tomb `` Specify the ID of the zone into which a unit will be interred. -``-a``, ``add-item`` +``-a``, ``--add-item`` Add a selected item, or multiple items at the keyboard cursor's position to be interred together with a unit. A unit or tomb zone ID must be specified when calling this option. -``-n``, ``haul-now`` +``-n``, ``--haul-now`` Task all of the unit's burial items for simultaneous hauling into the coffin of its assigned tomb zone. This option can be called even after a tomb zone is already assigned to the unit. From 8490a13ad4ac16682655b52a6c6acff8758b36f7 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 2 Aug 2025 18:11:38 +0200 Subject: [PATCH 092/272] simplify code and properly split off portions --- changelog.txt | 2 +- immortal-cravings.lua | 117 +++++++++++++++++++++++++++--------------- 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/changelog.txt b/changelog.txt index 142f3f5452..355043437a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,7 +36,7 @@ Template for new versions: - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 -- `immortal-cravings`: prioritize high-value meals and don't go eating or drinking on a full stomach +- `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach ## Misc Improvements diff --git a/immortal-cravings.lua b/immortal-cravings.lua index de251f3fc8..21ae333ecb 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -3,8 +3,18 @@ local idle = reqscript('idle-crafting') local repeatutil = require("repeat-util") + --- utility functions +local verbose = false +---conditional printing of debug messages +---@param message string +local function debug(message) + if verbose then + print(message) + end +end + ---3D city metric ---@param p1 df.coord ---@param p2 df.coord @@ -13,22 +23,20 @@ function distance(p1, p2) return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + math.abs(p1.z - p2.z) end ----find best item in an item vector (according to some metric) +---maybe a candidate for utils.lua? +---find best available item in an item vector (according to some metric) ---@generic T : df.item ---@param item_vector T[] ----@param metric fun(item: T): number ----@param is_good? fun(item: T): boolean +---@param metric fun(item: T): number? ---@return T? -function findBest(item_vector, metric, is_good) +function findBest(item_vector, metric, smallest) local best = nil - local mbest = -1 - for _,item in ipairs(item_vector) do - if not item.flags.in_job and (not is_good or is_good(item)) then - mitem = metric(item) - if not best or mitem > mbest then - best = item - mbest = mitem - end + local mbest = nil + for _, item in ipairs(item_vector) do + mitem = metric(item) + if mitem and (not best or (smallest and mitem < mbest or mitem > mbest)) then + best = item + mbest = mitem end end return best @@ -41,19 +49,14 @@ end ---@param is_good? fun(item: T): boolean ---@return T? local function findClosest(pos, item_vector, is_good) - local closest = nil - local dclosest = -1 - for _,item in ipairs(item_vector) do - if not item.flags.in_job and (not is_good or is_good(item)) then + local function metric(item) + if not is_good or is_good(item) then local pitem = xyz2pos(dfhack.items.getPosition(item)) - local ditem = distance(pos, pitem) - if dfhack.maps.canWalkBetween(pos, pitem) and (not closest or ditem < dclosest) then - closest = item - dclosest = ditem - end + return dfhack.maps.canWalkBetween(pos, pitem) and distance(pos, pitem) or nil end + return nil end - return closest + return findBest(item_vector, metric, true) end ---find a drink @@ -62,33 +65,32 @@ end local function get_closest_drink(pos) local is_good = function (drink) local container = dfhack.items.getContainer(drink) - return container and container:isFoodStorage() + return not drink.flags.in_job and container and container:isFoodStorage() end return findClosest(pos, df.global.world.items.other.DRINK, is_good) end ----find highest-value accessible meal +---find available meal with highest per-portion value ---@return df.item_foodst? local function get_best_meal(pos) ---@param meal df.item_foodst - local function is_good(meal) + local function portion_value(meal) local accessible = dfhack.maps.canWalkBetween(pos,xyz2pos(dfhack.items.getPosition(meal))) - if meal.flags.rotten or not accessible then - return false + if meal.flags.in_job or meal.flags.rotten or not accessible then + return nil else -- check that meal is either on the ground or in food storage (and not in a backpack) local container = dfhack.items.getContainer(meal) - return not container or container:isFoodStorage() + if not container or container:isFoodStorage() then + return dfhack.items.getValue(meal) / meal.stack_size + else + return nil + end end end - ---@param meal df.item_foodst - local function portion_value(meal) - return dfhack.items.getValue(meal) / meal.stack_size - end - - return findBest(df.global.world.items.other.FOOD, portion_value, is_good) + return findBest(df.global.world.items.other.FOOD, portion_value) end ---create a Drink job for the given unit @@ -116,11 +118,22 @@ end ---create Eat job for the given unit ---@param unit df.unit local function goEat(unit) - local meal = get_best_meal(unit.pos) - if not meal then + local meal_stack = get_best_meal(unit.pos) + if not meal_stack then -- print('no accessible meals found') return end + + ---@type df.item|df.item_foodst + local meal + if meal_stack.stack_size > 1 then + meal = meal_stack:splitStack(1, true) + meal:categorize(true) + else + meal = meal_stack + end + dfhack.items.setOwner(meal, unit) + local job = idle.make_job() job.job_type = df.job_type.Eat job.flags.special = true @@ -135,6 +148,25 @@ local function goEat(unit) print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name)) end +---unit is ready to take jobs (will interrupt social activities) +---@param unit df.unit +---@return boolean +function unitIsAvailable(unit) + if unit.job.current_job then + return false + elseif #unit.individual_drills > 0 then + return false + elseif unit.flags1.caged or unit.flags1.chained then + return false + elseif unit.military.squad_id ~= -1 then + local squad = df.squad.find(unit.military.squad_id) + -- this lookup should never fail + ---@diagnostic disable-next-line: need-check-nil + return #squad.orders == 0 and squad.activity == -1 + end + return true +end + --- script logic local GLOBAL_KEY = 'immortal-cravings' @@ -167,7 +199,7 @@ local threshold = -9000 ---unit loop: check for idle watched units and create eat/drink jobs for them local function unit_loop() - -- print(('immortal-cravings: running unit loop (%d watched units)'):format(#watched)) + debug(('immortal-cravings: running unit loop (%d watched units)'):format(#watched)) ---@type integer[] local kept = {} for _, unit_id in ipairs(watched) do @@ -178,7 +210,8 @@ local function unit_loop() then goto next_unit end - if not idle.unitIsAvailable(unit) then + if not unitIsAvailable(unit) then + debug("immortal-cravings: skipping busy"..dfhack.units.getReadableName(unit)) table.insert(kept, unit.id) else -- unit is available for jobs; satisfy one of its needs @@ -196,7 +229,7 @@ local function unit_loop() end watched = kept if #watched == 0 then - -- print('immortal-cravings: no more watched units, cancelling unit loop') + debug('immortal-cravings: no more watched units, cancelling unit loop') repeatutil.cancel(GLOBAL_KEY .. '-unit') end end @@ -208,9 +241,9 @@ end ---main loop: look for citizens with personality needs for food/drink but w/o physiological need local function main_loop() - -- print('immortal-cravings watching:') + debug('immortal-cravings watching:') watched = {} - for _, unit in ipairs(dfhack.units.getCitizens()) do + for _, unit in ipairs(dfhack.units.getCitizens(false, false)) do if not (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) or unit.counters2.stomach_content > 0 @@ -222,7 +255,7 @@ local function main_loop() need.id == EatGoodMeal and need.focus_level < threshold then table.insert(watched, unit.id) - -- print(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) + debug(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) goto next_unit end end From ab7dc7654b5c53aff0bc8821b69531544e4f1d76 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 6 Aug 2025 18:18:22 -0500 Subject: [PATCH 093/272] prevent `make-legendary` from assigning skill -1 fixes DFHack/dfhack#5541 --- changelog.txt | 1 + make-legendary.lua | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8b79bd7b2d..c6324adf87 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `make-legendary`: ``make-legendary all`` will no longer corrupt souls ## Misc Improvements diff --git a/make-legendary.lua b/make-legendary.lua index 2098ad0ba2..a0c96c0cb2 100644 --- a/make-legendary.lua +++ b/make-legendary.lua @@ -7,9 +7,11 @@ function getName(unit) end function legendize(unit, skill_idx) - utils.insert_or_update(unit.status.current_soul.skills, - {new=true, id=skill_idx, rating=df.skill_rating.Legendary5}, - 'id') + if skill_idx >= 0 and skill_idx <= df.job_skill._last_item then + utils.insert_or_update(unit.status.current_soul.skills, + {new=true, id=skill_idx, rating=df.skill_rating.Legendary5}, + 'id') + end end function make_legendary(skillname) @@ -50,7 +52,9 @@ function BreathOfArmok() return end for i in ipairs(df.job_skill) do - legendize(unit, i) + if i >= 0 then + legendize(unit, i) + end end print('The breath of Armok has engulfed ' .. getName(unit)) end From 61e0e181953e70679d0ed4dfc7e9e64922ad0c07 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 12:53:25 -0500 Subject: [PATCH 094/272] Update changelog for 52.03-r1 --- changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/changelog.txt b/changelog.txt index c6324adf87..23e458faf1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,18 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Removed + +# 52.03-r1 + +## New Tools + +## New Features + ## Fixes - `make-legendary`: ``make-legendary all`` will no longer corrupt souls From 062ef18de351faf25a471ab1d8e562d9515b3cb7 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 13 Aug 2025 11:42:50 +0800 Subject: [PATCH 095/272] Assign only active tomb zones and make it unavailable for auto assigment to other units --- entomb.lua | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/entomb.lua b/entomb.lua index dd4f22ca97..735b2d65f3 100644 --- a/entomb.lua +++ b/entomb.lua @@ -52,7 +52,14 @@ end -- Iterate through all available tomb zones. local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do - if CheckTombZone(building, unit_id) then return building end + if unit_id == -1 then + -- Use only active (unpaused) zones when assigning unassigned tomb zones. + if building.spec_sub_flag.active then + if CheckTombZone(building, unit_id) then return building end + end + else + if CheckTombZone(building, unit_id) then return building end + end end return nil end @@ -60,9 +67,9 @@ end -- Use when user inputs coffin building ID instead of tomb zone ID. function GetTombFromCoffin(building) if #building.relations > 0 then - for _, v in ipairs(building.relations) do - if df.building_civzonest:is_instance(v) and v.type == df.civzone_type.Tomb then - return v + for _, zone in ipairs(building.relations) do + if df.building_civzonest:is_instance(zone) and zone.type == df.civzone_type.Tomb then + return zone end end end @@ -134,6 +141,7 @@ function AssignToTomb(unit, tomb) local incident = df.incident.find(incident_id) -- Corpse will not be interred if not yet discovered, -- which never happens for units not belonging to player's civ. + -- Only needed for units that have a death incident. incident.flags.discovered = true end local burialItemCount = FlagForBurial(unit, corpseParts) @@ -145,6 +153,9 @@ function AssignToTomb(unit, tomb) if not utils.linear_index(unit.owned_buildings, tomb) then unit.owned_buildings:insert('#', tomb) end + -- Make tomb zone unavailable for automatic assignment to other dead units. + tomb.zone_settings.tomb.flags.no_pets = true + tomb.zone_settings.tomb.flags.no_citizens = true print(string.format(strBurial, strUnitName, strTomb)) print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) end From cdf3ebb2512e19c94a7c3a760b5ffd7f3cf84140 Mon Sep 17 00:00:00 2001 From: Jarkami Date: Wed, 13 Aug 2025 00:36:46 -0400 Subject: [PATCH 096/272] Fix uniform assignment state inconsistency caused by uniform-unstick --- changelog.txt | 1 + uniform-unstick.lua | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/changelog.txt b/changelog.txt index 23e458faf1..5f1a881f48 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `uniform-unstick`: no longer causes units to equip multiples of assigned items ## Misc Improvements diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 0fb501fd2d..929ca71d7b 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -196,6 +196,22 @@ local function process(unit, args, need_newline) end end + -- Make the equipment.assigned_items list consistent with what is present in equipment.uniform + for i=#(squad_position.equipment.assigned_items)-1,0,-1 do + local u_id = squad_position.equipment.assigned_items[i] + -- Quiver, backpack, and flask are assigned in their own locations rather than in equipment.uniform, and thus need their own checks + -- If more separately-assigned items are added in the future, this handling will need to be updated accordingly + if assigned_items[u_id] == nil and u_id ~= squad_position.equipment.quiver and u_id ~= squad_position.equipment.backpack and u_id ~= squad_position.equipment.flask then + local item = df.item.find(u_id) + if item ~= nil then + need_newline = print_line(unit_name .. " has an improperly assigned item, item # " .. u_id .. " '" .. item_description(item) .. "'; removing it") + else + need_newline = print_line(unit_name .. " has a nonexistent item assigned, item # " .. u_id .. "; removing it") + end + squad_position.equipment.assigned_items:erase(i) + end + end + -- Figure out which worn items should be dropped -- First, figure out which body parts are covered by the uniform pieces we have. From fb3f2d1b32b09605823677df00992b219dddce98 Mon Sep 17 00:00:00 2001 From: Jarkami Date: Wed, 13 Aug 2025 00:41:00 -0400 Subject: [PATCH 097/272] Refactor item description logging in uniform-unstick --- uniform-unstick.lua | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 929ca71d7b..7ea2b33d86 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -16,7 +16,7 @@ local validArgs = utils.invert({ -- Functions local function item_description(item) - return dfhack.df2console(dfhack.items.getDescription(item, 0, true)) + return "item #" .. item.id .. " '" .. dfhack.df2console(dfhack.items.getDescription(item, 0, true)) .. "'" end local function get_item_pos(item) @@ -166,11 +166,10 @@ local function process(unit, args, need_newline) for u_id, item in pairs(assigned_items) do if not worn_items[u_id] then if not silent then - need_newline = print_line(unit_name .. " is missing an assigned item, object #" .. u_id .. " '" .. - item_description(item) .. "'", need_newline) + need_newline = print_line(unit_name .. " is missing an assigned item, " .. item_description(item), need_newline) end if dfhack.items.getGeneralRef(item, df.general_ref_type.UNIT_HOLDER) then - need_newline = print_line(unit_name .. " cannot equip item: another unit has a claim on object #" .. u_id .. " '" .. item_description(item) .. "'", need_newline) + need_newline = print_line(unit_name .. " cannot equip item: another unit has a claim on " .. item_description(item), need_newline) if args.free then print(" Removing from uniform") assigned_items[u_id] = nil @@ -204,9 +203,9 @@ local function process(unit, args, need_newline) if assigned_items[u_id] == nil and u_id ~= squad_position.equipment.quiver and u_id ~= squad_position.equipment.backpack and u_id ~= squad_position.equipment.flask then local item = df.item.find(u_id) if item ~= nil then - need_newline = print_line(unit_name .. " has an improperly assigned item, item # " .. u_id .. " '" .. item_description(item) .. "'; removing it") + need_newline = print_line(unit_name .. " has an improperly assigned item, " .. item_description(item) .. '; removing it') else - need_newline = print_line(unit_name .. " has a nonexistent item assigned, item # " .. u_id .. "; removing it") + need_newline = print_line(unit_name .. " has a nonexistent item assigned, item # " .. u_id .. '; removing it') end squad_position.equipment.assigned_items:erase(i) end @@ -240,9 +239,7 @@ local function process(unit, args, need_newline) for w_id, item in pairs(worn_items) do if assigned_items[w_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands) if uncovered[worn_parts[w_id]] then - need_newline = print_line(unit_name .. - " potentially has object #" .. - w_id .. " '" .. item_description(item) .. "' blocking a missing uniform item.", need_newline) + need_newline = print_line(unit_name .. " potentially has " .. item_description(item) .. " blocking a missing uniform item.", need_newline) if args.drop then to_drop[w_id] = item end @@ -261,12 +258,12 @@ local function do_drop(item_list) for id, item in pairs(item_list) do local pos = get_item_pos(item) if not pos then - dfhack.printerr("Could not find drop location for item #" .. id .. " " .. item_description(item)) + dfhack.printerr("Could not find drop location for " .. item_description(item)) else if dfhack.items.moveToGround(item, pos) then - print("Dropped item #" .. id .. " '" .. item_description(item) .. "'") + print("Dropped " .. item_description(item)) else - dfhack.printerr("Could not drop object #" .. id .. " " .. item_description(item)) + dfhack.printerr("Could not drop " .. item_description(item)) end end end From 5f1c7b7658dc6f7c1902077e5ac416f13e711477 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 13 Aug 2025 13:32:38 +0800 Subject: [PATCH 098/272] Remove duplicated code in IterateTombZones() --- entomb.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/entomb.lua b/entomb.lua index 735b2d65f3..0455f6b71e 100644 --- a/entomb.lua +++ b/entomb.lua @@ -52,14 +52,10 @@ end -- Iterate through all available tomb zones. local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do - if unit_id == -1 then - -- Use only active (unpaused) zones when assigning unassigned tomb zones. - if building.spec_sub_flag.active then - if CheckTombZone(building, unit_id) then return building end - end - else - if CheckTombZone(building, unit_id) then return building end - end + -- Use only active (unpaused) zones when assigning unassigned tomb zones. + if unit_id == -1 and not building.spec_sub_flag.active then goto skipIteration end + if CheckTombZone(building, unit_id) then return building end + ::skipIteration:: end return nil end From fb76d7b3b46fe07917dd7cb549fe711ca3de386b Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:49:07 +0200 Subject: [PATCH 099/272] new tool: husbandry --- changelog.txt | 2 + docs/husbandry.rst | 61 +++++++++ husbandry.lua | 326 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 389 insertions(+) create mode 100644 docs/husbandry.rst create mode 100644 husbandry.lua diff --git a/changelog.txt b/changelog.txt index 23e458faf1..9ea3bc988c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,8 @@ Template for new versions: ## New Tools +- `husbandry`: Automatically milk and shear animals at nearby farmer's workshops + ## New Features ## Fixes diff --git a/docs/husbandry.rst b/docs/husbandry.rst new file mode 100644 index 0000000000..27fa459cfe --- /dev/null +++ b/docs/husbandry.rst @@ -0,0 +1,61 @@ +husbandry +========= + +.. dfhack-tool:: + :summary: Automatically milk and shear animals. + :tags: fort auto + +This tool will automatically create milking and shearing orders at farmer's +workshops. Unlike the ``automilk`` and ``autoshear`` options from the control +panel, which create general work orders for milking and shearing jobs, +``husbandry`` will directly create jobs for individual animals at specific +workshops. This allows milking and shearing jobs to reliably be created at +nearby workshops (e.g. inside the pasture that an animal is assigned to), +minimizing the labor required to re-pasture animals after milking or shearing, +in particular in the case of multiple pastures that are far apart. + + +Usage +----- + +:: + + enable husbandry + husbandry [status] + husbandry now + husbandry [set|unset] [shearing|milking|roaming|pasture]+ + +Flags can be set or unset using the command ``husbandry set`` or ``husbandry +unset``. The ``shearing`` and ``milking`` flags (both enabled by default) +control whether shearing or milking jobs are created at all. + +Further, ``husbandry`` distinguishes between animals that are assigned to +pastures and those that are "roaming". + +If an animal is pastured and the pasture contains at least one workshop with the +appropriate labour (i.e. milking or shearing) enabled, jobs will be created +exclusively at those workshops. If the pasture does not contain a workshop with +the appropriate labor enabled the behavior depends on the ``pasture`` flag +(disabled by default): if set, no jobs will be created at workshops outside of +pastures, otherwise jobs may be created at the closest workshop in your fort. + +For animals that are roaming, jobs will only be created if the ``roaming`` flag +is set, which is the default. In this case, jobs are created at the closest +workshop with the appropriate labours enabled. + +Examples +-------- + +``enable husbandry`` + Start generating milking and shearing orders for animals. + +``husbandry now`` + Run a single cycle, detecting animals that can be milked/sheared an creating + jobs. Does not require the tool to be enabled. + +``husbandry unset roaming`` + Disable the creation of jobs for roaming animals. + +``husbandry set milking shearing pasture`` + Create milking and shearing jobs for pastured animals, but only at workshops + inside their pastures. diff --git a/husbandry.lua b/husbandry.lua new file mode 100644 index 0000000000..f8dd1375ec --- /dev/null +++ b/husbandry.lua @@ -0,0 +1,326 @@ + +--@enable = true +--@module = true + +local utils = require 'utils' +local repeatutil = require("repeat-util") +local ic = reqscript('idle-crafting') + +local verbose = true +---conditional printing of debug messages +---@param message string +local function debug(message) + if verbose then + print(message) + end +end + +-- From workorder.lua +---------------------------8<----------------------------- + +local function isValidAnimal(unit) + -- this should also check for the absence of misc trait 55 (as of 50.09), but we don't + -- currently have an enum definition for that value yet + return dfhack.units.isOwnCiv(unit) + and dfhack.units.isAlive(unit) + and dfhack.units.isAdult(unit) + and dfhack.units.isActive(unit) + and dfhack.units.isFortControlled(unit) + and dfhack.units.isTame(unit) + and not dfhack.units.isMarkedForSlaughter(unit) + and not dfhack.units.getMiscTrait(unit, df.misc_trait_type.Migrant, false) +end + +-- true/false or nil if no shearable_tissue_layer with length > 0. +local function canShearCreature(unit) + local stls = df.global.world.raws.creatures + .all[unit.race] + .caste[unit.caste] + .shearable_tissue_layer + + local any + for _, stl in ipairs(stls) do + if stl.length > 0 then + for _, bpi in ipairs(stl.bp_modifiers_idx) do + any = { unit.appearance.bp_modifiers[bpi], stl.length } + if unit.appearance.bp_modifiers[bpi] >= stl.length then + return true, any + end + end + end + end + + if any then return false, any end + -- otherwise: nil +end + +---------------------------8<----------------------------- + +local function canMilkCreature(u) + if dfhack.units.isMilkable(u) and not dfhack.units.isPet(u) then + local mt_milk = dfhack.units.getMiscTrait(u, df.misc_trait_type.MilkCounter, false) + if not mt_milk then return true else return false end + else + return nil + end +end + +---@param p1 df.coord +---@param p2 df.coord +---@return number +function distance(p1, p2) + return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + 2 * math.abs(p1.z - p2.z) +end + +---find appropriate workshop to milk or shear an animal +---@param unit df.unit +---@param collection table +---@return df.building_workshopst? +local function getAppropriateWorkshop(unit, collection) + local zone_ref = dfhack.units.getGeneralRef(unit, df.general_ref_type.BUILDING_CIVZONE_ASSIGNED) + local zone = zone_ref and zone_ref:getBuilding() or nil + + -- if animal is assigned to a zone containing workshops, only use those + if zone then + local contains_workshop = false + local best = nil + local worst_load = 10 + for _, workshop in pairs(collection[zone.z] or {}) do + if dfhack.buildings.containsTile(zone, workshop.centerx, workshop.centery) then + contains_workshop = true + local workshop_pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) + if dfhack.maps.canWalkBetween(unit.pos, workshop_pos) and #workshop.jobs < worst_load then + worst_load = #workshop.jobs + best = workshop + end + end + end + if contains_workshop or state.pasture then + return best + end + elseif not state.roaming then + return nil -- not treating roaming animals + end + -- otherwise, use the closest workshop to the animal + local closest = nil + local dist = nil + for _, level in pairs(collection) do + for _, workshop in pairs(level) do + local workshop_pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) + if dfhack.maps.canWalkBetween(unit.pos, workshop_pos) then + local d = distance(unit.pos, workshop_pos) + if not closest or d < dist then + closest = workshop + dist = d + end + end + end + end + return #closest.jobs < 10 and closest or nil +end + +local function shearCreature(unit, workshop) + local job = ic.make_job() + job.job_type = df.job_type.ShearCreature + dfhack.job.addGeneralRef(job, df.general_ref_type.UNIT_SHEAREE, unit.id) + ic.assignToWorkshop(job, workshop) +end + +local function milkCreature(unit, workshop) + local job = ic.make_job() + job.job_type = df.job_type.MilkCreature + dfhack.job.addGeneralRef(job, df.general_ref_type.UNIT_MILKEE, unit.id) + ic.assignToWorkshop(job, workshop) +end + + +-- configuration management + +GLOBAL_KEY = 'husbandry' + +local function get_default_state() + return { + enabled = false, + milking = true, + shearing = true, + roaming = true; + pasture = false + } +end + +state = state or get_default_state() + +function isEnabled() + return state.enabled +end + +function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=state.enabled, + milking=state.milking, + shearing=state.shearing, + roaming=state.roaming, + pasture=state.pasture, + }) +end + +--- Load the saved state of the script +local function load_state() + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, get_default_state()) + state.enabled = persisted_data.enabled + state.milking = persisted_data.milking + state.shearing = persisted_data.shearing + state.roaming = persisted_data.roaming + state.pasture = persisted_data.pasture + return state +end + +-- main script action + +local function action() + debug('husbandry: running loop') + + -- organize workshops by allowed labors and z-level + ---@type table + local farmer_shearing = {} + ---@type table + local farmer_milking = {} + for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do + if not workshop.profile.blocked_labors[df.unit_labor.SHEARER] then + table.insert(ensure_key(farmer_shearing, workshop.z), workshop) + end + if not workshop.profile.blocked_labors[df.unit_labor.MILK] then + table.insert(ensure_key(farmer_milking, workshop.z), workshop) + end + end + + -- gather units that are already being milked or sheared + ---@type table + local unit_milking = {} + ---@type table + local unit_shearing = {} + + -- go over all workshops to to catch player-initiated jobs + for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do + for _, job in ipairs(workshop.jobs) do + if state.milking and job.job_type == df.job_type.MilkCreature then + local milkee = dfhack.job.getGeneralRef(job, df.general_ref_type.UNIT_MILKEE) + if milkee then + unit_milking[milkee.unit_id] = true + end + elseif state.shearing and job.job_type == df.job_type.ShearCreature then + local shearee = dfhack.job.getGeneralRef(job, df.general_ref_type.UNIT_SHEAREE) + if shearee then + unit_shearing[shearee.unit_id] = true + end + end + end + end + + -- look for units that can be milked/sheared and for which there is no active job + for _, unit in ipairs(df.global.world.units.active) do + if not isValidAnimal(unit) then goto skip end + + if state.shearing and canShearCreature(unit) and not unit_shearing[unit.id] then + local workshop = getAppropriateWorkshop(unit, farmer_shearing) + if workshop then + shearCreature(unit, workshop) + end + end + + if state.milking and canMilkCreature(unit) and not unit_milking[unit.id] then + local workshop = getAppropriateWorkshop(unit, farmer_milking) + if workshop then + milkCreature(unit, workshop) + end + end + + ::skip:: + end +end + +-- enable management + +local function start() + if state.enabled then + repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY, 1000, 'ticks', action) + end +end + +local function stop() + repeatutil.cancel(GLOBAL_KEY) +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + state.enabled = false + return + end + + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + load_state() + start() +end + +if dfhack_flags.module then + return +end + +if dfhack_flags.enable then + if dfhack_flags.enable_state then + enabled = true + start() + else + enabled = false + stop() + end + persist_state() + return +end + +-- command-line interface + +local argparse = require('argparse') +local positionals = argparse.processArgsGetopt({ ... }, {}) + +local state_vars = utils.invert({ "milking", "shearing", "roaming", "pasture" }) + +local function setFlags(positionals, value) + for i = 2, #positionals do + local flag = positionals[i] + if state_vars[flag] then + debug(("setting %s = %s"):format(flag, value)) + state[flag] = value + end + end +end + +load_state() +if not positionals[1] or positionals[1] == 'status' then + print(("husbandry is %s"):format(state.enabled and "enabled" or "not enabled")) + print(("currently %smilking%s%sshearing animals"):format( + state.milking and "" or "not ", + state.milking == state.shearing and " and " or " but ", + state.shearing and "" or "not ")) + print(("%s roaming animals"):format(state.roaming and "including" or "ignoring")) + if state.pasture then + print("not milking/shearing animals inside pastures without workshops") + end +elseif positionals[1] == "set" then + if positionals[2] == "default" then + state = get_default_state() + else + setFlags(positionals, true) + end +elseif positionals[1] == "unset" then + setFlags(positionals, false) +elseif positionals[1] == "now" then + action() +else + qerror("unrecognized option") +end +persist_state() From 0831e6e0ebd559c1debd2ca5f916bca781874f30 Mon Sep 17 00:00:00 2001 From: Jarkami Date: Sun, 17 Aug 2025 02:20:49 -0400 Subject: [PATCH 100/272] Clean up/improve code readability in uniform-unstick --- uniform-unstick.lua | 162 +++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 68 deletions(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 7ea2b33d86..bd8b550ba9 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -15,11 +15,15 @@ local validArgs = utils.invert({ -- Functions +-- @param item df.item +-- @return string local function item_description(item) return "item #" .. item.id .. " '" .. dfhack.df2console(dfhack.items.getDescription(item, 0, true)) .. "'" end -local function get_item_pos(item) +-- @param item df.item +-- @return df.coord|nil +local function get_visible_item_pos(item) local x, y, z = dfhack.items.getPosition(item) if not x or not y or not z then return @@ -30,24 +34,30 @@ local function get_item_pos(item) end end -local function get_squad_position(unit, unit_name) +-- @param unit df.unit +-- @return df.squad_position|nil +local function get_squad_position(unit) local squad = df.squad.find(unit.military.squad_id) - if squad then - if squad.entity_id ~= df.global.plotinfo.group_id then - print("WARNING: Unit " .. unit_name .. " is a member of a squad from another site!" .. - " This may be preventing them from doing any useful work." .. - " You can fix this by assigning them to a local squad and then unassigning them.") - print() - return - end - else + if not squad then + return + end + + if squad.entity_id ~= df.global.plotinfo.group_id then + print("WARNING: Unit " .. dfhack.df2console(dfhack.units.getReadableName(unit)) .. " is a member of a squad from another site!" .. + " This may be preventing them from doing any useful work." .. + " You can fix this by assigning them to a local squad and then unassigning them.") + print() return end + if #squad.positions > unit.military.squad_position then return squad.positions[unit.military.squad_position] end end +-- @param unit df.unit +-- @param item df.item +-- @return number[] list of body part ids local function bodyparts_that_can_wear(unit, item) local bodyparts = {} local unitparts = dfhack.units.getCasteRaw(unit).body_info.body_parts @@ -89,47 +99,61 @@ local function bodyparts_that_can_wear(unit, item) return bodyparts end --- returns new value of need_newline -local function print_line(text, need_newline) - if need_newline then - print() - end - print(text) - return false +-- @param unit_name string +-- @param labor_name string +local function print_bad_labor(unit_name, labor_name) + return print("WARNING: Unit " .. unit_name .. " has the " .. labor_name .. + " labor enabled, which conflicts with military uniforms.") end -local function print_bad_labor(unit_name, labor_name, need_newline) - return print_line("WARNING: Unit " .. unit_name .. " has the " .. labor_name .. - " labor enabled, which conflicts with military uniforms.", need_newline) +-- @param squad_position df.squad_position +-- @param item_id number +local function remove_item_from_position(squad_position, item_id) + for _, uniform_slot_specs in ipairs(squad_position.equipment.uniform) do + for _, uniform_spec in ipairs(uniform_slot_specs) do + for idx, assigned_item_id in ipairs(uniform_spec.assigned) do + if assigned_item_id == item_id then + uniform_spec.assigned:erase(idx) + return + end + end + end + end end -- Will figure out which items need to be moved to the floor, returns an item_id:item map -local function process(unit, args, need_newline) +local function process(unit, args) local silent = args.all -- Don't print details if we're iterating through all dwarves local unit_name = dfhack.df2console(dfhack.units.getReadableName(unit)) + local printed = false if not silent then - need_newline = print_line("Processing unit " .. unit_name, need_newline) + print("Processing unit " .. unit_name) + printed = true end -- The return value local to_drop = {} -- item id to item object -- First get squad position for an early-out for non-military dwarves - local squad_position = get_squad_position(unit, unit_name) + local squad_position = get_squad_position(unit) if not squad_position then if not silent then - need_newline = print_line(unit_name .. " does not have a military uniform.", need_newline) + print(unit_name .. " does not have a military uniform.") + print() end return end if unit.status.labors.MINE then - need_newline = print_bad_labor(unit_name, "mining", need_newline) + print_bad_labor(unit_name, "mining") + printed = true elseif unit.status.labors.CUTWOOD then - need_newline = print_bad_labor(unit_name, "woodcutting", need_newline) + print_bad_labor(unit_name, "woodcutting") + printed = true elseif unit.status.labors.HUNT then - need_newline = print_bad_labor(unit_name, "hunting", need_newline) + print_bad_labor(unit_name, "hunting") + printed = true end -- Find all worn items which may be at issue. @@ -148,12 +172,12 @@ local function process(unit, args, need_newline) end -- Now get info about which items have been assigned as part of the uniform - local assigned_items = {} -- assigned item ids mapped to item objects - for _, specs in ipairs(squad_position.equipment.uniform) do - for _, spec in ipairs(specs) do - for _, assigned in ipairs(spec.assigned) do + local uniform_assigned_items = {} -- assigned item ids mapped to item objects + for _, uniform_slot_specs in ipairs(squad_position.equipment.uniform) do + for _, uniform_spec in ipairs(uniform_slot_specs) do + for _, assigned_item_id in ipairs(uniform_spec.assigned) do -- Include weapon and shield so we can avoid dropping them, or pull them out of container/inventory later - assigned_items[assigned] = df.item.find(assigned) + uniform_assigned_items[assigned_item_id] = df.item.find(assigned_item_id) end end end @@ -163,50 +187,48 @@ local function process(unit, args, need_newline) local present_ids = {} -- map of item ID to item object local missing_ids = {} -- map of item ID to item object - for u_id, item in pairs(assigned_items) do - if not worn_items[u_id] then + for item_id, item in pairs(uniform_assigned_items) do + if not worn_items[item_id] then if not silent then - need_newline = print_line(unit_name .. " is missing an assigned item, " .. item_description(item), need_newline) + print(unit_name .. " is missing an assigned item, " .. item_description(item)) + printed = true end if dfhack.items.getGeneralRef(item, df.general_ref_type.UNIT_HOLDER) then - need_newline = print_line(unit_name .. " cannot equip item: another unit has a claim on " .. item_description(item), need_newline) + print(unit_name .. " cannot equip item: another unit has a claim on " .. item_description(item)) + printed = true if args.free then print(" Removing from uniform") - assigned_items[u_id] = nil - for _, specs in ipairs(squad_position.equipment.uniform) do - for _, spec in ipairs(specs) do - for idx, assigned in ipairs(spec.assigned) do - if assigned == u_id then - spec.assigned:erase(idx) - break - end - end - end - end + uniform_assigned_items[item_id] = nil + remove_item_from_position(squad_position, item_id) end else - missing_ids[u_id] = item + missing_ids[item_id] = item if args.free then - to_drop[u_id] = item + to_drop[item_id] = item end end else - present_ids[u_id] = item + present_ids[item_id] = item end end -- Make the equipment.assigned_items list consistent with what is present in equipment.uniform for i=#(squad_position.equipment.assigned_items)-1,0,-1 do - local u_id = squad_position.equipment.assigned_items[i] + local assigned_item_id = squad_position.equipment.assigned_items[i] -- Quiver, backpack, and flask are assigned in their own locations rather than in equipment.uniform, and thus need their own checks -- If more separately-assigned items are added in the future, this handling will need to be updated accordingly - if assigned_items[u_id] == nil and u_id ~= squad_position.equipment.quiver and u_id ~= squad_position.equipment.backpack and u_id ~= squad_position.equipment.flask then - local item = df.item.find(u_id) + if uniform_assigned_items[assigned_item_id] == nil and + assigned_item_id ~= squad_position.equipment.quiver and + assigned_item_id ~= squad_position.equipment.backpack and + assigned_item_id ~= squad_position.equipment.flask + then + local item = df.item.find(assigned_item_id) if item ~= nil then - need_newline = print_line(unit_name .. " has an improperly assigned item, " .. item_description(item) .. '; removing it') + print(unit_name .. " has an improperly assigned item, " .. item_description(item) .. "; removing it") else - need_newline = print_line(unit_name .. " has a nonexistent item assigned, item # " .. u_id .. '; removing it') + print(unit_name .. " has a nonexistent item assigned, item # " .. assigned_item_id .. "; removing it") end + printed = true squad_position.equipment.assigned_items:erase(i) end end @@ -217,10 +239,10 @@ local function process(unit, args, need_newline) -- unless --multi is specified, in which we don't care local covered = {} -- map of body part id to true/nil if not args.multi then - for id, item in pairs(present_ids) do + for item_id, item in pairs(present_ids) do -- weapons and shields don't "cover" the bodypart they're assigned to. (Needed to figure out if we're missing gloves.) if item._type ~= df.item_weaponst and item._type ~= df.item_shieldst then - covered[worn_parts[id]] = true + covered[worn_parts[item_id]] = true end end end @@ -236,17 +258,23 @@ local function process(unit, args, need_newline) end -- Drop everything (except uniform pieces) from body parts which should be covered but aren't - for w_id, item in pairs(worn_items) do - if assigned_items[w_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands) - if uncovered[worn_parts[w_id]] then - need_newline = print_line(unit_name .. " potentially has " .. item_description(item) .. " blocking a missing uniform item.", need_newline) + for worn_item_id, item in pairs(worn_items) do + if uniform_assigned_items[worn_item_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands) + if uncovered[worn_parts[worn_item_id]] then + print(unit_name .. " potentially has " .. item_description(item) .. " blocking a missing uniform item.") + printed = true if args.drop then - to_drop[w_id] = item + to_drop[worn_item_id] = item end end end end + -- add a spacing line if there was any output + if printed then + print() + end + return to_drop end @@ -255,8 +283,8 @@ local function do_drop(item_list) return end - for id, item in pairs(item_list) do - local pos = get_item_pos(item) + for _, item in pairs(item_list) do + local pos = get_visible_item_pos(item) if not pos then dfhack.printerr("Could not find drop location for " .. item_description(item)) else @@ -278,10 +306,8 @@ local function main(args) end if args.all then - local need_newline = false for _, unit in ipairs(dfhack.units.getCitizens(true)) do - do_drop(process(unit, args, need_newline)) - need_newline = true + do_drop(process(unit, args)) end else local unit = dfhack.gui.getSelectedUnit() From a02935d0bdf833486e80cf39351ea9edd2e48727 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 17 Aug 2025 12:31:14 -0500 Subject: [PATCH 101/272] New Feature: `autotraining` (#1411) * New Feature: `gym` Code for dwarves to hit the gym when they yearn for the gains. Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training * Fix whitespace * missed some * MORE whitespace (and some other cleanup) * Update gym.lua * Create gym.rst * Fix EOF * Update gym.rst * fix key error * more key errors * Update the documentation * Use the enable/disable stuff not args to start or stop * Do the documentation in one place * Various fixes - Clean up documentation - Add option to change squad name. - persist the enabled state, the threshold, and the squad name. - fixed findNeed function - renamed script to `autotraining` - made the ignore flag more clear and more changable - fixed 1 sided military link in `addTraining` * More cleanup Also tell the user when data was persisted (mostly for debugging) * rename the script itself and update the docs to account. * fix docs * Add credit where credit is due * add to control panel alert the user if the squad cant be found (since we cant reliably make a squad ourselves... yet) * Check the squad's entity_id to make sure we get *our* Gym * Update autotraining.lua remove the `.` because it could lead to confusion * Fix the ignore count never being reset * Fix units that need training but are already doing so being reported as queued * fix the ignore count (it should be global) * Apply suggestions from code review * fix typo * fix to actually check the unit's squad * Update for gui usage * clean up * initial gui and update from code review * show alias in gui too * clean up * Create gui docs * update the docs * remove non-existant name args in docs * fix typo in message * fix trainees being labeled as queued * add ignore nobles * Remove more debug code * Gui cleanup * Update to use the Military Module * use the squad position * Remove all training dwarves when you disable * disable autotraining on map unload * Apply suggestions from code review Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * fix erroneous training numbers * Update autotraining.rst * remove outdated debug logging * remove outdated comment * fix up silly code in `removeTraining` * Update autotraining.lua * Update autotraining.lua * clean and de-nest training candidates * remove units who don't need training * use our precomputed good squads list * forgot a nil check * only count as ignored if they are ignored * Apply suggestions from code review Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * code review changes * Fix up from testing improvements to `autotraining`: - fix the argument error - avoid the double execution of the loop when enabling - consistently only count ignored units when they would otherwise qualify for training - allow enabling the tool from within `gui/autotraining` - sort the list of training candidates, so that the most needed candidates are preferred for training - move the argument handling out of the `start` function Co-Authored-By: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * fix whitespace error * Update and fix changelog * only process cli args if we are running in the cli * skip units in squads (dont mark as ignored tho) --------- Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com> --- autotraining.lua | 296 ++++++++++++++++++++++++++++ changelog.txt | 2 + docs/autotraining.rst | 41 ++++ docs/gui/autotraining.rst | 15 ++ gui/autotraining.lua | 264 +++++++++++++++++++++++++ internal/control-panel/registry.lua | 2 + internal/notify/notifications.lua | 18 ++ 7 files changed, 638 insertions(+) create mode 100644 autotraining.lua create mode 100644 docs/autotraining.rst create mode 100644 docs/gui/autotraining.rst create mode 100644 gui/autotraining.lua diff --git a/autotraining.lua b/autotraining.lua new file mode 100644 index 0000000000..5c7df35781 --- /dev/null +++ b/autotraining.lua @@ -0,0 +1,296 @@ +-- Based on the original code by RNGStrategist (who also got some help from Uncle Danny) +--@ enable = true +--@ module = true + +local repeatUtil = require('repeat-util') +local utils=require('utils') + +local GLOBAL_KEY = "autotraining" +local MartialTraining = df.need_type['MartialTraining'] +local ignore_count = 0 + +local function get_default_state() + return { + enabled=false, + threshold=-5000, + ignored={}, + ignored_nobles={}, + training_squads = {}, + } +end + +state = state or get_default_state() + +function isEnabled() + return state.enabled +end + +-- persisting a table with numeric keys results in a json array with a huge number of null entries +-- therefore, we convert the keys to strings for persistence +local function to_persist(persistable) + local persistable_ignored = {} + for k, v in pairs(persistable) do + persistable_ignored[tostring(k)] = v + end + return persistable_ignored +end + +-- loads both from the older array format and the new string table format +local function from_persist(persistable) + if not persistable then + return + end + local ret = {} + for k, v in pairs(persistable) do + ret[tonumber(k)] = v + end + return ret +end + +function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=state.enabled, + threshold=state.threshold, + ignored=to_persist(state.ignored), + ignored_nobles=state.ignored_nobles, + training_squads=to_persist(state.training_squads) + }) +end + +--- Load the saved state of the script +local function load_state() + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + state.enabled = persisted_data.enabled or state.enabled + state.threshold = persisted_data.threshold or state.threshold + state.ignored = from_persist(persisted_data.ignored) or state.ignored + state.ignored_nobles = persisted_data.ignored_nobles or state.ignored_nobles + state.training_squads = from_persist(persisted_data.training_squads) or state.training_squads + return state +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + state.enabled = false + return + end + -- the state changed, is a map loaded and is that map in fort mode? + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + -- no its isnt, so bail + return + end + -- yes it was, so: + + -- retrieve state saved in game. merge with default state so config + -- saved from previous versions can pick up newer defaults. + load_state() + if state.enabled then + start() + end + persist_state() +end + + +--###### +--Functions +--###### +local function isIgnoredNoble(unit) + local noblePos = dfhack.units.getNoblePositions(unit) + if noblePos ~= nil then + for _, position in ipairs(noblePos) do + if state.ignored_nobles[position.position.code] then + return true + end + end + end + return false +end + +---@return table +function getTrainingCandidates() + local ret = {} + ignore_count = 0 + for _, unit in ipairs(dfhack.units.getCitizens(true)) do + if not dfhack.units.isAdult(unit) then + goto next_unit + end + local need = getTrainingNeed(unit) + if not need or need.focus_level >= state.threshold then + goto next_unit + end + -- ignored units are those that would like to train but are forbidden from doing so + if state.ignored[unit.id] then + ignore_count = ignore_count + 1 + goto next_unit + end + if isIgnoredNoble(unit) then + ignore_count = ignore_count + 1 + goto next_unit + end + if unit.military.squad_id ~= -1 then + goto next_unit + end + table.insert(ret, { unit = unit, need = need.focus_level }) + ::next_unit:: + end + table.sort(ret, function (a, b) return a.need < b.need end) + return ret +end + +function getTrainingSquads() + local squads = {} + for squad_id, _ in pairs(state.training_squads) do + local squad = df.squad.find(squad_id) + if squad then + table.insert(squads, squad) + else + -- setting to nil during iteration is permitted by lua + state.training_squads[squad_id] = nil + end + end + return squads +end + +function getTrainingNeed(unit) + if unit == nil then return nil end + local needs = unit.status.current_soul.personality.needs + for _, need in ipairs(needs) do + if need.id == MartialTraining then + return need + end + end + return nil +end + +--###### +--Main +--###### + +-- Find all training squads +-- Abort if no squads found +function checkSquads() + local squads = {} + for _, squad in ipairs(getTrainingSquads()) do + if squad.entity_id == df.global.plotinfo.group_id then + local leader = squad.positions[0].occupant + if leader ~= -1 then + table.insert(squads,squad) + end + end + end + + if #squads == 0 then + return nil + end + + return squads +end + +function addTraining(unit,good_squads) + if unit.military.squad_id ~= -1 then + for _, squad in ipairs(good_squads) do + if unit.military.squad_id == squad.id then + return true + end + end + return false + end + for _, squad in ipairs(good_squads) do + for i=1,9,1 do + if squad.positions[i].occupant == -1 then + return dfhack.military.addToSquad(unit.id,squad.id,i) + end + end + end + + return false +end + +function removeAll() + if state.training_squads == nil then return end + for _, squad in ipairs(getTrainingSquads()) do + for i=1,9,1 do + local hf = df.historical_figure.find(squad.positions[i].occupant) + if hf ~= nil then + dfhack.military.removeFromSquad(hf.unit_id) + end + end + end +end + + +function check() + local squads = checkSquads() + local intraining_count = 0 + local inque_count = 0 + if squads == nil then return end + for _,squad in ipairs(squads) do + for i=1,9,1 do + if squad.positions[i].occupant ~= -1 then + local hf = df.historical_figure.find(squad.positions[i].occupant) + if hf ~= nil then + local unit = df.unit.find(hf.unit_id) + local training_need = getTrainingNeed(unit) + if not training_need or training_need.focus_level >= state.threshold then + dfhack.military.removeFromSquad(unit.id) + end + end + end + end + end + for _, p in ipairs(getTrainingCandidates()) do + local added = addTraining(p.unit, squads) + if added then + intraining_count = intraining_count +1 + else + inque_count = inque_count +1 + end + end + print(("%s: %d training, %d waiting, and %d excluded units with training needs"): + format(GLOBAL_KEY, intraining_count, inque_count, ignore_count)) +end + +function start() + repeatUtil.scheduleEvery(GLOBAL_KEY, 1, 'days', check) +end + +function stop() + repeatUtil.cancel(GLOBAL_KEY) +end + +function enable() + state.enabled = true + persist_state() + start() +end + +function disable() + state.enabled = false + persist_state() + stop() + removeAll() +end + +if dfhack_flags.module then + return +end + +validArgs = utils.invert({ + 't' +}) + +local args = utils.processArgs({...}, validArgs) + +if dfhack_flags.enable then + if dfhack_flags.enable_state then + enable() + else + disable() + end +else + -- called on the command-line + if args.t then + state.threshold = 0-tonumber(args.t) + end + print(("autotraining is %s"):format(state.enabled and "enabled" or "disabled")) +end diff --git a/changelog.txt b/changelog.txt index ba3c45c706..d670aa8af2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,8 @@ Template for new versions: # Future ## New Tools +- `autotraining`: new tool to assign citizens to a military squad when they need Martial Training +- `gui/autotraining`: configuration tool for autotraining ## New Features diff --git a/docs/autotraining.rst b/docs/autotraining.rst new file mode 100644 index 0000000000..360f4d08e5 --- /dev/null +++ b/docs/autotraining.rst @@ -0,0 +1,41 @@ +autotraining +============ + +.. dfhack-tool:: + :summary: Assigns citizens to a military squad until they have fulfilled their need for Martial Training + :tags: fort auto bugfix units + +This script automatically assigns citizens with the need for military training to designated training squads. + +You need to have at least one squad that is set up for training. The squad should be set to "Constant Training" in the military screen. The squad doesn't need months off. The members leave the squad once they have satisfied their need for military training. + +The configured uniform determines the skills that are acquired by the training dwarves. Providing "No Uniform" is a perfectly valid choice and will make your militarily inclined civilians become wrestlers over time. However, you can also provide weapons and armor to pre-train civilians for future drafts. + +Once you have made squads for training use `gui/autotraining` to select the squads and ignored units, as well as the needs threshhold. + +Usage +----- + + ``autotraining []`` + +Examples +-------- + +``autotraining`` + Current status of script + +``enable autotraining`` + Checks to see if you have fullfilled the creation of a training squad. + If there is no squad marked for training use, a clickable notification will appear letting you know to set one up/ + Searches your fort for dwarves with a need for military training, and begins assigning them to a training squad. + Once they have fulfilled their need they will be removed from their squad to be replaced by the next dwarf in the list. + +``disable autotraining`` + Stops adding new units to the squad. + +Options +------- + ``-t`` + Use integer values. (Default 5000) + The negative need threshhold to trigger for each citizen + The greater the number the longer before a dwarf is added to the waiting list. diff --git a/docs/gui/autotraining.rst b/docs/gui/autotraining.rst new file mode 100644 index 0000000000..a86b28adf9 --- /dev/null +++ b/docs/gui/autotraining.rst @@ -0,0 +1,15 @@ +gui/autotraining +================ + +.. dfhack-tool:: + :summary: GUI interface for ``autotraining`` + :tags: fort auto interface + +This is an in-game configuration interface for `autotraining`. You can pick squads for training, select ignored units, and set the needs threshold. + +Usage +----- + +:: + + gui/autotraining diff --git a/gui/autotraining.lua b/gui/autotraining.lua new file mode 100644 index 0000000000..a878f5bb95 --- /dev/null +++ b/gui/autotraining.lua @@ -0,0 +1,264 @@ +---@diagnostic disable: missing-fields + +local gui = require('gui') +local widgets = require('gui.widgets') + +local autotraining = reqscript('autotraining') + +local training_squads = autotraining.state.training_squads +local ignored_units = autotraining.state.ignored +local ignored_nobles = autotraining.state.ignored_nobles + +AutoTrain = defclass(AutoTrain, widgets.Window) +AutoTrain.ATTRS { + frame_title='Training Setup', + frame={w=55, h=45}, + resizable=true, -- if resizing makes sense for your dialog + resize_min={w=55, h=20}, -- try to allow users to shrink your windows +} + +local SELECTED_ICON = dfhack.pen.parse{ch=string.char(251), fg=COLOR_LIGHTGREEN} +function AutoTrain:getSquadIcon(squad_id) + if training_squads[squad_id] then + return SELECTED_ICON + end + return nil +end + +function AutoTrain:getSquads() + local squads = {} + for _, squad in ipairs(df.global.world.squads.all) do + if not (squad.entity_id == df.global.plotinfo.group_id) then + goto continue + end + table.insert(squads, { + text = dfhack.translation.translateName(squad.name, true)..(squad.alias ~= '' and ' ('..squad.alias..')' or ''), + icon = self:callback("getSquadIcon", squad.id ), + id = squad.id + }) + + ::continue:: + end + return squads +end + +function AutoTrain:toggleSquad(_, choice) + training_squads[choice.id] = not training_squads[choice.id] + autotraining.persist_state() + self:updateLayout() +end + +local IGNORED_ICON = dfhack.pen.parse{ch='x', fg=COLOR_RED} +function AutoTrain:getUnitIcon(unit_id) + if ignored_units[unit_id] then + return IGNORED_ICON + end + return nil +end + +function AutoTrain:getNobleIcon(noble_code) + if ignored_nobles[noble_code] then + return IGNORED_ICON + end + return nil +end + +function AutoTrain:getUnits() + local unit_choices = {} + for _, unit in ipairs(dfhack.units.getCitizens(true,false)) do + if not dfhack.units.isAdult(unit) then + goto continue + end + + table.insert(unit_choices, { + text = dfhack.units.getReadableName(unit), + icon = self:callback("getUnitIcon", unit.id ), + id = unit.id + }) + ::continue:: + end + return unit_choices +end + +function AutoTrain:toggleUnit(_, choice) + ignored_units[choice.id] = not ignored_units[choice.id] + autotraining.persist_state() + self:updateLayout() +end + +local function to_title_case(str) + return dfhack.capitalizeStringWords(dfhack.lowerCp437(str:gsub('_', ' '))) +end + +function toSet(list) + local set = {} + for _, v in ipairs(list) do + set[v] = true + end + return set +end + +local function add_positions(positions, entity) + if not entity then return end + for _,position in pairs(entity.positions.own) do + positions[position.id] = { + id=position.id+1, + code=position.code, + } + end +end + +function AutoTrain:getPositions() + local positions = {} + local excludedPositions = toSet({ + 'MILITIA_CAPTAIN', + 'MILITIA_COMMANDER', + 'OUTPOST_LIAISON', + 'CAPTAIN_OF_THE_GUARD', + }) + + add_positions(positions, df.historical_entity.find(df.global.plotinfo.civ_id)) + add_positions(positions, df.historical_entity.find(df.global.plotinfo.group_id)) + + -- Step 1: Extract values into a sortable array + local sortedPositions = {} + for _, val in pairs(positions) do + if val and not excludedPositions[val.code] then + table.insert(sortedPositions, val) + end + end + + -- Step 2: Sort the positions (optional, adjust sorting criteria) + table.sort(sortedPositions, function(a, b) + return a.id < b.id -- Sort alphabetically by code + end) + + -- Step 3: Rebuild the table without gaps + positions = {} -- Reset positions table + for i, val in ipairs(sortedPositions) do + positions[i] = { + text = to_title_case(val.code), + value = val.code, + pen = COLOR_LIGHTCYAN, + icon = self:callback("getNobleIcon", val.code), + id = val.id + } + end + + return positions +end + + + +function AutoTrain:toggleNoble(_, choice) + ignored_nobles[choice.value] = not ignored_nobles[choice.value] + autotraining.persist_state() + self:updateLayout() +end + +function AutoTrain:init() + self:addviews{ + widgets.Label{ + frame={ t = 0 , h = 1 }, + text = "Select squads for automatic training:", + }, + widgets.List{ + view_id = "squad_list", + icon_width = 2, + frame = { t = 1, h = 5 }, + choices = self:getSquads(), + on_submit=self:callback("toggleSquad") + }, + widgets.Divider{ frame={t=6, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 7 , h = 1 }, + text = "General options:", + }, + widgets.EditField { + view_id = "threshold", + frame={ t = 8 , h = 1 }, + key = "CUSTOM_T", + label_text = "Need threshold for training: ", + text = tostring(-autotraining.state.threshold), + on_char = function (char, _) + return tonumber(char,10) + end, + on_submit = function (text) + -- still necessary, because on_char does not check pasted text + local entered_number = tonumber(text,10) or 5000 + autotraining.state.threshold = -entered_number + autotraining.persist_state() + -- make sure that the auto correction is reflected in the EditField + self.subviews.threshold:setText(tostring(entered_number)) + end + }, + widgets.ToggleHotkeyLabel { + view_id = 'enable_toggle', + frame = { t = 9, h = 1 }, + label = 'Autotraining is', + key = 'CUSTOM_E', + options = { { value = true, label = 'Enabled', pen = COLOR_GREEN }, + { value = false, label = 'Disabled', pen = COLOR_RED } }, + on_change = function(val) + if val then + autotraining.enable() + else + autotraining.disable() + end + end, + }, + widgets.Divider{ frame={t=10, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 11 , h = 1 }, + text = "Ignored noble positions:", + }, + widgets.List{ + frame = { t = 12 , h = 11}, + view_id = "nobles_list", + icon_width = 2, + choices = self:getPositions(), + on_submit=self:callback("toggleNoble") + }, + widgets.Divider{ frame={t=23, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 24 , h = 1 }, + text = "Select units to exclude from automatic training:" + }, + widgets.FilteredList{ + frame = { t = 25 }, + view_id = "unit_list", + edit_key = "CUSTOM_CTRL_F", + icon_width = 2, + choices = self:getUnits(), + on_submit=self:callback("toggleUnit") + } + } + --self.subviews.unit_list:setChoices(unit_choices) +end + +function AutoTrain:onRenderBody(painter) + self.subviews.enable_toggle:setOption(autotraining.state.enabled) +end + +function AutoTrain:onDismiss() + view = nil +end + +AutoTrainScreen = defclass(AutoTrainScreen, gui.ZScreen) +AutoTrainScreen.ATTRS { + focus_path='autotrain', +} + +function AutoTrainScreen:init() + self:addviews{AutoTrain{}} +end + +function AutoTrainScreen:onDismiss() + view = nil +end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('gui/autotraining requires a fortress map to be loaded') +end + +view = view and view:raise() or AutoTrainScreen{}:show() diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 76fbee5c10..08b721f8be 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -34,6 +34,8 @@ COMMANDS_BY_IDX = { desc='Automatically shear creatures that are ready for shearing.', params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, {command='autoslab', group='automation', mode='enable'}, + {command='autotraining', group='automation', mode='enable', + desc='Automatically assign units with training needs to training squads. '}, {command='ban-cooking all', group='automation', mode='run'}, {command='buildingplan set boulders false', group='automation', mode='run', desc='Enable if you usually don\'t want to use boulders for construction.'}, diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 8af7c2c187..653d3887d5 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -366,6 +366,24 @@ NOTIFICATIONS_BY_IDX = { dlg.showMessage('Rescue stuck squads', message, COLOR_WHITE) end, }, + { + name='auto_train', + desc='Notifies when there are no squads set up for training', + default=true, + dwarf_fn=function() + local at = reqscript('autotraining') + if (at.isEnabled() and at.checkSquads() == nil) then + return {{text="autotraining: no squads selected",pen=COLOR_LIGHTRED}} + end + end, + on_click=function() + local message = + "You have no squads selected for training.\n".. + "You should have a squad set up to be constantly training with about 8 units needed for training.\n".. + "Then you can select that squad for training in the config.\n\nWould you like to open the config? Alternatively, simply close this popup to go create a squad." + dlg.showYesNoPrompt('Training Squads not configured', message, COLOR_WHITE, function () dfhack.run_command('gui/autotraining') end) + end, + }, { name='traders_ready', desc='Notifies when traders are ready to trade at the depot.', From ebacabdcc2acb2ccd78479eef41522339f67ae2a Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 12:37:52 -0500 Subject: [PATCH 102/272] Update changelog.txt --- changelog.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4fdc17edf2..24573f22a9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,11 +27,11 @@ Template for new versions: # Future ## New Tools +- `devel/hello-world`: updated to show off the new Slider widget ## New Features ## Fixes - - `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach ## Misc Improvements @@ -171,7 +171,6 @@ Template for new versions: - `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. -- `devel/hello-world`: updated to show off the new Slider widget ## Removed - `gui/create-item`: now accepts a ``pos`` argument of where to spawn items From 56bca449d29e7db991965850984c720a80dc8155 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 12:41:37 -0500 Subject: [PATCH 103/272] Update changelog.txt not sure what happened here --- changelog.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 79df95938a..75fd442f29 100644 --- a/changelog.txt +++ b/changelog.txt @@ -172,9 +172,6 @@ Template for new versions: - `gui/notify`: save reminder now appears in adventure mode - `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete -- `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. - -## Removed - `gui/create-item`: now accepts a ``pos`` argument of where to spawn items - `modtools/create-item`: exported ``hackWish`` function now supports ``opts.pos`` for determining spawn location - `hfs-pit`: improve placement of stairs w/r/t eerie pits and ramp tops From 0895834b2a78dc3e920af24bbb0df49fa071b6a2 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 13:16:54 -0500 Subject: [PATCH 104/272] Update to use a wrapper function to accept unit OR histfig --- deathcause.lua | 17 +++++++++++++---- docs/deathcause.rst | 9 +++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index 6c212821ac..2da43fca46 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -26,7 +26,7 @@ local function getDeathStringFromCause(cause) end -- Returns a cause of death given a unit -function getDeathCauseFromUnit(unit) +local function getDeathCauseFromUnit(unit) local str = unit.name.has_name and '' or 'The ' str = str .. dfhack.units.getReadableName(unit) @@ -104,7 +104,7 @@ local function getDeathEventForHistFig(histfig_id) end -- Returns the cause of death given a histfig -function getDeathCauseFromHistFig(histfig) +local function getDeathCauseFromHistFig(histfig) local histfig_unit = df.unit.find(histfig.unit_id) if not histfig_unit then qerror("Cause of death not available") @@ -149,6 +149,15 @@ local function get_target() return selected_item.hist_figure_id, df.unit.find(selected_item.unit_id) end +-- wrapper function to take either a unit or a histfig and get the death cause +function getDeathCause(target) + if df.unit:is_instance(target) then + return getDeathCauseFromUnit(target) + else + return getDeathCauseFromHistFig(target) + end +end + if dfhack_flags.module then return end @@ -161,7 +170,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - print(dfhack.df2console(getDeathCauseFromUnit(selected_unit))) + print(dfhack.df2console(getDeathCause(selected_unit))) else - print(dfhack.df2console(getDeathCauseFromHistFig(df.historical_figure.find(hist_figure_id)))) + print(dfhack.df2console(getDeathCause(df.historical_figure.find(hist_figure_id)))) end diff --git a/docs/deathcause.rst b/docs/deathcause.rst index 20dddb11e6..dac8a39ab9 100644 --- a/docs/deathcause.rst +++ b/docs/deathcause.rst @@ -23,14 +23,11 @@ commandline interface with ``dfhack.run_script()`` or via the API functions defined in :source-scripts:`deathcause.lua`, available from the return value of ``reqscript('deathcause')``: -* ``getDeathCauseFromHistFig(histfig)`` +* ``getDeathCause(unit or historical_figure)`` -Returns a string with the historical figure's cause of death, sometimes with more information -than with a unit. +Returns a string with the unit or historical figure's cause of death. Note that using a historical +figure will sometimes provide more information than using a unit. -* ``getDeathCauseFromUnit(unit)`` - -Returns a string with the unit's cause of death. API usage example:: From eb1cd89483d157a0f16459f33ddbdd3ea73c6b4b Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 13:18:43 -0500 Subject: [PATCH 105/272] write down a note for someone more skilled in lua --- deathcause.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/deathcause.lua b/deathcause.lua index 2da43fca46..3fd62fd115 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -6,6 +6,7 @@ local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES -- Gets the first corpse item at the given location local function getItemAtPosition(pos) for _, item in ipairs(df.global.world.items.other.ANY_CORPSE) do + -- could this maybe be `if same_xyz(pos, item.pos) then`? if item.pos.x == pos.x and item.pos.y == pos.y and item.pos.z == pos.z then print("Automatically chose first corpse at the selected location.") return item From 44bf3c69a731006ae1c9d85c389bcc2c99544ea0 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 16:29:00 -0500 Subject: [PATCH 106/272] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 75fd442f29..7204144e7e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,7 +27,6 @@ Template for new versions: # Future ## New Tools -- `devel/hello-world`: updated to show off the new Slider widget - `autotraining`: new tool to assign citizens to a military squad when they need Martial Training - `gui/autotraining`: configuration tool for autotraining @@ -37,6 +36,7 @@ Template for new versions: - `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach ## Misc Improvements +- `devel/hello-world`: updated to show off the new Slider widget ## Removed From 9afb8c058b4e6a0c4c717ddbe779b377cf6f7a93 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 18:32:50 -0500 Subject: [PATCH 107/272] `ban-cooking`: don't fail when honey missing Do not attempt to ban honey if honey doesn't exist --- ban-cooking.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ban-cooking.lua b/ban-cooking.lua index fdb59fbe05..7ac6f3ea3b 100644 --- a/ban-cooking.lua +++ b/ban-cooking.lua @@ -81,7 +81,9 @@ end funcs.honey = function() local mat = dfhack.matinfo.find("CREATURE:HONEY_BEE:HONEY") - ban_cooking('honey bee honey', mat.type, mat.index, df.item_type.LIQUID_MISC, -1) + if mat then + ban_cooking('honey bee honey', mat.type, mat.index, df.item_type.LIQUID_MISC, -1) + end end funcs.tallow = function() From 6a893937289d287070ab4dfbb3be104aa36abc96 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 18:36:48 -0500 Subject: [PATCH 108/272] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 89fd20ca8a..6eeaffd48e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: - `deathcause`: added functionality to this script to fetch cause of death programatically ## Fixes +- `ban-cooking`: will not fail trying to ban honey if the world has no honey - `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach ## Misc Improvements From 5290750c5a233c8589a7f4645f76af6f57f964e5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 19 Aug 2025 09:35:20 -0500 Subject: [PATCH 109/272] fix enable/disable in husbandry --- husbandry.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/husbandry.lua b/husbandry.lua index f8dd1375ec..a5a8bff91a 100644 --- a/husbandry.lua +++ b/husbandry.lua @@ -272,10 +272,10 @@ end if dfhack_flags.enable then if dfhack_flags.enable_state then - enabled = true + state.enabled = true start() else - enabled = false + state.enabled = false stop() end persist_state() From e4ac14bd29b1fd5de26612aa16be40f4ba3ecc48 Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 19 Aug 2025 09:05:10 -0700 Subject: [PATCH 110/272] changelog.txt update --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index a5a7b06f0f..c25345a07a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -46,6 +46,8 @@ Template for new versions: - `confirm`: the pause option now pauses individual confirmation types, allowing multiple different confirmations to be paused independently - `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach - `uniform-unstick`: no longer causes units to equip multiples of assigned items +- `caravan`: in the pedestal item assignment dialog, add new items at the end of the list of displayed items instead of at a random position +- `caravan`: in the pedestal item assignment dialog, consistently remove items from the list of displayed items ## Misc Improvements - `devel/hello-world`: updated to show off the new Slider widget From 929e66147805da9fb3a29283b05812ceb68d1d1b Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:51:26 +0200 Subject: [PATCH 111/272] fix index of nil value --- husbandry.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/husbandry.lua b/husbandry.lua index a5a8bff91a..cf80857468 100644 --- a/husbandry.lua +++ b/husbandry.lua @@ -116,7 +116,7 @@ local function getAppropriateWorkshop(unit, collection) end end end - return #closest.jobs < 10 and closest or nil + return (closest and #closest.jobs < 10) and closest or nil end local function shearCreature(unit, workshop) From 539120522a808220e4ab23538bad5cfcd979211e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 Aug 2025 00:33:04 -0500 Subject: [PATCH 112/272] changelog for 52.03-r2 --- changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/changelog.txt b/changelog.txt index c25345a07a..5ecf8024e1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 52.03-r2 + ## New Tools - `autotraining`: new tool to assign citizens to a military squad when they need Martial Training - `gui/autotraining`: configuration tool for autotraining From ab665c18d8c91fc8690b1b526ce21951456ae8b5 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:19:07 +0200 Subject: [PATCH 113/272] adapt Lua tools to use new API functionality for creating and assigning jobs --- autocheese.lua | 32 ++++++--------------- changelog.txt | 3 ++ husbandry.lua | 9 +++--- idle-crafting.lua | 66 +++++++++---------------------------------- immortal-cravings.lua | 46 ++++++------------------------ 5 files changed, 38 insertions(+), 118 deletions(-) diff --git a/autocheese.lua b/autocheese.lua index 0e9fb52215..e9bdc146ae 100644 --- a/autocheese.lua +++ b/autocheese.lua @@ -1,14 +1,12 @@ --@module = true -local ic = reqscript('idle-crafting') - ---make cheese using a specific barrel and workshop ---@param barrel df.item ---@param workshop df.building_workshopst ---@return df.job function makeCheese(barrel, workshop) ---@type df.job - local job = ic.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCheese local jitem = df.job_item:new() @@ -22,29 +20,17 @@ function makeCheese(barrel, workshop) dfhack.error('could not attach item') end - ic.assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return job end - - ----unit is ready to take jobs +---checks that unit can path to workshop ---@param unit df.unit +---@param workshop df.building_workshopst ---@return boolean -function unitIsAvailable(unit) - if unit.job.current_job then - return false - elseif #unit.individual_drills > 0 then - return false - elseif unit.flags1.caged or unit.flags1.chained then - return false - elseif unit.military.squad_id ~= -1 then - local squad = df.squad.find(unit.military.squad_id) - -- this lookup should never fail - ---@diagnostic disable-next-line: need-check-nil - return #squad.orders == 0 and squad.activity == -1 - end - return true +function canAccessWorkshop(unit, workshop) + local workshop_position = xyz2pos(workshop.centerx, workshop.centery, workshop.z) + return dfhack.maps.canWalkBetween(unit.pos, workshop_position) end ---check if unit can perform labor at workshop @@ -54,8 +40,8 @@ end ---@return boolean function availableLaborer(unit, unit_labor, workshop) return unit.status.labors[unit_labor] - and unitIsAvailable(unit) - and ic.canAccessWorkshop(unit, workshop) + and dfhack.units.isJobAvailable(unit) + and canAccessWorkshop(unit, workshop) end ---find unit with a particular labor enabled diff --git a/changelog.txt b/changelog.txt index 5ecf8024e1..6fc58b711b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,9 @@ Template for new versions: ## Misc Improvements +- adapt Lua tools to use new API functionality for creating and assigning jobs +- `idle-crafting`: properly interrupt interruptible (i.e. "green") social activities + ## Removed # 52.03-r2 diff --git a/husbandry.lua b/husbandry.lua index cf80857468..a80e5b2417 100644 --- a/husbandry.lua +++ b/husbandry.lua @@ -4,7 +4,6 @@ local utils = require 'utils' local repeatutil = require("repeat-util") -local ic = reqscript('idle-crafting') local verbose = true ---conditional printing of debug messages @@ -120,17 +119,17 @@ local function getAppropriateWorkshop(unit, collection) end local function shearCreature(unit, workshop) - local job = ic.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.ShearCreature dfhack.job.addGeneralRef(job, df.general_ref_type.UNIT_SHEAREE, unit.id) - ic.assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) end local function milkCreature(unit, workshop) - local job = ic.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MilkCreature dfhack.job.addGeneralRef(job, df.general_ref_type.UNIT_MILKEE, unit.id) - ic.assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) end diff --git a/idle-crafting.lua b/idle-crafting.lua index 6744f02fde..e5d25751bf 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -55,26 +55,12 @@ function weightedChoice(choices) return nil --never reached on well-formed input end ----create a new linked job ----@return df.job -function make_job() - local job = df.job:new() - dfhack.job.linkIntoWorld(job, true) - return job -end - -function assignToWorkshop(job, workshop) - job.pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) - dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, workshop.id) - workshop.jobs:insert("#", job) -end - ---make totem at specified workshop ---@param unit df.unit ---@param workshop df.building_workshopst ---@return boolean function makeTotem(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeTotem job.mat_type = -1 @@ -89,7 +75,7 @@ function makeTotem(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -98,7 +84,7 @@ end ---@param workshop df.building_workshopst ---@return boolean function makeHornCrafts(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCrafts job.mat_type = -1 job.material_category.horn = true @@ -114,7 +100,7 @@ function makeHornCrafts(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -123,7 +109,7 @@ end ---@param workshop df.building_workshopst ---@return boolean function makeBoneCraft(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCrafts job.mat_type = -1 job.material_category.bone = true @@ -139,7 +125,7 @@ function makeBoneCraft(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -148,7 +134,7 @@ end ---@param workshop df.building_workshopst ---@return boolean function makeShellCraft(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCrafts job.mat_type = -1 job.material_category.shell = true @@ -164,7 +150,7 @@ function makeShellCraft(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -173,7 +159,7 @@ end ---@param workshop df.building_workshopst ---@return boolean "" function makeRockCraft(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCrafts job.mat_type = 0 @@ -187,7 +173,7 @@ function makeRockCraft(unit, workshop) jitem.flags3.hard = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -291,13 +277,8 @@ local STONE_CRAFT = df.unit_labor['STONE_CRAFT'] ---@param value_if_absent T ---@return number|T function getCraftingNeed(unit, value_if_absent) - local needs = unit.status.current_soul.personality.needs - for _, need in ipairs(needs) do - if need.id == CraftObject then - return -need.focus_level - end - end - return value_if_absent + local focus_penalty = dfhack.units.getFocusPenalty(unit, CraftObject) + return focus_penalty > 1000 and value_if_absent or -focus_penalty end local function stop() @@ -334,27 +315,6 @@ function canAccessWorkshop(unit, workshop) return dfhack.maps.canWalkBetween(unit.pos, workshop_position) end ----unit is ready to take jobs ----@param unit df.unit ----@return boolean -function unitIsAvailable(unit) - if unit.job.current_job then - return false - elseif #unit.specific_refs > 0 then -- activities such as "Conduct Meeting" - return false - elseif #unit.social_activities > 0 then - return false - elseif #unit.individual_drills > 0 then - return false - elseif unit.military.squad_id ~= -1 then - local squad = df.squad.find(unit.military.squad_id) - -- this lookup should never fail - ---@diagnostic disable-next-line: need-check-nil - return #squad.orders == 0 and squad.activity == -1 - end - return true -end - ---select crafting job based on available resources ---@param workshop df.building_workshopst ---@return (fun(unit:df.unit, workshop:df.building_workshopst):boolean)? @@ -397,7 +357,7 @@ local function processUnit(workshop, idx, unit_id) elseif not canAccessWorkshop(unit, workshop) then -- dfhack.print('-') return false - elseif not unitIsAvailable(unit) then + elseif not dfhack.units.isJobAvailable(unit) then -- dfhack.print('.') return false end diff --git a/immortal-cravings.lua b/immortal-cravings.lua index 21ae333ecb..e08e072ca5 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -1,12 +1,11 @@ --@enable = true --@module = true -local idle = reqscript('idle-crafting') local repeatutil = require("repeat-util") --- utility functions -local verbose = false +local verbose = true ---conditional printing of debug messages ---@param message string local function debug(message) @@ -101,7 +100,7 @@ local function goDrink(unit) -- print('no accessible drink found') return end - local job = idle.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.DrinkItem job.flags.special = true local dx, dy, dz = dfhack.items.getPosition(drink) @@ -134,7 +133,7 @@ local function goEat(unit) end dfhack.items.setOwner(meal, unit) - local job = idle.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.Eat job.flags.special = true local dx, dy, dz = dfhack.items.getPosition(meal) @@ -148,25 +147,6 @@ local function goEat(unit) print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name)) end ----unit is ready to take jobs (will interrupt social activities) ----@param unit df.unit ----@return boolean -function unitIsAvailable(unit) - if unit.job.current_job then - return false - elseif #unit.individual_drills > 0 then - return false - elseif unit.flags1.caged or unit.flags1.chained then - return false - elseif unit.military.squad_id ~= -1 then - local squad = df.squad.find(unit.military.squad_id) - -- this lookup should never fail - ---@diagnostic disable-next-line: need-check-nil - return #squad.orders == 0 and squad.activity == -1 - end - return true -end - --- script logic local GLOBAL_KEY = 'immortal-cravings' @@ -210,7 +190,7 @@ local function unit_loop() then goto next_unit end - if not unitIsAvailable(unit) then + if not dfhack.units.isJobAvailable(unit) then debug("immortal-cravings: skipping busy"..dfhack.units.getReadableName(unit)) table.insert(kept, unit.id) else @@ -245,21 +225,13 @@ local function main_loop() watched = {} for _, unit in ipairs(dfhack.units.getCitizens(false, false)) do if - not (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) or - unit.counters2.stomach_content > 0 + (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) and + unit.counters2.stomach_content == 0 and + dfhack.units.getFocusPenalty(unit, DrinkAlcohol, EatGoodMeal) < threshold then - goto next_unit - end - for _, need in ipairs(unit.status.current_soul.personality.needs) do - if need.id == DrinkAlcohol and need.focus_level < threshold or - need.id == EatGoodMeal and need.focus_level < threshold - then - table.insert(watched, unit.id) - debug(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) - goto next_unit - end + table.insert(watched, unit.id) + debug(' ' .. dfhack.df2console(dfhack.units.getReadableName(unit))) end - ::next_unit:: end if #watched > 0 then From 8ff279e3052c026ba9f526a174be004e39c7d7a6 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 21 Aug 2025 21:21:05 +0200 Subject: [PATCH 114/272] reduce verbosity --- husbandry.lua | 2 +- immortal-cravings.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/husbandry.lua b/husbandry.lua index a80e5b2417..9198013a1f 100644 --- a/husbandry.lua +++ b/husbandry.lua @@ -5,7 +5,7 @@ local utils = require 'utils' local repeatutil = require("repeat-util") -local verbose = true +local verbose = false ---conditional printing of debug messages ---@param message string local function debug(message) diff --git a/immortal-cravings.lua b/immortal-cravings.lua index e08e072ca5..da9f90d76c 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -5,7 +5,7 @@ local repeatutil = require("repeat-util") --- utility functions -local verbose = true +local verbose = false ---conditional printing of debug messages ---@param message string local function debug(message) From ff200329647fd504a939c09ae13f7d335b6d411b Mon Sep 17 00:00:00 2001 From: Droseran <97368320+Droseran@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:04:55 -0400 Subject: [PATCH 115/272] Support honey added by mods Instead of only banning honey from vanilla honey bees, support banning honey added by modded creatures as well. --- ban-cooking.lua | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ban-cooking.lua b/ban-cooking.lua index 7ac6f3ea3b..2254e116dd 100644 --- a/ban-cooking.lua +++ b/ban-cooking.lua @@ -80,9 +80,18 @@ funcs.booze = function() end funcs.honey = function() - local mat = dfhack.matinfo.find("CREATURE:HONEY_BEE:HONEY") - if mat then - ban_cooking('honey bee honey', mat.type, mat.index, df.item_type.LIQUID_MISC, -1) + for _, c in ipairs(df.global.world.raws.creatures.all) do + for _, m in ipairs(c.material) do + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "DRINK_MAT" then + local matinfo = dfhack.matinfo.find(c.creature_id, m.id) + ban_cooking(c.name[2] .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.LIQUID_MISC, -1) + break + end + end + end + end end end From 2f5aa107788b5fc1bbfbb5138d87ab14390ec659 Mon Sep 17 00:00:00 2001 From: Droseran <97368320+Droseran@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:18:16 -0400 Subject: [PATCH 116/272] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 5ecf8024e1..c22c4957a4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ Template for new versions: ]]] # Future +- `ban-cooking`: bans honey added by creatures other than vanilla honey bee ## New Tools From 18fa50477d40960f6f9aee08f1a8da922b866362 Mon Sep 17 00:00:00 2001 From: Droseran <97368320+Droseran@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:30:05 -0400 Subject: [PATCH 117/272] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c22c4957a4..e2d0e02107 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,13 +25,13 @@ Template for new versions: ]]] # Future -- `ban-cooking`: bans honey added by creatures other than vanilla honey bee ## New Tools ## New Features ## Fixes +- `ban-cooking`: bans honey added by creatures other than vanilla honey bee ## Misc Improvements From 0e3c7edebfce7ec8d10e921d694816415fc752bd Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 26 Aug 2025 12:18:07 -0700 Subject: [PATCH 118/272] =?UTF-8?q?internal/caravan/*=20allow=20searching?= =?UTF-8?q?=20for=20items=20with=20CP417=20names.=20Such=20as:=20=20=20hye?= =?UTF-8?q?na=20bone=20figurine=20of=20B=C3=ABr=C3=BBl=20Saviorstockade=20?= =?UTF-8?q?=20=20L=C3=A2ven=20=C3=B4sed,=20The=20Prairie=20of=20Mazes=20(S?= =?UTF-8?q?hield)=20=20=20(+=C2=ABgrown=20pear=20wood=20=C3=AD=C3=BF=C3=AD?= =?UTF-8?q?mo=C2=BB+)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.txt | 1 + internal/caravan/common.lua | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5ecf8024e1..e694a6371a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `caravan`: the ``Bring goods to depot``, ``Trade``, and ``Assign items for display`` screens now allow searching for items with non-ASCII characters in their description ## Removed diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 996a616562..f3ba1fa9ca 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -15,8 +15,8 @@ SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} ALL_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} function add_words(words, str) - for word in str:gmatch("[%w]+") do - table.insert(words, word:lower()) + for word in dfhack.toSearchNormalized(str):gmatch("[%w]+") do + table.insert(words, word) end end From dbd125b3bb16022e40222587098e0f603a71d621 Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 26 Aug 2025 14:20:10 -0700 Subject: [PATCH 119/272] internal/caravan/common.lua obfuscate_value() API change An optional parameter `threshold` was added to allow the caller to pass in that value instead of recalculating it on each call. `get_broker_skill()` is slow, and `obfuscate_value()` is typically called 1000s of times, so passing `threshold` improves performance. This API change maintains backwards compatiblity. In addition, `get_broker_skill()` and `get_threshold()` were exposed to scripts that use this module. Also minor code cleanup: * An alias was only used twice, right after it was defined. The code is better off written without the alias. * Integers should ideally be compared with integers. No user-visible changes. --- internal/caravan/common.lua | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index f3ba1fa9ca..8f22306261 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -10,9 +10,8 @@ CH_DN = string.char(31) CH_MONEY = string.char(15) CH_EXCEPTIONAL = string.char(240) -local to_pen = dfhack.pen.parse -SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} -ALL_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} +SOME_PEN = dfhack.pen.parse{ch=':', fg=COLOR_YELLOW} +ALL_PEN = dfhack.pen.parse{ch=string.char(251), fg=COLOR_LIGHTGREEN} function add_words(words, str) for word in dfhack.toSearchNormalized(str):gmatch("[%w]+") do @@ -35,7 +34,7 @@ function make_container_search_key(item, desc) return table.concat(words, ' ') end -local function get_broker_skill() +function get_broker_skill() local broker = dfhack.units.getUnitByNobleRole('broker') if not broker then return 0 end for _,skill in ipairs(broker.status.current_soul.skills) do @@ -46,7 +45,7 @@ local function get_broker_skill() return 0 end -local function get_threshold(broker_skill) +function get_threshold(broker_skill) if broker_skill <= df.skill_rating.Dabbling then return 0 end if broker_skill <= df.skill_rating.Novice then return 10 end if broker_skill <= df.skill_rating.Adequate then return 25 end @@ -62,7 +61,7 @@ local function get_threshold(broker_skill) if broker_skill <= df.skill_rating.Master then return 4000 end if broker_skill <= df.skill_rating.HighMaster then return 5000 end if broker_skill <= df.skill_rating.GrandMaster then return 10000 end - return math.huge + return math.maxinteger end local function estimate(value, round_base, granularity) @@ -76,8 +75,8 @@ end -- Otherwise, if it's less than or equal to [threshold + 50] * 3, it will round to the nearest multiple of 100 -- Otherwise, if it's less than or equal to [threshold + 50] * 30, it will round to the nearest multiple of 1000 -- Otherwise, it will display a guess equal to [threshold + 50] * 30 rounded up to the nearest multiple of 1000. -function obfuscate_value(value) - local threshold = get_threshold(get_broker_skill()) +function obfuscate_value(value, threshold) + threshold = threshold or get_threshold(get_broker_skill()) if value < threshold then return dfhack.formatInt(value) end threshold = threshold + 50 if value <= threshold then return ('~%s'):format(estimate(value, 5, 10)) end @@ -267,7 +266,7 @@ function get_slider_widgets(self, suffix) {label='100'..CH_MONEY, value={index=4, value=100}, pen=COLOR_BROWN}, {label='500'..CH_MONEY, value={index=5, value=500}, pen=COLOR_BROWN}, {label='1000'..CH_MONEY, value={index=6, value=1000}, pen=COLOR_BROWN}, - {label='Max', value={index=7, value=math.huge}, pen=COLOR_GREEN}, + {label='Max', value={index=7, value=math.maxinteger}, pen=COLOR_GREEN}, }, initial_option=7, on_change=function(val) From 761a677c9843f0450e349b20a69483c79257bf29 Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 26 Aug 2025 18:48:38 -0700 Subject: [PATCH 120/272] internal/caravan/movegoods.lua use new obfuscate_value() API Verified that this gives identical results to the unmodified code, with the exception of some icon closures that could not be verified. --- internal/caravan/movegoods.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index df90d7f886..77e8222979 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -399,10 +399,10 @@ local function get_entry_icon(data, item_id) return common.SOME_PEN end -local function make_choice_text(at_depot, dist, value, quantity, desc) +local function make_choice_text(at_depot, dist, value, quantity, desc, cache_threshold) return { {width=DIST_COL_WIDTH-2, rjustify=true, text=at_depot and 'depot' or tostring(dist)}, - {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value)}, + {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value, cache_threshold)}, {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, {gap=2, text=desc}, } @@ -559,18 +559,20 @@ function MoveGoods:cache_choices() end local group_choices, nogroup_choices = {}, {} + local cache_threshold = common.get_threshold(common.get_broker_skill()) for _, group in pairs(groups) do local data = group.data for item_id, item_data in pairs(data.items) do local nogroup_choice = copyall(group) nogroup_choice.icon = curry(get_entry_icon, data, item_id) nogroup_choice.text = make_choice_text(item_data.item.flags.in_building, - data.dist, data.per_item_value, 1, data.desc) + data.dist, data.per_item_value, 1, data.desc, cache_threshold) nogroup_choice.item_id = item_id table.insert(nogroup_choices, nogroup_choice) end data.total_value = data.per_item_value * data.quantity - group.text = make_choice_text(data.num_at_depot == data.quantity, data.dist, data.total_value, data.quantity, data.desc) + group.text = make_choice_text(data.num_at_depot == data.quantity, data.dist, + data.total_value, data.quantity, data.desc, cache_threshold) table.insert(group_choices, group) self.value_pending = self.value_pending + (data.per_item_value * data.selected) end From de12a181b1cd4bb96d0403423dd499c7461d80fd Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 26 Aug 2025 21:18:22 -0700 Subject: [PATCH 121/272] internal/caravan/pedestal.lua use new obfuscate_value() API Verified that this gives identical results to the unmodified code. --- internal/caravan/pedestal.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index dab100ef48..1184a85c3e 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -503,10 +503,10 @@ local function get_status(item, display_bld) return STATUS.AVAILABLE.value end -local function make_choice_text(data) +local function make_choice_text(data, threshold) return { {width=STATUS_COL_WIDTH, text=function() return STATUS[STATUS_REVMAP[data.status]].label end}, - {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(data.value)}, + {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(data.value, threshold)}, {gap=2, text=data.desc}, } end @@ -530,6 +530,7 @@ end function AssignItems:cache_choices(inside_containers, display_bld) if self.choices_cache[inside_containers] then return self.choices_cache[inside_containers] end + local cache_threshold = common.get_threshold(common.get_broker_skill()) local choices = {} for _, item in ipairs(df.global.world.items.other.IN_PLAY) do if not is_displayable_item(item) then goto continue end @@ -559,7 +560,7 @@ function AssignItems:cache_choices(inside_containers, display_bld) end local entry = { search_key=search_key, - text=make_choice_text(data), + text=make_choice_text(data, cache_threshold), data=data, } table.insert(choices, entry) From 76b0f7e4e029613da49f0994b712a00e479c9a6b Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 27 Aug 2025 07:44:12 -0700 Subject: [PATCH 122/272] internal/caravan/trade.lua use new obfuscate_value() API Verified that this gives identical results to the unmodified code. --- internal/caravan/trade.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index f27e949ea5..f78bc75c05 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -315,9 +315,9 @@ local function is_ethical_product(item, animal_ethics, wood_ethics) (not wood_ethics or not common.has_wood(item)) end -local function make_choice_text(value, desc) +local function make_choice_text(value, threshold, desc) return { - {width=STATUS_COL_WIDTH+VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value)}, + {width=STATUS_COL_WIDTH+VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value, threshold)}, {gap=2, text=desc}, } end @@ -328,6 +328,7 @@ function Trade:cache_choices(list_idx, trade_bins) local goodflags = trade.goodflag[list_idx] local trade_bins_choices, notrade_bins_choices = {}, {} local parent_data + local cache_threshold = common.get_threshold(common.get_broker_skill()) for item_idx, item in ipairs(trade.good[list_idx]) do local goodflag = goodflags[item_idx] if not goodflag.contained then @@ -374,7 +375,7 @@ function Trade:cache_choices(list_idx, trade_bins) search_key=search_key, icon=curry(get_entry_icon, data), data=data, - text=make_choice_text(data.value, desc), + text=make_choice_text(data.value, cache_threshold, desc), } if not data.update_container_fn then table.insert(trade_bins_choices, choice) From 4e93bee0647c5b3ce121d6b716824896adee713e Mon Sep 17 00:00:00 2001 From: SilasD Date: Thu, 28 Aug 2025 09:59:51 -0700 Subject: [PATCH 123/272] internal/caravan/common.lua use trader over broker When doing trading (i.e. the DF Trade window is open showing the two columns), it *can* happen that you *have* a broker, but you actually *trade* using a different unit. This can be triggered by opening the depot building view and choosing `Anyone requested at trade`, repeating this until some unit that is not the broker shows up to do the trading. When this happens, the DFHack `Select trade goods` overlay shows different obfuscated values than the DF Trade window. This patch fixes that case by using the trader's appraisal skill if trading is active. --- internal/caravan/common.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 8f22306261..9fffefe4c6 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -36,6 +36,13 @@ end function get_broker_skill() local broker = dfhack.units.getUnitByNobleRole('broker') + local interface_trade = df.global.game.main_interface.trade + if interface_trade.open == true + and interface_trade.choosing_merchant == false + and interface_trade.fortress_trader ~= nil + then + broker = interface_trade.fortress_trader + end if not broker then return 0 end for _,skill in ipairs(broker.status.current_soul.skills) do if skill.id == df.job_skill.APPRAISAL then From f07ef322e293f0fcab6c0095d2c04370f39afe78 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:33 +0000 Subject: [PATCH 124/272] [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) - [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) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ec6f9ff9f..21c674fe15 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 @@ -34,6 +34,6 @@ repos: - json # specific to scripts: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: forbid-new-submodules From 86456fa1f95c4b5a6bfbb644e27216540b82e71c Mon Sep 17 00:00:00 2001 From: git--amade Date: Sun, 7 Sep 2025 00:41:26 +0800 Subject: [PATCH 125/272] new script store-owned.lua and its documentation --- changelog.txt | 1 + docs/store-owned.rst | 43 +++++ store-owned.lua | 410 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 454 insertions(+) create mode 100644 docs/store-owned.rst create mode 100644 store-owned.lua diff --git a/changelog.txt b/changelog.txt index 5ecf8024e1..9d10f6fe84 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `store-owned`: task owned items to be stored in the owner's room furniture ## New Features diff --git a/docs/store-owned.rst b/docs/store-owned.rst new file mode 100644 index 0000000000..3fedc74336 --- /dev/null +++ b/docs/store-owned.rst @@ -0,0 +1,43 @@ +store-owned +=========== + +.. dfhack-tool:: + :summary: Task units to store their owned items. + :tags: fort items buildings + +Task any owned item to be stored in an appropriate storage furniture in +a room assigned to the item's owner. + +Usage +----- + +``store-owned [