From a859bfa079b62d70da81c16efe4c8c2eb2d2019e Mon Sep 17 00:00:00 2001 From: Stephen DeGrace Date: Sat, 4 Feb 2023 23:55:58 -0500 Subject: [PATCH 001/732] gm-editor add autoupdating while game unpaused and memory staleness checking --- gui/gm-editor.lua | 77 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index ff29409f3d..ce25bd0dbb 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -9,6 +9,9 @@ local utils = require 'utils' config = config or json.open('dfhack-config/gm-editor.json') +local REFRESH_MS = 100 + + find_funcs = find_funcs or (function() local t = {} for k in pairs(df) do @@ -31,6 +34,7 @@ local keybindings_raw = { {name='start_filter', key="CUSTOM_S",desc="Start typing filter, Enter to finish"}, {name='help', key="STRING_A063",desc="Show this help"}, {name='displace', key="STRING_A093",desc="Open reference offseted by index"}, + {name='autoupdate', key="CUSTOM_ALT_A",desc="Automatically keep values updated"}, --{name='NOT_USED', key="SEC_SELECT",desc="Edit selected entry as a number (for enums)"}, --not a binding... } @@ -122,6 +126,7 @@ function GmEditorUi:init(args) self.stack={} self.item_count=0 self.keys={} + self.autoupdate = false local helptext={{text="Help"},NEWLINE,NEWLINE} for _,v in ipairs(keybindings_raw) do table.insert(helptext,{text=v.desc,key=v.key,key_sep=': '}) @@ -135,16 +140,56 @@ function GmEditorUi:init(args) local mainList=widgets.List{view_id="list_main",choices={},frame = {l=1,t=3,yalign=0},on_submit=self:callback("editSelected"), on_submit2=self:callback("editSelectedRaw"), text_pen=COLOR_GREY, cursor_pen=COLOR_YELLOW} + self.autoupdateLabel = widgets.Label{text={{gap=1,text="Auto-Update stopped...",key=keybindings.autoupdate.key,key_sep = '()'}}, view_id = 'lbl_autoupdate',frame = {l=0,t=0,yalign=0}} local mainPage=widgets.Panel{ subviews={ mainList, widgets.Label{text={{text="",id="name"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, - widgets.EditField{frame={l=1,t=2,h=1},label_text="Search",key=keybindings.start_filter.key,key_sep='(): ',on_change=self:callback('text_input'),view_id="filter_input"}} + widgets.EditField{frame={l=1,t=2,h=1},label_text="Search",key=keybindings.start_filter.key,key_sep='(): ',on_change=self:callback('text_input'),view_id="filter_input"}, + self.autoupdateLabel} ,view_id='page_main'} self:addviews{widgets.Pages{subviews={mainPage,helpPage},view_id="pages"}} self:pushTarget(args.target) end +function GmEditorUi:verifyStack(args) + local failure = false + + local last_good_level = nil + + for i, level in pairs(self.stack) do + local obj=level.target + + local keys = level.keys + local selection = level.selected + local sel_key = keys[selection] + local next_by_ref + local status, _ = pcall( + function() + next_by_ref = obj[sel_key] + + + end + ) + if not status then + failure = true + last_good_level = i - 1 + break + end + + + if not next_in_stack == next_by_ref then + failure = true + break + end + end + if failure then + self.stack = {table.unpack(self.stack, 1, last_good_level)} + return false + else + return true + end +end function GmEditorUi:text_input(new_text) self:updateTarget(true,true) end @@ -284,6 +329,9 @@ function GmEditorUi:getSelectedField() end end function GmEditorUi:currentTarget() + if #self.stack == 0 then + return nil + end return self.stack[#self.stack] end function GmEditorUi:getSelectedEnumType() @@ -336,7 +384,6 @@ end function GmEditorUi:openOffseted(index,choice) local trg=self:currentTarget() local trg_key=trg.keys[index] - dialog.showInputPrompt(tostring(trg_key),"Enter offset:",COLOR_WHITE,"", function(choice) self:pushTarget(trg.target[trg_key]:_displace(tonumber(choice))) @@ -346,6 +393,10 @@ function GmEditorUi:editSelectedRaw(index,choice) self:editSelected(index, choice, {raw=true}) end function GmEditorUi:editSelected(index,choice,opts) + if not self:verifyStack() then + self:updateTarget() + return + end opts = opts or {} local trg=self:currentTarget() local trg_key=trg.keys[index] @@ -353,7 +404,6 @@ function GmEditorUi:editSelected(index,choice,opts) trg.target[trg_key]= not trg.target[trg_key] self:updateTarget(true) else - --print(type(trg.target[trg.keys[trg.selected]]),trg.target[trg.keys[trg.selected]]._kind or "") local trg_type=type(trg.target[trg_key]) if self:getSelectedEnumType() and not opts.raw then self:editSelectedEnum() @@ -453,6 +503,13 @@ function GmEditorUi:onInput(keys) elseif keys[keybindings.help.key] then self.subviews.pages:setSelected(2) return true + elseif keys[keybindings.autoupdate.key] then + if not self.autoupdate then + self.autoupdateLabel:setText({{gap=1,text="Auto-Update running... ",key=keybindings.autoupdate.key,key_sep = '()'}}) + else + self.autoupdateLabel:setText({{gap=1,text="Auto-Update stopped...",key=keybindings.autoupdate.key,key_sep = '()'}}) + end + self.autoupdate = not self.autoupdate end end function getStringValue(trg,field) @@ -474,6 +531,7 @@ function getStringValue(trg,field) return text end function GmEditorUi:updateTarget(preserve_pos,reindex) + self:verifyStack() local trg=self:currentTarget() local filter=self.subviews.filter_input.text:lower() @@ -507,6 +565,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) else self.subviews.list_main:setSelected(trg.selected) end + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS end function GmEditorUi:pushTarget(target_to_push) local new_tbl={} @@ -545,6 +604,12 @@ function GmEditorUi:postUpdateLayout() config:write(self.frame) end +function GmEditorUi:onRenderBody() + if self.next_refresh_ms <= dfhack.getTickCount() and self.autoupdate then + self:updateTarget() + end +end + GmScreen = defclass(GmScreen, gui.ZScreen) GmScreen.ATTRS { focus_path='gm-editor', @@ -566,9 +631,9 @@ local function get_editor(args) if #args~=0 then if args[1]=="dialog" then dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, - "", function(entry) - view = GmScreen{target=eval(entry)}:show() - end) + "", function(entry) + view = GmScreen{target=eval(entry)}:show() + end) elseif args[1]=="free" then return GmScreen{target=df.reinterpret_cast(df[args[2]],args[3])}:show() else From 0e5976781f22b2c9ba0edde07852ea7895f76579 Mon Sep 17 00:00:00 2001 From: Stephen DeGrace Date: Mon, 6 Feb 2023 22:20:08 -0500 Subject: [PATCH 002/732] revisions from pull request. Switch to ToggleHotketLabel, remove tabs, reorder conditional, fix uninitialized variable next_in_stack --- gui/gm-editor.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index ce25bd0dbb..b3f0dfc105 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -167,8 +167,6 @@ function GmEditorUi:verifyStack(args) local status, _ = pcall( function() next_by_ref = obj[sel_key] - - end ) if not status then @@ -176,8 +174,6 @@ function GmEditorUi:verifyStack(args) last_good_level = i - 1 break end - - if not next_in_stack == next_by_ref then failure = true break From fe04962794c94ad6eeefa15a4bf1937de032aafc Mon Sep 17 00:00:00 2001 From: Stephen DeGrace Date: Tue, 7 Feb 2023 16:27:49 -0500 Subject: [PATCH 003/732] take 2 on *removing tabs* --- gui/gm-editor.lua | 101 +++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 59 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index b3f0dfc105..5ad386f651 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -34,7 +34,7 @@ local keybindings_raw = { {name='start_filter', key="CUSTOM_S",desc="Start typing filter, Enter to finish"}, {name='help', key="STRING_A063",desc="Show this help"}, {name='displace', key="STRING_A093",desc="Open reference offseted by index"}, - {name='autoupdate', key="CUSTOM_ALT_A",desc="Automatically keep values updated"}, + {name='autoupdate', key="CUSTOM_ALT_A",desc="Automatically keep values updated"}, --{name='NOT_USED', key="SEC_SELECT",desc="Edit selected entry as a number (for enums)"}, --not a binding... } @@ -126,7 +126,6 @@ function GmEditorUi:init(args) self.stack={} self.item_count=0 self.keys={} - self.autoupdate = false local helptext={{text="Help"},NEWLINE,NEWLINE} for _,v in ipairs(keybindings_raw) do table.insert(helptext,{text=v.desc,key=v.key,key_sep=': '}) @@ -140,51 +139,50 @@ function GmEditorUi:init(args) local mainList=widgets.List{view_id="list_main",choices={},frame = {l=1,t=3,yalign=0},on_submit=self:callback("editSelected"), on_submit2=self:callback("editSelectedRaw"), text_pen=COLOR_GREY, cursor_pen=COLOR_YELLOW} - self.autoupdateLabel = widgets.Label{text={{gap=1,text="Auto-Update stopped...",key=keybindings.autoupdate.key,key_sep = '()'}}, view_id = 'lbl_autoupdate',frame = {l=0,t=0,yalign=0}} local mainPage=widgets.Panel{ subviews={ mainList, widgets.Label{text={{text="",id="name"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, widgets.EditField{frame={l=1,t=2,h=1},label_text="Search",key=keybindings.start_filter.key,key_sep='(): ',on_change=self:callback('text_input'),view_id="filter_input"}, - self.autoupdateLabel} + widgets.ToggleHotkeyLabel{label="Auto-Update", key=keybindings.autoupdate.key, initial_option=false, view_id = 'lbl_autoupdate', frame={l=1,t=0,yalign=0}}} ,view_id='page_main'} self:addviews{widgets.Pages{subviews={mainPage,helpPage},view_id="pages"}} self:pushTarget(args.target) end function GmEditorUi:verifyStack(args) - local failure = false - - local last_good_level = nil - - for i, level in pairs(self.stack) do - local obj=level.target - - local keys = level.keys - local selection = level.selected - local sel_key = keys[selection] - local next_by_ref - local status, _ = pcall( - function() - next_by_ref = obj[sel_key] - end - ) - if not status then - failure = true - last_good_level = i - 1 - break - end - if not next_in_stack == next_by_ref then - failure = true - break - end - end - if failure then - self.stack = {table.unpack(self.stack, 1, last_good_level)} - return false - else - return true - end + local failure = false + + local last_good_level = nil + + for i, level in pairs(self.stack) do + local obj=level.target + + local keys = level.keys + local selection = level.selected + local sel_key = keys[selection] + local next_by_ref + local status, _ = pcall( + function() + next_by_ref = obj[sel_key] + end + ) + if not status then + failure = true + last_good_level = i - 1 + break + end + if not self.stack[i+1] == next_by_ref then + failure = true + break + end + end + if failure then + self.stack = {table.unpack(self.stack, 1, last_good_level)} + return false + else + return true + end end function GmEditorUi:text_input(new_text) self:updateTarget(true,true) @@ -325,9 +323,6 @@ function GmEditorUi:getSelectedField() end end function GmEditorUi:currentTarget() - if #self.stack == 0 then - return nil - end return self.stack[#self.stack] end function GmEditorUi:getSelectedEnumType() @@ -389,10 +384,10 @@ function GmEditorUi:editSelectedRaw(index,choice) self:editSelected(index, choice, {raw=true}) end function GmEditorUi:editSelected(index,choice,opts) - if not self:verifyStack() then - self:updateTarget() - return - end + if not self:verifyStack() then + self:updateTarget() + return + end opts = opts or {} local trg=self:currentTarget() local trg_key=trg.keys[index] @@ -499,15 +494,9 @@ function GmEditorUi:onInput(keys) elseif keys[keybindings.help.key] then self.subviews.pages:setSelected(2) return true - elseif keys[keybindings.autoupdate.key] then - if not self.autoupdate then - self.autoupdateLabel:setText({{gap=1,text="Auto-Update running... ",key=keybindings.autoupdate.key,key_sep = '()'}}) - else - self.autoupdateLabel:setText({{gap=1,text="Auto-Update stopped...",key=keybindings.autoupdate.key,key_sep = '()'}}) - end - self.autoupdate = not self.autoupdate end end + function getStringValue(trg,field) local obj=trg.target @@ -527,7 +516,7 @@ function getStringValue(trg,field) return text end function GmEditorUi:updateTarget(preserve_pos,reindex) - self:verifyStack() + self:verifyStack() local trg=self:currentTarget() local filter=self.subviews.filter_input.text:lower() @@ -561,7 +550,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) else self.subviews.list_main:setSelected(trg.selected) end - self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS end function GmEditorUi:pushTarget(target_to_push) local new_tbl={} @@ -601,16 +590,10 @@ function GmEditorUi:postUpdateLayout() end function GmEditorUi:onRenderBody() - if self.next_refresh_ms <= dfhack.getTickCount() and self.autoupdate then + if self.subviews.lbl_autoupdate:getOptionValue() and self.next_refresh_ms <= dfhack.getTickCount() then self:updateTarget() end end - -GmScreen = defclass(GmScreen, gui.ZScreen) -GmScreen.ATTRS { - focus_path='gm-editor', -} - function GmScreen:init(args) local target = args.target if not target then From a34f430ed4ee4cf3f27af4a4f0db7423eff547c9 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 10 Feb 2023 23:31:08 -0800 Subject: [PATCH 004/732] Update gui/gm-editor.lua --- gui/gm-editor.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 5ad386f651..3dca8664cb 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -594,6 +594,12 @@ function GmEditorUi:onRenderBody() self:updateTarget() end end + +GmScreen = defclass(GmScreen, gui.ZScreen) +GmScreen.ATTRS { + focus_path='gm-editor', +} + function GmScreen:init(args) local target = args.target if not target then From 0f6729af9d2e2fedfa58a4d7edba9bb65f877cfc Mon Sep 17 00:00:00 2001 From: Robob27 Date: Thu, 16 Feb 2023 04:40:27 -0500 Subject: [PATCH 005/732] Use TabBar from widgets --- changelog.txt | 1 + gui/control-panel.lua | 143 ++---------------------------------------- gui/launcher.lua | 135 ++------------------------------------- 3 files changed, 9 insertions(+), 270 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0551e9d491..3fc15f811f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ that repo. - Collapsing all bins or a single bin - Collapsing all categories -@ `devel/hello-world`: updated to use ZScreen +-@ Update `gui/control-panel` and `gui/launcher` to use TabBar from widgets.lua ## Removed diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 316f4366aa..e68d38f8c4 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -746,143 +746,6 @@ function RepeatAutostart:on_submit() self:refresh() end --- --- Tab ---- - -local to_pen = dfhack.pen.parse -local active_tab_pens = { - text_mode_tab_pen=to_pen{fg=COLOR_YELLOW}, - text_mode_label_pen=to_pen{fg=COLOR_WHITE}, - lt=to_pen{tile=1005, write_to_lower=true}, - lt2=to_pen{tile=1006, write_to_lower=true}, - t=to_pen{tile=1007, fg=COLOR_BLACK, write_to_lower=true, top_of_text=true}, - rt2=to_pen{tile=1008, write_to_lower=true}, - rt=to_pen{tile=1009, write_to_lower=true}, - lb=to_pen{tile=1015, write_to_lower=true}, - lb2=to_pen{tile=1016, write_to_lower=true}, - b=to_pen{tile=1017, fg=COLOR_BLACK, write_to_lower=true, bottom_of_text=true}, - rb2=to_pen{tile=1018, write_to_lower=true}, - rb=to_pen{tile=1019, write_to_lower=true}, -} - -local inactive_tab_pens = { - text_mode_tab_pen=to_pen{fg=COLOR_BROWN}, - text_mode_label_pen=to_pen{fg=COLOR_DARKGREY}, - lt=to_pen{tile=1000, write_to_lower=true}, - lt2=to_pen{tile=1001, write_to_lower=true}, - t=to_pen{tile=1002, fg=COLOR_WHITE, write_to_lower=true, top_of_text=true}, - rt2=to_pen{tile=1003, write_to_lower=true}, - rt=to_pen{tile=1004, write_to_lower=true}, - lb=to_pen{tile=1010, write_to_lower=true}, - lb2=to_pen{tile=1011, write_to_lower=true}, - b=to_pen{tile=1012, fg=COLOR_WHITE, write_to_lower=true, bottom_of_text=true}, - rb2=to_pen{tile=1013, write_to_lower=true}, - rb=to_pen{tile=1014, write_to_lower=true}, -} - -Tab = defclass(Tabs, widgets.Widget) -Tab.ATTRS{ - id=DEFAULT_NIL, - label=DEFAULT_NIL, - on_select=DEFAULT_NIL, - get_pens=DEFAULT_NIL, -} - -function Tab:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.w = #init_table.label + 4 - init_table.frame.h = 2 -end - -function Tab:onRenderBody(dc) - local pens = self.get_pens() - dc:seek(0, 0) - if dfhack.screen.inGraphicsMode() then - dc:char(nil, pens.lt):char(nil, pens.lt2) - for i=1,#self.label do - dc:char(self.label:sub(i,i), pens.t) - end - dc:char(nil, pens.rt2):char(nil, pens.rt) - dc:seek(0, 1) - dc:char(nil, pens.lb):char(nil, pens.lb2) - for i=1,#self.label do - dc:char(self.label:sub(i,i), pens.b) - end - dc:char(nil, pens.rb2):char(nil, pens.rb) - else - local tp = pens.text_mode_tab_pen - dc:char(' ', tp):char('/', tp) - for i=1,#self.label do - dc:char('-', tp) - end - dc:char('\\', tp):char(' ', tp) - dc:seek(0, 1) - dc:char('/', tp):char('-', tp) - dc:string(self.label, pens.text_mode_label_pen) - dc:char('-', tp):char('\\', tp) - end -end - -function Tab:onInput(keys) - if Tab.super.onInput(self, keys) then return true end - if keys._MOUSE_L_DOWN and self:getMousePos() then - self.on_select(self.id) - return true - end -end - --- --- TabBar --- - -TabBar = defclass(TabBar, widgets.ResizingPanel) -TabBar.ATTRS{ - labels=DEFAULT_NIL, - on_select=DEFAULT_NIL, - get_cur_page=DEFAULT_NIL, -} - -function TabBar:init() - for idx,label in ipairs(self.labels) do - self:addviews{ - Tab{ - frame={t=0, l=0}, - id=idx, - label=label, - on_select=self.on_select, - get_pens=function() - return self.get_cur_page() == idx and - active_tab_pens or inactive_tab_pens - end, - } - } - end -end - -function TabBar:postComputeFrame(body) - local t, l, width = 0, 0, body.width - for _,tab in ipairs(self.subviews) do - if l > 0 and l + tab.frame.w > width then - t = t + 2 - l = 0 - end - tab.frame.t = t - tab.frame.l = l - l = l + tab.frame.w - end -end - -function TabBar:onInput(keys) - if TabBar.super.onInput(self, keys) then return true end - if keys.CUSTOM_CTRL_T then - local zero_idx = self.get_cur_page() - 1 - local next_zero_idx = (zero_idx + 1) % #self.labels - self.on_select(next_zero_idx + 1) - return true - end -end - -- -- ControlPanel -- @@ -899,7 +762,7 @@ ControlPanel.ATTRS { function ControlPanel:init() self:addviews{ - TabBar{ + widgets.TabBar{ frame={t=0}, labels={ 'Fort', @@ -910,7 +773,9 @@ function ControlPanel:init() 'Autostart', }, on_select=self:callback('set_page'), - get_cur_page=function() return self.subviews.pages:getSelected() end + get_cur_page=function() return self.subviews.pages:getSelected() end, + key='CUSTOM_ALT_T', + key_back='CUSTOM_ALT_R', }, widgets.Pages{ view_id='pages', diff --git a/gui/launcher.lua b/gui/launcher.lua index e8ed7422f6..5daca39430 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -326,135 +326,6 @@ end -- HelpPanel -- -local to_pen = dfhack.pen.parse -local active_tab_pens = { - text_mode_tab_pen=to_pen{fg=COLOR_YELLOW}, - text_mode_label_pen=to_pen{fg=COLOR_WHITE}, - lt=to_pen{tile=1005, write_to_lower=true}, - lt2=to_pen{tile=1006, write_to_lower=true}, - t=to_pen{tile=1007, fg=COLOR_BLACK, write_to_lower=true, top_of_text=true}, - rt2=to_pen{tile=1008, write_to_lower=true}, - rt=to_pen{tile=1009, write_to_lower=true}, - lb=to_pen{tile=1015, write_to_lower=true}, - lb2=to_pen{tile=1016, write_to_lower=true}, - b=to_pen{tile=1017, fg=COLOR_BLACK, write_to_lower=true, bottom_of_text=true}, - rb2=to_pen{tile=1018, write_to_lower=true}, - rb=to_pen{tile=1019, write_to_lower=true}, -} - -local inactive_tab_pens = { - text_mode_tab_pen=to_pen{fg=COLOR_BROWN}, - text_mode_label_pen=to_pen{fg=COLOR_DARKGREY}, - lt=to_pen{tile=1000, write_to_lower=true}, - lt2=to_pen{tile=1001, write_to_lower=true}, - t=to_pen{tile=1002, fg=COLOR_WHITE, write_to_lower=true, top_of_text=true}, - rt2=to_pen{tile=1003, write_to_lower=true}, - rt=to_pen{tile=1004, write_to_lower=true}, - lb=to_pen{tile=1010, write_to_lower=true}, - lb2=to_pen{tile=1011, write_to_lower=true}, - b=to_pen{tile=1012, fg=COLOR_WHITE, write_to_lower=true, bottom_of_text=true}, - rb2=to_pen{tile=1013, write_to_lower=true}, - rb=to_pen{tile=1014, write_to_lower=true}, -} - -Tab = defclass(Tabs, widgets.Widget) -Tab.ATTRS{ - id=DEFAULT_NIL, - label=DEFAULT_NIL, - on_select=DEFAULT_NIL, - get_pens=DEFAULT_NIL, -} - -function Tab:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.w = #init_table.label + 4 - init_table.frame.h = 2 -end - -function Tab:onRenderBody(dc) - local pens = self.get_pens() - dc:seek(0, 0) - if dfhack.screen.inGraphicsMode() then - dc:char(nil, pens.lt):char(nil, pens.lt2) - for i=1,#self.label do - dc:char(self.label:sub(i,i), pens.t) - end - dc:char(nil, pens.rt2):char(nil, pens.rt) - dc:seek(0, 1) - dc:char(nil, pens.lb):char(nil, pens.lb2) - for i=1,#self.label do - dc:char(self.label:sub(i,i), pens.b) - end - dc:char(nil, pens.rb2):char(nil, pens.rb) - else - local tp = pens.text_mode_tab_pen - dc:char(' ', tp):char('/', tp) - for i=1,#self.label do - dc:char('-', tp) - end - dc:char('\\', tp):char(' ', tp) - dc:seek(0, 1) - dc:char('/', tp):char('-', tp) - dc:string(self.label, pens.text_mode_label_pen) - dc:char('-', tp):char('\\', tp) - end -end - -function Tab:onInput(keys) - if Tab.super.onInput(self, keys) then return true end - if keys._MOUSE_L_DOWN and self:getMousePos() then - self.on_select(self.id) - return true - end -end - -TabBar = defclass(TabBar, widgets.ResizingPanel) -TabBar.ATTRS{ - labels=DEFAULT_NIL, - on_select=DEFAULT_NIL, - get_cur_page=DEFAULT_NIL, -} - -function TabBar:init() - for idx,label in ipairs(self.labels) do - self:addviews{ - Tab{ - frame={t=0, l=0}, - id=idx, - label=label, - on_select=self.on_select, - get_pens=function() - return self.get_cur_page() == idx and - active_tab_pens or inactive_tab_pens - end, - } - } - end -end - -function TabBar:postComputeFrame(body) - local t, l, width = 0, 0, body.width - for _,tab in ipairs(self.subviews) do - if l > 0 and l + tab.frame.w > width then - t = t + 2 - l = 0 - end - tab.frame.t = t - tab.frame.l = l - l = l + tab.frame.w - end -end - -function TabBar:onInput(keys) - if TabBar.super.onInput(self, keys) then return true end - if keys.CUSTOM_CTRL_T then - local zero_idx = self.get_cur_page() - 1 - local next_zero_idx = (zero_idx + 1) % #self.labels - self.on_select(next_zero_idx + 1) - return true - end -end - HelpPanel = defclass(HelpPanel, widgets.Panel) HelpPanel.ATTRS{ autoarrange_subviews=true, @@ -475,14 +346,16 @@ function HelpPanel:init() self.cur_entry = '' self:addviews{ - TabBar{ + widgets.TabBar{ frame={t=0, l=0}, labels={ 'Help', 'Output', }, on_select=function(idx) self.subviews.pages:setSelected(idx) end, - get_cur_page=function() return self.subviews.pages:getSelected() end + get_cur_page=function() return self.subviews.pages:getSelected() end, + key='CUSTOM_ALT_T', + key_back='CUSTOM_ALT_R', }, widgets.Pages{ view_id='pages', From 51c1c09377120dc27a037a16c61fb9dbfbe3b0fd Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sun, 19 Feb 2023 20:39:02 +1100 Subject: [PATCH 006/732] initial version of seedwatch gui initial version of seedwatch gui --- gui/seedwatch.lua | 302 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 gui/seedwatch.lua diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua new file mode 100644 index 0000000000..4573ea5a62 --- /dev/null +++ b/gui/seedwatch.lua @@ -0,0 +1,302 @@ +-- config ui for seedwatch + +local gui = require('gui') +local widgets = require('gui.widgets') +local widgets = require('gui.widgets') +local plugin = require('plugins.seedwatch') + +local PROPERTIES_HEADER = ' Quantity Target ' +local REFRESH_MS = 10000 +local MAX_TARGET = 2147483647 +-- +-- SeedSettings +-- +SeedSettings = defclass(SeedSettings, widgets.Window) +SeedSettings.ATTRS{ + lockable=false, + frame={l=5, t=5, w=35, h=9}, +} + +function SeedSettings:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Seed: ', + }, + widgets.Label{ + view_id='id', + frame={t=0, l=0}, + visible=false, + }, + widgets.Label{ + view_id='name', + frame={t=0, l=6}, + text_pen=COLOR_GREEN, + }, + widgets.Label{ + frame={t=1, l=0}, + text='Quantity: ', + }, + widgets.Label{ + view_id='quantity', + frame={t=1, l=10}, + text_pen=COLOR_GREEN, + }, + widgets.EditField{ + view_id='target', + frame={t=2, l=0}, + label_text='Target: ', + key='CUSTOM_CTRL_T', + on_char=function(ch) return ch:match('%d') end, + on_submit=self:callback('commit'), + }, + widgets.HotkeyLabel{ + frame={t=4, l=0}, + key='SELECT', + label='Apply', + on_activate=self:callback('commit'), + }, + } +end + +function SeedSettings:show(choice, on_commit) + self.data = choice.data + self.on_commit = on_commit + local data = self.data + self.subviews.id:setText(data.id) + self.subviews.name:setText(data.name) + self.subviews.quantity:setText(tostring(data.quantity)) + self.subviews.target:setText(tostring(data.target)) + self.visible = true + self:setFocus(true) + self:updateLayout() +end + +function SeedSettings:hide() + self:setFocus(false) + self.visible = false +end + +function SeedSettings:commit() + local target = math.tointeger(self.subviews.target.text) + if not target or target == '' then + target = 0 + elseif target > MAX_TARGET then + target = MAX_TARGET + end + plugin.seedwatch_setTarget(self.subviews.id.text, target) + self:hide() + self.on_commit() +end + +function SeedSettings:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + self:hide() + return true + end + SeedSettings.super.onInput(self, keys) + return true +end + +-- +-- Seedwatch +-- +Seedwatch = defclass(Seedwatch, widgets.Window) +Seedwatch.ATTRS { + frame_title='Seedwatch', + frame={w=60, h=27}, + resizable=true, + resize_min={h=25}, + hide_unmonitored=DEFAULT_NIL, + manual_hide_unmonitored_touched=DEFAULT_NIL, +} + +function Seedwatch:init() + local minimal = false + local saved_frame = {w=50, h=6, r=2, t=18} + local saved_resize_min = {w=saved_frame.w, h=saved_frame.h} + local function toggle_minimal() + minimal = not minimal + local swap = self.frame + self.frame = saved_frame + saved_frame = swap + swap = self.resize_min + self.resize_min = saved_resize_min + saved_resize_min = swap + self:updateLayout() + self:refresh_data() + end + local function is_minimal() + return minimal + end + local function is_not_minimal() + return not minimal + end + + self:addviews{ + widgets.ToggleHotkeyLabel{ + view_id='enable_toggle', + frame={t=0, l=0, w=31}, + label='Seedwatch is', + key='CUSTOM_CTRL_E', + options={{value=true, label='Enabled', pen=COLOR_GREEN}, + {value=false, label='Disabled', pen=COLOR_RED}}, + on_change=function(val) plugin.setEnabled(val) end, + }, + widgets.EditField{ + view_id='all', + frame={t=1, l=0}, + label_text='Target for all: ', + key='CUSTOM_CTRL_A', + on_char=function(ch) return ch:match('%d') end, + on_submit=function(text) + local target = math.tointeger(text) + if not target or target == '' then + target = 0 + elseif target > MAX_TARGET then + target = MAX_TARGET + end + plugin.seedwatch_setTarget('all', target) + self:refresh_data() + self:update_choices() + end, + visible=is_not_minimal, + text='30', + }, + + widgets.HotkeyLabel{ + frame={r=0, t=0, w=10}, + key='CUSTOM_ALT_M', + label=string.char(31)..string.char(30), + on_activate=toggle_minimal}, + widgets.Label{ + view_id='minimal_summary', + frame={t=1, l=0, h=1}, + auto_height=false, + visible=is_minimal, + }, + widgets.Label{ + frame={t=3, l=0}, + text='Seed', + auto_width=true, + visible=is_not_minimal, + }, + widgets.Label{ + frame={t=3, r=0}, + text=PROPERTIES_HEADER, + auto_width=true, + visible=is_not_minimal, + }, + widgets.List{ + view_id='list', + frame={t=5, l=0, r=0, b=3}, + on_submit=self:callback('configure_seed'), + visible=is_not_minimal, + }, + widgets.Label{ + view_id='summary', + frame={b=0, l=0}, + visible=is_not_minimal, + }, + SeedSettings{ + view_id='seed_settings', + visible=false, + }, + + } + + self:refresh_data() +end + +function Seedwatch:getDefaultHide() + return false +end + +function Seedwatch:configure_seed(idx, choice) + self.subviews.seed_settings:show(choice, function() + self:refresh_data() + self:update_choices() + end) +end + +function Seedwatch:update_choices() + local list = self.subviews.list + local name_width = list.frame_body.width - #PROPERTIES_HEADER + local fmt = '%-'..tostring(name_width)..'s %10d %10d ' + local choices = {} + for k, v in pairs(self.data.seeds) do + local text = (fmt):format(v.name:sub(1,name_width), v.quantity or 0, v.target or 0) + table.insert(choices, {text=text, data=v}) + end + + self.subviews.list:setChoices(choices) + self.subviews.list:updateLayout() +end + +function Seedwatch:refresh_data() + self.subviews.enable_toggle:setOption(plugin.isEnabled()) + local watch_map, seed_counts = plugin.seedwatch_getData() + self.data = {} + self.data.sum = 0 + self.data.seeds_qty = 0 + self.data.seeds_watched = 0 + self.data.seeds = {} + for k,v in pairs(seed_counts) do + local seed = {} + seed.id = df.global.world.raws.plants.all[k].id + seed.name = df.global.world.raws.plants.all[k].seed_singular + seed.quantity = v + seed.target = watch_map[k] or 0 + self.data.seeds[k] = seed + if self.data.seeds[k].target > 0 then + self.data.seeds_watched = self.data.seeds_watched + 1 + end + self.data.seeds_qty = self.data.seeds_qty + v + end + if self.subviews.all.text == '' then + self.subviews.all:setText('0') + end + local summary_text = ('Seeds quantity: %d watched: %d\n'):format(tostring(self.data.seeds_qty),tostring(self.data.seeds_watched)) + self.subviews.summary:setText(summary_text) + local minimal_summary_text = summary_text + self.subviews.minimal_summary:setText(minimal_summary_text) + + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + +end + + +function Seedwatch:postUpdateLayout() + self:update_choices() +end + +-- refreshes data every 10 seconds or so +function Seedwatch:onRenderBody() + if self.next_refresh_ms <= dfhack.getTickCount() then + self:refresh_data() + self:update_choices() + end +end + +-- +-- SeedwatchScreen +-- + +SeedwatchScreen = defclass(SeedwatchScreen, gui.ZScreen) +SeedwatchScreen.ATTRS { + focus_path='seedwatch', +} + +function SeedwatchScreen:init() + self:addviews{Seedwatch{}} +end + +function SeedwatchScreen:onDismiss() + view = nil +end + +if not dfhack.isMapLoaded() then + qerror('seedwatch requires a map to be loaded') +end + +view = view and view:raise() or SeedwatchScreen{}:show() From 6a344d8ea09d76348b92c2bb9e1136889d0b32b6 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 21 Feb 2023 15:23:57 +1100 Subject: [PATCH 007/732] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index a8023637b9..d4ab79f9f8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,7 @@ that repo. ## New Scripts - `combine`: combines stacks of food and plant items. Merge of `combine-plants` and `combine-drinks`. +- `gui/seedwatch`: GUI config and status panel interface for `seedwatch`. ## Fixes - `makeown`: fixes errors caused by using makeown on an invader From 9661c0de83a9547a4d7225fbf0d5186523fd2b1e Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 21 Feb 2023 15:24:15 +1100 Subject: [PATCH 008/732] Create seedwatch.rst --- docs/gui/seedwatch.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/gui/seedwatch.rst diff --git a/docs/gui/seedwatch.rst b/docs/gui/seedwatch.rst new file mode 100644 index 0000000000..e5417922a4 --- /dev/null +++ b/docs/gui/seedwatch.rst @@ -0,0 +1,19 @@ +gui/seedwatch +============= + +.. dfhack-tool:: + :summary: Manages seed and plant cooking based on seed stock levels. + :tags: fort auto stockpiles + +This is the configuration interface for the `seedwatch` plugin. You can configure +a target stock amount for each seed type. If the number of seeds of that type falls +below the target, then the plants and seeds of that type will be protected from +cookery. If the number rises above the target + 20, then cooking will be allowed +again. + +Usage +----- + +:: + + gui/seedwatch From 22ff7fd57ba0a8060627489bd0e30764cde41895 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 04:27:50 +0000 Subject: [PATCH 009/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/gui/seedwatch.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/gui/seedwatch.rst b/docs/gui/seedwatch.rst index e5417922a4..f65bdccbf2 100644 --- a/docs/gui/seedwatch.rst +++ b/docs/gui/seedwatch.rst @@ -7,8 +7,8 @@ gui/seedwatch This is the configuration interface for the `seedwatch` plugin. You can configure a target stock amount for each seed type. If the number of seeds of that type falls -below the target, then the plants and seeds of that type will be protected from -cookery. If the number rises above the target + 20, then cooking will be allowed +below the target, then the plants and seeds of that type will be protected from +cookery. If the number rises above the target + 20, then cooking will be allowed again. Usage From 6da30186ed6762d2f3bd96669f8ce6d89c639d04 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 21 Feb 2023 20:51:07 +1100 Subject: [PATCH 010/732] Update seedwatch.lua Use filtered list instead of list, in case of lots of seeds. Also, do not refresh data if the seed settings modal has focus, and if the focus is not on the all or search edit fields. --- gui/seedwatch.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua index 4573ea5a62..43323dd0fb 100644 --- a/gui/seedwatch.lua +++ b/gui/seedwatch.lua @@ -15,6 +15,7 @@ SeedSettings = defclass(SeedSettings, widgets.Window) SeedSettings.ATTRS{ lockable=false, frame={l=5, t=5, w=35, h=9}, + data={id='', name='', quantity=0, target=0,}, } function SeedSettings:init() @@ -109,6 +110,7 @@ Seedwatch.ATTRS { resize_min={h=25}, hide_unmonitored=DEFAULT_NIL, manual_hide_unmonitored_touched=DEFAULT_NIL, + data = {sum=0, seeds_qty=0, seeds_watched=0, seeds = {{id='', name='', quantity=0, target=0,}}}, } function Seedwatch:init() @@ -157,6 +159,7 @@ function Seedwatch:init() target = MAX_TARGET end plugin.seedwatch_setTarget('all', target) + self.subviews.list:setFilter('') self:refresh_data() self:update_choices() end, @@ -187,11 +190,12 @@ function Seedwatch:init() auto_width=true, visible=is_not_minimal, }, - widgets.List{ + widgets.FilteredList{ view_id='list', frame={t=5, l=0, r=0, b=3}, on_submit=self:callback('configure_seed'), visible=is_not_minimal, + edit_key = 'CUSTOM_S', }, widgets.Label{ view_id='summary', @@ -224,12 +228,14 @@ function Seedwatch:update_choices() local name_width = list.frame_body.width - #PROPERTIES_HEADER local fmt = '%-'..tostring(name_width)..'s %10d %10d ' local choices = {} + local prior_search=self.subviews.list.edit.text for k, v in pairs(self.data.seeds) do local text = (fmt):format(v.name:sub(1,name_width), v.quantity or 0, v.target or 0) table.insert(choices, {text=text, data=v}) end self.subviews.list:setChoices(choices) + if prior_search then self.subviews.list:setFilter(prior_search) end self.subviews.list:updateLayout() end @@ -272,7 +278,10 @@ end -- refreshes data every 10 seconds or so function Seedwatch:onRenderBody() - if self.next_refresh_ms <= dfhack.getTickCount() then + if self.next_refresh_ms <= dfhack.getTickCount() + and self.subviews.seed_settings.visible == false + and not self.subviews.all.focus + and not self.subviews.list.edit.focus then self:refresh_data() self:update_choices() end From 45642ea017e41b2db881bc98d9577c060241fcb4 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 21 Feb 2023 22:24:16 +1100 Subject: [PATCH 011/732] Update seedwatch.lua --- gui/seedwatch.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua index 43323dd0fb..d59937df87 100644 --- a/gui/seedwatch.lua +++ b/gui/seedwatch.lua @@ -2,7 +2,6 @@ local gui = require('gui') local widgets = require('gui.widgets') -local widgets = require('gui.widgets') local plugin = require('plugins.seedwatch') local PROPERTIES_HEADER = ' Quantity Target ' From e3c8f437350f7f46778b402dcfae3dc2bab160dc Mon Sep 17 00:00:00 2001 From: Jonathan Jordan Date: Fri, 24 Feb 2023 18:26:06 +0100 Subject: [PATCH 012/732] Added diplo.lua Added a script to conveniently change diplomatic relations. --- diplo.lua | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 diplo.lua diff --git a/diplo.lua b/diplo.lua new file mode 100644 index 0000000000..5a9d1f2b48 --- /dev/null +++ b/diplo.lua @@ -0,0 +1,87 @@ +--[[ diplo - Quick "Diplomacy" +Without arguments: + Print list of civs the player civ has diplomatic relations to. + This list might not match the ingame civ list, but is what matters. Does not include 'no contect' civs, etc. + Also shows if relations match between player civ and other civ - a mismatch can potentially happen 'naturally', but is important when trying to make peace proper, as both civs need to be at peace with the other. +With arguments: + diplo CIV_ID RELATION + Changes the relation to the civ identified by CIV_ID to the one specified in RELATION, making sure that this is mutual. + Relations: 0 = Peace; 1 = War; 3 = Alliance (seems to only apply to site govs...?) + Civ list is shown afterwards. +]] + +local args = {...} + +-- player civ references: +local p_civ_id = df.global.plotinfo.civ_id +local p_civ = df.historical_entity.find(df.global.plotinfo.civ_id) + +-- get a very rough, but readable name for a civ: +function get_raw_name(civ) + local raw_name = "" + for _, name_word in pairs(civ.name.words) do + if name_word ~= -1 then + raw_name = raw_name .. " " .. df.global.world.raws.language.words[name_word].word + end + end + raw_name = string.sub(raw_name, 2) + return raw_name +end + +-- if no civ ID is entered, just output list of civs: +if not args[1] then + goto outputlist +end + +-- make sure that there is a relation to change to: +if not args[2] then qerror("Missing relation!") end + +-- change relation: +print("Changing relation with " .. args[1] .. " to " .. args[2]) +for _, entity in pairs(p_civ.relations.diplomacy) do + local cur_civ_id = entity.group_id + local cur_civ = df.historical_entity.find(cur_civ_id) + if cur_civ.type == 0 and cur_civ_id == tonumber(args[1]) then + entity.relation = tonumber(args[2]) + for _, entity2 in pairs(cur_civ.relations.diplomacy) do + if entity2.group_id == p_civ_id then + entity2.relation = tonumber(args[2]) + end + end + end +end + +-- output list of civs +:: outputlist :: +local civ_list = {} +for _, entity in pairs(p_civ.relations.diplomacy) do + local cur_civ_id = entity.group_id + local cur_civ = df.historical_entity.find(cur_civ_id) + if cur_civ.type == 0 then + rel_str = "" + if entity.relation == 0 then + rel_str = "0 (Peace)" + elseif entity.relation == 1 then + rel_str = "1 (War)" + elseif entity.relation == 3 then + rel_str = "3 (Alliance)" + end + matched = "Yes" + for _, entity2 in pairs(cur_civ.relations.diplomacy) do + if entity2.group_id == p_civ_id and entity2.relation ~= entity.relation then + matched = "No" + end + end + table.insert(civ_list, { + cur_civ_id, + rel_str, + matched, + get_raw_name(cur_civ) + }) + end +end + +print(([[%4s %12s %8s %20s]]):format("ID", "Relation", "Matched", "Name")) +for _, civ in pairs(civ_list) do + print(([[%4s %12s %8s %20s]]):format(civ[1], civ[2], civ[3], civ[4])) +end \ No newline at end of file From d1ab8cdd8ac1dc831e94351fc8983d202fc2d853 Mon Sep 17 00:00:00 2001 From: Jonathan Jordan Date: Fri, 24 Feb 2023 19:46:51 +0100 Subject: [PATCH 013/732] Formatting/grammar fixes --- diplo.lua | 105 +++++++++++++++++++++++++++--------------------------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/diplo.lua b/diplo.lua index 5a9d1f2b48..d84c127b45 100644 --- a/diplo.lua +++ b/diplo.lua @@ -1,13 +1,14 @@ ---[[ diplo - Quick "Diplomacy" +--[[ +diplo - Quick "Diplomacy" Without arguments: - Print list of civs the player civ has diplomatic relations to. - This list might not match the ingame civ list, but is what matters. Does not include 'no contect' civs, etc. - Also shows if relations match between player civ and other civ - a mismatch can potentially happen 'naturally', but is important when trying to make peace proper, as both civs need to be at peace with the other. + Print list of civs the player civ has diplomatic relations to. + This list might not match the ingame civ list, but is what matters. Does not include 'no contact' civs, etc. + Also shows if relations match between player civ and other civ - a mismatch can potentially happen 'naturally', but is important when trying to make peace proper, as both civs need to be at peace with the other. With arguments: - diplo CIV_ID RELATION - Changes the relation to the civ identified by CIV_ID to the one specified in RELATION, making sure that this is mutual. - Relations: 0 = Peace; 1 = War; 3 = Alliance (seems to only apply to site govs...?) - Civ list is shown afterwards. + diplo CIV_ID RELATION + Changes the relation to the civ identified by CIV_ID to the one specified in RELATION, making sure that this is mutual. + Relations: 0 = Peace; 1 = War; 3 = Alliance (seems to only apply to site govs...?) + Civ list is shown afterwards. ]] local args = {...} @@ -18,19 +19,19 @@ local p_civ = df.historical_entity.find(df.global.plotinfo.civ_id) -- get a very rough, but readable name for a civ: function get_raw_name(civ) - local raw_name = "" - for _, name_word in pairs(civ.name.words) do - if name_word ~= -1 then - raw_name = raw_name .. " " .. df.global.world.raws.language.words[name_word].word - end - end - raw_name = string.sub(raw_name, 2) - return raw_name + local raw_name = "" + for _, name_word in pairs(civ.name.words) do + if name_word ~= -1 then + raw_name = raw_name .. " " .. df.global.world.raws.language.words[name_word].word + end + end + raw_name = string.sub(raw_name, 2) + return raw_name end -- if no civ ID is entered, just output list of civs: if not args[1] then - goto outputlist + goto outputlist end -- make sure that there is a relation to change to: @@ -39,49 +40,49 @@ if not args[2] then qerror("Missing relation!") end -- change relation: print("Changing relation with " .. args[1] .. " to " .. args[2]) for _, entity in pairs(p_civ.relations.diplomacy) do - local cur_civ_id = entity.group_id - local cur_civ = df.historical_entity.find(cur_civ_id) - if cur_civ.type == 0 and cur_civ_id == tonumber(args[1]) then - entity.relation = tonumber(args[2]) - for _, entity2 in pairs(cur_civ.relations.diplomacy) do - if entity2.group_id == p_civ_id then - entity2.relation = tonumber(args[2]) - end - end - end + local cur_civ_id = entity.group_id + local cur_civ = df.historical_entity.find(cur_civ_id) + if cur_civ.type == 0 and cur_civ_id == tonumber(args[1]) then + entity.relation = tonumber(args[2]) + for _, entity2 in pairs(cur_civ.relations.diplomacy) do + if entity2.group_id == p_civ_id then + entity2.relation = tonumber(args[2]) + end + end + end end -- output list of civs :: outputlist :: local civ_list = {} for _, entity in pairs(p_civ.relations.diplomacy) do - local cur_civ_id = entity.group_id - local cur_civ = df.historical_entity.find(cur_civ_id) - if cur_civ.type == 0 then - rel_str = "" - if entity.relation == 0 then - rel_str = "0 (Peace)" - elseif entity.relation == 1 then - rel_str = "1 (War)" - elseif entity.relation == 3 then - rel_str = "3 (Alliance)" - end - matched = "Yes" - for _, entity2 in pairs(cur_civ.relations.diplomacy) do - if entity2.group_id == p_civ_id and entity2.relation ~= entity.relation then - matched = "No" - end - end - table.insert(civ_list, { - cur_civ_id, - rel_str, - matched, - get_raw_name(cur_civ) - }) - end + local cur_civ_id = entity.group_id + local cur_civ = df.historical_entity.find(cur_civ_id) + if cur_civ.type == 0 then + rel_str = "" + if entity.relation == 0 then + rel_str = "0 (Peace)" + elseif entity.relation == 1 then + rel_str = "1 (War)" + elseif entity.relation == 3 then + rel_str = "3 (Alliance)" + end + matched = "Yes" + for _, entity2 in pairs(cur_civ.relations.diplomacy) do + if entity2.group_id == p_civ_id and entity2.relation ~= entity.relation then + matched = "No" + end + end + table.insert(civ_list, { + cur_civ_id, + rel_str, + matched, + get_raw_name(cur_civ) + }) + end end print(([[%4s %12s %8s %20s]]):format("ID", "Relation", "Matched", "Name")) for _, civ in pairs(civ_list) do print(([[%4s %12s %8s %20s]]):format(civ[1], civ[2], civ[3], civ[4])) -end \ No newline at end of file +end From 8b0f95092bda5805a8e9d0039e418dcd567f72c7 Mon Sep 17 00:00:00 2001 From: Jonathan Jordan Date: Sun, 26 Feb 2023 17:27:55 +0100 Subject: [PATCH 014/732] Civ name translation update Using dfhack `TranslteName` --- diplo.lua | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/diplo.lua b/diplo.lua index d84c127b45..cc4a9fd386 100644 --- a/diplo.lua +++ b/diplo.lua @@ -17,18 +17,6 @@ local args = {...} local p_civ_id = df.global.plotinfo.civ_id local p_civ = df.historical_entity.find(df.global.plotinfo.civ_id) --- get a very rough, but readable name for a civ: -function get_raw_name(civ) - local raw_name = "" - for _, name_word in pairs(civ.name.words) do - if name_word ~= -1 then - raw_name = raw_name .. " " .. df.global.world.raws.language.words[name_word].word - end - end - raw_name = string.sub(raw_name, 2) - return raw_name -end - -- if no civ ID is entered, just output list of civs: if not args[1] then goto outputlist @@ -77,7 +65,7 @@ for _, entity in pairs(p_civ.relations.diplomacy) do cur_civ_id, rel_str, matched, - get_raw_name(cur_civ) + dfhack.TranslateName(cur_civ.name, true) }) end end From 06d6c63f1b86dc07f152b06575f28bac671c595a Mon Sep 17 00:00:00 2001 From: Jonathan Jordan Date: Sun, 26 Feb 2023 17:33:41 +0100 Subject: [PATCH 015/732] Added script_help --- diplo.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/diplo.lua b/diplo.lua index cc4a9fd386..574f44eb81 100644 --- a/diplo.lua +++ b/diplo.lua @@ -23,7 +23,10 @@ if not args[1] then end -- make sure that there is a relation to change to: -if not args[2] then qerror("Missing relation!") end +if not args[2] then + print(dfhack.script_help()) + qerror("Missing relation!") +end -- change relation: print("Changing relation with " .. args[1] .. " to " .. args[2]) From 006eeee3a341d27d58b9e15a711d462afaedb974 Mon Sep 17 00:00:00 2001 From: Jonathan Jordan Date: Thu, 2 Mar 2023 12:16:51 +0100 Subject: [PATCH 016/732] Complete refactor & enhancements --- diplo.lua | 139 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 45 deletions(-) diff --git a/diplo.lua b/diplo.lua index 574f44eb81..3334ca14eb 100644 --- a/diplo.lua +++ b/diplo.lua @@ -1,3 +1,4 @@ +@@ -0,0 +1,79 @@ --[[ diplo - Quick "Diplomacy" Without arguments: @@ -7,7 +8,8 @@ Without arguments: With arguments: diplo CIV_ID RELATION Changes the relation to the civ identified by CIV_ID to the one specified in RELATION, making sure that this is mutual. - Relations: 0 = Peace; 1 = War; 3 = Alliance (seems to only apply to site govs...?) + CIV_ID can be 'all' to change relations with all civs. + RELATION can be 'peace'/0 or 'war'/1. Civ list is shown afterwards. ]] @@ -17,63 +19,110 @@ local args = {...} local p_civ_id = df.global.plotinfo.civ_id local p_civ = df.historical_entity.find(df.global.plotinfo.civ_id) --- if no civ ID is entered, just output list of civs: -if not args[1] then - goto outputlist +-- get list of civs: +function get_civ_list() + local civ_list = {} + for _, entity in pairs(p_civ.relations.diplomacy) do + local cur_civ_id = entity.group_id + local cur_civ = df.historical_entity.find(cur_civ_id) + if cur_civ.type == 0 then + -- if true then + rel_str = "" + if entity.relation == 0 then + rel_str = "0 (Peace)" + elseif entity.relation == 1 then + rel_str = "1 (War)" + end + matched = "No" + for _, entity2 in pairs(cur_civ.relations.diplomacy) do + if entity2.group_id == p_civ_id and entity2.relation == entity.relation then + matched = "Yes" + end + end + table.insert(civ_list, { + cur_civ_id, + rel_str, + matched, + dfhack.TranslateName(cur_civ.name, true) + }) + end + end + return civ_list end --- make sure that there is a relation to change to: -if not args[2] then - print(dfhack.script_help()) - qerror("Missing relation!") +-- output civ list: +function output_civ_list() + local civ_list = get_civ_list() + if not next(civ_list) then + print("Your civilisation has no diplomatic relations! This means something is going wrong, as it should have at least a relation to itself.") + else + print(([[%4s %12s %8s %30s]]):format("ID", "Relation", "Matched", "Name")) + for _, civ in pairs(civ_list) do + print(([[%4s %12s %8s %30s]]):format(civ[1], civ[2], civ[3], civ[4])) + end + end end -- change relation: -print("Changing relation with " .. args[1] .. " to " .. args[2]) -for _, entity in pairs(p_civ.relations.diplomacy) do - local cur_civ_id = entity.group_id - local cur_civ = df.historical_entity.find(cur_civ_id) - if cur_civ.type == 0 and cur_civ_id == tonumber(args[1]) then - entity.relation = tonumber(args[2]) - for _, entity2 in pairs(cur_civ.relations.diplomacy) do - if entity2.group_id == p_civ_id then - entity2.relation = tonumber(args[2]) +function change_relation(civ_id, relation) + print("Changing relation with " .. civ_id .. " to " .. relation) + for _, entity in pairs(p_civ.relations.diplomacy) do + local cur_civ_id = entity.group_id + local cur_civ = df.historical_entity.find(cur_civ_id) + if cur_civ.type == 0 and cur_civ_id == civ_id then + entity.relation = relation + for _, entity2 in pairs(cur_civ.relations.diplomacy) do + if entity2.group_id == p_civ_id then + entity2.relation = relation + end end end end end --- output list of civs -:: outputlist :: -local civ_list = {} -for _, entity in pairs(p_civ.relations.diplomacy) do - local cur_civ_id = entity.group_id - local cur_civ = df.historical_entity.find(cur_civ_id) - if cur_civ.type == 0 then - rel_str = "" - if entity.relation == 0 then - rel_str = "0 (Peace)" - elseif entity.relation == 1 then - rel_str = "1 (War)" - elseif entity.relation == 3 then - rel_str = "3 (Alliance)" - end - matched = "Yes" - for _, entity2 in pairs(cur_civ.relations.diplomacy) do - if entity2.group_id == p_civ_id and entity2.relation ~= entity.relation then - matched = "No" +-- parse relation string args: +function relation_parse(rel_str) + if rel_str:lower() == "peace" then + return 0 + elseif rel_str:lower() == "war" then + return 1 + elseif rel_str == "0" then + return 0 + elseif rel_str == "1" then + return 1 + else + print(dfhack.script_help()) + qerror("Cannot parse relation: " .. rel_str) + end +end + +-- handle 'all' civ argument: +function handle_all(arg1, arg2) + if arg1:lower() == "all" then + local civ_list = get_civ_list() + for _, civ in pairs(civ_list) do + if civ[1] ~= p_civ_id then + change_relation(civ[1], arg2) end end - table.insert(civ_list, { - cur_civ_id, - rel_str, - matched, - dfhack.TranslateName(cur_civ.name, true) - }) + else + change_relation(tonumber(arg1), arg2) end end -print(([[%4s %12s %8s %20s]]):format("ID", "Relation", "Matched", "Name")) -for _, civ in pairs(civ_list) do - print(([[%4s %12s %8s %20s]]):format(civ[1], civ[2], civ[3], civ[4])) +-- if no civ ID is entered, just output list of civs: +if not args[1] then + output_civ_list() + return +end + +-- make sure that there is a relation to change to: +if not args[2] then + print(dfhack.script_help()) + qerror("Missing relation!") end + +-- change relation(s) according to args: +handle_all(args[1], relation_parse(args[2])) +output_civ_list() +return From 224b8c16f9c6dbecc46620ad8b5f533a01662885 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Mar 2023 14:42:27 -0700 Subject: [PATCH 017/732] lint caravan --- caravan.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/caravan.lua b/caravan.lua index daed10747b..921cb7e5cc 100644 --- a/caravan.lua +++ b/caravan.lua @@ -297,7 +297,7 @@ local function caravans_from_ids(ids) local c = {} --as:df.caravan_state[] for _,id in ipairs(ids) do - local id = tonumber(id) + id = tonumber(id) if id then c[id] = caravans[id] end @@ -320,7 +320,7 @@ function commands.list() df.creature_raw.find(df.historical_entity.find(car.entity).race).name[2], -- adjective dfhack.TranslateName(df.historical_entity.find(car.entity).name) ))) - print(' ' .. (df.caravan_state.T_trade_state[car.trade_state] or 'Unknown state: ' .. car.trade_state)) + print(' ' .. (df.caravan_state.T_trade_state[car.trade_state] or ('Unknown state: ' .. car.trade_state))) print((' %d day(s) remaining'):format(math.floor(car.time_remaining / 120))) for flag, msg in pairs(INTERESTING_FLAGS) do if car.flags[flag] then From f93c9354f5c012099cafa5545908fa59fbc4566b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 10 Mar 2023 17:54:01 -0800 Subject: [PATCH 018/732] use new blueprints data dirs --- changelog.txt | 1 + docs/quickfort.rst | 52 ++++++++++------------------ gui/blueprint.lua | 3 +- internal/quickfort/list.lua | 67 +++++++++++++++++++------------------ internal/quickfort/set.lua | 61 ++++----------------------------- 5 files changed, 61 insertions(+), 123 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0d7d4d3e3c..ed8906be76 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes ## Misc Improvements +- `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your blueprints to the new directory! ## Removed diff --git a/docs/quickfort.rst b/docs/quickfort.rst index bcbd39ffbe..58f1a7b071 100644 --- a/docs/quickfort.rst +++ b/docs/quickfort.rst @@ -5,8 +5,7 @@ quickfort :summary: Apply pre-designed blueprints to your fort. :tags: fort design productivity buildings map stockpiles -Quickfort reads blueprint files stored in the ``blueprints`` subfolder under the -main DF game folder and applies the blueprint of your choice to the game map. +Quickfort reads stored blueprint files and applies them to the game map. You can apply blueprints that designate digging, build buildings, place stockpiles, mark zones, and/or configure settings. If you find yourself spending time doing similar or repetitive things in your forts, this tool can be an @@ -67,8 +66,7 @@ Usage ``quickfort set`` to show current settings. See the Configuration_ section below for available keys and values. ``quickfort reset`` - Resets quickfort configuration to the defaults in - :file:`dfhack-config/quickfort/quickfort.txt`. + Resets quickfort configuration to defaults. ```` is one of: @@ -170,46 +168,30 @@ cursor, regardless of how the blueprint is rotated or flipped. Configuration ------------- -The quickfort script reads its global configuration from the -:file:`dfhack-config/quickfort/quickfort.txt` file, which you can customize. The -settings may be dynamically modified by the ``quickfort set`` command for the -current session, but settings changed with the ``quickfort set`` command will -not change the configuration stored in the file: - -``blueprints_dir`` (default: ``blueprints``) - Directory tree to search for blueprints. Can be set to an absolute or - relative path. If set to a relative path, resolves to a directory under the - DF folder. Note that if you change this directory, you will not see - blueprints written by the DFHack `blueprint` plugin (which always writes to - the ``blueprints`` dir) or blueprints in the quickfort blueprint library. +The quickfort script has a few global configuration options that you can +customize with the ``quickfort set`` command. Modified settings are only kept +for the current session and will be reset when you restart DF. + +``blueprints_user_dir`` (default: ``dfhack-config/blueprints``) + Directory tree to search for player-created blueprints. It can be set to an + absolute or relative path. If set to a relative path, it resolves to a + directory under the DF folder. Note that if you change this directory, you + will not see blueprints written by the DFHack `blueprint` plugin (which + always writes to the ``dfhack-config/blueprints`` dir). +``blueprints_library_dir`` (default: ``hack/data/blueprints``) + Directory tree to search for library blueprints. ``force_marker_mode`` (default: ``false``) If true, will designate all dig blueprints in marker mode. If false, only cells with dig codes explicitly prefixed with ``m`` will be designated in marker mode. -``query_unsafe`` (default: ``false``) - Skip ``query`` blueprint sanity checks that detect common blueprint errors - and halt or skip keycode playback. Checks include ensuring a configurable - building exists at the designated cursor position and verifying the active - UI screen is the same before and after sending keys for the cursor - position. If you find you need to enable this for one of your own - blueprints, you should probably be using a - `config blueprint `, not a ``query`` blueprint. - Most players will never need to enable this setting. ``stockpiles_max_barrels``, ``stockpiles_max_bins``, and ``stockpiles_max_wheelbarrows`` (defaults: ``-1``, ``-1``, ``0``) Set to the maximum number of resources you want assigned to stockpiles of the relevant types. Set to ``-1`` for DF defaults (number of stockpile tiles for stockpiles that take barrels and bins, and 1 wheelbarrow for stone stockpiles). The default here for wheelbarrows is ``0`` since using - wheelbarrows can *decrease* the efficiency of your fort unless you know how - to use them properly. Blueprints can `override ` - this value for specific stockpiles. - -There is one other configuration file in the ``dfhack-config/quickfort`` folder: -:source:`aliases.txt `. It defines keycode -shortcuts for query blueprints. The format for this file is described in the -`quickfort-alias-guide`, and default aliases that all players can use and build -on are available in the `quickfort-alias-library`. Some quickfort library -aliases require the `search-plugin` plugin to be enabled. + wheelbarrows can *decrease* the efficiency of your fort unless you assign + an appropriate number of wheelbarrows to the stockpile. Blueprints can + `override ` this value for specific stockpiles. API --- diff --git a/gui/blueprint.lua b/gui/blueprint.lua index 74bcb0580e..78cee5ca56 100644 --- a/gui/blueprint.lua +++ b/gui/blueprint.lua @@ -111,7 +111,8 @@ function NamePanel:detect_name_collision() local suffix_pos = #name + 1 - local paths = dfhack.filesystem.listdir_recursive('blueprints', nil, false) + local paths = dfhack.filesystem.listdir_recursive('dfhack-config/blueprints', nil, false) + if not paths then return false end for _,v in ipairs(paths) do if (v.isdir and v.path..'/' == name) or (v.path:startswith(name) and diff --git a/internal/quickfort/list.lua b/internal/quickfort/list.lua index 88541b3cda..0d483f837b 100644 --- a/internal/quickfort/list.lua +++ b/internal/quickfort/list.lua @@ -12,9 +12,13 @@ local quickfort_set = reqscript('internal/quickfort/set') -- blueprint_name is relative to the blueprints dir function get_blueprint_filepath(blueprint_name) - return string.format("%s/%s", - quickfort_set.get_setting('blueprints_dir'), - blueprint_name) + local is_library = blueprint_name:startswith('library/') + if is_library then blueprint_name = blueprint_name:sub(9) end + return ('%s/%s'):format( + is_library and + quickfort_set.get_setting('blueprints_library_dir') or + quickfort_set.get_setting('blueprints_user_dir'), + blueprint_name) end local blueprint_cache = {} @@ -88,60 +92,55 @@ end local blueprints, blueprint_modes, file_scope_aliases = {}, {}, {} local num_library_blueprints = 0 -local function scan_blueprints() - local bp_dir = quickfort_set.get_setting('blueprints_dir') +local function scan_blueprint_dir(bp_dir, is_library) local paths = dfhack.filesystem.listdir_recursive(bp_dir, nil, false) if not paths then - qerror(('Cannot find blueprints directory: "%s". If you have moved' .. - ' your blueprints to another directory, please update' .. - ' "dfhack-config/quickfort/quickfort.txt" with the new' .. - ' location and run "quickfort reset".'):format(bp_dir)) + dfhack.printerr(('Cannot find blueprints directory: "%s"'):format(bp_dir)) + return end - blueprints, blueprint_modes, file_scope_aliases = {}, {}, {} - local library_blueprints = {} for _, v in ipairs(paths) do - local is_library = string.find(v.path, '^library/') ~= nil - local target_list = blueprints - if is_library then target_list = library_blueprints end local file_aliases = {} - if not v.isdir and string.find(v.path:lower(), '[.]csv$') then - local modelines, aliases = scan_csv_blueprint(v.path) + local path = (is_library and 'library/' or '') .. v.path + if not v.isdir and v.path:lower():endswith('.csv') then + local modelines, aliases = scan_csv_blueprint(path) file_aliases = aliases local first = true for _,modeline in ipairs(modelines) do - table.insert(target_list, - {path=v.path, modeline=modeline, is_library=is_library}) + table.insert(blueprints, + {path=path, modeline=modeline, is_library=is_library}) + if is_library then num_library_blueprints = num_library_blueprints + 1 end local key = make_blueprint_modes_key( - v.path, get_section_name(nil, modeline.label)) + path, get_section_name(nil, modeline.label)) blueprint_modes[key] = modeline.mode if first then -- first blueprint is also accessible via blank name - key = make_blueprint_modes_key(v.path) + key = make_blueprint_modes_key(path) blueprint_modes[key] = modeline.mode first = false end end - elseif not v.isdir and string.find(v.path:lower(), '[.]xlsx$') then - local sheet_infos = scan_xlsx_blueprint(v.path) + elseif not v.isdir and v.path:lower():endswith('.xlsx') then + local sheet_infos = scan_xlsx_blueprint(path) file_aliases = sheet_infos.aliases local first = true if #sheet_infos > 0 then for _,sheet_info in ipairs(sheet_infos) do local sheet_first = true for _,modeline in ipairs(sheet_info.modelines) do - table.insert(target_list, - {path=v.path, + table.insert(blueprints, + {path=path, sheet_name=sheet_info.name, modeline=modeline, is_library=is_library}) + if is_library then num_library_blueprints = num_library_blueprints + 1 end local key = make_blueprint_modes_key( - v.path, + path, get_section_name(sheet_info.name, modeline.label)) blueprint_modes[key] = modeline.mode if first then -- first blueprint in first sheet is also accessible -- via blank name - key = make_blueprint_modes_key(v.path) + key = make_blueprint_modes_key(path) blueprint_modes[key] = modeline.mode first = false end @@ -149,7 +148,7 @@ local function scan_blueprints() -- first blueprint in each sheet is also accessible -- via blank label key = make_blueprint_modes_key( - v.path, get_section_name(sheet_info.name)) + path, get_section_name(sheet_info.name)) blueprint_modes[key] = modeline.mode sheet_first = false end @@ -157,15 +156,17 @@ local function scan_blueprints() end end end - file_scope_aliases[normalize_path(v.path)] = file_aliases - end - -- tack library files on to the end so user files are contiguous - num_library_blueprints = #library_blueprints - for i=1, num_library_blueprints do - blueprints[#blueprints + 1] = library_blueprints[i] + file_scope_aliases[normalize_path(path)] = file_aliases end end +local function scan_blueprints() + blueprints, blueprint_modes, file_scope_aliases = {}, {}, {} + num_library_blueprints = 0 + scan_blueprint_dir(quickfort_set.get_setting('blueprints_user_dir'), false) + scan_blueprint_dir(quickfort_set.get_setting('blueprints_library_dir'), true) +end + function get_blueprint_by_number(list_num) scan_blueprints() list_num = tonumber(list_num) diff --git a/internal/quickfort/set.lua b/internal/quickfort/set.lua index 9e050a6e92..8bc33df5b5 100644 --- a/internal/quickfort/set.lua +++ b/internal/quickfort/set.lua @@ -5,17 +5,10 @@ if not dfhack_flags.module then qerror('this script cannot be called directly') end -local quickfort_reader = reqscript('internal/quickfort/reader') - -local config_file = 'dfhack-config/quickfort/quickfort.txt' - --- keep deprecated settings in the table so we don't break existing configs local settings = { - blueprints_dir={default_value='blueprints'}, - buildings_use_blocks={default_value=false, deprecated=true}, - force_interactive_build={default_value=false, deprecated=true}, + blueprints_user_dir={default_value='dfhack-config/blueprints'}, + blueprints_library_dir={default_value='hack/data/blueprints'}, force_marker_mode={default_value=false}, - query_unsafe={default_value=false}, stockpiles_max_barrels={default_value=-1}, stockpiles_max_bins={default_value=-1}, stockpiles_max_wheelbarrows={default_value=0}, @@ -59,44 +52,13 @@ local function set_setting(key, value) settings[key].value = value end -local function read_settings(reader) - local line = reader:get_next_row() - while line do - local _, _, key, value = string.find(line, '^%s*([%a_]+)%s*=%s*(%S.*)') - if (key) then - set_setting(key, value) - end - line = reader:get_next_row() - end -end - -local function reset_to_defaults() - for _,v in pairs(settings) do - v.value = nil - end -end - -local function reset_settings(get_reader_fn) - local reader = nil - local init_reader = function() reader = get_reader_fn() end - reset_to_defaults() - local ok, err = pcall(init_reader) - if ok then - read_settings(reader) - else - print(string.format('%s; using internal defaults', tostring(err))) - end -end - local function print_settings() print('active settings:') local width = 1 local settings_arr = {} for k,v in pairs(settings) do - if not v.deprecated then - if #k > width then width = #k end - table.insert(settings_arr, k) - end + if #k > width then width = #k end + table.insert(settings_arr, k) end table.sort(settings_arr) for _, k in ipairs(settings_arr) do @@ -114,16 +76,9 @@ function do_set(args) end function do_reset() - local get_reader_fn = function() - return quickfort_reader.TextReader{filepath=config_file} + for _,v in pairs(settings) do + v.value = nil end - reset_settings(get_reader_fn) -end - -if not initialized and not dfhack.internal.IN_TEST then - -- this is the first time we're initializing the environment - do_reset() - initialized = true end if dfhack.internal.IN_TEST then @@ -131,8 +86,6 @@ if dfhack.internal.IN_TEST then settings=settings, get_setting=get_setting, set_setting=set_setting, - read_settings=read_settings, - reset_to_defaults=reset_to_defaults, - reset_settings=reset_settings, + reset_to_defaults=do_reset, } end From 218cf10be34fd6c7a6684747b760143f0fc35b63 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 10 Mar 2023 17:59:55 -0800 Subject: [PATCH 019/732] missed a reference --- docs/quickfort.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickfort.rst b/docs/quickfort.rst index 58f1a7b071..4d3467d4ac 100644 --- a/docs/quickfort.rst +++ b/docs/quickfort.rst @@ -38,7 +38,7 @@ Usage Lists blueprints in the ``blueprints`` folder. Blueprints are ``.csv`` files or sheets within ``.xlsx`` files that contain a ``#`` comment in the upper-left cell (please see `quickfort-blueprint-guide` for more information - on modes). By default, blueprints in the ``blueprints/library/`` subfolder + on modes). By default, library blueprints in the ``hack/data/blueprints/`` subfolder are included and blueprints that contain a ``hidden()`` marker in their modeline are excluded from the returned list. Specify ``-u`` or ``-h`` to exclude library or include hidden blueprints, respectively. The list can From e1b3d015b32ac534cefcb17a8738152c762dd6dd Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Tue, 7 Mar 2023 22:18:08 +0100 Subject: [PATCH 020/732] New script: suspendmanager --- changelog.txt | 1 + docs/gui/suspendmanager.rst | 16 +++++ docs/suspend.rst | 32 +++++++++ docs/suspendmanager.rst | 25 +++++++ docs/unsuspend.rst | 9 +++ gui/control-panel.lua | 2 +- gui/mass-remove.lua | 9 +-- gui/suspendmanager.lua | 58 ++++++++++++++++ suspend.lua | 119 +++++++++++++++++++++++++++++++++ suspendmanager.lua | 127 ++++++++++++++++++++++++++++++++++++ unsuspend.lua | 26 ++++++-- 11 files changed, 409 insertions(+), 15 deletions(-) create mode 100644 docs/gui/suspendmanager.rst create mode 100644 docs/suspend.rst create mode 100644 docs/suspendmanager.rst create mode 100644 gui/suspendmanager.lua create mode 100644 suspend.lua create mode 100644 suspendmanager.lua diff --git a/changelog.txt b/changelog.txt index ed8906be76..e621acb5ca 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,6 +14,7 @@ that repo. # Future ## New Scripts +- `suspendmanager`: Automatic job suspension management (replaces `autounsuspend`) ## Fixes diff --git a/docs/gui/suspendmanager.rst b/docs/gui/suspendmanager.rst new file mode 100644 index 0000000000..7ff9dbb3c1 --- /dev/null +++ b/docs/gui/suspendmanager.rst @@ -0,0 +1,16 @@ +gui/suspendmanager +================== + +.. dfhack-tool:: + :summary: Intelligently suspend and unsuspend jobs. + :tags: fort auto jobs + +This is the graphical configuration interface for the `suspendmanager` +automation tool. + +Usage +===== + +:: + + gui/suspendmanager diff --git a/docs/suspend.rst b/docs/suspend.rst new file mode 100644 index 0000000000..e064c6a458 --- /dev/null +++ b/docs/suspend.rst @@ -0,0 +1,32 @@ +suspend +======= + +.. dfhack-tool:: + :summary: Suspends building construction jobs. + :tags: fort productivity jobs + +This tool will suspend jobs. It can either suspend all the current jobs, +or only construction jobs that are likely to block other jobs. When building walls, +it's common that wall corners get stuck because dwarves build the two adjacent +walls before the corner. The ``--onlyblocking`` option will only suspend jobs +that can potentially lead to this situation. + +Usage +----- + +:: + + suspend + +Options +------- + +``-b``, ``--onlyblocking`` + Only suspend jobs that are likely to block other jobs. + +.. note:: + + ``--onlyblocking`` does not check pathing (which would be very expensive); it only + looks at immediate neighbours. As such, it is possible that this tool will miss + suspending some jobs that prevent access to other farther away jobs, for example + when building a large rectangle of solid walls. diff --git a/docs/suspendmanager.rst b/docs/suspendmanager.rst new file mode 100644 index 0000000000..c4b70f46f0 --- /dev/null +++ b/docs/suspendmanager.rst @@ -0,0 +1,25 @@ +suspendmanager +============== + +.. dfhack-tool:: + :summary: Intelligently suspend and unsuspend jobs. + :tags: fort auto jobs + +This tool will watch your active jobs and: + +- unsuspend jobs that have become suspended due to inaccessible materials, + items temporarily in the way, or worker dwarves getting scared by wildlife +- suspend construction jobs that would prevent a dwarf from reaching an adjacent + construction job, such as when building a wall corner. + +Usage +----- + +``suspendmanager`` + Display the current status + +``suspendmanager (enable|disable)`` + Enable or disable ``suspendmanager`` + +``suspendmanager set preventblocking (true|false)`` + Prevent construction jobs from blocking each others (enabled by default). See `suspend`. diff --git a/docs/unsuspend.rst b/docs/unsuspend.rst index 867ce5d6e0..5bb6d9d240 100644 --- a/docs/unsuspend.rst +++ b/docs/unsuspend.rst @@ -19,6 +19,15 @@ Usage unsuspend +Options +------- + +``-q``, ``--quiet`` + Disable text output + +``-s``, ``--skipblocking`` + Don't unsuspend construction jobs that risk blocking other jobs + Overlay ------- diff --git a/gui/control-panel.lua b/gui/control-panel.lua index ca8cd27ed4..da204a6cda 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -20,7 +20,6 @@ local FORT_SERVICES = { 'autolabor', 'autonestbox', 'autoslab', - 'autounsuspend', 'emigration', 'fastdwarf', 'fix/protect-nicks', @@ -30,6 +29,7 @@ local FORT_SERVICES = { 'prioritize', 'seedwatch', 'starvingdead', + 'suspendmanager', 'tailor', } diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 54655198ef..2af472e6cd 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -4,6 +4,7 @@ local gui = require('gui') local guidm = require('gui.dwarfmode') local utils = require('utils') local widgets = require('gui.widgets') +local suspend = reqscript('suspend') local ok, buildingplan = pcall(require, 'plugins.buildingplan') if not ok then @@ -38,12 +39,6 @@ local function unremove_construction(pos, grid) if job then dfhack.job.removeJob(job) end end -local function suspend(job) - job.flags.suspend = true - job.flags.working = false - dfhack.job.removeWorker(job, 0) -end - local function unsuspend(job) job.flags.suspend = false end @@ -146,7 +141,7 @@ function MassRemove:init() key_back='CUSTOM_SHIFT_X', options={ {label='Leave alone', value=function() end}, - {label='Suspend', value=suspend}, + {label='Suspend', value=suspend.suspend}, {label='Unsuspend', value=unsuspend}, }, }, diff --git a/gui/suspendmanager.lua b/gui/suspendmanager.lua new file mode 100644 index 0000000000..aece24cd58 --- /dev/null +++ b/gui/suspendmanager.lua @@ -0,0 +1,58 @@ +-- config ui for suspendmanager + +local gui = require("gui") +local widgets = require("gui.widgets") +local suspendmanager = reqscript("suspendmanager") + +--- +-- Suspendmanager +--- + +SuspendmanagerWindow = defclass(SuspendmanagerWindow, widgets.Window) +SuspendmanagerWindow.ATTRS{ + frame_title = "Suspendmanager", + frame = {w=38, h=11}, +} + +function SuspendmanagerWindow:init() + self:addviews{ + widgets.ToggleHotkeyLabel{ + frame={t=0, l=0, w=34}, + label="Suspendmanager is", + key="CUSTOM_CTRL_E", + options={{value=true, label="Enabled", pen=COLOR_GREEN}, + {value=false, label="Disabled", pen=COLOR_RED}}, + on_change=function(val) dfhack.run_command{val and "enable" or "disable", "suspendmanager"} end + }, + widgets.ToggleHotkeyLabel{ + frame={t=2, l=0, w=33}, + label="Prevent blocking jobs:", + key="CUSTOM_ALT_B", + options={{value=true, label="Yes", pen=COLOR_GREEN}, + {value=false, label="No", pen=COLOR_RED}}, + on_change=function(val) + suspendmanager.set_prevent_blocking(val) + end + }, + + } +end + +--- +-- SuspendmanagerScreen +--- +SuspendmanagerScreen = defclass(SuspendmanagerScreen, gui.ZScreen) +SuspendmanagerScreen.ATTRS{focus_path = "suspendmanager"} + +function SuspendmanagerScreen:init() + self:addviews{SuspendmanagerWindow{}} +end + +function SuspendmanagerScreen:onDismiss() + view = nil +end +if not dfhack.isMapLoaded then + qerror("suspendmanager requires a map to be loaded") +end + +view = view and view:raise() or SuspendmanagerScreen{}:show() diff --git a/suspend.lua b/suspend.lua new file mode 100644 index 0000000000..c4aa573484 --- /dev/null +++ b/suspend.lua @@ -0,0 +1,119 @@ +-- Suspend jobs +--@ module = true + +-- It can either suspend all jobs, or just jobs that risk blocking others. + +local utils = require('utils') +local argparse = require('argparse') + +function suspend(job) + job.flags.suspend = true + job.flags.working = false + dfhack.job.removeWorker(job, 0) +end + +--- True if there is a construction plan to build an unwalkable tile +---@param pos coord +---@return boolean +local function plansToConstructImpassableAt(pos) + --- @type building_constructionst|building + local building = dfhack.buildings.findAtTile(pos) + if not building then return false end + if building.flags.exists then + -- The building is already created + return false + end + return building:isImpassableAtCreation() +end + +--- Check if the tile can be walked on +---@param pos coord +local function walkable(pos) + local tt = dfhack.maps.getTileType(pos) + if not tt then + return false + end + local attrs = df.tiletype.attrs[tt] + local shape_attrs = df.tiletype_shape.attrs[attrs.shape] + return shape_attrs.walkable +end + +--- List neighbour coordinates of a position +---@param pos coord +---@return table +local function neighbours(pos) + return { + {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x+1, y=pos.y, z=pos.z}, + {x=pos.x, y=pos.y-1, z=pos.z}, + {x=pos.x, y=pos.y+1, z=pos.z}, + } +end + + +--- Get the amount of risk a tile is to be blocked +--- -1: There is a nearby walkable area with no plan to build a wall +--- >=0: Surrounded by either unwalkable tiles, or tiles that will be constructed +--- with unwalkable buildings. The value is the number of already unwalkable tiles. +---@param pos coord +local function riskOfStuckConstructionAt(pos) + local risk = 0 + for _,neighbourPos in pairs(neighbours(pos)) do + if not walkable(neighbourPos) then + -- blocked neighbour, increase danger + risk = risk + 1 + elseif not plansToConstructImpassableAt(neighbourPos) then + -- walkable neighbour with no plan to build a wall, no danger + return -1 + end + end + return risk +end + +--- Return true if this job is at risk of blocking another one +function isBlocking(job) + -- Not a construction job, no risk + if job.job_type ~= df.job_type.ConstructBuilding then return false end + + local building = dfhack.job.getHolder(job) + --- Not building a blocking construction, no risk + if not building or not building:isImpassableAtCreation() then return false end + + --- job.pos is sometimes off by one, get the building pos + local pos = {x=building.centerx,y=building.centery,z=building.z} + + --- Get self risk of being blocked + local risk = riskOfStuckConstructionAt(pos) + + for _,neighbourPos in pairs(neighbours(pos)) do + if plansToConstructImpassableAt(neighbourPos) and riskOfStuckConstructionAt(neighbourPos) > risk then + --- This neighbour job is at greater risk of getting stuck + return true + end + end + + return false +end + +local function main(args) + local help, onlyblocking = false, false + argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() help = true end}, + {'b', 'onlyblocking', handler=function() onlyblocking = true end}, + }) + + if help then + print(dfhack.script_help()) + return + end + + for _,job in utils.listpairs(df.global.world.jobs.list) do + if not onlyblocking or isBlocking(job) then + suspend(job) + end + end +end + +if not dfhack_flags.module then + main({...}) +end diff --git a/suspendmanager.lua b/suspendmanager.lua new file mode 100644 index 0000000000..334f68bcee --- /dev/null +++ b/suspendmanager.lua @@ -0,0 +1,127 @@ +-- Avoid suspended jobs and creating unreachable jobs +--@module = true +--@enable = true + +local json = require('json') +local persist = require('persist-table') +local argparse = require('argparse') +local eventful = require('plugins.eventful') +local repeatUtil = require('repeat-util') + +local GLOBAL_KEY = 'suspendmanager' -- used for state change hooks and persistence + +enabled = enabled or false +prevent_blocking = prevent_blocking == nil and true or prevent_blocking + +eventful.enableEvent(eventful.eventType.JOB_INITIATED, 10) +eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 10) + +function isEnabled() + return enabled +end + +function preventBlockingEnabled() + return prevent_blocking +end + +local function persist_state() + persist.GlobalTable[GLOBAL_KEY] = json.encode({ + enabled=enabled, + prevent_blocking=prevent_blocking, + }) +end + +function set_prevent_blocking(enable) + prevent_blocking = enable + persist_state() +end + +local function run_now() + if prevent_blocking then + dfhack.run_script('suspend', '--onlyblocking') + dfhack.run_script('unsuspend', '--quiet', '--skipblocking') + else + dfhack.run_script('unsuspend', '--quiet') + end +end + +--- @param job job +local function on_job_change(job) + if prevent_blocking then + -- Note: this method could be made more efficient by running a single loop + -- on the jobs, or even take in account the changed job + run_now() + end +end + +local function update_triggers() + if enabled then + eventful.onJobInitiated[GLOBAL_KEY] = on_job_change + eventful.onJobCompleted[GLOBAL_KEY] = on_job_change + repeatUtil.scheduleEvery(GLOBAL_KEY, 1, "days", run_now) + else + eventful.onJobInitiated[GLOBAL_KEY] = nil + eventful.onJobCompleted[GLOBAL_KEY] = nil + repeatUtil.cancel(GLOBAL_KEY) + end +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + enabled = false + return + end + + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') + enabled = (persisted_data or {enabled=false})['enabled'] + prevent_blocking = (persisted_data or {prevent_blocking=true})['prevent_blocking'] + update_triggers() +end + +local function main(args) + if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then + dfhack.printerr('suspendmanager needs a loaded fortress map to work') + return + end + + if dfhack_flags and dfhack_flags.enable then + args = {dfhack_flags.enable_state and 'enable' or 'disable'} + end + + local help = false + local command = argparse.processArgsGetopt(args, { + {"h", "help", handler=function() help = true end}, + {"b", "preventblocking", handler=function() set_prevent_blocking(true) end}, + {"n", "nopreventblocking", handler=function() set_prevent_blocking(false) end} + })[1] + + if help or command == "help" then + print(dfhack.script_help()) + return + elseif command == "enable" then + run_now() + enabled = true + elseif command == "disable" then + enabled = false + elseif command == nil then + print(string.format("suspendmanager is currently %s", (enabled and "enabled" or "disabled"))) + if prevent_blocking then + print("It is configured to prevent construction jobs from blocking each others") + else + print("It is configured to unsuspend all jobs") + end + else + return + end + + persist_state() + update_triggers() +end + +if not dfhack_flags.module then + main({...}) +end diff --git a/unsuspend.lua b/unsuspend.lua index 249a235360..a1e99fb37b 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -3,6 +3,8 @@ local guidm = require('gui.dwarfmode') local utils = require('utils') +local argparse = require('argparse') +local suspend = reqscript('suspend') local overlay = require('plugins.overlay') @@ -184,7 +186,13 @@ if dfhack_flags.module then return end -local unsuspended_count, flow_count, buildingplan_count = 0, 0, 0 +local quiet, skipblocking = false, false +argparse.processArgsGetopt({...}, { + {'q', 'quiet', handler=function() quiet = true end}, + {'s', 'skipblocking', handler=function() skipblocking = true end}, +}) + +local unsuspended_count, flow_count, buildingplan_count, blocking_count = 0, 0, 0, 0 foreach_construction_job(function(job) if not job.flags.suspend then return end @@ -197,19 +205,23 @@ foreach_construction_job(function(job) buildingplan_count = buildingplan_count + 1 return end + if skipblocking and suspend.isBlocking(job) then + blocking_count = blocking_count + 1 + return + end job.flags.suspend = false unsuspended_count = unsuspended_count + 1 end) -local opts = utils.invert{...} -local quiet = opts['-q'] or opts['--quiet'] - -if flow_count > 0 then +if not quiet and flow_count > 0 then print(string.format('Not unsuspending %d underwater job(s)', flow_count)) end -if buildingplan_count > 0 then +if not quiet and buildingplan_count > 0 then print(string.format('Not unsuspending %d buildingplan job(s)', buildingplan_count)) end -if unsuspended_count > 0 or not quiet then +if not quiet and blocking_count > 0 then + print(string.format('Not unsuspending %d blocking job(s)', blocking_count)) +end +if not quiet and unsuspended_count > 0 then print(string.format('Unsuspended %d job(s).', unsuspended_count)) end From 19737bbe9a24cdeb9ecf9170c2abb94c07d4856c Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Tue, 7 Mar 2023 22:31:52 +0100 Subject: [PATCH 021/732] Fix initial status when opening gui --- gui/suspendmanager.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/suspendmanager.lua b/gui/suspendmanager.lua index aece24cd58..ca42d1bef1 100644 --- a/gui/suspendmanager.lua +++ b/gui/suspendmanager.lua @@ -22,6 +22,7 @@ function SuspendmanagerWindow:init() key="CUSTOM_CTRL_E", options={{value=true, label="Enabled", pen=COLOR_GREEN}, {value=false, label="Disabled", pen=COLOR_RED}}, + initial_option = suspendmanager.isEnabled(), on_change=function(val) dfhack.run_command{val and "enable" or "disable", "suspendmanager"} end }, widgets.ToggleHotkeyLabel{ @@ -30,6 +31,7 @@ function SuspendmanagerWindow:init() key="CUSTOM_ALT_B", options={{value=true, label="Yes", pen=COLOR_GREEN}, {value=false, label="No", pen=COLOR_RED}}, + initial_option = suspendmanager.preventBlockingEnabled(), on_change=function(val) suspendmanager.set_prevent_blocking(val) end From eb1a6ebc1c11e77b08c1167bf8f5d7cd6982131d Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 13 Mar 2023 10:19:30 +0100 Subject: [PATCH 022/732] Single loop, move reusable in separate module --- gui/mass-remove.lua | 6 +- .../suspendmanager/suspendmanager-utils.lua | 137 ++++++++++++++++++ suspend.lua | 99 +------------ suspendmanager.lua | 19 ++- unsuspend.lua | 55 +++---- 5 files changed, 176 insertions(+), 140 deletions(-) create mode 100644 internal/suspendmanager/suspendmanager-utils.lua diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 2af472e6cd..29935ef6c2 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -4,7 +4,7 @@ local gui = require('gui') local guidm = require('gui.dwarfmode') local utils = require('utils') local widgets = require('gui.widgets') -local suspend = reqscript('suspend') +local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') local ok, buildingplan = pcall(require, 'plugins.buildingplan') if not ok then @@ -141,8 +141,8 @@ function MassRemove:init() key_back='CUSTOM_SHIFT_X', options={ {label='Leave alone', value=function() end}, - {label='Suspend', value=suspend.suspend}, - {label='Unsuspend', value=unsuspend}, + {label='Suspend', value=suspendmanagerUtils.suspend}, + {label='Unsuspend', value=suspendmanagerUtils.unsuspend}, }, }, } diff --git a/internal/suspendmanager/suspendmanager-utils.lua b/internal/suspendmanager/suspendmanager-utils.lua new file mode 100644 index 0000000000..9109b5de82 --- /dev/null +++ b/internal/suspendmanager/suspendmanager-utils.lua @@ -0,0 +1,137 @@ +-- Reusable functions for job suspension management +--@ module = true + +local utils = require('utils') + +local ok, buildingplan = pcall(require, 'plugins.buildingplan') +if not ok then + buildingplan = nil +end + +--- Suspend a job +---@param job job +function suspend(job) + job.flags.suspend = true + job.flags.working = false + dfhack.job.removeWorker(job, 0) +end + +--- Unsuspend a job +---@param job job +function unsuspend(job) + job.flags.suspend = false +end + +--- Loop over all the construction jobs +---@param fn function A function taking a job as argument +function foreach_construction_job(fn) + for _,job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.ConstructBuilding then + fn(job) + end + end +end + +--- True if there is a construction plan to build an unwalkable tile +---@param pos coord +---@return boolean +local function plansToConstructImpassableAt(pos) + --- @type building_constructionst|building + local building = dfhack.buildings.findAtTile(pos) + if not building then return false end + if building.flags.exists then + -- The building is already created + return false + end + return building:isImpassableAtCreation() +end + +--- Check if the tile can be walked on +---@param pos coord +local function walkable(pos) + local tt = dfhack.maps.getTileType(pos) + if not tt then + return false + end + local attrs = df.tiletype.attrs[tt] + local shape_attrs = df.tiletype_shape.attrs[attrs.shape] + return shape_attrs.walkable +end + +--- List neighbour coordinates of a position +---@param pos coord +---@return table +local function neighbours(pos) + return { + {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x+1, y=pos.y, z=pos.z}, + {x=pos.x, y=pos.y-1, z=pos.z}, + {x=pos.x, y=pos.y+1, z=pos.z}, + } +end + + +--- Get the amount of risk a tile is to be blocked +--- -1: There is a nearby walkable area with no plan to build a wall +--- >=0: Surrounded by either unwalkable tiles, or tiles that will be constructed +--- with unwalkable buildings. The value is the number of already unwalkable tiles. +---@param pos coord +local function riskOfStuckConstructionAt(pos) + local risk = 0 + for _,neighbourPos in pairs(neighbours(pos)) do + if not walkable(neighbourPos) then + -- blocked neighbour, increase danger + risk = risk + 1 + elseif not plansToConstructImpassableAt(neighbourPos) then + -- walkable neighbour with no plan to build a wall, no danger + return -1 + end + end + return risk +end + +--- Return true if this job is at risk of blocking another one +function isBlocking(job) + -- Not a construction job, no risk + if job.job_type ~= df.job_type.ConstructBuilding then return false end + + local building = dfhack.job.getHolder(job) + --- Not building a blocking construction, no risk + if not building or not building:isImpassableAtCreation() then return false end + + --- job.pos is sometimes off by one, get the building pos + local pos = {x=building.centerx,y=building.centery,z=building.z} + + --- Get self risk of being blocked + local risk = riskOfStuckConstructionAt(pos) + + for _,neighbourPos in pairs(neighbours(pos)) do + if plansToConstructImpassableAt(neighbourPos) and riskOfStuckConstructionAt(neighbourPos) > risk then + --- This neighbour job is at greater risk of getting stuck + return true + end + end + + return false +end + +--- Return true with a reason if a job should be suspended. +--- It takes in account water flow, buildingplan plugin, and optionally +--- the risk of creating stuck construction buildings +--- @param job job +--- @param accountblocking boolean +function shouldBeSuspended(job, accountblocking) + if dfhack.maps.getTileFlags(job.pos).flow_size > 1 then + return true, 'underwater' + end + + local bld = dfhack.buildings.findAtTile(job.pos) + if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then + return true, 'buildingplan' + end + + if accountblocking and isBlocking(job) then + return true, 'blocking' + end + return false, nil +end diff --git a/suspend.lua b/suspend.lua index c4aa573484..3d4b1b19d3 100644 --- a/suspend.lua +++ b/suspend.lua @@ -3,97 +3,8 @@ -- It can either suspend all jobs, or just jobs that risk blocking others. -local utils = require('utils') local argparse = require('argparse') - -function suspend(job) - job.flags.suspend = true - job.flags.working = false - dfhack.job.removeWorker(job, 0) -end - ---- True if there is a construction plan to build an unwalkable tile ----@param pos coord ----@return boolean -local function plansToConstructImpassableAt(pos) - --- @type building_constructionst|building - local building = dfhack.buildings.findAtTile(pos) - if not building then return false end - if building.flags.exists then - -- The building is already created - return false - end - return building:isImpassableAtCreation() -end - ---- Check if the tile can be walked on ----@param pos coord -local function walkable(pos) - local tt = dfhack.maps.getTileType(pos) - if not tt then - return false - end - local attrs = df.tiletype.attrs[tt] - local shape_attrs = df.tiletype_shape.attrs[attrs.shape] - return shape_attrs.walkable -end - ---- List neighbour coordinates of a position ----@param pos coord ----@return table -local function neighbours(pos) - return { - {x=pos.x-1, y=pos.y, z=pos.z}, - {x=pos.x+1, y=pos.y, z=pos.z}, - {x=pos.x, y=pos.y-1, z=pos.z}, - {x=pos.x, y=pos.y+1, z=pos.z}, - } -end - - ---- Get the amount of risk a tile is to be blocked ---- -1: There is a nearby walkable area with no plan to build a wall ---- >=0: Surrounded by either unwalkable tiles, or tiles that will be constructed ---- with unwalkable buildings. The value is the number of already unwalkable tiles. ----@param pos coord -local function riskOfStuckConstructionAt(pos) - local risk = 0 - for _,neighbourPos in pairs(neighbours(pos)) do - if not walkable(neighbourPos) then - -- blocked neighbour, increase danger - risk = risk + 1 - elseif not plansToConstructImpassableAt(neighbourPos) then - -- walkable neighbour with no plan to build a wall, no danger - return -1 - end - end - return risk -end - ---- Return true if this job is at risk of blocking another one -function isBlocking(job) - -- Not a construction job, no risk - if job.job_type ~= df.job_type.ConstructBuilding then return false end - - local building = dfhack.job.getHolder(job) - --- Not building a blocking construction, no risk - if not building or not building:isImpassableAtCreation() then return false end - - --- job.pos is sometimes off by one, get the building pos - local pos = {x=building.centerx,y=building.centery,z=building.z} - - --- Get self risk of being blocked - local risk = riskOfStuckConstructionAt(pos) - - for _,neighbourPos in pairs(neighbours(pos)) do - if plansToConstructImpassableAt(neighbourPos) and riskOfStuckConstructionAt(neighbourPos) > risk then - --- This neighbour job is at greater risk of getting stuck - return true - end - end - - return false -end +local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') local function main(args) local help, onlyblocking = false, false @@ -107,11 +18,11 @@ local function main(args) return end - for _,job in utils.listpairs(df.global.world.jobs.list) do - if not onlyblocking or isBlocking(job) then - suspend(job) + suspendmanagerUtils.foreach_construction_job(function (job) + if not onlyblocking or suspendmanagerUtils.isBlocking(job) then + suspendmanagerUtils.suspend(job) end - end + end) end if not dfhack_flags.module then diff --git a/suspendmanager.lua b/suspendmanager.lua index 334f68bcee..29d369102c 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -7,6 +7,7 @@ local persist = require('persist-table') local argparse = require('argparse') local eventful = require('plugins.eventful') local repeatUtil = require('repeat-util') +local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') local GLOBAL_KEY = 'suspendmanager' -- used for state change hooks and persistence @@ -37,19 +38,21 @@ function set_prevent_blocking(enable) end local function run_now() - if prevent_blocking then - dfhack.run_script('suspend', '--onlyblocking') - dfhack.run_script('unsuspend', '--quiet', '--skipblocking') - else - dfhack.run_script('unsuspend', '--quiet') - end + suspendmanagerUtils.foreach_construction_job(function(job) + local shouldBeSuspended, _ = suspendmanagerUtils.shouldBeSuspended(job, prevent_blocking) + if shouldBeSuspended and not job.flags.suspend then + suspendmanagerUtils.suspend(job) + elseif not shouldBeSuspended and job.flags.suspend then + suspendmanagerUtils.unsuspend(job) + end + end) end --- @param job job local function on_job_change(job) if prevent_blocking then - -- Note: this method could be made more efficient by running a single loop - -- on the jobs, or even take in account the changed job + -- Note: This method could be made incremental by taking in account the + -- changed job run_now() end end diff --git a/unsuspend.lua b/unsuspend.lua index a1e99fb37b..90d4749d79 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -4,7 +4,7 @@ local guidm = require('gui.dwarfmode') local utils = require('utils') local argparse = require('argparse') -local suspend = reqscript('suspend') +local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') local overlay = require('plugins.overlay') @@ -13,14 +13,6 @@ if not ok then buildingplan = nil end -local function foreach_construction_job(fn) - for _,job in utils.listpairs(df.global.world.jobs.list) do - if job.job_type == df.job_type.ConstructBuilding then - fn(job) - end - end -end - SuspendOverlay = defclass(SuspendOverlay, overlay.OverlayWidget) SuspendOverlay.ATTRS{ viewscreens='dwarfmode', @@ -62,7 +54,7 @@ end function SuspendOverlay:overlay_onupdate() local added = false self.data_version = self.data_version + 1 - foreach_construction_job(function(job) + suspendmanagerUtils.foreach_construction_job(function(job) self:update_building(dfhack.job.getHolder(job).id, job) added = true end) @@ -180,6 +172,8 @@ function SuspendOverlay:onRenderFrame(dc) dc:map(false) end + + OVERLAY_WIDGETS = {overlay=SuspendOverlay} if dfhack_flags.module then @@ -192,36 +186,27 @@ argparse.processArgsGetopt({...}, { {'s', 'skipblocking', handler=function() skipblocking = true end}, }) -local unsuspended_count, flow_count, buildingplan_count, blocking_count = 0, 0, 0, 0 +local skipped_counts = {} +local unsuspended_count = 0 -foreach_construction_job(function(job) +suspendmanagerUtils.foreach_construction_job(function(job) if not job.flags.suspend then return end - if dfhack.maps.getTileFlags(job.pos).flow_size > 1 then - flow_count = flow_count + 1 - return - end - local bld = dfhack.buildings.findAtTile(job.pos) - if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then - buildingplan_count = buildingplan_count + 1 - return - end - if skipblocking and suspend.isBlocking(job) then - blocking_count = blocking_count + 1 + + local skip,reason=suspendmanagerUtils.shouldBeSuspended(job, skipblocking) + if skip then + skipped_counts[reason] = (skipped_counts[reason] or 0) + 1 return end - job.flags.suspend = false + suspendmanagerUtils.unsuspend(job) unsuspended_count = unsuspended_count + 1 end) -if not quiet and flow_count > 0 then - print(string.format('Not unsuspending %d underwater job(s)', flow_count)) -end -if not quiet and buildingplan_count > 0 then - print(string.format('Not unsuspending %d buildingplan job(s)', buildingplan_count)) -end -if not quiet and blocking_count > 0 then - print(string.format('Not unsuspending %d blocking job(s)', blocking_count)) -end -if not quiet and unsuspended_count > 0 then - print(string.format('Unsuspended %d job(s).', unsuspended_count)) +if not quiet then + for reason,count in pairs(skipped_counts) do + print(string.format('Not unsuspending %d %s job(s)', count, reason)) + end + + if unsuspended_count > 0 then + print(string.format('Unsuspended %d job(s).', unsuspended_count)) + end end From 9f23d0f36fea639e89ec11f7931b750a08371c9e Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 13 Mar 2023 11:19:56 +0100 Subject: [PATCH 023/732] Cleanup --- gui/suspendmanager.lua | 2 +- suspend.lua | 33 +++++++++++++-------------------- suspendmanager.lua | 40 +++++++++++++++++++++++++++------------- unsuspend.lua | 1 - 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/gui/suspendmanager.lua b/gui/suspendmanager.lua index ca42d1bef1..25b666a726 100644 --- a/gui/suspendmanager.lua +++ b/gui/suspendmanager.lua @@ -33,7 +33,7 @@ function SuspendmanagerWindow:init() {value=false, label="No", pen=COLOR_RED}}, initial_option = suspendmanager.preventBlockingEnabled(), on_change=function(val) - suspendmanager.set_prevent_blocking(val) + suspendmanager.update_setting("preventblocking", val) end }, diff --git a/suspend.lua b/suspend.lua index 3d4b1b19d3..dd5257f1d0 100644 --- a/suspend.lua +++ b/suspend.lua @@ -1,30 +1,23 @@ -- Suspend jobs ---@ module = true -- It can either suspend all jobs, or just jobs that risk blocking others. local argparse = require('argparse') local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') -local function main(args) - local help, onlyblocking = false, false - argparse.processArgsGetopt(args, { - {'h', 'help', handler=function() help = true end}, - {'b', 'onlyblocking', handler=function() onlyblocking = true end}, - }) +local help, onlyblocking = false, false +argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() help = true end}, + {'b', 'onlyblocking', handler=function() onlyblocking = true end}, +}) - if help then - print(dfhack.script_help()) - return - end - - suspendmanagerUtils.foreach_construction_job(function (job) - if not onlyblocking or suspendmanagerUtils.isBlocking(job) then - suspendmanagerUtils.suspend(job) - end - end) +if help then + print(dfhack.script_help()) + return end -if not dfhack_flags.module then - main({...}) -end +suspendmanagerUtils.foreach_construction_job(function (job) + if not onlyblocking or suspendmanagerUtils.isBlocking(job) then + suspendmanagerUtils.suspend(job) + end +end) diff --git a/suspendmanager.lua b/suspendmanager.lua index 29d369102c..76bc6e37ce 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -12,7 +12,7 @@ local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-ut local GLOBAL_KEY = 'suspendmanager' -- used for state change hooks and persistence enabled = enabled or false -prevent_blocking = prevent_blocking == nil and true or prevent_blocking +preventblocking = preventblocking == nil and true or preventblocking eventful.enableEvent(eventful.eventType.JOB_INITIATED, 10) eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 10) @@ -22,24 +22,36 @@ function isEnabled() end function preventBlockingEnabled() - return prevent_blocking + return preventblocking end local function persist_state() persist.GlobalTable[GLOBAL_KEY] = json.encode({ enabled=enabled, - prevent_blocking=prevent_blocking, + prevent_blocking=preventblocking, }) end -function set_prevent_blocking(enable) - prevent_blocking = enable +---@param setting string +---@param value string|boolean +function update_setting(setting, value) + if setting == "preventblocking" then + if (value == "true" or value == true) then + preventblocking = true + elseif (value == "false" or value == false) then + preventblocking = false + else + qerror(tostring(value) .. " is not a valid value for preventblocking, it must be true or false") + end + else + qerror(setting .. " is not a valid setting.") + end persist_state() end local function run_now() suspendmanagerUtils.foreach_construction_job(function(job) - local shouldBeSuspended, _ = suspendmanagerUtils.shouldBeSuspended(job, prevent_blocking) + local shouldBeSuspended, _ = suspendmanagerUtils.shouldBeSuspended(job, preventblocking) if shouldBeSuspended and not job.flags.suspend then suspendmanagerUtils.suspend(job) elseif not shouldBeSuspended and job.flags.suspend then @@ -50,7 +62,7 @@ end --- @param job job local function on_job_change(job) - if prevent_blocking then + if preventblocking then -- Note: This method could be made incremental by taking in account the -- changed job run_now() @@ -81,7 +93,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') enabled = (persisted_data or {enabled=false})['enabled'] - prevent_blocking = (persisted_data or {prevent_blocking=true})['prevent_blocking'] + preventblocking = (persisted_data or {prevent_blocking=true})['prevent_blocking'] update_triggers() end @@ -96,11 +108,10 @@ local function main(args) end local help = false - local command = argparse.processArgsGetopt(args, { + local positionals = argparse.processArgsGetopt(args, { {"h", "help", handler=function() help = true end}, - {"b", "preventblocking", handler=function() set_prevent_blocking(true) end}, - {"n", "nopreventblocking", handler=function() set_prevent_blocking(false) end} - })[1] + }) + local command = positionals[1] if help or command == "help" then print(dfhack.script_help()) @@ -110,14 +121,17 @@ local function main(args) enabled = true elseif command == "disable" then enabled = false + elseif command == "set" then + update_setting(positionals[2], positionals[3]) elseif command == nil then print(string.format("suspendmanager is currently %s", (enabled and "enabled" or "disabled"))) - if prevent_blocking then + if preventblocking then print("It is configured to prevent construction jobs from blocking each others") else print("It is configured to unsuspend all jobs") end else + qerror("Unknown command " .. command) return end diff --git a/unsuspend.lua b/unsuspend.lua index 90d4749d79..87be23ce25 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -2,7 +2,6 @@ --@module = true local guidm = require('gui.dwarfmode') -local utils = require('utils') local argparse = require('argparse') local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') From 1b635f816b759cccde3daf26a5eb65673194abd7 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 13 Mar 2023 11:39:51 +0100 Subject: [PATCH 024/732] Move back the logic to "suspendmanager" --- gui/mass-remove.lua | 6 +- .../suspendmanager/suspendmanager-utils.lua | 137 ----------------- suspend.lua | 10 +- suspendmanager.lua | 142 +++++++++++++++++- unsuspend.lua | 12 +- 5 files changed, 150 insertions(+), 157 deletions(-) delete mode 100644 internal/suspendmanager/suspendmanager-utils.lua diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 29935ef6c2..80c3e6fd8e 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -4,7 +4,7 @@ local gui = require('gui') local guidm = require('gui.dwarfmode') local utils = require('utils') local widgets = require('gui.widgets') -local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') +local suspendmanager = reqscript('suspendmanager') local ok, buildingplan = pcall(require, 'plugins.buildingplan') if not ok then @@ -141,8 +141,8 @@ function MassRemove:init() key_back='CUSTOM_SHIFT_X', options={ {label='Leave alone', value=function() end}, - {label='Suspend', value=suspendmanagerUtils.suspend}, - {label='Unsuspend', value=suspendmanagerUtils.unsuspend}, + {label='Suspend', value=suspendmanager.suspend}, + {label='Unsuspend', value=suspendmanager.unsuspend}, }, }, } diff --git a/internal/suspendmanager/suspendmanager-utils.lua b/internal/suspendmanager/suspendmanager-utils.lua deleted file mode 100644 index 9109b5de82..0000000000 --- a/internal/suspendmanager/suspendmanager-utils.lua +++ /dev/null @@ -1,137 +0,0 @@ --- Reusable functions for job suspension management ---@ module = true - -local utils = require('utils') - -local ok, buildingplan = pcall(require, 'plugins.buildingplan') -if not ok then - buildingplan = nil -end - ---- Suspend a job ----@param job job -function suspend(job) - job.flags.suspend = true - job.flags.working = false - dfhack.job.removeWorker(job, 0) -end - ---- Unsuspend a job ----@param job job -function unsuspend(job) - job.flags.suspend = false -end - ---- Loop over all the construction jobs ----@param fn function A function taking a job as argument -function foreach_construction_job(fn) - for _,job in utils.listpairs(df.global.world.jobs.list) do - if job.job_type == df.job_type.ConstructBuilding then - fn(job) - end - end -end - ---- True if there is a construction plan to build an unwalkable tile ----@param pos coord ----@return boolean -local function plansToConstructImpassableAt(pos) - --- @type building_constructionst|building - local building = dfhack.buildings.findAtTile(pos) - if not building then return false end - if building.flags.exists then - -- The building is already created - return false - end - return building:isImpassableAtCreation() -end - ---- Check if the tile can be walked on ----@param pos coord -local function walkable(pos) - local tt = dfhack.maps.getTileType(pos) - if not tt then - return false - end - local attrs = df.tiletype.attrs[tt] - local shape_attrs = df.tiletype_shape.attrs[attrs.shape] - return shape_attrs.walkable -end - ---- List neighbour coordinates of a position ----@param pos coord ----@return table -local function neighbours(pos) - return { - {x=pos.x-1, y=pos.y, z=pos.z}, - {x=pos.x+1, y=pos.y, z=pos.z}, - {x=pos.x, y=pos.y-1, z=pos.z}, - {x=pos.x, y=pos.y+1, z=pos.z}, - } -end - - ---- Get the amount of risk a tile is to be blocked ---- -1: There is a nearby walkable area with no plan to build a wall ---- >=0: Surrounded by either unwalkable tiles, or tiles that will be constructed ---- with unwalkable buildings. The value is the number of already unwalkable tiles. ----@param pos coord -local function riskOfStuckConstructionAt(pos) - local risk = 0 - for _,neighbourPos in pairs(neighbours(pos)) do - if not walkable(neighbourPos) then - -- blocked neighbour, increase danger - risk = risk + 1 - elseif not plansToConstructImpassableAt(neighbourPos) then - -- walkable neighbour with no plan to build a wall, no danger - return -1 - end - end - return risk -end - ---- Return true if this job is at risk of blocking another one -function isBlocking(job) - -- Not a construction job, no risk - if job.job_type ~= df.job_type.ConstructBuilding then return false end - - local building = dfhack.job.getHolder(job) - --- Not building a blocking construction, no risk - if not building or not building:isImpassableAtCreation() then return false end - - --- job.pos is sometimes off by one, get the building pos - local pos = {x=building.centerx,y=building.centery,z=building.z} - - --- Get self risk of being blocked - local risk = riskOfStuckConstructionAt(pos) - - for _,neighbourPos in pairs(neighbours(pos)) do - if plansToConstructImpassableAt(neighbourPos) and riskOfStuckConstructionAt(neighbourPos) > risk then - --- This neighbour job is at greater risk of getting stuck - return true - end - end - - return false -end - ---- Return true with a reason if a job should be suspended. ---- It takes in account water flow, buildingplan plugin, and optionally ---- the risk of creating stuck construction buildings ---- @param job job ---- @param accountblocking boolean -function shouldBeSuspended(job, accountblocking) - if dfhack.maps.getTileFlags(job.pos).flow_size > 1 then - return true, 'underwater' - end - - local bld = dfhack.buildings.findAtTile(job.pos) - if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then - return true, 'buildingplan' - end - - if accountblocking and isBlocking(job) then - return true, 'blocking' - end - return false, nil -end diff --git a/suspend.lua b/suspend.lua index dd5257f1d0..2456f009bc 100644 --- a/suspend.lua +++ b/suspend.lua @@ -3,10 +3,10 @@ -- It can either suspend all jobs, or just jobs that risk blocking others. local argparse = require('argparse') -local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') +local suspendmanager = reqscript('suspendmanager') local help, onlyblocking = false, false -argparse.processArgsGetopt(args, { +argparse.processArgsGetopt({...}, { {'h', 'help', handler=function() help = true end}, {'b', 'onlyblocking', handler=function() onlyblocking = true end}, }) @@ -16,8 +16,8 @@ if help then return end -suspendmanagerUtils.foreach_construction_job(function (job) - if not onlyblocking or suspendmanagerUtils.isBlocking(job) then - suspendmanagerUtils.suspend(job) +suspendmanager.foreach_construction_job(function (job) + if not onlyblocking or suspendmanager.isBlocking(job) then + suspendmanager.suspend(job) end end) diff --git a/suspendmanager.lua b/suspendmanager.lua index 76bc6e37ce..67370bfaf6 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -6,8 +6,12 @@ local json = require('json') local persist = require('persist-table') local argparse = require('argparse') local eventful = require('plugins.eventful') +local utils = require('utils') local repeatUtil = require('repeat-util') -local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') +local ok, buildingplan = pcall(require, 'plugins.buildingplan') +if not ok then + buildingplan = nil +end local GLOBAL_KEY = 'suspendmanager' -- used for state change hooks and persistence @@ -49,13 +53,141 @@ function update_setting(setting, value) persist_state() end + +--- Suspend a job +---@param job job +function suspend(job) + job.flags.suspend = true + job.flags.working = false + dfhack.job.removeWorker(job, 0) +end + +--- Unsuspend a job +---@param job job +function unsuspend(job) + job.flags.suspend = false +end + +--- Loop over all the construction jobs +---@param fn function A function taking a job as argument +function foreach_construction_job(fn) + for _,job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.ConstructBuilding then + fn(job) + end + end +end + +--- True if there is a construction plan to build an unwalkable tile +---@param pos coord +---@return boolean +local function plansToConstructImpassableAt(pos) + --- @type building_constructionst|building + local building = dfhack.buildings.findAtTile(pos) + if not building then return false end + if building.flags.exists then + -- The building is already created + return false + end + return building:isImpassableAtCreation() +end + +--- Check if the tile can be walked on +---@param pos coord +local function walkable(pos) + local tt = dfhack.maps.getTileType(pos) + if not tt then + return false + end + local attrs = df.tiletype.attrs[tt] + local shape_attrs = df.tiletype_shape.attrs[attrs.shape] + return shape_attrs.walkable +end + +--- List neighbour coordinates of a position +---@param pos coord +---@return table +local function neighbours(pos) + return { + {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x+1, y=pos.y, z=pos.z}, + {x=pos.x, y=pos.y-1, z=pos.z}, + {x=pos.x, y=pos.y+1, z=pos.z}, + } +end + +--- Get the amount of risk a tile is to be blocked +--- -1: There is a nearby walkable area with no plan to build a wall +--- >=0: Surrounded by either unwalkable tiles, or tiles that will be constructed +--- with unwalkable buildings. The value is the number of already unwalkable tiles. +---@param pos coord +local function riskOfStuckConstructionAt(pos) + local risk = 0 + for _,neighbourPos in pairs(neighbours(pos)) do + if not walkable(neighbourPos) then + -- blocked neighbour, increase danger + risk = risk + 1 + elseif not plansToConstructImpassableAt(neighbourPos) then + -- walkable neighbour with no plan to build a wall, no danger + return -1 + end + end + return risk +end + +--- Return true if this job is at risk of blocking another one +function isBlocking(job) + -- Not a construction job, no risk + if job.job_type ~= df.job_type.ConstructBuilding then return false end + + local building = dfhack.job.getHolder(job) + --- Not building a blocking construction, no risk + if not building or not building:isImpassableAtCreation() then return false end + + --- job.pos is sometimes off by one, get the building pos + local pos = {x=building.centerx,y=building.centery,z=building.z} + + --- Get self risk of being blocked + local risk = riskOfStuckConstructionAt(pos) + + for _,neighbourPos in pairs(neighbours(pos)) do + if plansToConstructImpassableAt(neighbourPos) and riskOfStuckConstructionAt(neighbourPos) > risk then + --- This neighbour job is at greater risk of getting stuck + return true + end + end + + return false +end + +--- Return true with a reason if a job should be suspended. +--- It takes in account water flow, buildingplan plugin, and optionally +--- the risk of creating stuck construction buildings +--- @param job job +--- @param accountblocking boolean +function shouldBeSuspended(job, accountblocking) + if dfhack.maps.getTileFlags(job.pos).flow_size > 1 then + return true, 'underwater' + end + + local bld = dfhack.buildings.findAtTile(job.pos) + if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then + return true, 'buildingplan' + end + + if accountblocking and isBlocking(job) then + return true, 'blocking' + end + return false, nil +end + local function run_now() - suspendmanagerUtils.foreach_construction_job(function(job) - local shouldBeSuspended, _ = suspendmanagerUtils.shouldBeSuspended(job, preventblocking) + foreach_construction_job(function(job) + local shouldBeSuspended, _ = shouldBeSuspended(job, preventblocking) if shouldBeSuspended and not job.flags.suspend then - suspendmanagerUtils.suspend(job) + suspend(job) elseif not shouldBeSuspended and job.flags.suspend then - suspendmanagerUtils.unsuspend(job) + unsuspend(job) end end) end diff --git a/unsuspend.lua b/unsuspend.lua index 87be23ce25..13b2d5d492 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -3,7 +3,7 @@ local guidm = require('gui.dwarfmode') local argparse = require('argparse') -local suspendmanagerUtils = reqscript('internal/suspendmanager/suspendmanager-utils') +local suspendmanager = reqscript('suspendmanager') local overlay = require('plugins.overlay') @@ -53,7 +53,7 @@ end function SuspendOverlay:overlay_onupdate() local added = false self.data_version = self.data_version + 1 - suspendmanagerUtils.foreach_construction_job(function(job) + suspendmanager.foreach_construction_job(function(job) self:update_building(dfhack.job.getHolder(job).id, job) added = true end) @@ -171,8 +171,6 @@ function SuspendOverlay:onRenderFrame(dc) dc:map(false) end - - OVERLAY_WIDGETS = {overlay=SuspendOverlay} if dfhack_flags.module then @@ -188,15 +186,15 @@ argparse.processArgsGetopt({...}, { local skipped_counts = {} local unsuspended_count = 0 -suspendmanagerUtils.foreach_construction_job(function(job) +suspendmanager.foreach_construction_job(function(job) if not job.flags.suspend then return end - local skip,reason=suspendmanagerUtils.shouldBeSuspended(job, skipblocking) + local skip,reason=suspendmanager.shouldBeSuspended(job, skipblocking) if skip then skipped_counts[reason] = (skipped_counts[reason] or 0) + 1 return end - suspendmanagerUtils.unsuspend(job) + suspendmanager.unsuspend(job) unsuspended_count = unsuspended_count + 1 end) From 5039b31437f1ce92a22d5969818aab960c5cb17c Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 13 Mar 2023 11:55:23 +0100 Subject: [PATCH 025/732] cleanup --- gui/mass-remove.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 80c3e6fd8e..3c2781a380 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -39,10 +39,6 @@ local function unremove_construction(pos, grid) if job then dfhack.job.removeJob(job) end end -local function unsuspend(job) - job.flags.suspend = false -end - -- -- ActionPanel -- From 0f9f4591703cae2351828e9e7f77c985fb2d6fc0 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 13 Mar 2023 17:10:19 +0100 Subject: [PATCH 026/732] Doc and changelog update --- changelog.txt | 2 ++ docs/suspend.rst | 13 ++++++++----- docs/unsuspend.rst | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index e621acb5ca..0293049e1c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,8 @@ that repo. ## New Scripts - `suspendmanager`: Automatic job suspension management (replaces `autounsuspend`) +- `gui/suspendmanager`: Graphical configuration interface for `suspendmanager` +- `suspend`: uspends building construction jobs ## Fixes diff --git a/docs/suspend.rst b/docs/suspend.rst index e064c6a458..6c50ab0f37 100644 --- a/docs/suspend.rst +++ b/docs/suspend.rst @@ -5,11 +5,14 @@ suspend :summary: Suspends building construction jobs. :tags: fort productivity jobs -This tool will suspend jobs. It can either suspend all the current jobs, -or only construction jobs that are likely to block other jobs. When building walls, -it's common that wall corners get stuck because dwarves build the two adjacent -walls before the corner. The ``--onlyblocking`` option will only suspend jobs -that can potentially lead to this situation. +This tool will suspend jobs. It can either suspend all the current jobs, or only +construction jobs that are likely to block other jobs. When building walls, it's +common that wall corners get stuck because dwarves build the two adjacent walls +before the corner. The ``--onlyblocking`` option will only suspend jobs that can +potentially lead to this situation. + +See `suspendmanager` in `gui/control-panel` to automatically suspend and +unsuspend jobs. Usage ----- diff --git a/docs/unsuspend.rst b/docs/unsuspend.rst index 5bb6d9d240..b82811a734 100644 --- a/docs/unsuspend.rst +++ b/docs/unsuspend.rst @@ -10,7 +10,8 @@ and those where water flow is greater than 1. This allows you to quickly recover if a bunch of jobs were suspended due to the workers getting scared off by wildlife or items temporarily blocking building sites. -See `autounsuspend` for periodic automatic unsuspending of suspended jobs. +See `suspendmanager` in `gui/control-panel` to automatically suspend and +unsuspend jobs. Usage ----- From 8b9dc9512ea52df2f23aafc1166d93892aa5c7b3 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 13 Mar 2023 09:16:24 -0700 Subject: [PATCH 027/732] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 0293049e1c..e2edf9983e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,7 +16,7 @@ that repo. ## New Scripts - `suspendmanager`: Automatic job suspension management (replaces `autounsuspend`) - `gui/suspendmanager`: Graphical configuration interface for `suspendmanager` -- `suspend`: uspends building construction jobs +- `suspend`: suspends building construction jobs ## Fixes From 85dc7fe89feabdbd75dd6fa49f99952356c3a5b4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Mar 2023 14:29:16 -0700 Subject: [PATCH 028/732] fix autosave with code reverse engineered by ab9rf --- docs/quicksave.rst | 6 +++--- quicksave.lua | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/quicksave.rst b/docs/quicksave.rst index 81acf85351..9711d106ba 100644 --- a/docs/quicksave.rst +++ b/docs/quicksave.rst @@ -2,11 +2,11 @@ quicksave ========= .. dfhack-tool:: - :summary: Immediately save the game. + :summary: Immediately autosave the game. :tags: dfhack fort -When called in dwarf mode, this tool makes DF immediately do an autosave. Note -that the game only keeps the last 3 autosaves. +When this tool is called with a fort loaded, DF will immediately do an +autosave. Note that the game only keeps the last 3 autosaves. Usage ----- diff --git a/quicksave.lua b/quicksave.lua index ae89716dcb..abbcf21773 100644 --- a/quicksave.lua +++ b/quicksave.lua @@ -1,12 +1,4 @@ -- Makes the game immediately save the state. ---[====[ - -quicksave -========= -If called in dwarf mode, makes DF immediately saves the game by setting a flag -normally used in seasonal auto-save. - -]====] local gui = require("gui") @@ -42,8 +34,17 @@ local function restore_autobackup() end function save() - -- Request auto-save + -- Request auto-save (preparation steps below discovered from rev eng) ui_main.autosave_request = true + ui_main.autosave_unk = 5 + ui_main.save_progress.substage = 0 + ui_main.save_progress.stage = 0 + ui_main.save_progress.unk_v50_6.nemesis_save_file_id:resize(0) + ui_main.save_progress.unk_v50_6.nemesis_member_idx:resize(0) + ui_main.save_progress.unk_v50_6.units:resize(0) + ui_main.save_progress.unk_v50_6.cur_unit_chunk = nil + ui_main.save_progress.unk_v50_6.cur_unit_chunk_num = -1 + ui_main.save_progress.unk_v50_6.units_offloaded = -1 -- And since it will overwrite the backup, disable it temporarily if flags4.AUTOBACKUP then @@ -51,7 +52,7 @@ function save() restore_autobackup() end - print 'The game should save the state now.' + print 'The game should autosave now.' end QuicksaveOverlay():show() From dded04b2ec0161162a282af35dd914fbd504d726 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Mar 2023 17:50:28 -0700 Subject: [PATCH 029/732] use new autosave var names --- quicksave.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/quicksave.lua b/quicksave.lua index abbcf21773..6e267bccff 100644 --- a/quicksave.lua +++ b/quicksave.lua @@ -36,15 +36,15 @@ end function save() -- Request auto-save (preparation steps below discovered from rev eng) ui_main.autosave_request = true - ui_main.autosave_unk = 5 + ui_main.autosave_timer = 5 ui_main.save_progress.substage = 0 ui_main.save_progress.stage = 0 - ui_main.save_progress.unk_v50_6.nemesis_save_file_id:resize(0) - ui_main.save_progress.unk_v50_6.nemesis_member_idx:resize(0) - ui_main.save_progress.unk_v50_6.units:resize(0) - ui_main.save_progress.unk_v50_6.cur_unit_chunk = nil - ui_main.save_progress.unk_v50_6.cur_unit_chunk_num = -1 - ui_main.save_progress.unk_v50_6.units_offloaded = -1 + ui_main.save_progress.info.nemesis_save_file_id:resize(0) + ui_main.save_progress.info.nemesis_member_idx:resize(0) + ui_main.save_progress.info.units:resize(0) + ui_main.save_progress.info.cur_unit_chunk = nil + ui_main.save_progress.info.cur_unit_chunk_num = -1 + ui_main.save_progress.info.units_offloaded = -1 -- And since it will overwrite the backup, disable it temporarily if flags4.AUTOBACKUP then From b6d1316277018275536acab2a84d5f5a6b02a087 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Mar 2023 17:51:32 -0700 Subject: [PATCH 030/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index ed8906be76..033ddc4fc3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,7 @@ that repo. ## New Scripts ## Fixes +- `quicksave`: now reliably triggers an autosave, even if one has been performed recently ## Misc Improvements - `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your blueprints to the new directory! From a6f4b87133e4a9f7eefdbd7f6f52d082d1ddbe61 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Mar 2023 12:42:28 -0700 Subject: [PATCH 031/732] fix typo --- gui/launcher.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index e8ed7422f6..13dd441c2b 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -357,7 +357,7 @@ local inactive_tab_pens = { rb=to_pen{tile=1014, write_to_lower=true}, } -Tab = defclass(Tabs, widgets.Widget) +Tab = defclass(Tab, widgets.Widget) Tab.ATTRS{ id=DEFAULT_NIL, label=DEFAULT_NIL, From 109f58b68166a327b15256ec927c34c487083d64 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Mar 2023 12:43:39 -0700 Subject: [PATCH 032/732] replace tabs with spaces in command output --- changelog.txt | 1 + gui/launcher.lua | 2 ++ 2 files changed, 3 insertions(+) diff --git a/changelog.txt b/changelog.txt index b8198b47d1..97654619f7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ that repo. ## Fixes - `quicksave`: now reliably triggers an autosave, even if one has been performed recently +- `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" ## Misc Improvements - `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your blueprints to the new directory! diff --git a/gui/launcher.lua b/gui/launcher.lua index 13dd441c2b..1468c302ba 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -907,6 +907,8 @@ function LauncherUI:run_command(reappear, command) self:on_edit_input('') if #output == 0 then output = 'Command finished successfully' + else + output = output:gsub('\t', ' ') end self.subviews.help:add_output(('> %s\n\n%s'):format(command, output)) end From 9f17eb67a1ffea0fde30c023df4ac798771973cc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Mar 2023 16:32:25 -0700 Subject: [PATCH 033/732] allow gm-editor to open the selected stockpile --- changelog.txt | 1 + docs/gui/gm-editor.rst | 2 +- gui/gm-editor.lua | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index b8198b47d1..794a512549 100644 --- a/changelog.txt +++ b/changelog.txt @@ -23,6 +23,7 @@ that repo. ## Misc Improvements - `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your blueprints to the new directory! +- `gui/gm-editor`: can now open the selected stockpile if run without parameters ## Removed diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index 4a6e429c76..0267afe9bf 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -24,7 +24,7 @@ Examples -------- ``gui/gm-editor`` - Opens the editor on the selected unit/item/job/workorder/etc. + Opens the editor on the selected unit/item/job/workorder/stockpile etc. ``gui/gm-editor df.global.world.items.all`` Opens the editor on the items list. diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 5117086593..e609749f70 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -45,6 +45,7 @@ end function getTargetFromScreens() local my_trg = dfhack.gui.getSelectedUnit(true) or dfhack.gui.getSelectedItem(true) or dfhack.gui.getSelectedJob(true) or dfhack.gui.getSelectedBuilding(true) + or dfhack.gui.getSelectedStockpile(true) if not my_trg then qerror("No valid target found") end From 1607aafc784f959fe2ef478b27e7965920a20d5a Mon Sep 17 00:00:00 2001 From: John Cosker Date: Thu, 16 Mar 2023 12:20:33 -0400 Subject: [PATCH 034/732] Initial commit --- gui/dig.lua | 155 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 138 insertions(+), 17 deletions(-) diff --git a/gui/dig.lua b/gui/dig.lua index abb91ee9c9..f5a2adeb43 100644 --- a/gui/dig.lua +++ b/gui/dig.lua @@ -39,6 +39,7 @@ local guidm = require("gui.dwarfmode") local widgets = require("gui.widgets") local quickfort = reqscript("quickfort") local shapes = reqscript("internal/dig/shapes") +local filterselection = require('plugins.buildingplan.filterselection') local tile_attrs = df.tiletype.attrs @@ -74,6 +75,26 @@ local function same_xyz(pos1, pos2) return same_xy(pos1, pos2) and pos1.z == pos2.z end +local function get_configure_button_pen() + local start = dfhack.textures.getControlPanelTexposStart() + local valid = start > 0 + print(valid) + start = start + 10 + + return dfhack.pen.parse { + tile = valid and (start + 9) or nil, ch = 15 + } -- gear/masterwork symbol +end + +local CONFIGURE_PEN = get_configure_button_pen() + +local uibs = df.global.buildreq + +local function get_cur_filters() + return dfhack.buildings.getFiltersByType({}, uibs.building_type, + uibs.building_subtype, uibs.custom_type) +end + -- Debug window SHOW_DEBUG_WINDOW = false @@ -330,6 +351,30 @@ function GenericOptionsPanel:init() value = "j", }, } + + local build_options = { + { + label = "Walls", + value = "Cw", + }, + { + label = "Floor", + value = "Cf", + }, + { + label = "Fortification", + value = "CF", + }, + { + label = "Ramps", + value = "Cr", + }, + { + label = "None", + value = "`", + }, + } + self:addviews { widgets.WrappedLabel { view_id = "settings_label", @@ -640,39 +685,43 @@ function GenericOptionsPanel:init() options = { { label = "Dig", - value = "d", + value = { desig = "d", mode = "dig" }, }, { label = "Channel", - value = "h", + value = { desig = "h", mode = "dig" }, }, { label = "Remove Designation", - value = "x", + value = { desig = "x", mode = "dig" }, }, { label = "Remove Ramps", - value = "z", + value = { desig = "z", mode = "dig" }, }, { label = "Remove Constructions", - value = "n", + value = { desig = "n", mode = "dig" }, }, { label = "Stairs", - value = "i", + value = { desig = "i", mode = "dig" }, }, { label = "Ramp", - value = "r", + value = { desig = "r", mode = "dig" }, }, { label = "Smooth", - value = "s", + value = { desig = "s", mode = "dig" }, }, { label = "Engrave", - value = "e", + value = { desig = "e", mode = "dig" }, + }, + { + label = "Building", + value = { desig = "b", mode = "build" }, } }, disabled = false, @@ -683,29 +732,82 @@ function GenericOptionsPanel:init() view_id = "stairs_top_subtype", key = "CUSTOM_R", label = "Top Stair Type: ", + frame = { l = 1 }, active = true, enabled = true, - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue() == "i" end, + visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "i" end, options = stair_options, }, widgets.CycleHotkeyLabel { view_id = "stairs_middle_subtype", key = "CUSTOM_G", label = "Middle Stair Type: ", + frame = { l = 1 }, active = true, enabled = true, - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue() == "i" end, + visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "i" end, options = stair_options, }, widgets.CycleHotkeyLabel { view_id = "stairs_bottom_subtype", - key = "CUSTOM_B", + key = "CUSTOM_N", label = "Bottom Stair Type: ", + frame = { l = 1 }, active = true, enabled = true, - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue() == "i" end, + visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "i" end, options = stair_options, }, + widgets.ResizingPanel { + view_id = 'transform_panel_rotate', + visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "b" end, + subviews = { + widgets.Label { + view_id = "building_outer_config", + frame = { t = 0, l = 1 }, + text = { { tile = CONFIGURE_PEN } }, + on_click = function() + uibs.building_type = df.building_type.Construction + uibs.building_subtype = df.construction_type.Wall -- TODO + filterselection.FilterSelectionScreen { index = 1, + desc = require('plugins.buildingplan').get_desc(get_cur_filters()[1])}:show() + end + }, + widgets.CycleHotkeyLabel { + view_id = "building_outer_tiles", + key = "CUSTOM_R", + label = "Outer Tiles: ", + frame = { t = 0, l = 3 }, + active = true, + enabled = true, + initial_option = 1, + visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "b" end, + options = build_options, + }, + widgets.Label { + view_id = "building_inner_config", + frame = { t = 1, l = 1}, + text = { { tile = CONFIGURE_PEN } }, + on_click = function() + uibs.building_type = df.building_type.Construction + uibs.building_subtype = df.construction_type.Floor -- TODO + filterselection.FilterSelectionScreen { index = 1, + desc = require('plugins.buildingplan').get_desc(get_cur_filters()[1])}:show() + end + }, + widgets.CycleHotkeyLabel { + view_id = "building_inner_tiles", + key = "CUSTOM_G", + label = "Inner Tiles: ", + frame = { t = 1, l = 3 }, + active = true, + enabled = true, + initial_option = 2, + visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "b" end, + options = build_options, + }, + }, + }, widgets.WrappedLabel { view_id = "shape_prio_label", text_to_wrap = function() @@ -1581,9 +1683,10 @@ function Dig:get_designation(x, y, z) local mode = self.subviews.mode_name:getOptionValue() local view_bounds = self:get_view_bounds() + local top_left, bot_right = self.shape:get_true_dims() -- Stairs - if mode == "i" then + if mode.desig == "i" then local stairs_top_type = self.subviews.stairs_top_subtype:getOptionValue() local stairs_middle_type = self.subviews.stairs_middle_subtype:getOptionValue() local stairs_bottom_type = self.subviews.stairs_bottom_subtype:getOptionValue() @@ -1609,9 +1712,25 @@ function Dig:get_designation(x, y, z) else return stairs_middle_type == "auto" and 'i' or stairs_middle_type end + elseif mode.desig == "b" then + local building_outer_tiles = self.subviews.building_outer_tiles:getOptionValue() + local building_inner_tiles = self.subviews.building_inner_tiles:getOptionValue() + local darr = { { 1, 1 }, { 1, 0 }, { 0, 1 }, { 0, 0 }, { -1, 0 }, { -1, -1 }, { 0, -1 }, { 1, -1 }, { -1, 1 } } + + -- If not completed surrounded, then use outer tile + for i, d in ipairs(darr) do + -- print(view_bounds.x + x + d[1], view_bounds.y + y + d[2]) + if not (self.shape:get_point(top_left.x + x + d[1], top_left.y + y + d[2])) then + + return building_outer_tiles + end + end + + -- Is inner tile + return building_inner_tiles end - return self.subviews.mode_name:getOptionValue() + return mode.desig end -- Commit the shape using quickfort API @@ -1623,6 +1742,7 @@ function Dig:commit() -- Means mo marks set if not view_bounds then return end + local mode = self.subviews.mode_name:getOptionValue().mode -- Generates the params for quickfort API local function generate_params(grid, position) -- local top_left, bot_right = self.shape:get_true_dims() @@ -1635,7 +1755,7 @@ function Dig:commit() local desig = self:get_designation(col, row, zlevel) if desig ~= "`" then data[zlevel][row][col] = - desig .. tostring(self.prio) + desig..(mode ~= "build" and tostring(self.prio) or "") end end end @@ -1645,7 +1765,7 @@ function Dig:commit() return { data = data, pos = position, - mode = "dig", + mode = mode, } end @@ -1760,6 +1880,7 @@ DigScreen = defclass(DigScreen, gui.ZScreen) DigScreen.ATTRS { focus_path = "dig", pass_pause = true, + initial_pause = false, pass_movement_keys = true, } From cffac20a47c80c73d9084cb1eac0a0063a042f6f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Mar 2023 10:06:49 -0700 Subject: [PATCH 035/732] bump changelog to 50.07-beta1 --- changelog.txt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0107cab773..c1b1756e00 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,8 +14,18 @@ that repo. # Future ## New Scripts -- `suspendmanager`: Automatic job suspension management (replaces `autounsuspend`) -- `gui/suspendmanager`: Graphical configuration interface for `suspendmanager` + +## Fixes + +## Misc Improvements + +## Removed + +# 50.07-beta1 + +## New Scripts +- `suspendmanager`: automatic job suspension management (replaces `autounsuspend`) +- `gui/suspendmanager`: graphical configuration interface for `suspendmanager` - `suspend`: suspends building construction jobs ## Fixes @@ -23,11 +33,10 @@ that repo. - `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" ## Misc Improvements -- `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your blueprints to the new directory! +- `quickfort`: now reads player-created blueprints from +``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! - `gui/gm-editor`: can now open the selected stockpile if run without parameters -## Removed - # 50.07-alpha3 ## Fixes From 2baf2a293b022e74bda169904182386ed414c84f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Mar 2023 10:13:41 -0700 Subject: [PATCH 036/732] fix typo in changelog --- changelog.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index c1b1756e00..f12cc04f08 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,8 +33,7 @@ that repo. - `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" ## Misc Improvements -- `quickfort`: now reads player-created blueprints from -``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! +- `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! - `gui/gm-editor`: can now open the selected stockpile if run without parameters # 50.07-alpha3 From 2dcd35b4a486f59cedc38d25113e02f718855b31 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Fri, 17 Mar 2023 14:25:21 -0400 Subject: [PATCH 037/732] Add help pop up to inform user to use buildingplan to set filters --- gui/dig.lua | 112 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 21 deletions(-) diff --git a/gui/dig.lua b/gui/dig.lua index f5a2adeb43..ca5602e283 100644 --- a/gui/dig.lua +++ b/gui/dig.lua @@ -64,6 +64,50 @@ local mirror_guide_pen = to_pen { ), } +--- +--- HelpWindow +--- + +DIG_HELP_DEFAULT = { + "gui/dig Help", + "============", + NEWLINE, + "This is a default help text." +} + +CONSTRUCTION_HELP = { + "gui/dig Help: Building Filters", + "===============================", + NEWLINE, + "Adding material filters to this tool is planned but not implemented at this time.", + NEWLINE, + "Use `buildingplan` to configure filters for the desired construction types. This tool will use the current buildingplan filters for an building type." +} + +HelpWindow = defclass(HelpWindow, widgets.Window) +HelpWindow.ATTRS { + frame_title = 'gui/dig Help', + frame = { w = 43, h = 20, t = 10, l = 10 }, + resizable = true, + resize_min = { w = 43, h = 20 }, + message = DIG_HELP_DEFAULT +} + +function HelpWindow:init() + self:addviews { + widgets.ResizingPanel { autoarrange_subviews = true, + frame = { t = 0, l = 0}, + subviews = { + widgets.WrappedLabel { + view_id = 'help_text', + frame = { t = 0, l = 0}, + text_to_wrap = function() return self.message end, + } + } + } + } +end + -- Utilities local function same_xy(pos1, pos2) @@ -75,24 +119,49 @@ local function same_xyz(pos1, pos2) return same_xy(pos1, pos2) and pos1.z == pos2.z end -local function get_configure_button_pen() +local function get_icon_pens() local start = dfhack.textures.getControlPanelTexposStart() local valid = start > 0 - print(valid) start = start + 10 - return dfhack.pen.parse { + local enabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, + tile = valid and (start + 0) or nil, ch = string.byte('[') } + local enabled_pen_center = dfhack.pen.parse { fg = COLOR_LIGHTGREEN, + tile = valid and (start + 1) or nil, ch = 251 } -- check + local enabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, + tile = valid and (start + 2) or nil, ch = string.byte(']') } + local disabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, + tile = valid and (start + 3) or nil, ch = string.byte('[') } + local disabled_pen_center = dfhack.pen.parse { fg = COLOR_RED, + tile = valid and (start + 4) or nil, ch = string.byte('x') } + local disabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, + tile = valid and (start + 5) or nil, ch = string.byte(']') } + local button_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, + tile = valid and (start + 6) or nil, ch = string.byte('[') } + local button_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, + tile = valid and (start + 7) or nil, ch = string.byte(']') } + local help_pen_center = dfhack.pen.parse { + tile = valid and (start + 8) or nil, ch = string.byte('?') + } + local configure_pen_center = dfhack.pen.parse { tile = valid and (start + 9) or nil, ch = 15 } -- gear/masterwork symbol + return enabled_pen_left, enabled_pen_center, enabled_pen_right, + disabled_pen_left, disabled_pen_center, disabled_pen_right, + button_pen_left, button_pen_right, + help_pen_center, configure_pen_center end -local CONFIGURE_PEN = get_configure_button_pen() +local ENABLED_PEN_LEFT, ENABLED_PEN_CENTER, ENABLED_PEN_RIGHT, +DISABLED_PEN_LEFT, DISABLED_PEN_CENTER, DISABLED_PEN_RIGHT, +BUTTON_PEN_LEFT, BUTTON_PEN_RIGHT, +HELP_PEN_CENTER, CONFIGURE_PEN_CENTER = get_icon_pens() local uibs = df.global.buildreq local function get_cur_filters() return dfhack.buildings.getFiltersByType({}, uibs.building_type, - uibs.building_subtype, uibs.custom_type) + uibs.building_subtype, uibs.custom_type) end -- Debug window @@ -191,7 +260,6 @@ end --Show mark point coordinates MarksPanel = defclass(MarksPanel, widgets.ResizingPanel) MarksPanel.ATTRS { - get_area_fn = DEFAULT_NIL, autoarrange_subviews = true, dig_panel = DEFAULT_NIL } @@ -240,7 +308,6 @@ end -- Panel to show the Mouse position/dimensions/etc ActionPanel = defclass(ActionPanel, widgets.ResizingPanel) ActionPanel.ATTRS { - get_area_fn = DEFAULT_NIL, autoarrange_subviews = true, dig_panel = DEFAULT_NIL } @@ -765,19 +832,18 @@ function GenericOptionsPanel:init() widgets.Label { view_id = "building_outer_config", frame = { t = 0, l = 1 }, - text = { { tile = CONFIGURE_PEN } }, + text = { { tile = BUTTON_PEN_LEFT }, { tile = HELP_PEN_CENTER }, { tile = BUTTON_PEN_RIGHT } }, on_click = function() - uibs.building_type = df.building_type.Construction - uibs.building_subtype = df.construction_type.Wall -- TODO - filterselection.FilterSelectionScreen { index = 1, - desc = require('plugins.buildingplan').get_desc(get_cur_filters()[1])}:show() + view.help_window.message = CONSTRUCTION_HELP + view.help_window.visible = true + view:updateLayout() end }, widgets.CycleHotkeyLabel { view_id = "building_outer_tiles", key = "CUSTOM_R", label = "Outer Tiles: ", - frame = { t = 0, l = 3 }, + frame = { t = 0, l = 5 }, active = true, enabled = true, initial_option = 1, @@ -786,20 +852,19 @@ function GenericOptionsPanel:init() }, widgets.Label { view_id = "building_inner_config", - frame = { t = 1, l = 1}, - text = { { tile = CONFIGURE_PEN } }, + frame = { t = 1, l = 1 }, + text = { { tile = BUTTON_PEN_LEFT }, { tile = HELP_PEN_CENTER }, { tile = BUTTON_PEN_RIGHT } }, on_click = function() - uibs.building_type = df.building_type.Construction - uibs.building_subtype = df.construction_type.Floor -- TODO - filterselection.FilterSelectionScreen { index = 1, - desc = require('plugins.buildingplan').get_desc(get_cur_filters()[1])}:show() + view.help_window.message = CONSTRUCTION_HELP + view.help_window.visible = true + view:updateLayout() end }, widgets.CycleHotkeyLabel { view_id = "building_inner_tiles", key = "CUSTOM_G", label = "Inner Tiles: ", - frame = { t = 1, l = 3 }, + frame = { t = 1, l = 5 }, active = true, enabled = true, initial_option = 2, @@ -1522,6 +1587,9 @@ function Dig:onInput(keys) -- end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + -- Close help window if open + if view.help_window.visible then view.help_window.visible = false return true end + -- If center draggin, put the shape back to the original center if self.prev_center then local transform = { x = self.start_center.x - self.prev_center.x, @@ -1887,7 +1955,9 @@ DigScreen.ATTRS { function DigScreen:init() self.dig_window = Dig {} - self:addviews { self.dig_window } + self.help_window = HelpWindow {} + self.help_window.visible = false + self:addviews { self.dig_window, self.help_window } if SHOW_DEBUG_WINDOW then self.debug_window = DigDebugWindow { dig_window = self.dig_window } self:addviews { self.debug_window } From 1f3f61fbabd76539ffed1d88533807c58cf8dd89 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Fri, 17 Mar 2023 14:34:43 -0400 Subject: [PATCH 038/732] Cleanup --- gui/dig.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/gui/dig.lua b/gui/dig.lua index ca5602e283..20f66141fa 100644 --- a/gui/dig.lua +++ b/gui/dig.lua @@ -39,7 +39,6 @@ local guidm = require("gui.dwarfmode") local widgets = require("gui.widgets") local quickfort = reqscript("quickfort") local shapes = reqscript("internal/dig/shapes") -local filterselection = require('plugins.buildingplan.filterselection') local tile_attrs = df.tiletype.attrs @@ -1787,9 +1786,7 @@ function Dig:get_designation(x, y, z) -- If not completed surrounded, then use outer tile for i, d in ipairs(darr) do - -- print(view_bounds.x + x + d[1], view_bounds.y + y + d[2]) if not (self.shape:get_point(top_left.x + x + d[1], top_left.y + y + d[2])) then - return building_outer_tiles end end From f359bd03daba8215bb4bcca8bee7b229efb05a51 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Fri, 17 Mar 2023 14:53:02 -0400 Subject: [PATCH 039/732] changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index f12cc04f08..cd809a6433 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ that repo. ## Misc Improvements - `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! +- `gui/dig`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. - `gui/gm-editor`: can now open the selected stockpile if run without parameters # 50.07-alpha3 From 1424777793b672b105a1785434518233eb0692da Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Mar 2023 23:48:25 -0700 Subject: [PATCH 040/732] remove references to v0.47 sidebar modes --- docs/deathcause.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deathcause.rst b/docs/deathcause.rst index 2f47ee528e..c9a2ae0a06 100644 --- a/docs/deathcause.rst +++ b/docs/deathcause.rst @@ -5,8 +5,8 @@ deathcause :summary: Find out the cause of death for a creature. :tags: fort inspection units -Select a body part on the ground in the :kbd:`k` menu or a unit in the :kbd:`u` -unit list, and ``deathcause`` will detail the cause of death of the creature. +Select a corpse or body part on the ground, and ``deathcause`` will detail the +cause of death of the creature. Usage ----- From c9657657811bedb24f20e1ae9b6ec1742a6807cd Mon Sep 17 00:00:00 2001 From: John Cosker Date: Sun, 19 Mar 2023 10:27:55 -0400 Subject: [PATCH 041/732] function cleanup --- gui/dig.lua | 100 ++++++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/gui/dig.lua b/gui/dig.lua index 20f66141fa..11505485cf 100644 --- a/gui/dig.lua +++ b/gui/dig.lua @@ -95,11 +95,11 @@ HelpWindow.ATTRS { function HelpWindow:init() self:addviews { widgets.ResizingPanel { autoarrange_subviews = true, - frame = { t = 0, l = 0}, + frame = { t = 0, l = 0 }, subviews = { widgets.WrappedLabel { view_id = 'help_text', - frame = { t = 0, l = 0}, + frame = { t = 0, l = 0 }, text_to_wrap = function() return self.message end, } } @@ -794,49 +794,48 @@ function GenericOptionsPanel:init() show_tooltip = true, on_change = function(new, old) self.dig_panel:updateLayout() end, }, - widgets.CycleHotkeyLabel { - view_id = "stairs_top_subtype", - key = "CUSTOM_R", - label = "Top Stair Type: ", - frame = { l = 1 }, - active = true, - enabled = true, - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "i" end, - options = stair_options, - }, - widgets.CycleHotkeyLabel { - view_id = "stairs_middle_subtype", - key = "CUSTOM_G", - label = "Middle Stair Type: ", - frame = { l = 1 }, - active = true, - enabled = true, - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "i" end, - options = stair_options, - }, - widgets.CycleHotkeyLabel { - view_id = "stairs_bottom_subtype", - key = "CUSTOM_N", - label = "Bottom Stair Type: ", - frame = { l = 1 }, - active = true, - enabled = true, - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "i" end, - options = stair_options, + widgets.ResizingPanel { + view_id = 'stairs_type_panel', + visible = self:callback("is_mode_selected", "i"), + subviews = { + widgets.CycleHotkeyLabel { + view_id = "stairs_top_subtype", + key = "CUSTOM_R", + label = "Top Stair Type: ", + frame = { t = 0, l = 1 }, + active = true, + enabled = true, + options = stair_options, + }, + widgets.CycleHotkeyLabel { + view_id = "stairs_middle_subtype", + key = "CUSTOM_G", + label = "Middle Stair Type: ", + frame = { t = 1, l = 1 }, + active = true, + enabled = true, + options = stair_options, + }, + widgets.CycleHotkeyLabel { + view_id = "stairs_bottom_subtype", + key = "CUSTOM_N", + label = "Bottom Stair Type: ", + frame = { t = 2, l = 1 }, + active = true, + enabled = true, + options = stair_options, + } + } }, widgets.ResizingPanel { - view_id = 'transform_panel_rotate', - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "b" end, + view_id = 'building_types_panel', + visible = self:callback("is_mode_selected", "b"), subviews = { widgets.Label { view_id = "building_outer_config", frame = { t = 0, l = 1 }, text = { { tile = BUTTON_PEN_LEFT }, { tile = HELP_PEN_CENTER }, { tile = BUTTON_PEN_RIGHT } }, - on_click = function() - view.help_window.message = CONSTRUCTION_HELP - view.help_window.visible = true - view:updateLayout() - end + on_click = self.dig_panel:callback("show_help", CONSTRUCTION_HELP) }, widgets.CycleHotkeyLabel { view_id = "building_outer_tiles", @@ -846,18 +845,13 @@ function GenericOptionsPanel:init() active = true, enabled = true, initial_option = 1, - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "b" end, options = build_options, }, widgets.Label { view_id = "building_inner_config", frame = { t = 1, l = 1 }, text = { { tile = BUTTON_PEN_LEFT }, { tile = HELP_PEN_CENTER }, { tile = BUTTON_PEN_RIGHT } }, - on_click = function() - view.help_window.message = CONSTRUCTION_HELP - view.help_window.visible = true - view:updateLayout() - end + on_click = self.dig_panel:callback("show_help", CONSTRUCTION_HELP) }, widgets.CycleHotkeyLabel { view_id = "building_inner_tiles", @@ -867,7 +861,6 @@ function GenericOptionsPanel:init() active = true, enabled = true, initial_option = 2, - visible = function() return self.dig_panel.subviews.mode_name:getOptionValue().desig == "b" end, options = build_options, }, }, @@ -942,6 +935,10 @@ function GenericOptionsPanel:init() } end +function GenericOptionsPanel:is_mode_selected(mode) + return self.dig_panel.subviews.mode_name:getOptionValue().desig == mode +end + function GenericOptionsPanel:change_shape(new, old) self.dig_panel.shape = shapes.all_shapes[new] if self.dig_panel.shape.max_points and #self.dig_panel.marks > self.dig_panel.shape.max_points then @@ -1587,7 +1584,7 @@ function Dig:onInput(keys) if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then -- Close help window if open - if view.help_window.visible then view.help_window.visible = false return true end + if view.help_window.visible then view:dismiss_help() return true end -- If center draggin, put the shape back to the original center if self.prev_center then @@ -1937,6 +1934,16 @@ function Dig:get_mirrored_points(points) return points end +function Dig:show_help(text) + self.parent_view.help_window.message = text + self.parent_view.help_window.visible = true + self.parent_view:updateLayout() +end + +function Dig:dismiss_help() + self.parent_view.help_window.visible = false +end + -- -- DigScreen -- @@ -1945,7 +1952,6 @@ DigScreen = defclass(DigScreen, gui.ZScreen) DigScreen.ATTRS { focus_path = "dig", pass_pause = true, - initial_pause = false, pass_movement_keys = true, } From 8c173c8b0735061adc8fec3651028d4152c4b3db Mon Sep 17 00:00:00 2001 From: John Cosker Date: Sun, 19 Mar 2023 18:37:44 -0400 Subject: [PATCH 042/732] rename dig to design --- gui/{dig.lua => design.lua} | 0 internal/{ => design}/dig/shapes.lua | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename gui/{dig.lua => design.lua} (100%) rename internal/{ => design}/dig/shapes.lua (100%) diff --git a/gui/dig.lua b/gui/design.lua similarity index 100% rename from gui/dig.lua rename to gui/design.lua diff --git a/internal/dig/shapes.lua b/internal/design/dig/shapes.lua similarity index 100% rename from internal/dig/shapes.lua rename to internal/design/dig/shapes.lua From 878c3f18ce7be2950c1fb55ce5865a1dd68db719 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Sun, 19 Mar 2023 19:42:01 -0400 Subject: [PATCH 043/732] moving files around --- gui/design.lua | 67 ++++++++++++++-------------- internal/design/{dig => }/shapes.lua | 0 2 files changed, 33 insertions(+), 34 deletions(-) rename internal/design/{dig => }/shapes.lua (100%) diff --git a/gui/design.lua b/gui/design.lua index abb91ee9c9..b9d33fc960 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1,4 +1,4 @@ --- A GUI front-end for the digging designations +-- A GUI front-end for creating designs --@ module = false -- TODOS ==================== @@ -38,7 +38,7 @@ local gui = require("gui") local guidm = require("gui.dwarfmode") local widgets = require("gui.widgets") local quickfort = reqscript("quickfort") -local shapes = reqscript("internal/dig/shapes") +local shapes = reqscript("internal/design/shapes") local tile_attrs = df.tiletype.attrs @@ -102,8 +102,8 @@ local function table_to_string(tbl, indent) return result end -DigDebugWindow = defclass(DigDebugWindow, widgets.Window) -DigDebugWindow.ATTRS { +DesignDebugWindow = defclass(DesignDebugWindow, widgets.Window) +DesignDebugWindow.ATTRS { frame_title = "Debug", frame = { w = 47, @@ -117,7 +117,7 @@ DigDebugWindow.ATTRS { autoarrange_gap = 1, dig_window = DEFAULT_NIL } -function DigDebugWindow:init() +function DesignDebugWindow:init() local attrs = { -- "shape", -- prints a lot of lines due to the self.arr, best to disable unless needed, TODO add a 'get debug string' function @@ -829,13 +829,13 @@ local PEN_MASK = { local PENS = {} -- --- Dig +-- Design -- -Dig = defclass(Dig, widgets.Window) -Dig.ATTRS { +Design = defclass(Design, widgets.Window) +Design.ATTRS { name = "dig_window", - frame_title = "Dig", + frame_title = "Design", frame = { w = 40, h = 45, @@ -866,7 +866,7 @@ Dig.ATTRS { -- Check to see if we're moving a point, or some change was made that implise we need to update the shape -- This stop us needing to update the shape geometery every frame which can tank FPS -function Dig:shape_needs_update() +function Design:shape_needs_update() -- if #self.marks < self.shape.min_points then return false end if self.needs_update then return true end @@ -894,7 +894,7 @@ end -- neighboring tiles. The first time a certain tile type needs to be drawn, it's pen -- is generated and stored in PENS. On subsequent calls, the cached pen will be used for -- other tiles with the same position/direction -function Dig:get_pen(x, y, mousePos) +function Design:get_pen(x, y, mousePos) local get_point = self.shape:get_point(x, y) local mouse_over = (mousePos) and (x == mousePos.x and y == mousePos.y) or false @@ -990,7 +990,7 @@ function Dig:get_pen(x, y, mousePos) return PENS[pen_key] end -function Dig:init() +function Design:init() self:addviews { ActionPanel { view_id = "action_panel", @@ -1010,7 +1010,7 @@ function Dig:init() } end -function Dig:postinit() +function Design:postinit() self.shape = shapes.all_shapes[self.subviews.shape_name:getOptionValue()] if self.shape then self:add_shape_options() @@ -1021,7 +1021,7 @@ end -- Currently only supports 'bool' aka toggle and 'plusminus' which creates -- a pair of HotKeyLabel's to increment/decrement a value -- Will need to update as needed to add more option types -function Dig:add_shape_options() +function Design:add_shape_options() local prefix = "shape_option_" for i, view in ipairs(self.subviews or {}) do if view.view_id:sub(1, #prefix) == prefix then @@ -1126,7 +1126,7 @@ function Dig:add_shape_options() end end -function Dig:on_transform(val) +function Design:on_transform(val) local center_x, center_y = self.shape:get_center() -- Save mirrored points first @@ -1189,7 +1189,7 @@ function Dig:on_transform(val) self.needs_update = true end -function Dig:get_view_bounds() +function Design:get_view_bounds() if #self.marks == 0 then return nil end local min_x = self.marks[1].x @@ -1218,7 +1218,7 @@ function Dig:get_view_bounds() end -- return the pen, alter based on if we want to display a corner and a mouse over corner -function Dig:make_pen(direction, is_corner, is_mouse_over, inshape, extra_point) +function Design:make_pen(direction, is_corner, is_mouse_over, inshape, extra_point) local color = COLOR_GREEN local ycursor_mod = 0 @@ -1253,7 +1253,7 @@ function Dig:make_pen(direction, is_corner, is_mouse_over, inshape, extra_point) end -- Generate a bit field to store as keys in PENS -function Dig:gen_pen_key(n, s, e, w, is_corner, is_mouse_over, inshape, extra_point) +function Design:gen_pen_key(n, s, e, w, is_corner, is_mouse_over, inshape, extra_point) local ret = 0 if n then ret = ret + (1 << PEN_MASK.NORTH) end if s then ret = ret + (1 << PEN_MASK.SOUTH) end @@ -1268,13 +1268,13 @@ function Dig:gen_pen_key(n, s, e, w, is_corner, is_mouse_over, inshape, extra_po end -- TODO Function is too long -function Dig:onRenderFrame(dc, rect) +function Design:onRenderFrame(dc, rect) if (SHOW_DEBUG_WINDOW) then self.parent_view.debug_window:updateLayout() end - Dig.super.onRenderFrame(self, dc, rect) + Design.super.onRenderFrame(self, dc, rect) if not self.shape then self.shape = shapes.all_shapes[self.subviews.shape_name:getOptionValue()] @@ -1407,8 +1407,8 @@ function Dig:onRenderFrame(dc, rect) end -- TODO function too long -function Dig:onInput(keys) - if Dig.super.onInput(self, keys) then +function Design:onInput(keys) + if Design.super.onInput(self, keys) then return true end @@ -1577,7 +1577,7 @@ end -- Put any special logic for designation type here -- Right now it's setting the stair type based on the z-level -- Fell through, pass through the option directly from the options value -function Dig:get_designation(x, y, z) +function Design:get_designation(x, y, z) local mode = self.subviews.mode_name:getOptionValue() local view_bounds = self:get_view_bounds() @@ -1615,7 +1615,7 @@ function Dig:get_designation(x, y, z) end -- Commit the shape using quickfort API -function Dig:commit() +function Design:commit() local data = {} local top_left, bot_right = self.shape:get_true_dims() local view_bounds = self:get_view_bounds() @@ -1682,7 +1682,7 @@ function Dig:commit() self:updateLayout() end -function Dig:get_mirrored_points(points) +function Design:get_mirrored_points(points) local mirror_horiz_value = self.subviews.mirror_horiz_label:getOptionValue() local mirror_diag_value = self.subviews.mirror_diag_label:getOptionValue() local mirror_vert_value = self.subviews.mirror_vert_label:getOptionValue() @@ -1753,28 +1753,27 @@ function Dig:get_mirrored_points(points) end -- --- DigScreen +-- DesignScreen -- -DigScreen = defclass(DigScreen, gui.ZScreen) -DigScreen.ATTRS { +DesignScreen = defclass(DesignScreen, gui.ZScreen) +DesignScreen.ATTRS { focus_path = "dig", pass_pause = true, pass_movement_keys = true, } -function DigScreen:init() +function DesignScreen:init() - self.dig_window = Dig {} + self.dig_window = Design {} self:addviews { self.dig_window } if SHOW_DEBUG_WINDOW then - self.debug_window = DigDebugWindow { dig_window = self.dig_window } + self.debug_window = DesignDebugWindow { dig_window = self.dig_window } self:addviews { self.debug_window } end - -- self:addviews { Dig {} } end -function DigScreen:onDismiss() +function DesignScreen:onDismiss() view = nil end @@ -1784,4 +1783,4 @@ if not dfhack.isMapLoaded() then qerror("This script requires a fortress map to be loaded") end -view = view and view:raise() or DigScreen {}:show() +view = view and view:raise() or DesignScreen {}:show() diff --git a/internal/design/dig/shapes.lua b/internal/design/shapes.lua similarity index 100% rename from internal/design/dig/shapes.lua rename to internal/design/shapes.lua From d9a00a55eeb8c45caf4f87f4d3b3d6ad54b56125 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Sun, 19 Mar 2023 19:50:25 -0400 Subject: [PATCH 044/732] update docs --- docs/gui/design.rst | 17 +++++++++++++++++ docs/gui/dig.rst | 17 ----------------- 2 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 docs/gui/design.rst delete mode 100644 docs/gui/dig.rst diff --git a/docs/gui/design.rst b/docs/gui/design.rst new file mode 100644 index 0000000000..40150345b1 --- /dev/null +++ b/docs/gui/design.rst @@ -0,0 +1,17 @@ + +gui/design +======= + +.. dfhack-tool:: + :summary: Design designation utility with shapes. + :tags: fort design productivity map + +This tool provides a point and click interface to make designating shapes +and patterns easier. Supports both digging designations and placing constructions. + +Usage +----- + +:: + + gui/design diff --git a/docs/gui/dig.rst b/docs/gui/dig.rst deleted file mode 100644 index f684b8bc7d..0000000000 --- a/docs/gui/dig.rst +++ /dev/null @@ -1,17 +0,0 @@ - -gui/dig -======= - -.. dfhack-tool:: - :summary: Digging designation utility with shapes. - :tags: fort design productivity map - -This tool provides a point and click interface to make designating shapes -and patterns easier. - -Usage ------ - -:: - - gui/dig From 9a410f1a2b3584a3d9bbc9d8a36d4ff9d49de8b8 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Sun, 19 Mar 2023 19:55:58 -0400 Subject: [PATCH 045/732] More renaming --- gui/design.lua | 260 ++++++++++++++++++++++++------------------------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index e08959c113..8b9f0a8699 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -67,15 +67,15 @@ local mirror_guide_pen = to_pen { --- HelpWindow --- -DIG_HELP_DEFAULT = { - "gui/dig Help", +DESIGN_HELP_DEFAULT = { + "gui/design Help", "============", NEWLINE, "This is a default help text." } CONSTRUCTION_HELP = { - "gui/dig Help: Building Filters", + "gui/design Help: Building Filters", "===============================", NEWLINE, "Adding material filters to this tool is planned but not implemented at this time.", @@ -85,11 +85,11 @@ CONSTRUCTION_HELP = { HelpWindow = defclass(HelpWindow, widgets.Window) HelpWindow.ATTRS { - frame_title = 'gui/dig Help', + frame_title = 'gui/design Help', frame = { w = 43, h = 20, t = 10, l = 10 }, resizable = true, resize_min = { w = 43, h = 20 }, - message = DIG_HELP_DEFAULT + message = DESIGN_HELP_DEFAULT } function HelpWindow:init() @@ -204,7 +204,7 @@ DesignDebugWindow.ATTRS { resize_min = { h = 30 }, autoarrange_subviews = true, autoarrange_gap = 1, - dig_window = DEFAULT_NIL + design_window = DEFAULT_NIL } function DesignDebugWindow:init() @@ -227,7 +227,7 @@ function DesignDebugWindow:init() "show_guides" } - if not self.dig_window then + if not self.design_window then return end @@ -242,14 +242,14 @@ function DesignDebugWindow:init() self:addviews { widgets.WrappedLabel { view_id = "debug_label_"..attr, text_to_wrap = function() - if type(self.dig_window[attr]) ~= "table" then - return tostring(attr)..": "..tostring(self.dig_window[attr]) + if type(self.design_window[attr]) ~= "table" then + return tostring(attr)..": "..tostring(self.design_window[attr]) end if sizeOnly then - return '#'..tostring(attr)..": "..tostring(#self.dig_window[attr]) + return '#'..tostring(attr)..": "..tostring(#self.design_window[attr]) else - return { tostring(attr)..": ", table.unpack(table_to_string(self.dig_window[attr], " ")) } + return { tostring(attr)..": ", table.unpack(table_to_string(self.design_window[attr], " ")) } end end, } } @@ -260,7 +260,7 @@ end MarksPanel = defclass(MarksPanel, widgets.ResizingPanel) MarksPanel.ATTRS { autoarrange_subviews = true, - dig_panel = DEFAULT_NIL + design_panel = DEFAULT_NIL } function MarksPanel:init() @@ -269,19 +269,19 @@ end function MarksPanel:update_mark_labels() self.subviews = {} local label_text = {} - if #self.dig_panel.marks >= 1 then - local first_mark = self.dig_panel.marks[1] + if #self.design_panel.marks >= 1 then + local first_mark = self.design_panel.marks[1] if first_mark then table.insert(label_text, string.format("First Mark (%d): %d, %d, %d ", 1, first_mark.x, first_mark.y, first_mark.z)) end end - if #self.dig_panel.marks > 1 then - local last_mark = self.dig_panel.marks[#self.dig_panel.marks] + if #self.design_panel.marks > 1 then + local last_mark = self.design_panel.marks[#self.design_panel.marks] if last_mark then table.insert(label_text, - string.format("Last Mark (%d): %d, %d, %d ", #self.dig_panel.marks, last_mark.x, last_mark.y, last_mark.z)) + string.format("Last Mark (%d): %d, %d, %d ", #self.design_panel.marks, last_mark.x, last_mark.y, last_mark.z)) end end @@ -290,7 +290,7 @@ function MarksPanel:update_mark_labels() table.insert(label_text, string.format("Mouse: %d, %d, %d", mouse_pos.x, mouse_pos.y, mouse_pos.z)) end - local mirror = self.dig_panel.mirror_point + local mirror = self.design_panel.mirror_point if mirror then table.insert(label_text, string.format("Mirror Point: %d, %d, %d", mirror.x, mirror.y, mirror.z)) end @@ -308,7 +308,7 @@ end ActionPanel = defclass(ActionPanel, widgets.ResizingPanel) ActionPanel.ATTRS { autoarrange_subviews = true, - dig_panel = DEFAULT_NIL + design_panel = DEFAULT_NIL } function ActionPanel:init() @@ -330,9 +330,9 @@ end function ActionPanel:get_action_text() local text = "" - if self.dig_panel.marks[1] and self.dig_panel.placing_mark.active then + if self.design_panel.marks[1] and self.design_panel.placing_mark.active then text = "Place the next point" - elseif not self.dig_panel.marks[1] then + elseif not self.design_panel.marks[1] then text = "Place the first point" elseif not self.parent_view.placing_extra.active and not self.parent_view.prev_center then text = "Select any draggable points" @@ -349,12 +349,12 @@ end function ActionPanel:get_area_text() local label = "Area: " - local bounds = self.dig_panel:get_view_bounds() + local bounds = self.design_panel:get_view_bounds() if not bounds then return label.."N/A" end local width = math.abs(bounds.x2 - bounds.x1) + 1 local height = math.abs(bounds.y2 - bounds.y1) + 1 local depth = math.abs(bounds.z2 - bounds.z1) + 1 - local tiles = self.dig_panel.shape.num_tiles * depth + local tiles = self.design_panel.shape.num_tiles * depth local plural = tiles > 1 and "s" or "" return label..("%dx%dx%d (%d tile%s)"):format( width, @@ -366,7 +366,7 @@ function ActionPanel:get_area_text() end function ActionPanel:get_mark_text(num) - local mark = self.dig_panel.marks[num] + local mark = self.design_panel.marks[num] local label = string.format("Mark %d: ", num) @@ -386,7 +386,7 @@ GenericOptionsPanel = defclass(GenericOptionsPanel, widgets.ResizingPanel) GenericOptionsPanel.ATTRS { name = DEFAULT_NIL, autoarrange_subviews = true, - dig_panel = DEFAULT_NIL, + design_panel = DEFAULT_NIL, on_layout_change = DEFAULT_NIL, } @@ -473,17 +473,17 @@ function GenericOptionsPanel:init() }, widgets.ResizingPanel { view_id = 'transform_panel_rotate', - visible = function() return self.dig_panel.subviews.transform:getOptionValue() end, + visible = function() return self.design_panel.subviews.transform:getOptionValue() end, subviews = { widgets.HotkeyLabel { key = 'STRING_A040', frame = { t = 1, l = 1 }, key_sep = '', - on_activate = self.dig_panel:callback('on_transform', 'ccw'), + on_activate = self.design_panel:callback('on_transform', 'ccw'), }, widgets.HotkeyLabel { key = 'STRING_A041', frame = { t = 1, l = 2 }, key_sep = ':', - on_activate = self.dig_panel:callback('on_transform', 'cw'), + on_activate = self.design_panel:callback('on_transform', 'cw'), }, widgets.WrappedLabel { frame = { t = 1, l = 5 }, @@ -492,12 +492,12 @@ function GenericOptionsPanel:init() widgets.HotkeyLabel { key = 'STRING_A095', frame = { t = 2, l = 1 }, key_sep = '', - on_activate = self.dig_panel:callback('on_transform', 'flipv'), + on_activate = self.design_panel:callback('on_transform', 'flipv'), }, widgets.HotkeyLabel { key = 'STRING_A061', frame = { t = 2, l = 2 }, key_sep = ':', - on_activate = self.dig_panel:callback('on_transform', 'fliph'), + on_activate = self.design_panel:callback('on_transform', 'fliph'), }, widgets.WrappedLabel { frame = { t = 2, l = 5 }, @@ -512,27 +512,27 @@ function GenericOptionsPanel:init() widgets.HotkeyLabel { key = 'CUSTOM_M', view_id = 'mirror_point_panel', - visible = function() return self.dig_panel.shape.can_mirror end, - label = function() if not self.dig_panel.mirror_point then return 'Place Mirror Point' else return 'Delete Mirror Point' end end, + visible = function() return self.design_panel.shape.can_mirror end, + label = function() if not self.design_panel.mirror_point then return 'Place Mirror Point' else return 'Delete Mirror Point' end end, active = true, - enabled = function() return not self.dig_panel.placing_extra.active and - not self.dig_panel.placing_mark.active and not self.prev_center + enabled = function() return not self.design_panel.placing_extra.active and + not self.design_panel.placing_mark.active and not self.prev_center end, on_activate = function() - if not self.dig_panel.mirror_point then - self.dig_panel.placing_mark.active = false - self.dig_panel.placing_extra.active = false - self.dig_panel.placing_extra.active = false - self.dig_panel.placing_mirror = true + if not self.design_panel.mirror_point then + self.design_panel.placing_mark.active = false + self.design_panel.placing_extra.active = false + self.design_panel.placing_extra.active = false + self.design_panel.placing_mirror = true else - self.dig_panel.placing_mirror = false - self.dig_panel.mirror_point = nil + self.design_panel.placing_mirror = false + self.design_panel.mirror_point = nil end end }, widgets.ResizingPanel { view_id = 'transform_panel_rotate', - visible = function() return self.dig_panel.mirror_point end, + visible = function() return self.design_panel.mirror_point end, subviews = { widgets.CycleHotkeyLabel { view_id = "mirror_horiz_label", @@ -545,7 +545,7 @@ function GenericOptionsPanel:init() options = { { label = "Off", value = 1 }, { label = "On (odd)", value = 2 }, { label = "On (even)", value = 3 } }, frame = { t = 1, l = 1 }, key_sep = '', - on_change = function() self.dig_panel.needs_update = true end + on_change = function() self.design_panel.needs_update = true end }, widgets.CycleHotkeyLabel { view_id = "mirror_diag_label", @@ -558,7 +558,7 @@ function GenericOptionsPanel:init() options = { { label = "Off", value = 1 }, { label = "On (odd)", value = 2 }, { label = "On (even)", value = 3 } }, frame = { t = 2, l = 1 }, key_sep = '', - on_change = function() self.dig_panel.needs_update = true end + on_change = function() self.design_panel.needs_update = true end }, widgets.CycleHotkeyLabel { view_id = "mirror_vert_label", @@ -571,7 +571,7 @@ function GenericOptionsPanel:init() options = { { label = "Off", value = 1 }, { label = "On (odd)", value = 2 }, { label = "On (even)", value = 3 } }, frame = { t = 3, l = 1 }, key_sep = '', - on_change = function() self.dig_panel.needs_update = true end + on_change = function() self.design_panel.needs_update = true end }, widgets.HotkeyLabel { view_id = "mirror_vert_label", @@ -583,9 +583,9 @@ function GenericOptionsPanel:init() initial_option = 1, frame = { t = 4, l = 1 }, key_sep = ': ', on_activate = function() - local points = self.dig_panel:get_mirrored_points(self.dig_panel.marks) - self.dig_panel.marks = points - self.dig_panel.mirror_point = nil + local points = self.design_panel:get_mirrored_points(self.design_panel.marks) + self.design_panel.marks = points + self.design_panel.mirror_point = nil end }, } @@ -599,13 +599,13 @@ function GenericOptionsPanel:init() label_width = 8, active = true, enabled = function() - return self.dig_panel.shape.invertable == true + return self.design_panel.shape.invertable == true end, show_tooltip = true, initial_option = false, on_change = function(new, old) - self.dig_panel.shape.invert = new - self.dig_panel.needs_update = true + self.design_panel.shape.invert = new + self.design_panel.needs_update = true end, }, widgets.HotkeyLabel { @@ -613,46 +613,46 @@ function GenericOptionsPanel:init() key = "CUSTOM_V", label = function() local msg = "Place extra point: " - if #self.dig_panel.extra_points < #self.dig_panel.shape.extra_points then - return msg..self.dig_panel.shape.extra_points[#self.dig_panel.extra_points + 1].label + if #self.design_panel.extra_points < #self.design_panel.shape.extra_points then + return msg..self.design_panel.shape.extra_points[#self.design_panel.extra_points + 1].label end return msg.."N/A" end, active = true, - visible = function() return self.dig_panel.shape and #self.dig_panel.shape.extra_points > 0 end, + visible = function() return self.design_panel.shape and #self.design_panel.shape.extra_points > 0 end, enabled = function() - if self.dig_panel.shape then - return #self.dig_panel.extra_points < #self.dig_panel.shape.extra_points + if self.design_panel.shape then + return #self.design_panel.extra_points < #self.design_panel.shape.extra_points end return false end, show_tooltip = true, on_activate = function() - if not self.dig_panel.placing_mark.active then - self.dig_panel.placing_extra.active = true - self.dig_panel.placing_extra.index = #self.dig_panel.extra_points + 1 - elseif #self.dig_panel.marks then + if not self.design_panel.placing_mark.active then + self.design_panel.placing_extra.active = true + self.design_panel.placing_extra.index = #self.design_panel.extra_points + 1 + elseif #self.design_panel.marks then local mouse_pos = dfhack.gui.getMousePos() - if mouse_pos then table.insert(self.dig_panel.extra_points, { x = mouse_pos.x, y = mouse_pos.y }) end + if mouse_pos then table.insert(self.design_panel.extra_points, { x = mouse_pos.x, y = mouse_pos.y }) end end - self.dig_panel.needs_update = true + self.design_panel.needs_update = true end, }, widgets.HotkeyLabel { view_id = "shape_toggle_placing_marks", key = "CUSTOM_B", label = function() - return (self.dig_panel.placing_mark.active) and "Stop placing" or "Start placing" + return (self.design_panel.placing_mark.active) and "Stop placing" or "Start placing" end, active = true, visible = true, enabled = function() - if not self.dig_panel.placing_mark.active and not self.dig_panel.prev_center then - return not self.dig_panel.shape.max_points or - #self.dig_panel.marks < self.dig_panel.shape.max_points - elseif not self.dig_panel.placing_extra.active and not self.dig_panel.prev_centerl then + if not self.design_panel.placing_mark.active and not self.design_panel.prev_center then + return not self.design_panel.shape.max_points or + #self.design_panel.marks < self.design_panel.shape.max_points + elseif not self.design_panel.placing_extra.active and not self.design_panel.prev_centerl then return true end @@ -660,16 +660,16 @@ function GenericOptionsPanel:init() end, show_tooltip = true, on_activate = function() - self.dig_panel.placing_mark.active = not self.dig_panel.placing_mark.active - self.dig_panel.placing_mark.index = (self.dig_panel.placing_mark.active) and #self.dig_panel.marks + 1 or + self.design_panel.placing_mark.active = not self.design_panel.placing_mark.active + self.design_panel.placing_mark.index = (self.design_panel.placing_mark.active) and #self.design_panel.marks + 1 or nil - if not self.dig_panel.placing_mark.active then - table.remove(self.dig_panel.marks, #self.dig_panel.marks) + if not self.design_panel.placing_mark.active then + table.remove(self.design_panel.marks, #self.design_panel.marks) else - self.dig_panel.placing_mark.continue = true + self.design_panel.placing_mark.continue = true end - self.dig_panel.needs_update = true + self.design_panel.needs_update = true end, }, widgets.HotkeyLabel { @@ -678,9 +678,9 @@ function GenericOptionsPanel:init() label = "Clear all points", active = true, enabled = function() - if #self.dig_panel.marks > 0 then return true - elseif self.dig_panel.shape then - if #self.dig_panel.extra_points < #self.dig_panel.shape.extra_points then + if #self.design_panel.marks > 0 then return true + elseif self.design_panel.shape then + if #self.design_panel.extra_points < #self.design_panel.shape.extra_points then return true end end @@ -690,13 +690,13 @@ function GenericOptionsPanel:init() disabled = false, show_tooltip = true, on_activate = function() - self.dig_panel.marks = {} - self.dig_panel.placing_mark.active = true - self.dig_panel.placing_mark.index = 1 - self.dig_panel.extra_points = {} - self.dig_panel.prev_center = nil - self.dig_panel.start_center = nil - self.dig_panel.needs_update = true + self.design_panel.marks = {} + self.design_panel.placing_mark.active = true + self.design_panel.placing_mark.index = 1 + self.design_panel.extra_points = {} + self.design_panel.prev_center = nil + self.design_panel.start_center = nil + self.design_panel.needs_update = true end, }, widgets.HotkeyLabel { @@ -705,8 +705,8 @@ function GenericOptionsPanel:init() label = "Clear extra points", active = true, enabled = function() - if self.dig_panel.shape then - if #self.dig_panel.extra_points > 0 then + if self.design_panel.shape then + if #self.design_panel.extra_points > 0 then return true end end @@ -714,16 +714,16 @@ function GenericOptionsPanel:init() return false end, disabled = false, - visible = function() return self.dig_panel.shape and #self.dig_panel.shape.extra_points > 0 end, + visible = function() return self.design_panel.shape and #self.design_panel.shape.extra_points > 0 end, show_tooltip = true, on_activate = function() - if self.dig_panel.shape then - self.dig_panel.extra_points = {} - self.dig_panel.prev_center = nil - self.dig_panel.start_center = nil - self.dig_panel.placing_extra = { active = false, index = 0 } - self.dig_panel:updateLayout() - self.dig_panel.needs_update = true + if self.design_panel.shape then + self.design_panel.extra_points = {} + self.design_panel.prev_center = nil + self.design_panel.start_center = nil + self.design_panel.placing_extra = { active = false, index = 0 } + self.design_panel:updateLayout() + self.design_panel.needs_update = true end end, }, @@ -737,7 +737,7 @@ function GenericOptionsPanel:init() show_tooltip = true, initial_option = true, on_change = function(new, old) - self.dig_panel.show_guides = new + self.design_panel.show_guides = new end, }, widgets.CycleHotkeyLabel { @@ -792,7 +792,7 @@ function GenericOptionsPanel:init() }, disabled = false, show_tooltip = true, - on_change = function(new, old) self.dig_panel:updateLayout() end, + on_change = function(new, old) self.design_panel:updateLayout() end, }, widgets.ResizingPanel { view_id = 'stairs_type_panel', @@ -835,7 +835,7 @@ function GenericOptionsPanel:init() view_id = "building_outer_config", frame = { t = 0, l = 1 }, text = { { tile = BUTTON_PEN_LEFT }, { tile = HELP_PEN_CENTER }, { tile = BUTTON_PEN_RIGHT } }, - on_click = self.dig_panel:callback("show_help", CONSTRUCTION_HELP) + on_click = self.design_panel:callback("show_help", CONSTRUCTION_HELP) }, widgets.CycleHotkeyLabel { view_id = "building_outer_tiles", @@ -851,7 +851,7 @@ function GenericOptionsPanel:init() view_id = "building_inner_config", frame = { t = 1, l = 1 }, text = { { tile = BUTTON_PEN_LEFT }, { tile = HELP_PEN_CENTER }, { tile = BUTTON_PEN_RIGHT } }, - on_click = self.dig_panel:callback("show_help", CONSTRUCTION_HELP) + on_click = self.design_panel:callback("show_help", CONSTRUCTION_HELP) }, widgets.CycleHotkeyLabel { view_id = "building_inner_tiles", @@ -868,7 +868,7 @@ function GenericOptionsPanel:init() widgets.WrappedLabel { view_id = "shape_prio_label", text_to_wrap = function() - return "Priority: "..tostring(self.dig_panel.prio) + return "Priority: "..tostring(self.design_panel.prio) end, }, widgets.HotkeyLabel { @@ -877,14 +877,14 @@ function GenericOptionsPanel:init() label = "Increase Priority", active = true, enabled = function() - return self.dig_panel.prio > 1 + return self.design_panel.prio > 1 end, disabled = false, show_tooltip = true, on_activate = function() - self.dig_panel.prio = self.dig_panel.prio - 1 - self.dig_panel:updateLayout() - self.dig_panel.needs_update = true + self.design_panel.prio = self.design_panel.prio - 1 + self.design_panel:updateLayout() + self.design_panel.needs_update = true end, }, widgets.HotkeyLabel { @@ -893,14 +893,14 @@ function GenericOptionsPanel:init() label = "Decrease Priority", active = true, enabled = function() - return self.dig_panel.prio < 7 + return self.design_panel.prio < 7 end, disabled = false, show_tooltip = true, on_activate = function() - self.dig_panel.prio = self.dig_panel.prio + 1 - self.dig_panel:updateLayout() - self.dig_panel.needs_update = true + self.design_panel.prio = self.design_panel.prio + 1 + self.design_panel:updateLayout() + self.design_panel.needs_update = true end, }, widgets.ToggleHotkeyLabel { @@ -908,13 +908,13 @@ function GenericOptionsPanel:init() key = "CUSTOM_C", label = "Auto-Commit: ", active = true, - enabled = function() return self.dig_panel.shape.max_points end, + enabled = function() return self.design_panel.shape.max_points end, disabled = false, show_tooltip = true, initial_option = true, on_change = function(new, old) - self.dig_panel.autocommit = new - self.dig_panel.needs_update = true + self.design_panel.autocommit = new + self.design_panel.needs_update = true end, }, widgets.HotkeyLabel { @@ -923,33 +923,33 @@ function GenericOptionsPanel:init() label = "Commit Designation", active = true, enabled = function() - return #self.dig_panel.marks >= self.dig_panel.shape.min_points + return #self.design_panel.marks >= self.design_panel.shape.min_points end, disabled = false, show_tooltip = true, on_activate = function() - self.dig_panel:commit() - self.dig_panel.needs_update = true + self.design_panel:commit() + self.design_panel.needs_update = true end, }, } end function GenericOptionsPanel:is_mode_selected(mode) - return self.dig_panel.subviews.mode_name:getOptionValue().desig == mode + return self.design_panel.subviews.mode_name:getOptionValue().desig == mode end function GenericOptionsPanel:change_shape(new, old) - self.dig_panel.shape = shapes.all_shapes[new] - if self.dig_panel.shape.max_points and #self.dig_panel.marks > self.dig_panel.shape.max_points then + self.design_panel.shape = shapes.all_shapes[new] + if self.design_panel.shape.max_points and #self.design_panel.marks > self.design_panel.shape.max_points then -- pop marks until we're down to the max of the new shape - for i = #self.dig_panel.marks, self.dig_panel.shape.max_points, -1 do - table.remove(self.dig_panel.marks, i) + for i = #self.design_panel.marks, self.design_panel.shape.max_points, -1 do + table.remove(self.design_panel.marks, i) end end - self.dig_panel:add_shape_options() - self.dig_panel.needs_update = true - self.dig_panel:updateLayout() + self.design_panel:add_shape_options() + self.design_panel.needs_update = true + self.design_panel:updateLayout() end -- @@ -997,7 +997,7 @@ local PENS = {} Design = defclass(Design, widgets.Window) Design.ATTRS { - name = "dig_window", + name = "design_window", frame_title = "Design", frame = { w = 40, @@ -1157,18 +1157,18 @@ function Design:init() self:addviews { ActionPanel { view_id = "action_panel", - dig_panel = self, + design_panel = self, get_extra_pt_count = function() return #self.extra_points end, }, MarksPanel { view_id = "marks_panel", - dig_panel = self, + design_panel = self, }, GenericOptionsPanel { view_id = "generic_panel", - dig_panel = self, + design_panel = self, } } end @@ -1934,13 +1934,13 @@ function Design:get_mirrored_points(points) return points end -function Dig:show_help(text) +function Design:show_help(text) self.parent_view.help_window.message = text self.parent_view.help_window.visible = true self.parent_view:updateLayout() end -function Dig:dismiss_help() +function Design:dismiss_help() self.parent_view.help_window.visible = false end @@ -1950,19 +1950,19 @@ end DesignScreen = defclass(DesignScreen, gui.ZScreen) DesignScreen.ATTRS { - focus_path = "dig", + focus_path = "design", pass_pause = true, pass_movement_keys = true, } function DesignScreen:init() - self.dig_window = Dig {} + self.design_window = Design {} self.help_window = HelpWindow {} self.help_window.visible = false - self:addviews { self.dig_window, self.help_window } + self:addviews { self.design_window, self.help_window } if SHOW_DEBUG_WINDOW then - self.debug_window = DesignDebugWindow { dig_window = self.dig_window } + self.debug_window = DesignDebugWindow { design_window = self.design_window } self:addviews { self.debug_window } end end From bd295cdcf7315539616fbb1fc3f91c681aad00b3 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Sun, 19 Mar 2023 20:02:08 -0400 Subject: [PATCH 046/732] Add fix for dismissing help --- gui/design.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/design.lua b/gui/design.lua index 8b9f0a8699..bc0aec5bcd 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1584,7 +1584,7 @@ function Design:onInput(keys) if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then -- Close help window if open - if view.help_window.visible then view:dismiss_help() return true end + if view.help_window.visible then self:dismiss_help() return true end -- If center draggin, put the shape back to the original center if self.prev_center then From 2ad4dd64f7a595f85430de645ac53ff0a8681fa0 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Sun, 19 Mar 2023 22:27:38 -0400 Subject: [PATCH 047/732] changelog --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index cd809a6433..136847112f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,8 @@ that repo. ## Fixes ## Misc Improvements +- `gui/design`: ``gui/dig`` renamed to ``gui/design`` +- `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. ## Removed @@ -34,7 +36,6 @@ that repo. ## Misc Improvements - `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! -- `gui/dig`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. - `gui/gm-editor`: can now open the selected stockpile if run without parameters # 50.07-alpha3 From 370721c81710466a26cec27a182b438ad230ff36 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Sun, 19 Mar 2023 23:05:20 -0400 Subject: [PATCH 048/732] fix title underline --- docs/gui/design.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 40150345b1..2c2e2feb8b 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -1,6 +1,6 @@ gui/design -======= +========== .. dfhack-tool:: :summary: Design designation utility with shapes. From b898c308f5032a7061cda24baa1a8e9e918782d2 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 21 Mar 2023 13:00:53 +1100 Subject: [PATCH 049/732] Update to feedback. --- gui/seedwatch.lua | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua index d59937df87..7b824665c3 100644 --- a/gui/seedwatch.lua +++ b/gui/seedwatch.lua @@ -12,9 +12,7 @@ local MAX_TARGET = 2147483647 -- SeedSettings = defclass(SeedSettings, widgets.Window) SeedSettings.ATTRS{ - lockable=false, frame={l=5, t=5, w=35, h=9}, - data={id='', name='', quantity=0, target=0,}, } function SeedSettings:init() @@ -23,11 +21,6 @@ function SeedSettings:init() frame={t=0, l=0}, text='Seed: ', }, - widgets.Label{ - view_id='id', - frame={t=0, l=0}, - visible=false, - }, widgets.Label{ view_id='name', frame={t=0, l=6}, @@ -62,11 +55,9 @@ end function SeedSettings:show(choice, on_commit) self.data = choice.data self.on_commit = on_commit - local data = self.data - self.subviews.id:setText(data.id) - self.subviews.name:setText(data.name) - self.subviews.quantity:setText(tostring(data.quantity)) - self.subviews.target:setText(tostring(data.target)) + self.subviews.name:setText(self.data.name) + self.subviews.quantity:setText(tostring(self.data.quantity)) + self.subviews.target:setText(tostring(self.data.target)) self.visible = true self:setFocus(true) self:updateLayout() @@ -78,13 +69,10 @@ function SeedSettings:hide() end function SeedSettings:commit() - local target = math.tointeger(self.subviews.target.text) - if not target or target == '' then - target = 0 - elseif target > MAX_TARGET then - target = MAX_TARGET - end - plugin.seedwatch_setTarget(self.subviews.id.text, target) + local target = math.tointeger(self.subviews.target.text) or 0 + target = math.min(MAX_TARGET, math.max(0, target)) + + plugin.seedwatch_setTarget(self.data.id, target) self:hide() self.on_commit() end @@ -109,7 +97,6 @@ Seedwatch.ATTRS { resize_min={h=25}, hide_unmonitored=DEFAULT_NIL, manual_hide_unmonitored_touched=DEFAULT_NIL, - data = {sum=0, seeds_qty=0, seeds_watched=0, seeds = {{id='', name='', quantity=0, target=0,}}}, } function Seedwatch:init() @@ -211,10 +198,6 @@ function Seedwatch:init() self:refresh_data() end -function Seedwatch:getDefaultHide() - return false -end - function Seedwatch:configure_seed(idx, choice) self.subviews.seed_settings:show(choice, function() self:refresh_data() From ad3cd59b795e00900ec1d7048defdc93da461cce Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 21 Mar 2023 14:33:19 +1100 Subject: [PATCH 050/732] Latest change log --- changelog.txt | 59 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/changelog.txt b/changelog.txt index d4ab79f9f8..cc0e6fc19d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,28 +14,68 @@ that repo. # Future ## New Scripts -- `combine`: combines stacks of food and plant items. Merge of `combine-plants` and `combine-drinks`. - `gui/seedwatch`: GUI config and status panel interface for `seedwatch`. ## Fixes + +## Misc Improvements +- `gui/design`: ``gui/dig`` renamed to ``gui/design`` +- `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. + +## Removed + +# 50.07-beta1 + +## New Scripts +- `suspendmanager`: automatic job suspension management (replaces `autounsuspend`) +- `gui/suspendmanager`: graphical configuration interface for `suspendmanager` +- `suspend`: suspends building construction jobs + +## Fixes +- `quicksave`: now reliably triggers an autosave, even if one has been performed recently +- `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" + +## Misc Improvements +- `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! +- `gui/gm-editor`: can now open the selected stockpile if run without parameters + +# 50.07-alpha3 + +## Fixes +-@ `gui/create-item`: fix generic corpsepiece spawning + +## Misc Improvements +- `gui/create-item`: added ability to spawn 'whole' corpsepieces (every layer of a part) +-@ `gui/dig`: Allow placing an extra point (curve) while still placing the second main point +-@ `gui/dig`: Allow placing n-point shapes, shape rotation/mirroring +-@ `gui/dig`: Allow second bezier point, mirror-mode for freeform shapes, symmetry mode + +# 50.07-alpha2 + +## New Scripts +- `combine`: combines stacks of food and plant items. + +## Fixes +- `troubleshoot-item`: fix printing of job details for chosen item - `makeown`: fixes errors caused by using makeown on an invader -- `gui/blueprint`: correctly use setting presets passed on the commandline -- `gui/quickfort`: correctly use settings presets passed on the commandline +-@ `gui/blueprint`: correctly use setting presets passed on the commandline +-@ `gui/quickfort`: correctly use settings presets passed on the commandline - `devel/query`: can now properly index vectors in the --table argument -@ `forbid`: fix detection of unreachable items for items in containers -@ `unforbid`: fix detection of unreachable items for items in containers ## Misc Improvements +- `troubleshoot-item`: output as bullet point list with indenting, with item description and ID at top +- `troubleshoot-item`: reports on items that are hidden, artifacts, in containers, and held by a unit +- `troubleshoot-item`: reports on the contents of containers with counts for each contained item type - `devel/visualize-structure`: now automatically inspects the contents of most pointer fields, rather than inspecting the pointers themselves - `devel/query`: will now search for jobs at the map coordinate highlighted, if no explicit job is highlighted and there is a map tile highlighted -- Add overlay to caravan.lua visible on the trade screen that enables: - - Selecting all items inside a bin, minus the bin - - Collapsing all bins or a single bin - - Collapsing all categories --@ `devel/hello-world`: updated to use ZScreen +- `caravan`: add trade screen overlay that assists with seleting groups of items and collapsing groups in the UI +- `gui/gm-editor` will now inspect a selected building itself if the building has no current jobs ## Removed -- `combine-drinks` and `combine-plants` +- `combine-drinks`: replaced by `combine` +- `combine-plants`: replaced by `combine` # 50.07-alpha1 @@ -53,6 +93,7 @@ that repo. - `gui/gm-editor`: now supports multiple independent data inspection windows - `gui/gm-editor`: now prints out contents of coordinate vars instead of just the type - `rejuvenate`: now takes an --age parameter to choose a desired age. +- `gui/dig` : Added 'Line' shape that also can draw curves, added draggable center handle # 50.05-alpha3.1 From 9890979d08a11a144504d53185910988f769daf3 Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 21 Mar 2023 21:49:03 -0700 Subject: [PATCH 051/732] Update gui/seedwatch.lua remove leftover (seeming) code --- gui/seedwatch.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua index 7b824665c3..d1dc134bdf 100644 --- a/gui/seedwatch.lua +++ b/gui/seedwatch.lua @@ -95,8 +95,6 @@ Seedwatch.ATTRS { frame={w=60, h=27}, resizable=true, resize_min={h=25}, - hide_unmonitored=DEFAULT_NIL, - manual_hide_unmonitored_touched=DEFAULT_NIL, } function Seedwatch:init() From dc439c58ee9b5d9f4ee78e30f6d2888cf2b25bfb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 Mar 2023 04:49:46 +0000 Subject: [PATCH 052/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- gui/seedwatch.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua index d1dc134bdf..30ae9dc8d0 100644 --- a/gui/seedwatch.lua +++ b/gui/seedwatch.lua @@ -71,7 +71,7 @@ end function SeedSettings:commit() local target = math.tointeger(self.subviews.target.text) or 0 target = math.min(MAX_TARGET, math.max(0, target)) - + plugin.seedwatch_setTarget(self.data.id, target) self:hide() self.on_commit() From 4874d8c331b7d19d9af1fdf749d5b9d4b73fed8a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Mar 2023 18:33:00 -0700 Subject: [PATCH 053/732] support overlays from scripts with capital letters --- changelog.txt | 3 ++- gui/control-panel.lua | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index cc0e6fc19d..234bd689d0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,10 +19,11 @@ that repo. ## Fixes ## Misc Improvements -- `gui/design`: ``gui/dig`` renamed to ``gui/design`` +- `gui/control-panel`: Now detects overlays from scripts named with capital letters - `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. ## Removed +- ``gui/dig``: renamed to ``gui/design`` # 50.07-beta1 diff --git a/gui/control-panel.lua b/gui/control-panel.lua index da204a6cda..6359dff376 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -221,11 +221,13 @@ function ConfigPanel:onInput(keys) return handled end +local COMMAND_REGEX = '^([%w/_-]+)' + function ConfigPanel:refresh() local choices = {} for _,choice in ipairs(self:get_choices()) do local command = choice.target or choice.command - command = command:match('^([%l/_-]+)') + command = command:match(COMMAND_REGEX) local gui_config = 'gui/' .. command local want_gui_config = utils.getval(self.is_configurable) and helpdb.is_entry(gui_config) @@ -293,7 +295,7 @@ end function ConfigPanel:show_help() _,choice = self.subviews.list:getSelected() if not choice then return end - local command = choice.target:match('^([%l/_-]+)') + local command = choice.target:match(COMMAND_REGEX) dfhack.run_command('gui/launcher', command .. ' ') end From f258004924f77931ef9defaf28653cd910f32be2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 23 Mar 2023 01:32:53 -0700 Subject: [PATCH 054/732] support quiet combine and add maintenace script --- changelog.txt | 3 +++ combine.lua | 42 ++++++++++++++++++++++++++---------------- docs/combine.rst | 7 +++++-- gui/control-panel.lua | 5 ++++- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/changelog.txt b/changelog.txt index cc0e6fc19d..22687687bd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -21,6 +21,9 @@ that repo. ## Misc Improvements - `gui/design`: ``gui/dig`` renamed to ``gui/design`` - `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. +- `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor +- `combine`: added ``--quiet`` option for no output when there are no changes +- `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles ## Removed diff --git a/combine.lua b/combine.lua index a6045981d3..b1b4037474 100644 --- a/combine.lua +++ b/combine.lua @@ -8,7 +8,8 @@ local opts, args = { here = nil, dry_run = false, types = nil, - verbose = false + quiet = false, + verbose = false, }, {...} -- default max stack size of 30 @@ -156,11 +157,18 @@ local function print_stacks_details(stacks) end end -local function print_stacks_summary(stacks) +local function print_stacks_summary(stacks, quiet) -- print stacks summary to the console - dfhack.print('Summary:\n') - for _, stacks_type in pairs(stacks) do - dfhack.print((' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(stacks_type.type_name, stacks_type.type_id, stacks_type.item_qty, stacks_type.max_stack_size, stacks_type.before_stacks, stacks_type.after_stacks)) + local printed = 0 + for _, s in pairs(stacks) do + if s.before_stacks ~= s.after_stacks then + printed = printed + 1 + print(('combined %d %s items from %d stacks into %d') + :format(s.item_qty, s.type_name, s.before_stacks, s.after_stacks)) + end + end + if printed == 0 and not quiet then + print('All stacks already optimally combined.') end end @@ -306,20 +314,23 @@ local function get_stockpile_all() table.insert(stockpiles, building) end end - dfhack.print(('Stockpile(all): %d found\n'):format(#stockpiles)) + if opts.verbose then + print(('Stockpile(all): %d found'):format(#stockpiles)) + end return stockpiles end local function get_stockpile_here() - -- attempt to get the stockpile located at the game cursor, or exit with error + -- attempt to get the selected stockpile, or exit with error -- return the stockpile as a table local stockpiles = {} - local pos = argparse.coords('here', 'here') - local building = dfhack.buildings.findAtTile(pos) - if not building or building:getType() ~= df.building_type.Stockpile then qerror('Stockpile not found at game cursor position.') end + local building = dfhack.gui.getSelectedStockpile() + if not building then qerror('Please select a stockpile.') end table.insert(stockpiles, building) local items = dfhack.buildings.getStockpileContents(building) - dfhack.print(('Stockpile(here): %s <%d> #items:%d\n'):format(building.name, building.id, #items)) + if opts.verbose then + print(('Stockpile(here): %s <%d> #items:%d'):format(building.name, building.id, #items)) + end return stockpiles end @@ -363,8 +374,9 @@ local function parse_commandline(opts, args) local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, - {'d', 'dry-run', handler=function(optarg) opts.dry_run = true end}, - {'v', 'verbose', handler=function(optarg) opts.verbose = true end}, + {'d', 'dry-run', handler=function() opts.dry_run = true end}, + {'q', 'quiet', handler=function() opts.quiet = true end}, + {'v', 'verbose', handler=function() opts.verbose = true end}, }) -- if stockpile option is not specificed, then default to all @@ -380,10 +392,8 @@ local function parse_commandline(opts, args) if not opts.types then opts.types = valid_types_map['all'] end - end - -- main program starts here local function main() @@ -409,7 +419,7 @@ local function main() end print_stacks_details(stacks) - print_stacks_summary(stacks) + print_stacks_summary(stacks, opts.quiet) end diff --git a/docs/combine.rst b/docs/combine.rst index be5724edf6..e9ca96aa39 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -23,14 +23,14 @@ Examples ``combine all --types=meat,plant`` Merge ``meat`` and ``plant`` type stacks in all stockpiles. ``combine here`` - Merge stacks in stockpile located at game cursor. + Merge stacks in the selected stockpile. Commands -------- ``all`` Search all stockpiles. ``here`` - Search the stockpile under the game cursor. + Search the currently selected stockpile. Options ------- @@ -55,5 +55,8 @@ Options ``plant``: PLANT and PLANT_GROWTH +``-q``, ``--quiet`` + Only print changes instead of a summary of all processed stockpiles. + ``-v``, ``--verbose`` Print verbose output. diff --git a/gui/control-panel.lua b/gui/control-panel.lua index da204a6cda..d02a05ea4a 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -76,6 +76,9 @@ local REPEATS = { ['cleanowned']={ desc='Encourage dwarves to drop tattered clothing and grab new ones.', command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, + ['combine']={ + desc='Combine partial stacks in stockpiles into full stacks.', + command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, ['orders-sort']={ desc='Sort manager orders by repeat frequency so one-time orders can be completed.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, @@ -92,7 +95,7 @@ table.sort(REPEATS_LIST) -- save_fn takes the file as a param and should call f:write() to write data local function save_file(path, save_fn) local ok, f = pcall(io.open, path, 'w') - if not ok then + if not ok or not f then dialogs.showMessage('Error', ('Cannot open file for writing: "%s"'):format(path)) return From 12a4ec5567effd34fe6a8d0f7ee93943f0b4094b Mon Sep 17 00:00:00 2001 From: John Cosker Date: Thu, 23 Mar 2023 09:32:09 -0400 Subject: [PATCH 055/732] call updateLayout less, improve performance --- gui/design.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index bc0aec5bcd..ef2e0dcdd0 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -302,6 +302,7 @@ function MarksPanel:update_mark_labels() } } + self:updateLayout() end -- Panel to show the Mouse position/dimensions/etc @@ -469,7 +470,7 @@ function GenericOptionsPanel:init() active = true, enabled = true, initial_option = false, - on_change = nil + on_change = function() self.design_panel.needs_update = true end }, widgets.ResizingPanel { view_id = 'transform_panel_rotate', @@ -1513,10 +1514,10 @@ function Design:onRenderFrame(dc, rect) self.shape:update(points, self.extra_points) self.last_mouse_point = mouse_pos self.needs_update = false + self:add_shape_options() + self:updateLayout() end - self:add_shape_options() - -- Generate bounds based on the shape's dimensions local bounds = self:get_view_bounds() if self.shape and bounds then @@ -1565,8 +1566,6 @@ function Design:onRenderFrame(dc, rect) end guidm.renderMapOverlay(get_overlay_pen, bounds) - - self:updateLayout() end -- TODO function too long From 43a5a4712da762017754f29bf98b198bed0dd19e Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Thu, 23 Mar 2023 20:25:11 +0100 Subject: [PATCH 056/732] Suspendmanager: Don't suspend non blocking jobs --- suspendmanager.lua | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 67370bfaf6..1a370024bd 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -78,6 +78,32 @@ function foreach_construction_job(fn) end end +local CONSTRUCTION_IMPASSABLE = { + [df.construction_type.Wall]=true, + [df.construction_type.Fortification]=true, +} + +local BUILDING_IMPASSABLE = { + [df.building_type.Floodgate]=true, + [df.building_type.Statue]=true, + [df.building_type.WindowGlass]=true, + [df.building_type.WindowGem]=true, + [df.building_type.GrateWall]=true, + [df.building_type.BarsVertical]=true, +} + +--- Check if a building is blocking once constructed +---@param building building_constructionst|building +---@return boolean +local function isImpassable(building) + local type = building:getType() + if type == df.building_type.Construction then + return CONSTRUCTION_IMPASSABLE[building.type] + else + return BUILDING_IMPASSABLE[type] + end +end + --- True if there is a construction plan to build an unwalkable tile ---@param pos coord ---@return boolean @@ -89,7 +115,7 @@ local function plansToConstructImpassableAt(pos) -- The building is already created return false end - return building:isImpassableAtCreation() + return isImpassable(building) end --- Check if the tile can be walked on @@ -142,7 +168,7 @@ function isBlocking(job) local building = dfhack.job.getHolder(job) --- Not building a blocking construction, no risk - if not building or not building:isImpassableAtCreation() then return false end + if not building or not isImpassable(building) then return false end --- job.pos is sometimes off by one, get the building pos local pos = {x=building.centerx,y=building.centery,z=building.z} From e456128382fc7803d17839503ab00bea59762be8 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Thu, 23 Mar 2023 20:27:06 +0100 Subject: [PATCH 057/732] changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 807b21c604..f883e7df74 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ that repo. - `gui/seedwatch`: GUI config and status panel interface for `seedwatch`. ## Fixes +- `suspendmanager`: does not suspend non blocking jobs such as floor bars or bridges anymore ## Misc Improvements - `gui/control-panel`: Now detects overlays from scripts named with capital letters From 797880be49045c4c61281f0e0b45b3c7b71f9909 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Mar 2023 22:31:46 -0700 Subject: [PATCH 058/732] untested -> unavailable --- docs/adaptation.rst | 2 +- docs/add-recipe.rst | 2 +- docs/add-thought.rst | 2 +- docs/adv-fix-sleepers.rst | 2 +- docs/adv-max-skills.rst | 2 +- docs/adv-rumors.rst | 2 +- docs/assign-minecarts.rst | 2 +- docs/assign-profile.rst | 2 +- docs/autolabor-artisans.rst | 2 +- docs/autounsuspend.rst | 2 +- docs/binpatch.rst | 2 +- docs/bodyswap.rst | 2 +- docs/break-dance.rst | 2 +- docs/build-now.rst | 2 +- docs/burial.rst | 2 +- docs/cannibalism.rst | 2 +- docs/color-schemes.rst | 2 +- docs/combat-harden.rst | 2 +- docs/deteriorate.rst | 2 +- docs/devel/block-borders.rst | 2 +- docs/devel/cmptiles.rst | 2 +- docs/devel/dump-offsets.rst | 2 +- docs/devel/export-dt-ini.rst | 2 +- docs/devel/find-offsets.rst | 2 +- docs/devel/find-primitive.rst | 2 +- docs/devel/find-twbt.rst | 2 +- docs/devel/inject-raws.rst | 2 +- docs/devel/kill-hf.rst | 2 +- docs/devel/light.rst | 2 +- docs/devel/list-filters.rst | 2 +- docs/devel/lsmem.rst | 2 +- docs/devel/lua-example.rst | 2 +- docs/devel/luacov.rst | 2 +- docs/devel/nuke-items.rst | 2 +- docs/devel/prepare-save.rst | 2 +- docs/devel/print-args.rst | 2 +- docs/devel/print-args2.rst | 2 +- docs/devel/print-event.rst | 2 +- docs/devel/save-version.rst | 2 +- docs/devel/sc.rst | 2 +- docs/devel/test-perlin.rst | 2 +- docs/devel/unit-path.rst | 2 +- docs/devel/watch-minecarts.rst | 2 +- docs/do-job-now.rst | 2 +- docs/dwarf-op.rst | 2 +- docs/embark-skills.rst | 2 +- docs/exportlegends.rst | 2 +- docs/fix-ster.rst | 2 +- docs/fix/corrupt-equipment.rst | 2 +- docs/fix/item-occupancy.rst | 2 +- docs/fix/population-cap.rst | 2 +- docs/fix/tile-occupancy.rst | 2 +- docs/fixnaked.rst | 2 +- docs/flashstep.rst | 2 +- docs/forget-dead-body.rst | 2 +- docs/forum-dwarves.rst | 2 +- docs/gaydar.rst | 2 +- docs/ghostly.rst | 2 +- docs/growcrops.rst | 2 +- docs/gui/advfort.rst | 2 +- docs/gui/autogems.rst | 2 +- docs/gui/choose-weapons.rst | 2 +- docs/gui/clone-uniform.rst | 2 +- docs/gui/color-schemes.rst | 2 +- docs/gui/companion-order.rst | 2 +- docs/gui/create-tree.rst | 2 +- docs/gui/design.rst | 2 +- docs/gui/dfstatus.rst | 2 +- docs/gui/extended-status.rst | 2 +- docs/gui/family-affairs.rst | 2 +- docs/gui/guide-path.rst | 2 +- docs/gui/kitchen-info.rst | 2 +- docs/gui/launcher.rst | 2 +- docs/gui/load-screen.rst | 2 +- docs/gui/manager-quantity.rst | 2 +- docs/gui/mechanisms.rst | 2 +- docs/gui/mod-manager.rst | 2 +- docs/gui/petitions.rst | 2 +- docs/gui/power-meter.rst | 2 +- docs/gui/quantum.rst | 2 +- docs/gui/rename.rst | 2 +- docs/gui/room-list.rst | 2 +- docs/gui/seedwatch.rst | 2 +- docs/gui/settings-manager.rst | 2 +- docs/gui/siege-engine.rst | 2 +- docs/gui/stamper.rst | 2 +- docs/gui/stockpiles.rst | 2 +- docs/gui/suspendmanager.rst | 2 +- docs/gui/teleport.rst | 2 +- docs/gui/unit-info-viewer.rst | 2 +- docs/gui/workflow.rst | 2 +- docs/gui/workorder-details.rst | 2 +- docs/gui/workshop-job.rst | 2 +- docs/hotkey-notes.rst | 2 +- docs/launch.rst | 2 +- docs/light-aquifers-only.rst | 2 +- docs/linger.rst | 2 +- docs/list-waves.rst | 2 +- docs/load-save.rst | 2 +- docs/make-legendary.rst | 2 +- docs/markdown.rst | 2 +- docs/max-wave.rst | 2 +- docs/modtools/add-syndrome.rst | 2 +- docs/modtools/anonymous-script.rst | 2 +- docs/modtools/change-build-menu.rst | 2 +- docs/modtools/create-item.rst | 2 +- docs/modtools/create-tree.rst | 2 +- docs/modtools/create-unit.rst | 2 +- docs/modtools/equip-item.rst | 2 +- docs/modtools/extra-gamelog.rst | 2 +- docs/modtools/fire-rate.rst | 2 +- docs/modtools/if-entity.rst | 2 +- docs/modtools/interaction-trigger.rst | 2 +- docs/modtools/invader-item-destroyer.rst | 2 +- docs/modtools/item-trigger.rst | 2 +- docs/modtools/moddable-gods.rst | 2 +- docs/modtools/outside-only.rst | 2 +- docs/modtools/pref-edit.rst | 2 +- docs/modtools/projectile-trigger.rst | 2 +- docs/modtools/random-trigger.rst | 2 +- docs/modtools/raw-lint.rst | 2 +- docs/modtools/reaction-product-trigger.rst | 2 +- docs/modtools/reaction-trigger-transition.rst | 2 +- docs/modtools/reaction-trigger.rst | 2 +- docs/modtools/set-belief.rst | 2 +- docs/modtools/set-need.rst | 2 +- docs/modtools/set-personality.rst | 2 +- docs/modtools/skill-change.rst | 2 +- docs/modtools/spawn-flow.rst | 2 +- docs/modtools/syndrome-trigger.rst | 2 +- docs/modtools/transform-unit.rst | 2 +- docs/names.rst | 2 +- docs/open-legends.rst | 2 +- docs/points.rst | 2 +- docs/pop-control.rst | 2 +- docs/prefchange.rst | 2 +- docs/putontable.rst | 2 +- docs/questport.rst | 2 +- docs/region-pops.rst | 2 +- docs/resurrect-adv.rst | 2 +- docs/reveal-adv-map.rst | 2 +- docs/season-palette.rst | 2 +- docs/set-orientation.rst | 2 +- docs/siren.rst | 2 +- docs/spawnunit.rst | 2 +- docs/startdwarf.rst | 2 +- docs/suspend.rst | 2 +- docs/tidlers.rst | 2 +- docs/timestream.rst | 2 +- docs/undump-buildings.rst | 2 +- docs/uniform-unstick.rst | 2 +- docs/unretire-anyone.rst | 2 +- docs/view-item-info.rst | 2 +- docs/view-unit-reports.rst | 2 +- docs/warn-stealers.rst | 2 +- docs/workorder-recheck.rst | 2 +- gui/launcher.lua | 2 +- 157 files changed, 157 insertions(+), 157 deletions(-) diff --git a/docs/adaptation.rst b/docs/adaptation.rst index a216a30e5a..140fa81a21 100644 --- a/docs/adaptation.rst +++ b/docs/adaptation.rst @@ -3,7 +3,7 @@ adaptation .. dfhack-tool:: :summary: Adjust a unit's cave adaptation level. - :tags: untested fort armok units + :tags: unavailable fort armok units View or set level of cavern adaptation for the selected unit or the whole fort. diff --git a/docs/add-recipe.rst b/docs/add-recipe.rst index 23f7b1ca12..2263dafe39 100644 --- a/docs/add-recipe.rst +++ b/docs/add-recipe.rst @@ -3,7 +3,7 @@ add-recipe .. dfhack-tool:: :summary: Add crafting recipes to a civ. - :tags: untested adventure fort gameplay + :tags: unavailable adventure fort gameplay Civilizations pick randomly from a pool of possible recipes, which means not all civs get high boots, for instance. This script can help fix that. Only weapons, diff --git a/docs/add-thought.rst b/docs/add-thought.rst index aee8ed84a4..b2d9e5e5c8 100644 --- a/docs/add-thought.rst +++ b/docs/add-thought.rst @@ -3,7 +3,7 @@ add-thought .. dfhack-tool:: :summary: Adds a thought to the selected unit. - :tags: untested fort armok units + :tags: unavailable fort armok units Usage ----- diff --git a/docs/adv-fix-sleepers.rst b/docs/adv-fix-sleepers.rst index 2a8fe80821..befa194dc0 100644 --- a/docs/adv-fix-sleepers.rst +++ b/docs/adv-fix-sleepers.rst @@ -3,7 +3,7 @@ adv-fix-sleepers .. dfhack-tool:: :summary: Fix units who refuse to awaken in adventure mode. - :tags: untested adventure bugfix units + :tags: unavailable adventure bugfix units Use this tool if you encounter sleeping units who refuse to awaken regardless of talking to them, hitting them, or waiting so long you die of thirst diff --git a/docs/adv-max-skills.rst b/docs/adv-max-skills.rst index e08caf5026..34c7ad25c5 100644 --- a/docs/adv-max-skills.rst +++ b/docs/adv-max-skills.rst @@ -3,7 +3,7 @@ adv-max-skills .. dfhack-tool:: :summary: Raises adventurer stats to max. - :tags: untested adventure embark armok + :tags: unavailable adventure embark armok When creating an adventurer, raises all changeable skills and attributes to their maximum level. diff --git a/docs/adv-rumors.rst b/docs/adv-rumors.rst index 24105109a7..f07cb324d8 100644 --- a/docs/adv-rumors.rst +++ b/docs/adv-rumors.rst @@ -3,7 +3,7 @@ adv-rumors .. dfhack-tool:: :summary: Improves the rumors menu in adventure mode. - :tags: untested adventure interface + :tags: unavailable adventure interface In adventure mode, start a conversation with someone and then run this tool to improve the "Bring up specific incident or rumor" menu. Specifically, this diff --git a/docs/assign-minecarts.rst b/docs/assign-minecarts.rst index 81c67fb27e..abd7ff856f 100644 --- a/docs/assign-minecarts.rst +++ b/docs/assign-minecarts.rst @@ -3,7 +3,7 @@ assign-minecarts .. dfhack-tool:: :summary: Assign minecarts to hauling routes. - :tags: untested fort productivity + :tags: unavailable fort productivity This script allows you to assign minecarts to hauling routes without having to use the in-game interface. diff --git a/docs/assign-profile.rst b/docs/assign-profile.rst index 30b05eb273..c491428180 100644 --- a/docs/assign-profile.rst +++ b/docs/assign-profile.rst @@ -3,7 +3,7 @@ assign-profile .. dfhack-tool:: :summary: Adjust characteristics of a unit according to saved profiles. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool can load a profile stored in a JSON file and apply the characteristics to a unit. diff --git a/docs/autolabor-artisans.rst b/docs/autolabor-artisans.rst index fb36a25f71..7e1b662123 100644 --- a/docs/autolabor-artisans.rst +++ b/docs/autolabor-artisans.rst @@ -3,7 +3,7 @@ autolabor-artisans .. dfhack-tool:: :summary: Configures autolabor to produce artisan dwarves. - :tags: untested fort labors + :tags: unavailable fort labors This script runs an `autolabor` command for all labors where skill level influences output quality (e.g. Carpentry, Stone detailing, Weaponsmithing, diff --git a/docs/autounsuspend.rst b/docs/autounsuspend.rst index 2b64c54585..c00b81726e 100644 --- a/docs/autounsuspend.rst +++ b/docs/autounsuspend.rst @@ -3,7 +3,7 @@ autounsuspend .. dfhack-tool:: :summary: Keep construction jobs unsuspended. - :tags: fort auto jobs + This tool will unsuspend jobs that have become suspended due to inaccessible materials, items in the way, or worker dwarves getting scared by wildlife. diff --git a/docs/binpatch.rst b/docs/binpatch.rst index c790cc1576..517af00afc 100644 --- a/docs/binpatch.rst +++ b/docs/binpatch.rst @@ -3,7 +3,7 @@ binpatch .. dfhack-tool:: :summary: Applies or removes binary patches. - :tags: untested dev + :tags: unavailable dev See `binpatches` for more info. diff --git a/docs/bodyswap.rst b/docs/bodyswap.rst index ef17510d96..b9b880eead 100644 --- a/docs/bodyswap.rst +++ b/docs/bodyswap.rst @@ -3,7 +3,7 @@ bodyswap .. dfhack-tool:: :summary: Take direct control of any visible unit. - :tags: untested adventure armok units + :tags: unavailable adventure armok units This script allows the player to take direct control of any unit present in adventure mode whilst giving up control of their current player character. diff --git a/docs/break-dance.rst b/docs/break-dance.rst index d22641559d..9a58be4c73 100644 --- a/docs/break-dance.rst +++ b/docs/break-dance.rst @@ -3,7 +3,7 @@ break-dance .. dfhack-tool:: :summary: Fixes buggy tavern dances. - :tags: untested fort bugfix units + :tags: unavailable fort bugfix units Sometimes when a unit can't find a dance partner, the dance becomes stuck and never stops. This tool can get them unstuck. diff --git a/docs/build-now.rst b/docs/build-now.rst index 759de2b603..3a3d472a5d 100644 --- a/docs/build-now.rst +++ b/docs/build-now.rst @@ -3,7 +3,7 @@ build-now .. dfhack-tool:: :summary: Instantly completes building construction jobs. - :tags: untested fort armok buildings + :tags: unavailable fort armok buildings By default, all unsuspended buildings on the map are completed, but the area of effect is configurable. diff --git a/docs/burial.rst b/docs/burial.rst index fbb3b4b60c..19e58b9b87 100644 --- a/docs/burial.rst +++ b/docs/burial.rst @@ -3,7 +3,7 @@ burial .. dfhack-tool:: :summary: Configures all unowned coffins to allow burial. - :tags: untested fort productivity buildings + :tags: unavailable fort productivity buildings Usage ----- diff --git a/docs/cannibalism.rst b/docs/cannibalism.rst index 21939991b0..75a63df0ea 100644 --- a/docs/cannibalism.rst +++ b/docs/cannibalism.rst @@ -3,7 +3,7 @@ cannibalism .. dfhack-tool:: :summary: Allows a player character to consume sapient corpses. - :tags: untested adventure gameplay + :tags: unavailable adventure gameplay This tool clears the flag from items that mark them as being from a sapient creature. Use from an adventurer's inventory screen or an individual item's diff --git a/docs/color-schemes.rst b/docs/color-schemes.rst index a3375487f0..13f13ee145 100644 --- a/docs/color-schemes.rst +++ b/docs/color-schemes.rst @@ -3,7 +3,7 @@ color-schemes .. dfhack-tool:: :summary: Modify the colors used by the DF UI. - :tags: untested fort gameplay graphics + :tags: unavailable fort gameplay graphics This tool allows you to set exactly which shades of colors should be used in the DF interface color palette. diff --git a/docs/combat-harden.rst b/docs/combat-harden.rst index 6331e95b80..f77ff1f979 100644 --- a/docs/combat-harden.rst +++ b/docs/combat-harden.rst @@ -3,7 +3,7 @@ combat-harden .. dfhack-tool:: :summary: Set the combat-hardened value on a unit. - :tags: untested fort armok military units + :tags: unavailable fort armok military units This tool can make a unit care more/less about seeing corpses. diff --git a/docs/deteriorate.rst b/docs/deteriorate.rst index 4a7e663e9c..1e0b49f944 100644 --- a/docs/deteriorate.rst +++ b/docs/deteriorate.rst @@ -3,7 +3,7 @@ deteriorate .. dfhack-tool:: :summary: Cause corpses, clothes, and/or food to rot away over time. - :tags: untested fort auto fps gameplay items plants + :tags: unavailable fort auto fps gameplay items plants When enabled, this script will cause the specified item types to slowly rot away. By default, items disappear after a few months, but you can choose to slow diff --git a/docs/devel/block-borders.rst b/docs/devel/block-borders.rst index 8e87a00ce4..b13f5b074a 100644 --- a/docs/devel/block-borders.rst +++ b/docs/devel/block-borders.rst @@ -3,7 +3,7 @@ devel/block-borders .. dfhack-tool:: :summary: Outline map blocks on the map screen. - :tags: untested dev map + :tags: unavailable dev map This tool displays an overlay that highlights the borders of map blocks. See :doc:`/docs/api/Maps` for details on map blocks. diff --git a/docs/devel/cmptiles.rst b/docs/devel/cmptiles.rst index c98c0b7c08..c10949e9e7 100644 --- a/docs/devel/cmptiles.rst +++ b/docs/devel/cmptiles.rst @@ -3,7 +3,7 @@ devel/cmptiles .. dfhack-tool:: :summary: List or compare two tiletype material groups. - :tags: untested dev + :tags: unavailable dev Lists and/or compares two tiletype material groups. You can see the list of valid material groups by running:: diff --git a/docs/devel/dump-offsets.rst b/docs/devel/dump-offsets.rst index 63ad92307e..80748e0d32 100644 --- a/docs/devel/dump-offsets.rst +++ b/docs/devel/dump-offsets.rst @@ -3,7 +3,7 @@ devel/dump-offsets .. dfhack-tool:: :summary: Dump the contents of the table of global addresses. - :tags: untested dev + :tags: unavailable dev .. warning:: diff --git a/docs/devel/export-dt-ini.rst b/docs/devel/export-dt-ini.rst index 81ba6d9ea0..34f6f05e88 100644 --- a/docs/devel/export-dt-ini.rst +++ b/docs/devel/export-dt-ini.rst @@ -3,7 +3,7 @@ devel/export-dt-ini .. dfhack-tool:: :summary: Export memory addresses for Dwarf Therapist configuration. - :tags: untested dev + :tags: unavailable dev This tool exports an ini file containing memory addresses for Dwarf Therapist. diff --git a/docs/devel/find-offsets.rst b/docs/devel/find-offsets.rst index 9034c135b4..160253ebf8 100644 --- a/docs/devel/find-offsets.rst +++ b/docs/devel/find-offsets.rst @@ -3,7 +3,7 @@ devel/find-offsets .. dfhack-tool:: :summary: Find memory offsets of DF data structures. - :tags: untested dev + :tags: unavailable dev .. warning:: diff --git a/docs/devel/find-primitive.rst b/docs/devel/find-primitive.rst index 2887d073b1..d162560e07 100644 --- a/docs/devel/find-primitive.rst +++ b/docs/devel/find-primitive.rst @@ -3,7 +3,7 @@ devel/find-primitive .. dfhack-tool:: :summary: Discover memory offsets for new variables. - :tags: untested dev + :tags: unavailable dev This tool helps find a primitive variable in DF's data section, relying on the user to change its value and then scanning for memory that has changed to that diff --git a/docs/devel/find-twbt.rst b/docs/devel/find-twbt.rst index f0eded1450..1d10c12c1e 100644 --- a/docs/devel/find-twbt.rst +++ b/docs/devel/find-twbt.rst @@ -3,7 +3,7 @@ devel/find-twbt .. dfhack-tool:: :summary: Display the memory offsets of some important TWBT functions. - :tags: untested dev + :tags: unavailable dev Finds some TWBT-related offsets - currently just ``twbt_render_map``. diff --git a/docs/devel/inject-raws.rst b/docs/devel/inject-raws.rst index e1ef56708e..42dd30ce3a 100644 --- a/docs/devel/inject-raws.rst +++ b/docs/devel/inject-raws.rst @@ -3,7 +3,7 @@ devel/inject-raws .. dfhack-tool:: :summary: Add objects and reactions into an existing world. - :tags: untested dev + :tags: unavailable dev WARNING: THIS SCRIPT CAN PERMANENTLY DAMAGE YOUR SAVE. diff --git a/docs/devel/kill-hf.rst b/docs/devel/kill-hf.rst index d9435c7bc0..9e9a29f2d2 100644 --- a/docs/devel/kill-hf.rst +++ b/docs/devel/kill-hf.rst @@ -3,7 +3,7 @@ devel/kill-hf .. dfhack-tool:: :summary: Kill a historical figure. - :tags: untested dev + :tags: unavailable dev This tool can kill the specified historical figure, even if off-site, or terminate a pregnancy. Useful for working around :bug:`11549`. diff --git a/docs/devel/light.rst b/docs/devel/light.rst index 095b127d7a..ce175767cd 100644 --- a/docs/devel/light.rst +++ b/docs/devel/light.rst @@ -3,7 +3,7 @@ devel/light .. dfhack-tool:: :summary: Experiment with lighting overlays. - :tags: untested dev graphics + :tags: unavailable dev graphics This is an experimental lighting engine for DF, using the `rendermax` plugin. diff --git a/docs/devel/list-filters.rst b/docs/devel/list-filters.rst index 893b6a6281..36642018f6 100644 --- a/docs/devel/list-filters.rst +++ b/docs/devel/list-filters.rst @@ -3,7 +3,7 @@ devel/list-filters .. dfhack-tool:: :summary: List input items for the selected building type. - :tags: untested dev + :tags: unavailable dev This tool lists input items for the building that is currently being built. You must be in build mode and have a building type selected for placement. This is diff --git a/docs/devel/lsmem.rst b/docs/devel/lsmem.rst index e1a897062b..f046e4f41d 100644 --- a/docs/devel/lsmem.rst +++ b/docs/devel/lsmem.rst @@ -3,7 +3,7 @@ devel/lsmem .. dfhack-tool:: :summary: Print memory ranges of the DF process. - :tags: untested dev + :tags: unavailable dev Useful for checking whether a pointer is valid, whether a certain library/plugin is loaded, etc. diff --git a/docs/devel/lua-example.rst b/docs/devel/lua-example.rst index 9df8ef3d9b..63634a9931 100644 --- a/docs/devel/lua-example.rst +++ b/docs/devel/lua-example.rst @@ -3,7 +3,7 @@ devel/lua-example .. dfhack-tool:: :summary: An example lua script. - :tags: untested dev + :tags: unavailable dev This is an example Lua script which just reports the number of times it has been called. Useful for testing environment persistence. diff --git a/docs/devel/luacov.rst b/docs/devel/luacov.rst index b948bc9fcb..e3af3e7899 100644 --- a/docs/devel/luacov.rst +++ b/docs/devel/luacov.rst @@ -3,7 +3,7 @@ devel/luacov .. dfhack-tool:: :summary: Lua script coverage report generator. - :tags: untested dev + :tags: unavailable dev This script generates a coverage report from collected statistics. By default it reports on every Lua file in all of DFHack. To filter filenames, specify one or diff --git a/docs/devel/nuke-items.rst b/docs/devel/nuke-items.rst index 21e7dc07cf..26c6dd6a84 100644 --- a/docs/devel/nuke-items.rst +++ b/docs/devel/nuke-items.rst @@ -3,7 +3,7 @@ devel/nuke-items .. dfhack-tool:: :summary: Deletes all free items in the game. - :tags: untested dev fps items + :tags: unavailable dev fps items This tool deletes **ALL** items not referred to by units, buildings, or jobs. Intended solely for lag investigation. diff --git a/docs/devel/prepare-save.rst b/docs/devel/prepare-save.rst index a795392c81..7498e92840 100644 --- a/docs/devel/prepare-save.rst +++ b/docs/devel/prepare-save.rst @@ -3,7 +3,7 @@ devel/prepare-save .. dfhack-tool:: :summary: Set internal game state to known values for memory analysis. - :tags: untested dev + :tags: unavailable dev .. warning:: diff --git a/docs/devel/print-args.rst b/docs/devel/print-args.rst index f4be5130d5..f5ad8af85b 100644 --- a/docs/devel/print-args.rst +++ b/docs/devel/print-args.rst @@ -3,7 +3,7 @@ devel/print-args .. dfhack-tool:: :summary: Echo parameters to the output. - :tags: untested dev + :tags: unavailable dev Prints all the arguments you supply to the script, one per line. diff --git a/docs/devel/print-args2.rst b/docs/devel/print-args2.rst index baf4d35534..aac9b27f94 100644 --- a/docs/devel/print-args2.rst +++ b/docs/devel/print-args2.rst @@ -3,7 +3,7 @@ devel/print-args2 .. dfhack-tool:: :summary: Echo parameters to the output. - :tags: untested dev + :tags: unavailable dev Prints all the arguments you supply to the script, one per line, with quotes around them. diff --git a/docs/devel/print-event.rst b/docs/devel/print-event.rst index 450b019383..7d1bb97945 100644 --- a/docs/devel/print-event.rst +++ b/docs/devel/print-event.rst @@ -3,7 +3,7 @@ devel/print-event .. dfhack-tool:: :summary: Show historical events. - :tags: untested dev + :tags: unavailable dev This tool displays the description of a historical event. diff --git a/docs/devel/save-version.rst b/docs/devel/save-version.rst index 01ce14ae74..5b33d6680f 100644 --- a/docs/devel/save-version.rst +++ b/docs/devel/save-version.rst @@ -3,7 +3,7 @@ devel/save-version .. dfhack-tool:: :summary: Display what DF version has handled the current save. - :tags: untested dev + :tags: unavailable dev This tool displays the DF version that created the game, the most recent DF version that has loaded and saved the game, and the current DF version. diff --git a/docs/devel/sc.rst b/docs/devel/sc.rst index b7e394459f..3a7df80547 100644 --- a/docs/devel/sc.rst +++ b/docs/devel/sc.rst @@ -3,7 +3,7 @@ devel/sc .. dfhack-tool:: :summary: Scan DF structures for errors. - :tags: untested dev + :tags: unavailable dev Size Check: scans structures for invalid vectors, misaligned structures, and unidentified enum values. diff --git a/docs/devel/test-perlin.rst b/docs/devel/test-perlin.rst index b17dfdb981..5f8deceef1 100644 --- a/docs/devel/test-perlin.rst +++ b/docs/devel/test-perlin.rst @@ -3,7 +3,7 @@ devel/test-perlin .. dfhack-tool:: :summary: Generate an image based on perlin noise. - :tags: untested dev + :tags: unavailable dev Generates an image using multiple octaves of perlin noise. diff --git a/docs/devel/unit-path.rst b/docs/devel/unit-path.rst index 76f58fc99d..8bca45cc46 100644 --- a/docs/devel/unit-path.rst +++ b/docs/devel/unit-path.rst @@ -3,7 +3,7 @@ devel/unit-path .. dfhack-tool:: :summary: Inspect where a unit is going and how it's getting there. - :tags: untested dev + :tags: unavailable dev When run with a unit selected, the path that the unit is currently following is highlighted on the map. You can jump between the unit and the destination tile. diff --git a/docs/devel/watch-minecarts.rst b/docs/devel/watch-minecarts.rst index 11933d8ecb..6b915a5aa1 100644 --- a/docs/devel/watch-minecarts.rst +++ b/docs/devel/watch-minecarts.rst @@ -3,7 +3,7 @@ devel/watch-minecarts .. dfhack-tool:: :summary: Inspect minecart coordinates and speeds. - :tags: untested dev + :tags: unavailable dev When running, this tool will log minecart coordinates and speeds to the console. diff --git a/docs/do-job-now.rst b/docs/do-job-now.rst index ca85121da9..291defd133 100644 --- a/docs/do-job-now.rst +++ b/docs/do-job-now.rst @@ -3,7 +3,7 @@ do-job-now .. dfhack-tool:: :summary: Mark the job related to what you're looking at as high priority. - :tags: untested fort productivity jobs + :tags: unavailable fort productivity jobs The script will try its best to find a job related to the selected entity (which can be a job, dwarf, animal, item, building, plant or work order) and then mark diff --git a/docs/dwarf-op.rst b/docs/dwarf-op.rst index ac3dcd41b3..4b60a88802 100644 --- a/docs/dwarf-op.rst +++ b/docs/dwarf-op.rst @@ -3,7 +3,7 @@ dwarf-op .. dfhack-tool:: :summary: Tune units to perform underrepresented job roles in your fortress. - :tags: untested fort armok units + :tags: unavailable fort armok units ``dwarf-op`` examines the distribution of skills and attributes across the dwarves in your fortress and can rewrite the characteristics of a dwarf (or diff --git a/docs/embark-skills.rst b/docs/embark-skills.rst index 7e9d832efe..a43c5b2672 100644 --- a/docs/embark-skills.rst +++ b/docs/embark-skills.rst @@ -3,7 +3,7 @@ embark-skills .. dfhack-tool:: :summary: Adjust dwarves' skills when embarking. - :tags: untested embark fort armok units + :tags: unavailable embark fort armok units When selecting starting skills for your dwarves on the embark screen, this tool can manipulate the skill values or adjust the number of points you have diff --git a/docs/exportlegends.rst b/docs/exportlegends.rst index 6121c177e8..33bf20afd7 100644 --- a/docs/exportlegends.rst +++ b/docs/exportlegends.rst @@ -3,7 +3,7 @@ exportlegends .. dfhack-tool:: :summary: Exports legends data for external viewing. - :tags: untested legends inspection + :tags: unavailable legends inspection When run from legends mode, you can export detailed data about your world so that it can be browsed with external programs like diff --git a/docs/fix-ster.rst b/docs/fix-ster.rst index b6e0ca7c65..d21e62f725 100644 --- a/docs/fix-ster.rst +++ b/docs/fix-ster.rst @@ -3,7 +3,7 @@ fix-ster .. dfhack-tool:: :summary: Toggle infertility for units. - :tags: untested fort armok animals + :tags: unavailable fort armok animals Now you can restore fertility to infertile creatures or inflict infertility on creatures that you do not want to breed. diff --git a/docs/fix/corrupt-equipment.rst b/docs/fix/corrupt-equipment.rst index 5e7db15051..e9cc0e16b9 100644 --- a/docs/fix/corrupt-equipment.rst +++ b/docs/fix/corrupt-equipment.rst @@ -3,7 +3,7 @@ fix/corrupt-equipment .. dfhack-tool:: :summary: Fixes some game crashes caused by corrupt military equipment. - :tags: untested fort bugfix military + :tags: unavailable fort bugfix military This fix corrects some kinds of corruption that can occur in equipment lists, as in :bug:`11014`. Run this script at least every time a squad comes back from a diff --git a/docs/fix/item-occupancy.rst b/docs/fix/item-occupancy.rst index 635b67420c..193d3899aa 100644 --- a/docs/fix/item-occupancy.rst +++ b/docs/fix/item-occupancy.rst @@ -3,7 +3,7 @@ fix/item-occupancy .. dfhack-tool:: :summary: Fixes errors with phantom items occupying site. - :tags: untested fort bugfix map + :tags: unavailable fort bugfix map This tool diagnoses and fixes issues with nonexistent 'items occupying site', usually caused by hacking mishaps with items being improperly moved about. diff --git a/docs/fix/population-cap.rst b/docs/fix/population-cap.rst index 02039280ff..59fcbdb2ea 100644 --- a/docs/fix/population-cap.rst +++ b/docs/fix/population-cap.rst @@ -3,7 +3,7 @@ fix/population-cap .. dfhack-tool:: :summary: Ensure the population cap is respected. - :tags: untested fort bugfix units + :tags: unavailable fort bugfix units Run this after every migrant wave to ensure your population cap is not exceeded. diff --git a/docs/fix/tile-occupancy.rst b/docs/fix/tile-occupancy.rst index 0fc869f1c7..25210f6121 100644 --- a/docs/fix/tile-occupancy.rst +++ b/docs/fix/tile-occupancy.rst @@ -3,7 +3,7 @@ fix/tile-occupancy .. dfhack-tool:: :summary: Fix tile occupancy flags. - :tags: untested fort bugfix map + :tags: unavailable fort bugfix map This tool clears bad occupancy flags at the selected tile. It is useful for getting rid of phantom "building present" messages when trying to build diff --git a/docs/fixnaked.rst b/docs/fixnaked.rst index 3e1feab24a..c416c35694 100644 --- a/docs/fixnaked.rst +++ b/docs/fixnaked.rst @@ -3,7 +3,7 @@ fixnaked .. dfhack-tool:: :summary: Removes all unhappy thoughts due to lack of clothing. - :tags: untested fort armok units + :tags: unavailable fort armok units If you're having trouble keeping your dwarves properly clothed and the stress is mounting, this tool can help you calm things down. ``fixnaked`` will go through diff --git a/docs/flashstep.rst b/docs/flashstep.rst index 4a39847dcb..888ae36826 100644 --- a/docs/flashstep.rst +++ b/docs/flashstep.rst @@ -3,7 +3,7 @@ flashstep .. dfhack-tool:: :summary: Teleport your adventurer to the cursor. - :tags: untested adventure armok + :tags: unavailable adventure armok ``flashstep`` is a hotkey-friendly teleport that places your adventurer where your cursor is. diff --git a/docs/forget-dead-body.rst b/docs/forget-dead-body.rst index 2f42851a30..494555c0e7 100644 --- a/docs/forget-dead-body.rst +++ b/docs/forget-dead-body.rst @@ -3,7 +3,7 @@ forget-dead-body .. dfhack-tool:: :summary: Removes emotions associated with seeing a dead body. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool can help your dwarves recover from seeing a massacre. It removes all emotions associated with seeing a dead body. If your dwarves are traumatized and diff --git a/docs/forum-dwarves.rst b/docs/forum-dwarves.rst index a206ca06ec..fd3f34fcbc 100644 --- a/docs/forum-dwarves.rst +++ b/docs/forum-dwarves.rst @@ -3,7 +3,7 @@ forum-dwarves .. dfhack-tool:: :summary: Exports the text you see on the screen for posting to the forums. - :tags: untested dfhack + :tags: unavailable dfhack This tool saves a copy of a text screen, formatted in BBcode for posting to the Bay12 Forums. Text color and layout is preserved. See `markdown` if you want to diff --git a/docs/gaydar.rst b/docs/gaydar.rst index 863bf2af19..9067a19098 100644 --- a/docs/gaydar.rst +++ b/docs/gaydar.rst @@ -3,7 +3,7 @@ gaydar .. dfhack-tool:: :summary: Shows the sexual orientation of units. - :tags: untested fort inspection animals units + :tags: unavailable fort inspection animals units ``gaydar`` is useful for social engineering or checking the viability of livestock breeding programs. diff --git a/docs/ghostly.rst b/docs/ghostly.rst index 93464a080e..0afc2dd854 100644 --- a/docs/ghostly.rst +++ b/docs/ghostly.rst @@ -3,7 +3,7 @@ ghostly .. dfhack-tool:: :summary: Toggles an adventurer's ghost status. - :tags: untested adventure armok units + :tags: unavailable adventure armok units This is useful for walking through walls, avoiding attacks, or recovering after a death. diff --git a/docs/growcrops.rst b/docs/growcrops.rst index b7b1374d27..c53d4b5b67 100644 --- a/docs/growcrops.rst +++ b/docs/growcrops.rst @@ -3,7 +3,7 @@ growcrops .. dfhack-tool:: :summary: Instantly grow planted seeds into crops. - :tags: untested fort armok plants + :tags: unavailable fort armok plants With no parameters, this command lists the seed types currently planted in your farming plots. With a seed type, the script will grow those seeds, ready to be diff --git a/docs/gui/advfort.rst b/docs/gui/advfort.rst index 487d46900d..7575213b5b 100644 --- a/docs/gui/advfort.rst +++ b/docs/gui/advfort.rst @@ -3,7 +3,7 @@ gui/advfort .. dfhack-tool:: :summary: Perform fort-like jobs in adventure mode. - :tags: untested adventure gameplay + :tags: unavailable adventure gameplay This script allows performing jobs in adventure mode. For interactive help, press :kbd:`?` while the script is running. diff --git a/docs/gui/autogems.rst b/docs/gui/autogems.rst index 9eb91a4556..64f61c0fad 100644 --- a/docs/gui/autogems.rst +++ b/docs/gui/autogems.rst @@ -4,7 +4,7 @@ gui/autogems .. dfhack-tool:: :summary: Automatically cut rough gems. - :tags: untested fort auto workorders + :tags: unavailable fort auto workorders This is a frontend for the `autogems` plugin that allows interactively configuring the gem types that you want to be cut. diff --git a/docs/gui/choose-weapons.rst b/docs/gui/choose-weapons.rst index 522a4da0ea..acd197fb29 100644 --- a/docs/gui/choose-weapons.rst +++ b/docs/gui/choose-weapons.rst @@ -3,7 +3,7 @@ gui/choose-weapons .. dfhack-tool:: :summary: Ensure military dwarves choose appropriate weapons. - :tags: untested fort productivity military + :tags: unavailable fort productivity military Activate in the :guilabel:`Equip->View/Customize` page of the military screen. diff --git a/docs/gui/clone-uniform.rst b/docs/gui/clone-uniform.rst index ba24e21b1d..f3bbe94da4 100644 --- a/docs/gui/clone-uniform.rst +++ b/docs/gui/clone-uniform.rst @@ -3,7 +3,7 @@ gui/clone-uniform .. dfhack-tool:: :summary: Duplicate an existing military uniform. - :tags: untested fort productivity military + :tags: unavailable fort productivity military When invoked, this tool duplicates the currently selected uniform template and selects the newly created copy. Activate in the :guilabel:`Uniforms` page of the diff --git a/docs/gui/color-schemes.rst b/docs/gui/color-schemes.rst index f7d1eb7193..83e99a021b 100644 --- a/docs/gui/color-schemes.rst +++ b/docs/gui/color-schemes.rst @@ -3,7 +3,7 @@ gui/color-schemes .. dfhack-tool:: :summary: Modify the colors in the DF UI. - :tags: untested graphics + :tags: unavailable graphics This is an in-game interface for `color-schemes`, which allows you to modify the colors in the Dwarf Fortress interface. This script must be called from either diff --git a/docs/gui/companion-order.rst b/docs/gui/companion-order.rst index 0bffeaef49..c55ebf038b 100644 --- a/docs/gui/companion-order.rst +++ b/docs/gui/companion-order.rst @@ -3,7 +3,7 @@ gui/companion-order .. dfhack-tool:: :summary: Issue orders to companions. - :tags: untested adventure interface + :tags: unavailable adventure interface This tool allows you to issue orders to your adventurer's companions. Select which companions to issue orders to with lower case letters (green when diff --git a/docs/gui/create-tree.rst b/docs/gui/create-tree.rst index 7a20d8d04c..78ef616998 100644 --- a/docs/gui/create-tree.rst +++ b/docs/gui/create-tree.rst @@ -3,7 +3,7 @@ gui/create-tree .. dfhack-tool:: :summary: Create a tree. - :tags: untested fort armok plants + :tags: unavailable fort armok plants This tool provides a graphical interface for creating trees. diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 2c2e2feb8b..5bbbce4ecc 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -4,7 +4,7 @@ gui/design .. dfhack-tool:: :summary: Design designation utility with shapes. - :tags: fort design productivity map + This tool provides a point and click interface to make designating shapes and patterns easier. Supports both digging designations and placing constructions. diff --git a/docs/gui/dfstatus.rst b/docs/gui/dfstatus.rst index 085c4a9137..be45390ca7 100644 --- a/docs/gui/dfstatus.rst +++ b/docs/gui/dfstatus.rst @@ -3,7 +3,7 @@ gui/dfstatus .. dfhack-tool:: :summary: Show a quick overview of critical stock quantities. - :tags: untested fort inspection + :tags: unavailable fort inspection This tool show a quick overview of stock quantities for: diff --git a/docs/gui/extended-status.rst b/docs/gui/extended-status.rst index fc5c6380f1..52e2a086c5 100644 --- a/docs/gui/extended-status.rst +++ b/docs/gui/extended-status.rst @@ -3,7 +3,7 @@ gui/extended-status .. dfhack-tool:: :summary: Add information on beds and bedrooms to the status screen. - :tags: untested fort inspection interface + :tags: unavailable fort inspection interface Adds an additional page to the ``z`` status screen where you can see information about beds, bedrooms, and whether your dwarves have bedrooms of their own. diff --git a/docs/gui/family-affairs.rst b/docs/gui/family-affairs.rst index 1661622af6..170e2a1a9d 100644 --- a/docs/gui/family-affairs.rst +++ b/docs/gui/family-affairs.rst @@ -3,7 +3,7 @@ gui/family-affairs .. dfhack-tool:: :summary: Inspect or meddle with romantic relationships. - :tags: untested fort armok inspection units + :tags: unavailable fort armok inspection units This tool provides a user-friendly interface to view romantic relationships, with the ability to add, remove, or otherwise change them at your whim - diff --git a/docs/gui/guide-path.rst b/docs/gui/guide-path.rst index 65335d5fc4..5d76993cb8 100644 --- a/docs/gui/guide-path.rst +++ b/docs/gui/guide-path.rst @@ -3,7 +3,7 @@ gui/guide-path .. dfhack-tool:: :summary: Visualize minecart guide paths. - :tags: untested fort inspection map + :tags: unavailable fort inspection map This tool displays the cached path that will be used by the minecart guide order. The game computes this path when the order is executed for the first diff --git a/docs/gui/kitchen-info.rst b/docs/gui/kitchen-info.rst index 7ea5338b2a..86584a88b7 100644 --- a/docs/gui/kitchen-info.rst +++ b/docs/gui/kitchen-info.rst @@ -3,7 +3,7 @@ gui/kitchen-info .. dfhack-tool:: :summary: Show food item uses in the kitchen status screen. - :tags: untested fort inspection + :tags: unavailable fort inspection This tool is an overlay that adds more info to the Kitchen screen, such as the potential alternate uses of the items that you could mark for cooking. diff --git a/docs/gui/launcher.rst b/docs/gui/launcher.rst index 5c56b384b4..04d81b41f3 100644 --- a/docs/gui/launcher.rst +++ b/docs/gui/launcher.rst @@ -104,5 +104,5 @@ Dev mode -------- By default, commands intended for developers and modders are filtered out of the -autocomplete list. This includes any tools tagged with ``untested``. You can +autocomplete list. This includes any tools tagged with ``unavailable``. You can toggle this filtering by hitting :kbd:`Ctrl`:kbd:`D` at any time. diff --git a/docs/gui/load-screen.rst b/docs/gui/load-screen.rst index f00aa49495..1301ab09f3 100644 --- a/docs/gui/load-screen.rst +++ b/docs/gui/load-screen.rst @@ -3,7 +3,7 @@ gui/load-screen .. dfhack-tool:: :summary: Replace DF's continue game screen with a searchable list. - :tags: untested dfhack + :tags: unavailable dfhack If you tend to have many ongoing games, this tool can make it much easier to load the one you're looking for. It replaces DF's "continue game" screen with diff --git a/docs/gui/manager-quantity.rst b/docs/gui/manager-quantity.rst index ff72b47132..5eb9a44c36 100644 --- a/docs/gui/manager-quantity.rst +++ b/docs/gui/manager-quantity.rst @@ -3,7 +3,7 @@ gui/manager-quantity .. dfhack-tool:: :summary: Set the quantity of the selected manager workorder. - :tags: untested fort workorders + :tags: unavailable fort workorders There is no way in the base DF game to change the quantity for an existing manager workorder. Select a workorder on the j-m or u-m screens and run this diff --git a/docs/gui/mechanisms.rst b/docs/gui/mechanisms.rst index 28129fb157..266fb280d2 100644 --- a/docs/gui/mechanisms.rst +++ b/docs/gui/mechanisms.rst @@ -3,7 +3,7 @@ gui/mechanisms .. dfhack-tool:: :summary: List mechanisms and links connected to a building. - :tags: untested fort inspection buildings + :tags: unavailable fort inspection buildings This convenient tool lists the mechanisms connected to the building and the buildings linked via the mechanisms. Navigating the list centers the view on the diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index c6d0dcee63..6eb56b2937 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -3,7 +3,7 @@ gui/mod-manager .. dfhack-tool:: :summary: Easily install and uninstall mods. - :tags: untested dfhack + :tags: unavailable dfhack This tool provides a simple way to install and remove small mods that you have downloaded from the internet -- or have created yourself! Several mods are diff --git a/docs/gui/petitions.rst b/docs/gui/petitions.rst index 8f1ab91348..0f48bc90a4 100644 --- a/docs/gui/petitions.rst +++ b/docs/gui/petitions.rst @@ -3,7 +3,7 @@ gui/petitions .. dfhack-tool:: :summary: Show information about your fort's petitions. - :tags: untested fort inspection + :tags: unavailable fort inspection Show your fort's petitions, both pending and fulfilled. diff --git a/docs/gui/power-meter.rst b/docs/gui/power-meter.rst index d966d25dc7..fc7e59bff1 100644 --- a/docs/gui/power-meter.rst +++ b/docs/gui/power-meter.rst @@ -3,7 +3,7 @@ gui/power-meter .. dfhack-tool:: :summary: Allow pressure plates to measure power. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings If you run this tool after selecting :guilabel:`Pressure Plate` in the build menu, you will build a power meter building instead of a regular pressure plate. diff --git a/docs/gui/quantum.rst b/docs/gui/quantum.rst index 9072690857..534f35fcbd 100644 --- a/docs/gui/quantum.rst +++ b/docs/gui/quantum.rst @@ -3,7 +3,7 @@ gui/quantum .. dfhack-tool:: :summary: Quickly and easily create quantum stockpiles. - :tags: untested fort productivity stockpiles + :tags: unavailable fort productivity stockpiles This tool provides a visual, interactive interface for creating quantum stockpiles. diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index 3d1ce9e26b..b23aa8c783 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -3,7 +3,7 @@ gui/rename .. dfhack-tool:: :summary: Give buildings and units new names, optionally with special chars. - :tags: untested fort productivity buildings stockpiles units + :tags: unavailable fort productivity buildings stockpiles units Once you select a target on the game map, this tool allows you to rename it. It is more powerful than the in-game rename functionality since it allows you to diff --git a/docs/gui/room-list.rst b/docs/gui/room-list.rst index 4973ccc2e6..6a5749215e 100644 --- a/docs/gui/room-list.rst +++ b/docs/gui/room-list.rst @@ -3,7 +3,7 @@ gui/room-list .. dfhack-tool:: :summary: Manage rooms owned by a dwarf. - :tags: untested fort inspection + :tags: unavailable fort inspection When invoked in :kbd:`q` mode with the cursor over an owned room, this tool lists other rooms owned by the same owner, or by the unit selected in the assign diff --git a/docs/gui/seedwatch.rst b/docs/gui/seedwatch.rst index f65bdccbf2..61412a1c51 100644 --- a/docs/gui/seedwatch.rst +++ b/docs/gui/seedwatch.rst @@ -3,7 +3,7 @@ gui/seedwatch .. dfhack-tool:: :summary: Manages seed and plant cooking based on seed stock levels. - :tags: fort auto stockpiles + This is the configuration interface for the `seedwatch` plugin. You can configure a target stock amount for each seed type. If the number of seeds of that type falls diff --git a/docs/gui/settings-manager.rst b/docs/gui/settings-manager.rst index 6bf10c6f63..6ea2a623fe 100644 --- a/docs/gui/settings-manager.rst +++ b/docs/gui/settings-manager.rst @@ -3,7 +3,7 @@ gui/settings-manager .. dfhack-tool:: :summary: Dynamically adjust global DF settings. - :tags: untested dfhack + :tags: unavailable dfhack This tool is an in-game editor for settings defined in :file:`data/init/init.txt` and :file:`data/init/d_init.txt`. Changes are written diff --git a/docs/gui/siege-engine.rst b/docs/gui/siege-engine.rst index 634f19e522..704fd8c931 100644 --- a/docs/gui/siege-engine.rst +++ b/docs/gui/siege-engine.rst @@ -3,7 +3,7 @@ gui/siege-engine .. dfhack-tool:: :summary: Extend the functionality and usability of siege engines. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings This tool is an in-game interface for `siege-engine`, which allows you to link siege engines to stockpiles, restrict operation to certain dwarves, fire a diff --git a/docs/gui/stamper.rst b/docs/gui/stamper.rst index b9a965e876..779e57b6e2 100644 --- a/docs/gui/stamper.rst +++ b/docs/gui/stamper.rst @@ -3,7 +3,7 @@ gui/stamper .. dfhack-tool:: :summary: Copy, paste, and transform dig designations. - :tags: untested fort design map + :tags: unavailable fort design map This tool allows you to copy and paste blocks of dig designations. You can also transform what you have copied by shifting it, reflecting it, rotating it, diff --git a/docs/gui/stockpiles.rst b/docs/gui/stockpiles.rst index 2b8d234d20..80405f6c37 100644 --- a/docs/gui/stockpiles.rst +++ b/docs/gui/stockpiles.rst @@ -3,7 +3,7 @@ gui/stockpiles .. dfhack-tool:: :summary: Import and export stockpile settings. - :tags: untested fort design stockpiles + :tags: unavailable fort design stockpiles With a stockpile selected in :kbd:`q` mode, you can use this tool to load stockpile settings from a file or save them to a file for later loading, in diff --git a/docs/gui/suspendmanager.rst b/docs/gui/suspendmanager.rst index 7ff9dbb3c1..4940dc7c30 100644 --- a/docs/gui/suspendmanager.rst +++ b/docs/gui/suspendmanager.rst @@ -3,7 +3,7 @@ gui/suspendmanager .. dfhack-tool:: :summary: Intelligently suspend and unsuspend jobs. - :tags: fort auto jobs + This is the graphical configuration interface for the `suspendmanager` automation tool. diff --git a/docs/gui/teleport.rst b/docs/gui/teleport.rst index c6b6b67749..e2cd9229e4 100644 --- a/docs/gui/teleport.rst +++ b/docs/gui/teleport.rst @@ -3,7 +3,7 @@ gui/teleport .. dfhack-tool:: :summary: Teleport a unit anywhere. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool is a front-end for the `teleport` tool. It allows you to interactively choose a unit to teleport and a destination tile using the in-game cursor. diff --git a/docs/gui/unit-info-viewer.rst b/docs/gui/unit-info-viewer.rst index 7fa6c334c0..dac54e87e5 100644 --- a/docs/gui/unit-info-viewer.rst +++ b/docs/gui/unit-info-viewer.rst @@ -3,7 +3,7 @@ gui/unit-info-viewer .. dfhack-tool:: :summary: Display detailed information about a unit. - :tags: untested fort inspection units + :tags: unavailable fort inspection units Displays information about age, birth, maxage, shearing, milking, grazing, egg laying, body size, and death for the selected unit. diff --git a/docs/gui/workflow.rst b/docs/gui/workflow.rst index 1af67a7879..d4724efb9a 100644 --- a/docs/gui/workflow.rst +++ b/docs/gui/workflow.rst @@ -3,7 +3,7 @@ gui/workflow .. dfhack-tool:: :summary: Manage automated item production rules. - :tags: untested fort auto jobs + :tags: unavailable fort auto jobs This tool provides a simple interface to item production constraints managed by `workflow`. When a workshop job is selected in :kbd:`q` mode and this tool is diff --git a/docs/gui/workorder-details.rst b/docs/gui/workorder-details.rst index cf7f30a122..ff3639f436 100644 --- a/docs/gui/workorder-details.rst +++ b/docs/gui/workorder-details.rst @@ -3,7 +3,7 @@ gui/workorder-details .. dfhack-tool:: :summary: Adjust input materials and traits for workorders. - :tags: untested fort inspection workorders + :tags: unavailable fort inspection workorders This tool allows you to adjust item types, materials, and/or traits for items used in manager workorders. The jobs created from those workorders will inherit diff --git a/docs/gui/workshop-job.rst b/docs/gui/workshop-job.rst index e1a8ed93af..3caaeef591 100644 --- a/docs/gui/workshop-job.rst +++ b/docs/gui/workshop-job.rst @@ -3,7 +3,7 @@ gui/workshop-job .. dfhack-tool:: :summary: Adjust the input materials used for a job at a workshop. - :tags: untested fort inspection jobs + :tags: unavailable fort inspection jobs This tool allows you to inspect or change the input reagents for the selected workshop job (in :kbd:`q` mode). diff --git a/docs/hotkey-notes.rst b/docs/hotkey-notes.rst index a418fca10c..b0f2be8184 100644 --- a/docs/hotkey-notes.rst +++ b/docs/hotkey-notes.rst @@ -3,7 +3,7 @@ hotkey-notes .. dfhack-tool:: :summary: Show info on DF map location hotkeys. - :tags: untested fort inspection + :tags: unavailable fort inspection This command lists the key (e.g. :kbd:`F1`), name, and jump position of the map location hotkeys you set in the :kbd:`H` menu. diff --git a/docs/launch.rst b/docs/launch.rst index c364e56e20..d6d0f88096 100644 --- a/docs/launch.rst +++ b/docs/launch.rst @@ -3,7 +3,7 @@ launch .. dfhack-tool:: :summary: Thrash your enemies with a flying suplex. - :tags: untested adventure armok units + :tags: unavailable adventure armok units Attack another unit and then run this command to grab them and fly in a glorious parabolic arc to where you have placed the cursor. You'll land safely and your diff --git a/docs/light-aquifers-only.rst b/docs/light-aquifers-only.rst index 53e5296bf8..f3a77206d2 100644 --- a/docs/light-aquifers-only.rst +++ b/docs/light-aquifers-only.rst @@ -3,7 +3,7 @@ light-aquifers-only .. dfhack-tool:: :summary: Change heavy and varied aquifers to light aquifers. - :tags: untested embark fort armok map + :tags: unavailable embark fort armok map This script behaves differently depending on whether it's called pre-embark or post-embark. Pre-embark, it changes all aquifers in the world to light ones, diff --git a/docs/linger.rst b/docs/linger.rst index ea85f9c630..c985ac95a0 100644 --- a/docs/linger.rst +++ b/docs/linger.rst @@ -3,7 +3,7 @@ linger .. dfhack-tool:: :summary: Take control of your adventurer's killer. - :tags: untested adventure armok + :tags: unavailable adventure armok Run this script after being presented with the "You are deceased." message to abandon your dead adventurer and take control of your adventurer's killer. diff --git a/docs/list-waves.rst b/docs/list-waves.rst index e3228d0bc4..574315bbb8 100644 --- a/docs/list-waves.rst +++ b/docs/list-waves.rst @@ -3,7 +3,7 @@ list-waves .. dfhack-tool:: :summary: Show migration wave information for your dwarves. - :tags: untested fort inspection units + :tags: unavailable fort inspection units This script displays information about migration waves or identifies which wave a particular dwarf came from. diff --git a/docs/load-save.rst b/docs/load-save.rst index 00ccdd02b0..5d3e281273 100644 --- a/docs/load-save.rst +++ b/docs/load-save.rst @@ -3,7 +3,7 @@ load-save .. dfhack-tool:: :summary: Load a savegame. - :tags: untested dfhack + :tags: unavailable dfhack When run on the Dwarf Fortress title screen or "load game" screen, this script will load the save with the given folder name without requiring interaction. diff --git a/docs/make-legendary.rst b/docs/make-legendary.rst index 6dd2278c57..4964947aff 100644 --- a/docs/make-legendary.rst +++ b/docs/make-legendary.rst @@ -3,7 +3,7 @@ make-legendary .. dfhack-tool:: :summary: Boost skills of the selected dwarf. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool can make the selected dwarf legendary in one skill, a group of skills, or all skills. diff --git a/docs/markdown.rst b/docs/markdown.rst index 5a4df7c3ca..ae4be55a94 100644 --- a/docs/markdown.rst +++ b/docs/markdown.rst @@ -3,7 +3,7 @@ markdown .. dfhack-tool:: :summary: Exports the text you see on the screen for posting online. - :tags: untested dfhack + :tags: unavailable dfhack This tool saves a copy of a text screen, formatted in markdown, for posting to Reddit (among other places). See `forum-dwarves` if you want to export BBCode diff --git a/docs/max-wave.rst b/docs/max-wave.rst index f738c46fbf..6b53e7c2b1 100644 --- a/docs/max-wave.rst +++ b/docs/max-wave.rst @@ -3,7 +3,7 @@ max-wave .. dfhack-tool:: :summary: Dynamically limit the next immigration wave. - :tags: untested fort gameplay + :tags: unavailable fort gameplay Limit the number of migrants that can arrive in the next wave by overriding the population cap value from data/init/d_init.txt. diff --git a/docs/modtools/add-syndrome.rst b/docs/modtools/add-syndrome.rst index fc4427d7c7..5492db23c7 100644 --- a/docs/modtools/add-syndrome.rst +++ b/docs/modtools/add-syndrome.rst @@ -3,7 +3,7 @@ modtools/add-syndrome .. dfhack-tool:: :summary: Add and remove syndromes from units. - :tags: untested dev + :tags: unavailable dev This allows adding and removing syndromes from units. diff --git a/docs/modtools/anonymous-script.rst b/docs/modtools/anonymous-script.rst index 8df4dd023c..9c2074fcb4 100644 --- a/docs/modtools/anonymous-script.rst +++ b/docs/modtools/anonymous-script.rst @@ -3,7 +3,7 @@ modtools/anonymous-script .. dfhack-tool:: :summary: Run dynamically generated script code. - :tags: untested dev + :tags: unavailable dev This allows running a short simple Lua script passed as an argument instead of running a script from a file. This is useful when you want to do something too diff --git a/docs/modtools/change-build-menu.rst b/docs/modtools/change-build-menu.rst index 522c4a50ca..2fffec91a7 100644 --- a/docs/modtools/change-build-menu.rst +++ b/docs/modtools/change-build-menu.rst @@ -3,7 +3,7 @@ modtools/change-build-menu .. dfhack-tool:: :summary: Add or remove items from the build sidebar menus. - :tags: untested dev + :tags: unavailable dev Change the build sidebar menus. diff --git a/docs/modtools/create-item.rst b/docs/modtools/create-item.rst index 58ea8af99f..4704502c08 100644 --- a/docs/modtools/create-item.rst +++ b/docs/modtools/create-item.rst @@ -3,7 +3,7 @@ modtools/create-item .. dfhack-tool:: :summary: Create arbitrary items. - :tags: untested dev + :tags: unavailable dev Replaces the `createitem` plugin, with standard arguments. The other versions will be phased out in a later version. diff --git a/docs/modtools/create-tree.rst b/docs/modtools/create-tree.rst index 3a4b422819..e425c02229 100644 --- a/docs/modtools/create-tree.rst +++ b/docs/modtools/create-tree.rst @@ -3,7 +3,7 @@ modtools/create-tree .. dfhack-tool:: :summary: Spawn trees. - :tags: untested dev + :tags: unavailable dev Spawns a tree. diff --git a/docs/modtools/create-unit.rst b/docs/modtools/create-unit.rst index 44bb9c868a..9c338bde20 100644 --- a/docs/modtools/create-unit.rst +++ b/docs/modtools/create-unit.rst @@ -3,7 +3,7 @@ modtools/create-unit .. dfhack-tool:: :summary: Create arbitrary units. - :tags: untested dev + :tags: unavailable dev Creates a unit. Usage:: diff --git a/docs/modtools/equip-item.rst b/docs/modtools/equip-item.rst index c01fc764a8..f590b4a4ad 100644 --- a/docs/modtools/equip-item.rst +++ b/docs/modtools/equip-item.rst @@ -3,7 +3,7 @@ modtools/equip-item .. dfhack-tool:: :summary: Force a unit to equip an item. - :tags: untested dev + :tags: unavailable dev Force a unit to equip an item with a particular body part; useful in conjunction with the ``create`` scripts above. See also `forceequip`. diff --git a/docs/modtools/extra-gamelog.rst b/docs/modtools/extra-gamelog.rst index f4c2ebe136..2c79594771 100644 --- a/docs/modtools/extra-gamelog.rst +++ b/docs/modtools/extra-gamelog.rst @@ -3,7 +3,7 @@ modtools/extra-gamelog .. dfhack-tool:: :summary: Write info to the gamelog for Soundsense. - :tags: untested dev + :tags: unavailable dev This script writes extra information to the gamelog. This is useful for tools like :forums:`Soundsense <60287>`. diff --git a/docs/modtools/fire-rate.rst b/docs/modtools/fire-rate.rst index ff427e3054..35310d873d 100644 --- a/docs/modtools/fire-rate.rst +++ b/docs/modtools/fire-rate.rst @@ -3,7 +3,7 @@ modtools/fire-rate .. dfhack-tool:: :summary: Alter the fire rate of ranged weapons. - :tags: untested dev + :tags: unavailable dev Allows altering the fire rates of ranged weapons. Each are defined on a per-item basis. As this is done in an on-world basis, commands for this should be placed diff --git a/docs/modtools/if-entity.rst b/docs/modtools/if-entity.rst index c38dfafe69..9846842b31 100644 --- a/docs/modtools/if-entity.rst +++ b/docs/modtools/if-entity.rst @@ -3,7 +3,7 @@ modtools/if-entity .. dfhack-tool:: :summary: Run DFHack commands based on current civ id. - :tags: untested dev + :tags: unavailable dev Run a command if the current entity matches a given ID. diff --git a/docs/modtools/interaction-trigger.rst b/docs/modtools/interaction-trigger.rst index 6ab287ad8e..f1ea4b61df 100644 --- a/docs/modtools/interaction-trigger.rst +++ b/docs/modtools/interaction-trigger.rst @@ -3,7 +3,7 @@ modtools/interaction-trigger .. dfhack-tool:: :summary: Run DFHack commands when a unit attacks or defends. - :tags: untested dev + :tags: unavailable dev This triggers events when a unit uses an interaction on another. It works by scanning the announcements for the correct attack verb, so the attack verb diff --git a/docs/modtools/invader-item-destroyer.rst b/docs/modtools/invader-item-destroyer.rst index 7ad0aa3a59..20f300109d 100644 --- a/docs/modtools/invader-item-destroyer.rst +++ b/docs/modtools/invader-item-destroyer.rst @@ -3,7 +3,7 @@ modtools/invader-item-destroyer .. dfhack-tool:: :summary: Destroy invader items when they die. - :tags: untested dev + :tags: unavailable dev This tool can destroy invader items to prevent clutter or to prevent the player from getting tools exclusive to certain races. diff --git a/docs/modtools/item-trigger.rst b/docs/modtools/item-trigger.rst index c5211a03b6..8db00cef7d 100644 --- a/docs/modtools/item-trigger.rst +++ b/docs/modtools/item-trigger.rst @@ -3,7 +3,7 @@ modtools/item-trigger .. dfhack-tool:: :summary: Run DFHack commands when a unit uses an item. - :tags: untested dev + :tags: unavailable dev This powerful tool triggers DFHack commands when a unit equips, unequips, or attacks another unit with specified item types, specified item materials, or diff --git a/docs/modtools/moddable-gods.rst b/docs/modtools/moddable-gods.rst index a5d4872800..763082762b 100644 --- a/docs/modtools/moddable-gods.rst +++ b/docs/modtools/moddable-gods.rst @@ -3,7 +3,7 @@ modtools/moddable-gods .. dfhack-tool:: :summary: Create deities. - :tags: untested dev + :tags: unavailable dev This is a standardized version of Putnam's moddableGods script. It allows you to create gods on the command-line. diff --git a/docs/modtools/outside-only.rst b/docs/modtools/outside-only.rst index 8c753180e0..15c9751c61 100644 --- a/docs/modtools/outside-only.rst +++ b/docs/modtools/outside-only.rst @@ -3,7 +3,7 @@ modtools/outside-only .. dfhack-tool:: :summary: Set building inside/outside restrictions. - :tags: untested dev + :tags: unavailable dev This allows you to specify certain custom buildings as outside only, or inside only. If the player attempts to build a building in an inappropriate location, diff --git a/docs/modtools/pref-edit.rst b/docs/modtools/pref-edit.rst index 84a0fe5e16..220ebf70b8 100644 --- a/docs/modtools/pref-edit.rst +++ b/docs/modtools/pref-edit.rst @@ -3,7 +3,7 @@ modtools/pref-edit .. dfhack-tool:: :summary: Modify unit preferences. - :tags: untested dev + :tags: unavailable dev Add, remove, or edit the preferences of a unit. Requires a modifier, a unit argument, and filters. diff --git a/docs/modtools/projectile-trigger.rst b/docs/modtools/projectile-trigger.rst index e697d9c6f5..d50c434dec 100644 --- a/docs/modtools/projectile-trigger.rst +++ b/docs/modtools/projectile-trigger.rst @@ -3,7 +3,7 @@ modtools/projectile-trigger .. dfhack-tool:: :summary: Run DFHack commands when projectiles hit their targets. - :tags: untested dev + :tags: unavailable dev This triggers dfhack commands when projectiles hit their targets. Usage:: diff --git a/docs/modtools/random-trigger.rst b/docs/modtools/random-trigger.rst index 463c4cec18..56b6c21868 100644 --- a/docs/modtools/random-trigger.rst +++ b/docs/modtools/random-trigger.rst @@ -3,7 +3,7 @@ modtools/random-trigger .. dfhack-tool:: :summary: Randomly select DFHack scripts to run. - :tags: untested dev + :tags: unavailable dev Trigger random dfhack commands with specified probabilities. Register a few scripts, then tell it to "go" and it will pick one diff --git a/docs/modtools/raw-lint.rst b/docs/modtools/raw-lint.rst index 75ff257426..13311b1f06 100644 --- a/docs/modtools/raw-lint.rst +++ b/docs/modtools/raw-lint.rst @@ -3,6 +3,6 @@ modtools/raw-lint .. dfhack-tool:: :summary: Check for errors in raw files. - :tags: untested dev + :tags: unavailable dev Checks for simple issues with raw files. Can be run automatically. diff --git a/docs/modtools/reaction-product-trigger.rst b/docs/modtools/reaction-product-trigger.rst index 92c44076a3..32ede32949 100644 --- a/docs/modtools/reaction-product-trigger.rst +++ b/docs/modtools/reaction-product-trigger.rst @@ -3,7 +3,7 @@ modtools/reaction-product-trigger .. dfhack-tool:: :summary: Call DFHack commands when reaction products are produced. - :tags: untested dev + :tags: unavailable dev This triggers dfhack commands when reaction products are produced, once per product. Usage:: diff --git a/docs/modtools/reaction-trigger-transition.rst b/docs/modtools/reaction-trigger-transition.rst index 2674e295e2..bc3c3e45c3 100644 --- a/docs/modtools/reaction-trigger-transition.rst +++ b/docs/modtools/reaction-trigger-transition.rst @@ -3,7 +3,7 @@ modtools/reaction-trigger-transition .. dfhack-tool:: :summary: Help create reaction triggers. - :tags: untested dev + :tags: unavailable dev Prints useful things to the console and a file to help modders transition from ``autoSyndrome`` to `modtools/reaction-trigger`. diff --git a/docs/modtools/reaction-trigger.rst b/docs/modtools/reaction-trigger.rst index 3b94637a70..bf35bcf0aa 100644 --- a/docs/modtools/reaction-trigger.rst +++ b/docs/modtools/reaction-trigger.rst @@ -3,7 +3,7 @@ modtools/reaction-trigger .. dfhack-tool:: :summary: Run DFHack commands when custom reactions complete. - :tags: untested dev + :tags: unavailable dev Triggers dfhack commands when custom reactions complete, regardless of whether it produced anything, once per completion. Arguments:: diff --git a/docs/modtools/set-belief.rst b/docs/modtools/set-belief.rst index 4ad997c590..4d010a0cd5 100644 --- a/docs/modtools/set-belief.rst +++ b/docs/modtools/set-belief.rst @@ -3,7 +3,7 @@ modtools/set-belief .. dfhack-tool:: :summary: Change the beliefs/values of a unit. - :tags: untested dev + :tags: unavailable dev Changes the beliefs (values) of units. Requires a belief, modifier, and a target. diff --git a/docs/modtools/set-need.rst b/docs/modtools/set-need.rst index e753ed6403..b4beea169b 100644 --- a/docs/modtools/set-need.rst +++ b/docs/modtools/set-need.rst @@ -3,7 +3,7 @@ modtools/set-need .. dfhack-tool:: :summary: Change the needs of a unit. - :tags: untested dev + :tags: unavailable dev Sets and edits unit needs. diff --git a/docs/modtools/set-personality.rst b/docs/modtools/set-personality.rst index 7e3969d27b..3e34c7db0c 100644 --- a/docs/modtools/set-personality.rst +++ b/docs/modtools/set-personality.rst @@ -3,7 +3,7 @@ modtools/set-personality .. dfhack-tool:: :summary: Change a unit's personality. - :tags: untested dev + :tags: unavailable dev Changes the personality of units. diff --git a/docs/modtools/skill-change.rst b/docs/modtools/skill-change.rst index 474df9e673..348ae38128 100644 --- a/docs/modtools/skill-change.rst +++ b/docs/modtools/skill-change.rst @@ -3,7 +3,7 @@ modtools/skill-change .. dfhack-tool:: :summary: Modify unit skills. - :tags: untested dev + :tags: unavailable dev Sets or modifies a skill of a unit. diff --git a/docs/modtools/spawn-flow.rst b/docs/modtools/spawn-flow.rst index eccf3efef0..1565e75a6d 100644 --- a/docs/modtools/spawn-flow.rst +++ b/docs/modtools/spawn-flow.rst @@ -3,7 +3,7 @@ modtools/spawn-flow .. dfhack-tool:: :summary: Creates flows at the specified location. - :tags: untested dev + :tags: unavailable dev Creates flows at the specified location. diff --git a/docs/modtools/syndrome-trigger.rst b/docs/modtools/syndrome-trigger.rst index 48ef5f00e2..24e15ef8f5 100644 --- a/docs/modtools/syndrome-trigger.rst +++ b/docs/modtools/syndrome-trigger.rst @@ -3,7 +3,7 @@ modtools/syndrome-trigger .. dfhack-tool:: :summary: Trigger DFHack commands when units acquire syndromes. - :tags: untested dev + :tags: unavailable dev This script helps you set up commands that trigger when syndromes are applied to units. diff --git a/docs/modtools/transform-unit.rst b/docs/modtools/transform-unit.rst index 1137c590dc..9fe68975c3 100644 --- a/docs/modtools/transform-unit.rst +++ b/docs/modtools/transform-unit.rst @@ -3,7 +3,7 @@ modtools/transform-unit .. dfhack-tool:: :summary: Transform a unit into another unit type. - :tags: untested dev + :tags: unavailable dev This tool transforms a unit into another unit type, either temporarily or permanently. diff --git a/docs/names.rst b/docs/names.rst index b7a8c2933d..40b9ed38b3 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -3,7 +3,7 @@ names .. dfhack-tool:: :summary: Rename units or items with the DF name generator. - :tags: untested fort productivity units + :tags: unavailable fort productivity units This tool allows you to rename the selected unit or item (including artifacts) with the native Dwarf Fortress name generation interface. diff --git a/docs/open-legends.rst b/docs/open-legends.rst index 593da992a1..85f258f8f2 100644 --- a/docs/open-legends.rst +++ b/docs/open-legends.rst @@ -3,7 +3,7 @@ open-legends .. dfhack-tool:: :summary: Open a legends screen from fort or adventure mode. - :tags: untested adventure fort legends + :tags: unavailable adventure fort legends You can use this tool to open legends mode from a world loaded in fortress or adventure mode. You can browse around, or even run `exportlegends` while you're diff --git a/docs/points.rst b/docs/points.rst index 41c1f2c562..0016f779ef 100644 --- a/docs/points.rst +++ b/docs/points.rst @@ -3,7 +3,7 @@ points .. dfhack-tool:: :summary: Sets available points at the embark screen. - :tags: untested embark fort armok + :tags: unavailable embark fort armok Run at the embark screen when you are choosing items to bring with you and skills to assign to your dwarves. You can set the available points to any diff --git a/docs/pop-control.rst b/docs/pop-control.rst index cb9a916598..d648451241 100644 --- a/docs/pop-control.rst +++ b/docs/pop-control.rst @@ -3,7 +3,7 @@ pop-control .. dfhack-tool:: :summary: Controls population and migration caps persistently per-fort. - :tags: untested fort auto gameplay + :tags: unavailable fort auto gameplay This script controls `hermit` and the various population caps per-fortress. It is intended to be run from ``dfhack-config/init/onMapLoad.init`` as diff --git a/docs/prefchange.rst b/docs/prefchange.rst index 7ce46ae486..11eebd6efd 100644 --- a/docs/prefchange.rst +++ b/docs/prefchange.rst @@ -3,7 +3,7 @@ prefchange .. dfhack-tool:: :summary: Set strange mood preferences. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool sets preferences for strange moods to include a weapon type, equipment type, and material. If you also wish to trigger a mood, see `strangemood`. diff --git a/docs/putontable.rst b/docs/putontable.rst index 566c4402a4..99c3f3778b 100644 --- a/docs/putontable.rst +++ b/docs/putontable.rst @@ -3,7 +3,7 @@ putontable .. dfhack-tool:: :summary: Make an item appear on a table. - :tags: untested fort armok items + :tags: unavailable fort armok items To use this tool, move an item to the ground on the same tile as a built table. Then, place the cursor over the table and item and run this command. The item diff --git a/docs/questport.rst b/docs/questport.rst index ea52bdbb02..c2c2712e1a 100644 --- a/docs/questport.rst +++ b/docs/questport.rst @@ -3,7 +3,7 @@ questport .. dfhack-tool:: :summary: Teleport to your quest log map cursor. - :tags: untested adventure armok + :tags: unavailable adventure armok If you open the quest log map and move the cursor to your target location, you can run this command to teleport straight there. This can be done both within diff --git a/docs/region-pops.rst b/docs/region-pops.rst index 8c7db255df..9627b7fd18 100644 --- a/docs/region-pops.rst +++ b/docs/region-pops.rst @@ -3,7 +3,7 @@ region-pops .. dfhack-tool:: :summary: Change regional animal populations. - :tags: untested fort inspection animals + :tags: unavailable fort inspection animals This tool can show or modify the populations of animals in the region. diff --git a/docs/resurrect-adv.rst b/docs/resurrect-adv.rst index 744779197c..6bd10393af 100644 --- a/docs/resurrect-adv.rst +++ b/docs/resurrect-adv.rst @@ -3,7 +3,7 @@ resurrect-adv .. dfhack-tool:: :summary: Bring a dead adventurer back to life. - :tags: untested adventure armok + :tags: unavailable adventure armok Have you ever died, but wish you hadn't? This tool can help : ) When you see the "You are deceased" message, run this command to be resurrected and fully healed. diff --git a/docs/reveal-adv-map.rst b/docs/reveal-adv-map.rst index dd5ba4c0c2..af92f06b39 100644 --- a/docs/reveal-adv-map.rst +++ b/docs/reveal-adv-map.rst @@ -3,7 +3,7 @@ reveal-adv-map .. dfhack-tool:: :summary: Reveal or hide the world map. - :tags: untested adventure armok map + :tags: unavailable adventure armok map This tool can be used to either reveal or hide all tiles on the world map in adventure mode (visible when viewing the quest log or fast traveling). diff --git a/docs/season-palette.rst b/docs/season-palette.rst index 92f147636d..5b7dcc000d 100644 --- a/docs/season-palette.rst +++ b/docs/season-palette.rst @@ -3,7 +3,7 @@ season-palette .. dfhack-tool:: :summary: Swap color palettes when the seasons change. - :tags: untested fort auto graphics + :tags: unavailable fort auto graphics For this tool to work you need to add *at least* one color palette file to your save raw directory. These files must be in the same format as diff --git a/docs/set-orientation.rst b/docs/set-orientation.rst index 37bac9dcd2..c1c549252a 100644 --- a/docs/set-orientation.rst +++ b/docs/set-orientation.rst @@ -3,7 +3,7 @@ set-orientation .. dfhack-tool:: :summary: Alter a unit's romantic inclinations. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool lets you tinker with the interest levels your dwarves have towards dwarves of the same/different sex. diff --git a/docs/siren.rst b/docs/siren.rst index 7488e5f34e..c62668a2cb 100644 --- a/docs/siren.rst +++ b/docs/siren.rst @@ -3,7 +3,7 @@ siren .. dfhack-tool:: :summary: Wake up sleeping units and stop parties. - :tags: untested fort armok units + :tags: unavailable fort armok units Sound the alarm! This tool can shake your sleeping units awake and knock some sense into your party animal military dwarves so they can address a siege. diff --git a/docs/spawnunit.rst b/docs/spawnunit.rst index 3f8cbfb223..42a0687108 100644 --- a/docs/spawnunit.rst +++ b/docs/spawnunit.rst @@ -3,7 +3,7 @@ spawnunit .. dfhack-tool:: :summary: Create a unit. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool allows you to easily spawn a unit of your choice. It is a simplified interface to `modtools/create-unit`, which this tool uses to actually create diff --git a/docs/startdwarf.rst b/docs/startdwarf.rst index f650be7d5e..4f63442a99 100644 --- a/docs/startdwarf.rst +++ b/docs/startdwarf.rst @@ -3,7 +3,7 @@ startdwarf .. dfhack-tool:: :summary: Increase the number of dwarves you embark with. - :tags: untested embark fort armok + :tags: unavailable embark fort armok You must use this tool before embarking (e.g. at the site selection screen or any time before) to change the number of dwarves you embark with from the diff --git a/docs/suspend.rst b/docs/suspend.rst index 6c50ab0f37..f7fb9a3cd4 100644 --- a/docs/suspend.rst +++ b/docs/suspend.rst @@ -3,7 +3,7 @@ suspend .. dfhack-tool:: :summary: Suspends building construction jobs. - :tags: fort productivity jobs + :tags: auto graphics military This tool will suspend jobs. It can either suspend all the current jobs, or only construction jobs that are likely to block other jobs. When building walls, it's diff --git a/docs/tidlers.rst b/docs/tidlers.rst index 03fea7e24d..295bd8d998 100644 --- a/docs/tidlers.rst +++ b/docs/tidlers.rst @@ -3,7 +3,7 @@ tidlers .. dfhack-tool:: :summary: Change where the idlers count is displayed. - :tags: untested interface + :tags: unavailable interface This tool simply cycles the idlers count among the possible positions where the idlers count can be placed, including making it disappear entirely. diff --git a/docs/timestream.rst b/docs/timestream.rst index de4e65a5cb..07e359278c 100644 --- a/docs/timestream.rst +++ b/docs/timestream.rst @@ -3,7 +3,7 @@ timestream .. dfhack-tool:: :summary: Fix FPS death. - :tags: untested fort auto fps + :tags: unavailable fort auto fps Do you remember when you first start a new fort, your initial 7 dwarves zip around the screen and get things done so quickly? As a player, you never had diff --git a/docs/undump-buildings.rst b/docs/undump-buildings.rst index f3e90439a2..cf76ca7234 100644 --- a/docs/undump-buildings.rst +++ b/docs/undump-buildings.rst @@ -3,7 +3,7 @@ undump-buildings .. dfhack-tool:: :summary: Undesignate building base materials for dumping. - :tags: untested fort productivity buildings + :tags: unavailable fort productivity buildings If you designate a bunch of tiles in dump mode, all the items on those tiles will be marked for dumping. Unfortunately, if there are buildings on any of diff --git a/docs/uniform-unstick.rst b/docs/uniform-unstick.rst index 3e164d977d..d9b08d7a1a 100644 --- a/docs/uniform-unstick.rst +++ b/docs/uniform-unstick.rst @@ -3,7 +3,7 @@ uniform-unstick .. dfhack-tool:: :summary: Make military units reevaluate their uniforms. - :tags: untested fort bugfix military + :tags: unavailable fort bugfix military This tool prompts military units to reevaluate their uniform, making them remove and drop potentially conflicting worn items. diff --git a/docs/unretire-anyone.rst b/docs/unretire-anyone.rst index 0df984dd25..9adce5ad0d 100644 --- a/docs/unretire-anyone.rst +++ b/docs/unretire-anyone.rst @@ -3,7 +3,7 @@ unretire-anyone .. dfhack-tool:: :summary: Adventure as any living historical figure. - :tags: untested adventure embark armok + :tags: unavailable adventure embark armok This tool allows you to play as any living (or undead) historical figure (except for deities) in adventure mode. diff --git a/docs/view-item-info.rst b/docs/view-item-info.rst index c91601b19b..84b6eb47a3 100644 --- a/docs/view-item-info.rst +++ b/docs/view-item-info.rst @@ -3,7 +3,7 @@ view-item-info .. dfhack-tool:: :summary: Extend item and unit descriptions with more information. - :tags: untested adventure fort interface + :tags: unavailable adventure fort interface This tool extends the item or unit description viewscreen with additional information, including a custom description of each item (when available), and diff --git a/docs/view-unit-reports.rst b/docs/view-unit-reports.rst index 963f03137b..b649038ded 100644 --- a/docs/view-unit-reports.rst +++ b/docs/view-unit-reports.rst @@ -3,7 +3,7 @@ view-unit-reports .. dfhack-tool:: :summary: Show combat reports for a unit. - :tags: untested fort inspection military + :tags: unavailable fort inspection military Show combat reports specifically for the selected unit. You can select a unit with the cursor in :kbd:`v` mode, from the list in :kbd:`u` mode, or from the diff --git a/docs/warn-stealers.rst b/docs/warn-stealers.rst index f90544a4c0..bf81f0b602 100644 --- a/docs/warn-stealers.rst +++ b/docs/warn-stealers.rst @@ -3,7 +3,7 @@ warn-stealers .. dfhack-tool:: :summary: Watch for and warn about units that like to steal your stuff. - :tags: untested fort armok auto units + :tags: unavailable fort armok auto units This script will watch for new units entering the map and will make a zoomable announcement whenever a creature that can eat food, guzzle drinks, or steal diff --git a/docs/workorder-recheck.rst b/docs/workorder-recheck.rst index 21da3622b7..70f18fdf48 100644 --- a/docs/workorder-recheck.rst +++ b/docs/workorder-recheck.rst @@ -3,7 +3,7 @@ workorder-recheck .. dfhack-tool:: :summary: Recheck start conditions for a manager workorder. - :tags: untested fort workorders + :tags: unavailable fort workorders Sets the status to ``Checking`` (from ``Active``) of the selected work order (in the ``j-m`` or ``u-m`` screens). This makes the manager reevaluate its diff --git a/gui/launcher.lua b/gui/launcher.lua index 1468c302ba..5a5a2a6d26 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -794,7 +794,7 @@ local function sort_by_freq(entries) table.sort(entries, stable_sort_by_frequency) end -local DEV_FILTER = {tag={'dev', 'untested'}} +local DEV_FILTER = {tag={'dev', 'unavailable'}} -- adds the n most closely affiliated peer entries for the given entry that -- aren't already in the entries list. affiliation is determined by how many From 85c6b40e35b43604a5f75aa1f62bde52c8be35bd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Mar 2023 00:43:25 -0700 Subject: [PATCH 059/732] widgetify the onscreen keyboard and add backspace button --- changelog.txt | 1 + gui/cp437-table.lua | 60 +++++++++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/changelog.txt b/changelog.txt index f883e7df74..d6f7685ba1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ that repo. - `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor - `combine`: added ``--quiet`` option for no output when there are no changes - `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles +- `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button ## Removed - ``gui/dig``: renamed to ``gui/design`` diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index dd29cf3279..a655641a84 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -6,10 +6,8 @@ local widgets = require('gui.widgets') CPDialog = defclass(CPDialog, widgets.Window) CPDialog.ATTRS { - focus_path='cp437-table', frame_title='CP437 table', - drag_anchors={frame=true, body=true}, - frame={w=36, h=17}, + frame={w=36, h=20}, } function CPDialog:init(info) @@ -17,32 +15,48 @@ function CPDialog:init(info) widgets.EditField{ view_id='edit', frame={t=0, l=0}, - on_submit=self:callback('submit'), }, widgets.Panel{ view_id='board', frame={t=2, l=0, w=32, h=9}, - on_render=self:callback('render_board'), }, widgets.Label{ - frame={b=1, l=0}, + frame={b=4, l=0}, text='Click characters or type', }, - widgets.Label{ + widgets.HotkeyLabel{ + frame={b=2, l=0}, + key='SELECT', + label='Send text to parent', + on_activate=self:callback('submit'), + }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='STRING_A000', + label='Backspace', + on_activate=function() self.subviews.edit:onInput{_STRING=0} end, + }, + widgets.HotkeyLabel{ frame={b=0, l=0}, - text={ - {key='LEAVESCREEN', text=': Cancel'}, - ' ', - {key='SELECT', text=': Done'}, - }, + key='LEAVESCREEN', + label='Cancel', + on_activate=function() self.parent_view:dismiss() end, }, } -end -function CPDialog:render_board(dc) + local board = self.subviews.board + local edit = self.subviews.edit for ch = 0,255 do if dfhack.screen.charToKey(ch) then - dc:seek(ch % 32, math.floor(ch / 32)):char(ch) + local chr = string.char(ch) + board:addviews{ + widgets.Label{ + frame={t=ch//32, l=ch%32, w=1, h=1}, + auto_height=false, + text=chr, + on_click=function() if ch ~= 0 then edit:insert(chr) end end, + }, + } end end end @@ -69,18 +83,10 @@ function CPDialog:submit() end end) screen:dismiss() -end -function CPDialog:onInput(keys) - local x, y = self.subviews.board:getMousePos() - if keys._MOUSE_L_DOWN and x then - local ch = x + (32 * y) - if ch ~= 0 and dfhack.screen.charToKey(ch) then - self.subviews.edit:insert(string.char(ch)) - end - return true - end - return CPDialog.super.onInput(self, keys) + -- ensure clicks on "submit" don't bleed through + df.global.enabler.mouse_lbut = 0 + df.global.enabler.mouse_lbut_down = 0 end CPScreen = defclass(CPScreen, gui.ZScreen) @@ -89,7 +95,7 @@ CPScreen.ATTRS { } function CPScreen:init() - self:addviews{CPDialog{view_id='main'}} + self:addviews{CPDialog{}} end function CPScreen:onDismiss() From 17fb57754f65e7a8b054d5246795a68b0d3e770e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Mar 2023 00:53:04 -0700 Subject: [PATCH 060/732] change background to red for better visibility update docs --- docs/gui/cp437-table.rst | 2 +- gui/cp437-table.lua | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/gui/cp437-table.rst b/docs/gui/cp437-table.rst index 65ca7995be..c092c34e0d 100644 --- a/docs/gui/cp437-table.rst +++ b/docs/gui/cp437-table.rst @@ -9,7 +9,7 @@ This tool provides an in-game virtual keyboard. You can choose from all the characters that DF supports (code page 437). Just click on the characters to build the text that you want to send to the parent screen. The text is sent as soon as you hit :kbd:`Enter`, so make sure there is a text field selected -before starting this UI! +in the parent window before starting this UI! Usage ----- diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index a655641a84..11007f10b6 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -46,6 +46,7 @@ function CPDialog:init(info) local board = self.subviews.board local edit = self.subviews.edit + local hpen = dfhack.pen.parse{fg=COLOR_WHITE, bg=COLOR_RED} for ch = 0,255 do if dfhack.screen.charToKey(ch) then local chr = string.char(ch) @@ -54,6 +55,7 @@ function CPDialog:init(info) frame={t=ch//32, l=ch%32, w=1, h=1}, auto_height=false, text=chr, + text_hpen=hpen, on_click=function() if ch ~= 0 then edit:insert(chr) end end, }, } From 8b2e2c007f71b575e8d7953995f0301e0ca4fe00 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Mar 2023 01:31:44 -0700 Subject: [PATCH 061/732] add fix/general-strike --- docs/fix/general-strike.rst | 25 +++++++++++++++++++++++++ fix/general-strike.lua | 35 +++++++++++++++++++++++++++++++++++ gui/control-panel.lua | 3 +++ 3 files changed, 63 insertions(+) create mode 100644 docs/fix/general-strike.rst create mode 100644 fix/general-strike.lua diff --git a/docs/fix/general-strike.rst b/docs/fix/general-strike.rst new file mode 100644 index 0000000000..7bdd5ccf2c --- /dev/null +++ b/docs/fix/general-strike.rst @@ -0,0 +1,25 @@ +fix/general-strike +================== + +.. dfhack-tool:: + :summary: Prevent dwarves from getting stuck and refusing to work. + :tags: fort bugfix + +This script attempts to fix known causes of the "general strike bug", where +dwarves just stop accepting work and stand around with "No job". + +You can enable automatic running of this fix in the "Maintenance" tab of +`gui/control-panel`. + +Usage +----- + +:: + + fix/general-strike [] + +Options +------- + +``-q``, ``--quiet`` + Only output status when something was actually fixed. diff --git a/fix/general-strike.lua b/fix/general-strike.lua new file mode 100644 index 0000000000..bbe3411484 --- /dev/null +++ b/fix/general-strike.lua @@ -0,0 +1,35 @@ +local argparse = require('argparse') + +-- sometimes, planted seeds lose their 'in_building' flag. This causes massive +-- amounts of job cancellation spam as everyone tries, then fails, to re-plant +-- the seeds. +local function fix_seeds(quiet) + local count = 0 + for _,v in ipairs(df.global.world.items.other.SEEDS) do + if (dfhack.items.getGeneralRef(v, df.general_ref_type.BUILDING_HOLDER)) then + v.flags.in_building = true + count = count + 1 + end + end + if not quiet or count > 0 then + print(('fixed %d seed(s)'):format(count)) + end +end + +local function main(args) + local help = false + local quiet = false + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() help = true end}, + {'q', 'quiet', handler=function() quiet = true end}, + }) + + if help or positionals[1] == 'help' then + print(dfhack.script_help()) + return + end + + fix_seeds(quiet) +end + +main{...} diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 15d7ae0ff1..e5b9f7db22 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -79,6 +79,9 @@ local REPEATS = { ['combine']={ desc='Combine partial stacks in stockpiles into full stacks.', command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, + ['general-strike']={ + desc='Prevent dwarves from getting stuck and refusing to work.', + command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, ['orders-sort']={ desc='Sort manager orders by repeat frequency so one-time orders can be completed.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, From cd588eec2b811d8a657c637d2122ee5bd1ea3c0e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Mar 2023 01:33:24 -0700 Subject: [PATCH 062/732] update changelog --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index f883e7df74..09a0d8ca7e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,7 +14,8 @@ that repo. # Future ## New Scripts -- `gui/seedwatch`: GUI config and status panel interface for `seedwatch`. +- `fix/general-strike`: fix known causes of the general strike bug +- `gui/seedwatch`: GUI config and status panel interface for `seedwatch` ## Fixes - `suspendmanager`: does not suspend non blocking jobs such as floor bars or bridges anymore From bc721bce8b1344574ee6f477399997da23f70380 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Mar 2023 01:34:32 -0700 Subject: [PATCH 063/732] update changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 09a0d8ca7e..fabfba95d9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,7 +14,7 @@ that repo. # Future ## New Scripts -- `fix/general-strike`: fix known causes of the general strike bug +- `fix/general-strike`: fix known causes of the general strike bug (contributed by Putnam) - `gui/seedwatch`: GUI config and status panel interface for `seedwatch` ## Fixes From 0f5fc78e38c3dd782bb4bd2ad5c2949c1d0dd023 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Mar 2023 01:59:29 -0700 Subject: [PATCH 064/732] only increment counter if seeds are fixed --- fix/general-strike.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fix/general-strike.lua b/fix/general-strike.lua index bbe3411484..eabc42881f 100644 --- a/fix/general-strike.lua +++ b/fix/general-strike.lua @@ -6,7 +6,8 @@ local argparse = require('argparse') local function fix_seeds(quiet) local count = 0 for _,v in ipairs(df.global.world.items.other.SEEDS) do - if (dfhack.items.getGeneralRef(v, df.general_ref_type.BUILDING_HOLDER)) then + if not v.flags.in_building and + (dfhack.items.getGeneralRef(v, df.general_ref_type.BUILDING_HOLDER)) then v.flags.in_building = true count = count + 1 end From 9b857167a8070cd00309d8d15f0403d81bd27c57 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sat, 25 Mar 2023 16:56:49 +0100 Subject: [PATCH 065/732] Fix suspendmanager building identification sometimes being wrong --- changelog.txt | 1 + suspendmanager.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index f883e7df74..c1c5b8c1e4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes - `suspendmanager`: does not suspend non blocking jobs such as floor bars or bridges anymore +- `suspendmanager`: fix occasional bad identification of buildingplan jobs ## Misc Improvements - `gui/control-panel`: Now detects overlays from scripts named with capital letters diff --git a/suspendmanager.lua b/suspendmanager.lua index 1a370024bd..b4a56e4957 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -196,7 +196,7 @@ function shouldBeSuspended(job, accountblocking) return true, 'underwater' end - local bld = dfhack.buildings.findAtTile(job.pos) + local bld = dfhack.job.getHolder(job) if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then return true, 'buildingplan' end From ff32c72896f0ba51b79936ee8729a4b13eafe340 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Mar 2023 23:22:58 -0700 Subject: [PATCH 066/732] remove autounsuspend --- autounsuspend.lua | 66 ------------------------------------------ changelog.txt | 3 +- docs/autounsuspend.rst | 18 ------------ 3 files changed, 2 insertions(+), 85 deletions(-) delete mode 100644 autounsuspend.lua delete mode 100644 docs/autounsuspend.rst diff --git a/autounsuspend.lua b/autounsuspend.lua deleted file mode 100644 index 2be18ff5f8..0000000000 --- a/autounsuspend.lua +++ /dev/null @@ -1,66 +0,0 @@ --- Automate periodic running of the unsuspend script ---@module = true ---@enable = true - -local json = require('json') -local persist = require('persist-table') - -local GLOBAL_KEY = 'autounsuspend' -- used for state change hooks and persistence - -enabled = enabled or false - -function isEnabled() - return enabled -end - -local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled}) -end - -local function event_loop() - if enabled then - dfhack.run_script('unsuspend', '--quiet') - dfhack.timeout(1, 'days', event_loop) - end -end - -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - enabled = false - return - end - - if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return - end - - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') - enabled = (persisted_data or {enabled=false})['enabled'] - event_loop() -end - -if dfhack_flags.module then - return -end - -if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then - dfhack.printerr('autounsuspend needs a loaded fortress map to work') - return -end - -local args = {...} -if dfhack_flags and dfhack_flags.enable then - args = {dfhack_flags.enable_state and 'enable' or 'disable'} -end - -local command = args[1] -if command == "enable" then - enabled = true -elseif command == "disable" then - enabled = false -else - return -end - -event_loop() -persist_state() diff --git a/changelog.txt b/changelog.txt index 81b4f21a78..325c568464 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,7 +30,8 @@ that repo. - `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button ## Removed -- ``gui/dig``: renamed to ``gui/design`` +- `autounsuspend`: replaced by `suspendmanager` +- `gui/dig`: renamed to `gui/design` # 50.07-beta1 diff --git a/docs/autounsuspend.rst b/docs/autounsuspend.rst deleted file mode 100644 index c00b81726e..0000000000 --- a/docs/autounsuspend.rst +++ /dev/null @@ -1,18 +0,0 @@ -autounsuspend -============= - -.. dfhack-tool:: - :summary: Keep construction jobs unsuspended. - - -This tool will unsuspend jobs that have become suspended due to inaccessible -materials, items in the way, or worker dwarves getting scared by wildlife. - -Also see `unsuspend` for one-time use. - -Usage ------ - -:: - - enable autounsuspend From 3a259a3322fceba409ab0ff8c44ea87278c52c63 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 25 Mar 2023 15:21:21 -0700 Subject: [PATCH 067/732] overhaul onscreen kbd to be usable on steam deck --- changelog.txt | 1 + gui/cp437-table.lua | 89 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/changelog.txt b/changelog.txt index 325c568464..1e1ba6847f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -23,6 +23,7 @@ that repo. ## Misc Improvements - `gui/control-panel`: Now detects overlays from scripts named with capital letters +- `gui/cp437-table`: now has larger key buttons and clickable backspace/submit/cancel buttons, making it fully usable on the Steam Deck and other systems that don't have an accessible keyboard - `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. - `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor - `combine`: added ``--quiet`` option for no output when there are no changes diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index 11007f10b6..e38ac7a401 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -4,10 +4,38 @@ local dialog = require('gui.dialogs') local gui = require('gui') local widgets = require('gui.widgets') +local to_pen = dfhack.pen.parse + +local tb_texpos = dfhack.textures.getThinBordersTexposStart() +local tp = function(offset) + if tb_texpos == -1 then return nil end + return tb_texpos + offset +end + +local function get_key_pens(ch) + return { + lt=to_pen{tile=tp(0), write_to_lower=true}, + t=to_pen{tile=tp(1), ch=ch, write_to_lower=true, top_of_text=true}, + t_ascii=to_pen{ch=32}, + rt=to_pen{tile=tp(2), write_to_lower=true}, + lb=to_pen{tile=tp(14), write_to_lower=true}, + b=to_pen{tile=tp(15), ch=ch, write_to_lower=true, bottom_of_text=true}, + rb=to_pen{tile=tp(16), write_to_lower=true}, + } +end + +local function get_key_hover_pens(ch) + return { + t=to_pen{tile=tp(1), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, top_of_text=true}, + t_ascii=to_pen{fg=COLOR_WHITE, bg=COLOR_RED, ch=ch == 0 and 0 or 32}, + b=to_pen{tile=tp(15), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, bottom_of_text=true}, + } +end + CPDialog = defclass(CPDialog, widgets.Window) CPDialog.ATTRS { frame_title='CP437 table', - frame={w=36, h=20}, + frame={w=100, h=26}, } function CPDialog:init(info) @@ -18,48 +46,64 @@ function CPDialog:init(info) }, widgets.Panel{ view_id='board', - frame={t=2, l=0, w=32, h=9}, + frame={t=2, l=0, w=96, h=18}, }, widgets.Label{ - frame={b=4, l=0}, + frame={b=2, l=0}, text='Click characters or type', }, widgets.HotkeyLabel{ - frame={b=2, l=0}, + frame={b=0, l=0}, key='SELECT', label='Send text to parent', + auto_width=true, on_activate=self:callback('submit'), }, widgets.HotkeyLabel{ - frame={b=1, l=0}, + frame={b=0}, key='STRING_A000', label='Backspace', + auto_width=true, on_activate=function() self.subviews.edit:onInput{_STRING=0} end, }, widgets.HotkeyLabel{ - frame={b=0, l=0}, + frame={b=0, r=0}, key='LEAVESCREEN', label='Cancel', + auto_width=true, on_activate=function() self.parent_view:dismiss() end, }, } local board = self.subviews.board local edit = self.subviews.edit - local hpen = dfhack.pen.parse{fg=COLOR_WHITE, bg=COLOR_RED} for ch = 0,255 do - if dfhack.screen.charToKey(ch) then - local chr = string.char(ch) - board:addviews{ - widgets.Label{ - frame={t=ch//32, l=ch%32, w=1, h=1}, - auto_height=false, - text=chr, - text_hpen=hpen, - on_click=function() if ch ~= 0 then edit:insert(chr) end end, - }, - } + local xoff, yoff = (ch%32) * 3, (ch//32) * 2 + if not dfhack.screen.charToKey(ch) then ch = 0 end + local pens = get_key_pens(ch) + local hpens = get_key_hover_pens(ch) + local function get_top_tile() + return dfhack.screen.inGraphicsMode() and pens.t or pens.t_ascii + end + local function get_top_htile() + return dfhack.screen.inGraphicsMode() and hpens.t or hpens.t_ascii end + board:addviews{ + widgets.Label{ + frame={t=yoff, l=xoff, w=3, h=2}, + auto_height=false, + text={ + {tile=pens.lt}, + {tile=get_top_tile, htile=get_top_htile}, + {tile=pens.rt}, + NEWLINE, + {tile=pens.lb}, + {tile=pens.b, htile=hpens.b}, + {tile=pens.rb}, + }, + on_click=function() if ch ~= 0 then edit:insert(string.char(ch)) end end, + }, + } end end @@ -77,6 +121,11 @@ function CPDialog:submit() end keys[i] = k end + + -- ensure clicks on "submit" don't bleed through + df.global.enabler.mouse_lbut = 0 + df.global.enabler.mouse_lbut_down = 0 + local screen = self.parent_view local parent = screen._native.parent dfhack.screen.hideGuard(screen, function() @@ -85,10 +134,6 @@ function CPDialog:submit() end end) screen:dismiss() - - -- ensure clicks on "submit" don't bleed through - df.global.enabler.mouse_lbut = 0 - df.global.enabler.mouse_lbut_down = 0 end CPScreen = defclass(CPScreen, gui.ZScreen) From 37f8fb777e12238d62986a3d513f4c65a71188a7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Mar 2023 02:30:17 -0700 Subject: [PATCH 068/732] add gui/civ-alert --- gui/civ-alert.lua | 274 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 gui/civ-alert.lua diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua new file mode 100644 index 0000000000..825944d442 --- /dev/null +++ b/gui/civ-alert.lua @@ -0,0 +1,274 @@ +--@ module=true + +local gui = require('gui') +local widgets = require('gui.widgets') +local overlay = require('plugins.overlay') + +local function get_civ_alert() + local list = df.global.plotinfo.alerts.list + while #list < 2 do + local list_item = df.plotinfost.T_alerts.T_list:new() + list_item.id = df.global.plotinfo.alerts.next_id + df.global.plotinfo.alerts.next_id = df.global.plotinfo.alerts.next_id + 1 + list_item.name = 'civ-alert' + list:insert('#', list_item) + end + return list[1] +end + +local function can_sound_alarm() + return df.global.plotinfo.alerts.civ_alert_idx == 0 and + #get_civ_alert().burrows > 0 +end + +local function sound_alarm() + if not can_sound_alarm() then return end + df.global.plotinfo.alerts.civ_alert_idx = 1 +end + +local function can_clear_alarm() + return df.global.plotinfo.alerts.civ_alert_idx ~= 0 +end + +local function clear_alarm() + df.global.plotinfo.alerts.civ_alert_idx = 0 +end + +local function toggle_civalert_burrow(id) + local burrows = get_civ_alert().burrows + if #burrows == 0 then + burrows:insert('#', id) + elseif burrows[0] == id then + burrows:resize(0) + else + burrows[0] = id + end +end + +-- +-- BigRedButton +-- + +local to_pen = dfhack.pen.parse +local BUTTON_TEXT_ON = to_pen{fg=COLOR_BLACK, bg=COLOR_LIGHTRED} +local BUTTON_TEXT_OFF = to_pen{fg=COLOR_WHITE, bg=COLOR_RED} + +BigRedButton = defclass(BigRedButton, widgets.Panel) +BigRedButton.ATTRS{ +} + +function BigRedButton:init() + self.frame = self.frame or {} + self.frame.w = 10 + self.frame.h = 3 + + self:addviews{ + widgets.Label{ + text={ + ' Activate ', NEWLINE, + ' civilian ', NEWLINE, + ' alert ', + }, + text_pen=BUTTON_TEXT_ON, + text_hpen=BUTTON_TEXT_OFF, + visible=can_sound_alarm, + on_click=sound_alarm, + }, + widgets.Label{ + text={ + ' Clear ', NEWLINE, + ' civilian ', NEWLINE, + ' alert ', + }, + text_pen=BUTTON_TEXT_OFF, + text_hpen=BUTTON_TEXT_ON, + visible=can_clear_alarm, + on_click=clear_alarm, + }, + } +end + +-- +-- CivalertOverlay +-- + +CivalertOverlay = defclass(CivalertOverlay, overlay.OverlayWidget) +CivalertOverlay.ATTRS{ + default_pos={x=-30,y=20}, + default_enabled=true, + viewscreens='dwarfmode', + frame={w=20, h=7}, +} + +local function should_show_alert_button() + return can_clear_alarm() or + (df.global.game.main_interface.squads.open and can_sound_alarm()) +end + +local function should_show_configure_button() + return df.global.game.main_interface.squads.open + and not can_sound_alarm() and not can_clear_alarm() +end + +local function get_burrow_name(burrow) + if #burrow.name > 0 then return burrow.name end + return ('Burrow %d'):format(burrow.id) +end + +local function get_civ_alert_burrow_name() + local burrows = get_civ_alert().burrows + if #burrows == 0 then return '' end + local burrow = df.burrow.find(burrows[0]) + if not burrow then return '' end + return get_burrow_name(burrow) +end + +local function launch_config() + dfhack.run_script('gui/civ-alert') +end + +function CivalertOverlay:init() + self:addviews{ + widgets.Panel{ + frame={t=0, r=0, w=12, h=7}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + visible=should_show_alert_button, + subviews={ + BigRedButton{ + frame={t=0}, + }, + widgets.Label{ + frame={t=3, l=0}, + text='Burrow:', + }, + widgets.Label{ + frame={t=4, l=0}, + text={ + {text=get_civ_alert_burrow_name}, + }, + on_click=launch_config, + } + }, + }, + widgets.Panel{ + frame={t=0, r=0, w=20, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + visible=should_show_configure_button, + subviews={ + widgets.Label{ + text={ + 'Click to configure', NEWLINE, + {gap=2, text='civilian alert'}, + }, + on_click=launch_config, + }, + }, + }, + } +end + +OVERLAY_WIDGETS = {big_red_button=CivalertOverlay} + +-- +-- Civalert +-- + +Civalert = defclass(Civalert, widgets.Window) +Civalert.ATTRS{ + frame_title='Civilian alert', + frame={w=60, h=20}, + resizable=true, + resize_min={h=15}, +} + +function Civalert:init() + self:addviews{ + widgets.Panel{ + frame={t=0, l=0, r=12}, + subviews={ + widgets.WrappedLabel{ + frame={t=0, r=0, h=2}, + text_to_wrap='Choose a burrow where you want your civilians to hide during danger.', + }, + widgets.HotkeyLabel{ + frame={t=3, l=0}, + key='CUSTOM_CTRL_W', + label='Sound alarm! Citizens run to safety!', + on_activate=sound_alarm, + enabled=can_sound_alarm, + }, + widgets.HotkeyLabel{ + frame={t=4, l=0}, + key='CUSTOM_CTRL_D', + label='All clear! Citizens return to normal', + on_activate=clear_alarm, + enabled=can_clear_alarm, + }, + }, + }, + BigRedButton{ + frame={t=0, r=0}, + }, + widgets.FilteredList{ + frame={t=6, l=0, b=0, r=0}, + choices=self:get_burrow_choices(), + icon_width=2, + on_submit=self:callback('select_burrow'), + }, + } +end + +local SELECTED_ICON = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} + +function Civalert:get_burrow_icon(id) + local burrows = get_civ_alert().burrows + if #burrows == 0 or burrows[0] ~= id then return nil end + return SELECTED_ICON +end + +function Civalert:get_burrow_choices() + local choices = {} + for _,burrow in ipairs(df.global.plotinfo.burrows.list) do + local choice = { + text=get_burrow_name(burrow), + id=burrow.id, + icon=self:callback('get_burrow_icon', burrow.id), + } + table.insert(choices, choice) + end + return choices +end + +function Civalert:select_burrow(_, choice) + toggle_civalert_burrow(choice.id) + self:updateLayout() +end + +-- +-- CivalertScreen +-- + +CivalertScreen = defclass(CivalertScreen, gui.ZScreen) +CivalertScreen.ATTRS { + focus_path='civalert', +} + +function CivalertScreen:init() + self:addviews{Civalert{}} +end + +function CivalertScreen:onDismiss() + view = nil +end + +if dfhack_flags.module then + return +end + +if not dfhack.isMapLoaded() then + qerror('gui/civ-alert requires a map to be loaded') +end + +view = view and view:raise() or CivalertScreen{}:show() From f9e9a4d9094f7ff8f7ecf51459db04a03b2712a9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Mar 2023 03:19:00 -0700 Subject: [PATCH 069/732] add docs for gui/civ-alert --- docs/gui/civ-alert.rst | 57 ++++++++++++++++++++++++++++++++++++++++++ gui/civ-alert.lua | 3 ++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 docs/gui/civ-alert.rst diff --git a/docs/gui/civ-alert.rst b/docs/gui/civ-alert.rst new file mode 100644 index 0000000000..31fed84a11 --- /dev/null +++ b/docs/gui/civ-alert.rst @@ -0,0 +1,57 @@ +gui/civ-alert +============= + +.. dfhack-tool:: + :summary: Quickly get your civilians to safety. + :tags: fort gameplay interface military units + +Normally, assigning a unit to a burrow is treated more like a suggestion than a +command. In contrast, civilian alerts cause all your non-military citizens to +immediately rush to a specified burrow ASAP and stay there. This allows you to +keep your civilians safe when there is danger about. + +Usage +----- + +:: + + gui/civ-alert + +How to set up and use a civilian alert +-------------------------------------- + +A civ alert needs a burrow to send civilians to. Go set one up if you haven't +already. If you have walls around a secure interior, you can include all your +below-ground area and the safe parts inside your walls. You can name the burrow +"Inside" or "Safety" or "Panic room" or whatever you like. + +Then, start up `gui/civ-alert` and select the burrow from the list. You can +activate the civ alert right away with the button in the upper right corner. +You can also access this button at any time from the squads panel. + +When danger appears, open up the squads menu and click on the new "Activate +civilian alert" button. It's big and red; you can't miss it. Your civilians +will rush off to safety and you can concentrate on dealing with the incursion +without Urist McArmorsmith getting in the way. + +When the civ alert is active, the civilian alert button will stay on the +screen, even if the squads menu is closed. After the danger has passed, +remember to turn the civ alert off again by clicking the button. Otherwise, +your units will continue to be confined to their burrow and may eventually +become unhappy or starve. + +Overlay +------- + +The position of the "Activate civilian alert" button that appears when the +squads panel is open is configurable via `gui/overlay`. The button will also +show you the current target burrow and give you hyperlink targets to quickly +launch `gui/civ-alert` for configuration. + +Technical notes +--------------- + +The functionality for civilian alerts is actually already inside the vanilla +game. The ability to configure civilian alerts was lost when the DF UI was +updated for the v50 release. This tool simply provides an interface layer for +the vanilla functionality. diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 825944d442..7ce87cf5da 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -40,6 +40,7 @@ local function toggle_civalert_burrow(id) burrows:insert('#', id) elseif burrows[0] == id then burrows:resize(0) + clear_alarm() else burrows[0] = id end @@ -152,7 +153,7 @@ function CivalertOverlay:init() }, }, widgets.Panel{ - frame={t=0, r=0, w=20, h=4}, + frame={b=0, r=0, w=20, h=4}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, visible=should_show_configure_button, From 2b607144c014351a7b7a8b3368da3edab8b1434a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Mar 2023 10:25:55 -0700 Subject: [PATCH 070/732] revise overlay position and layout Thanks for the feedback, Taxiservice! --- docs/gui/civ-alert.rst | 20 ++++++++------- gui/civ-alert.lua | 55 ++++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/docs/gui/civ-alert.rst b/docs/gui/civ-alert.rst index 31fed84a11..cc87518550 100644 --- a/docs/gui/civ-alert.rst +++ b/docs/gui/civ-alert.rst @@ -6,9 +6,11 @@ gui/civ-alert :tags: fort gameplay interface military units Normally, assigning a unit to a burrow is treated more like a suggestion than a -command. In contrast, civilian alerts cause all your non-military citizens to -immediately rush to a specified burrow ASAP and stay there. This allows you to -keep your civilians safe when there is danger about. +command. This can be frustrating when you're assigning units to burrows in +order to get them out of danger. In contrast, triggering a civilian alert with +`gui/civ-alert` will cause all your non-military citizens to immediately rush +to a burrow ASAP and stay there. This gives you a way to keep your civilians +safe when there is danger about. Usage ----- @@ -30,9 +32,9 @@ activate the civ alert right away with the button in the upper right corner. You can also access this button at any time from the squads panel. When danger appears, open up the squads menu and click on the new "Activate -civilian alert" button. It's big and red; you can't miss it. Your civilians -will rush off to safety and you can concentrate on dealing with the incursion -without Urist McArmorsmith getting in the way. +civilian alert" button in the lower left corner. It's big and red; you can't +miss it. Your civilians will rush off to safety and you can concentrate on +dealing with the incursion without Urist McArmorsmith getting in the way. When the civ alert is active, the civilian alert button will stay on the screen, even if the squads menu is closed. After the danger has passed, @@ -44,9 +46,9 @@ Overlay ------- The position of the "Activate civilian alert" button that appears when the -squads panel is open is configurable via `gui/overlay`. The button will also -show you the current target burrow and give you hyperlink targets to quickly -launch `gui/civ-alert` for configuration. +squads panel is open is configurable via `gui/overlay`. The overlay panel also +gives you a way to launch `gui/civ-alert` if you need to change which burrow +civilians should be gathering at. Technical notes --------------- diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 7ce87cf5da..e7363e8cbf 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -95,10 +95,10 @@ end CivalertOverlay = defclass(CivalertOverlay, overlay.OverlayWidget) CivalertOverlay.ATTRS{ - default_pos={x=-30,y=20}, + default_pos={x=-15,y=-1}, default_enabled=true, viewscreens='dwarfmode', - frame={w=20, h=7}, + frame={w=20, h=5}, } local function should_show_alert_button() @@ -111,45 +111,49 @@ local function should_show_configure_button() and not can_sound_alarm() and not can_clear_alarm() end -local function get_burrow_name(burrow) - if #burrow.name > 0 then return burrow.name end - return ('Burrow %d'):format(burrow.id) +local function launch_config() + dfhack.run_script('gui/civ-alert') end -local function get_civ_alert_burrow_name() - local burrows = get_civ_alert().burrows - if #burrows == 0 then return '' end - local burrow = df.burrow.find(burrows[0]) - if not burrow then return '' end - return get_burrow_name(burrow) -end +last_tp_start = last_tp_start or 0 +CONFIG_BUTTON_PENS = CONFIG_BUTTON_PENS or {} +local function get_button_pen(idx) + local start = dfhack.textures.getControlPanelTexposStart() + if last_tp_start == start then return CONFIG_BUTTON_PENS[idx] end + last_tp_start = start -local function launch_config() - dfhack.run_script('gui/civ-alert') + local tp = function(offset) + if start == -1 then return nil end + return start + offset + end + + CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=tp(6), ch=string.byte('[')} + CONFIG_BUTTON_PENS[2] = to_pen{tile=tp(9), ch=15} -- gear/masterwork symbol + CONFIG_BUTTON_PENS[3] = to_pen{fg=COLOR_CYAN, tile=tp(7), ch=string.byte(']')} + + return CONFIG_BUTTON_PENS[idx] end function CivalertOverlay:init() self:addviews{ widgets.Panel{ - frame={t=0, r=0, w=12, h=7}, + frame={t=0, r=0, w=16, h=5}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, visible=should_show_alert_button, subviews={ BigRedButton{ - frame={t=0}, - }, - widgets.Label{ - frame={t=3, l=0}, - text='Burrow:', + frame={t=0, l=0}, }, widgets.Label{ - frame={t=4, l=0}, + frame={t=1, r=0, w=3}, text={ - {text=get_civ_alert_burrow_name}, + {tile=curry(get_button_pen, 1)}, + {tile=curry(get_button_pen, 2)}, + {tile=curry(get_button_pen, 3)}, }, on_click=launch_config, - } + }, }, }, widgets.Panel{ @@ -221,6 +225,11 @@ function Civalert:init() } end +local function get_burrow_name(burrow) + if #burrow.name > 0 then return burrow.name end + return ('Burrow %d'):format(burrow.id) +end + local SELECTED_ICON = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} function Civalert:get_burrow_icon(id) From 6e7727dcf14c3bf29f78c6404922d219323a31fe Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Mar 2023 21:38:47 -0700 Subject: [PATCH 071/732] give configure text a highlighted background --- gui/civ-alert.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index e7363e8cbf..0d41a0db9a 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -167,6 +167,7 @@ function CivalertOverlay:init() 'Click to configure', NEWLINE, {gap=2, text='civilian alert'}, }, + text_pen=to_pen{fg=COLOR_BLACK, bg=COLOR_YELLOW}, on_click=launch_config, }, }, From f2f74dc77a47400c1c1396ecc4d5422dade0569f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Mar 2023 01:31:19 -0700 Subject: [PATCH 072/732] add vaporize kill method for exterminate --- changelog.txt | 1 + docs/exterminate.rst | 57 +++++++++++++++----------- exterminate.lua | 96 +++++++++++++++++++++----------------------- 3 files changed, 79 insertions(+), 75 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1e1ba6847f..f77091cc9d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ that repo. - `gui/control-panel`: Now detects overlays from scripts named with capital letters - `gui/cp437-table`: now has larger key buttons and clickable backspace/submit/cancel buttons, making it fully usable on the Steam Deck and other systems that don't have an accessible keyboard - `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. +- `exterminate`: add support for ``vaporize`` kill method for when you don't want to leave a corpse - `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor - `combine`: added ``--quiet`` option for no output when there are no changes - `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles diff --git a/docs/exterminate.rst b/docs/exterminate.rst index a84e27d65a..1e2609a81c 100644 --- a/docs/exterminate.rst +++ b/docs/exterminate.rst @@ -2,29 +2,22 @@ exterminate =========== .. dfhack-tool:: - :summary: Kills creatures. + :summary: Kill things. :tags: fort armok units -Kills any unit, or all units of a given race. You can target any unit on a -revealed tile of the map, including ambushers, but caged/chained creatures are -ignored. - -If ``method`` is specified, ``exterminate`` will kill the selected units -using the provided method. ``Instant`` will instantly kill the units. -``Butcher`` will mark the units for butchering, not kill them, useful for pets -and not for armed enemies. ``Drown`` and ``Magma`` will spawn a 7/7 column of -water or magma on the units respectively, cleaning up the liquid as they move -and die. Magma not recommended for magma-safe creatures... +Kills any unit, or all undead, or all units of a given race. You can target any +unit on a revealed tile of the map, including ambushers, but caged/chained +creatures cannot be killed with this tool. Usage ----- -``exterminate`` - List the available targets. -``exterminate this []`` - Kills the selected unit, instantly by default. -``exterminate [:] []`` - Kills all available units of the specified race, or all undead units. +:: + + exterminate + exterminate this [] + exterminate undead [] + exterminate [:] [] Examples -------- @@ -35,27 +28,43 @@ Examples List the targets on your map. ``exterminate BIRD_RAVEN:MALE`` Kill the ravens flying around the map (but only the male ones). -``exterminate GOBLIN --method MAGMA --only-visible --only-hostile`` - Kill all visible, hostile goblins on the map by drowning them in magma. +``exterminate GOBLIN --method magma --only-visible`` + Kill all visible, hostile goblins on the map by boiling them in magma. Options ------- ``-m``, ``--method `` - Specifies the "method" of killing units. + Specifies the "method" of killing units. See below for details. ``-o``, ``--only-visible`` - Specifies the tool should only kill units visible to the player + Specifies the tool should only kill units visible to the player. on the map. ``-f``, ``--include-friendly`` Specifies the tool should also kill units friendly to the player. +Methods +------- + +`exterminate` can kill units using any of the following methods: + +:instant: Kill by blood loss, and if this is ineffective, then kill by + vaporization (default). +:vaporize: Make the unit disappear in a puff of smoke. Note that units killed + this way will not leave a corpse behind, but any items they were carrying + will still drop. +:drown: Drown the unit in water. +:magma: Boil the unit in magma (not recommended for magma-safe creatures). +:butcher: Will mark the units for butchering instead of killing them. This is + more useful for pets than armed enemies. + Technical details ----------------- This tool kills by setting a unit's ``blood_count`` to 0, which means immediate death at the next game tick. For creatures where this is not enough, -such as vampires, it also sets animal.vanish_countdown to 2. +such as vampires, it also sets ``animal.vanish_countdown``, allowing the unit +to vanish in a puff of smoke if the blood loss doesn't kill them. -The script drowns units in the liquid of choice by modifying the tile with a -liquid level of 7 every tick. If the unit moves, the liquid moves along with +If the method of choice involves liquids, the tile is filled with a liquid +level of 7 every tick. If the target unit moves, the liquid moves along with it, leaving the vacated tiles clean. diff --git a/exterminate.lua b/exterminate.lua index 37b7524913..f179b28648 100644 --- a/exterminate.lua +++ b/exterminate.lua @@ -1,5 +1,3 @@ --- Exterminate creatures based on criteria - local argparse = require('argparse') local function spawnLiquid(position, liquid_level, liquid_type, update_liquids) @@ -38,12 +36,20 @@ local killMethod = { BUTCHER = 1, MAGMA = 2, DROWN = 3, + VAPORIZE = 4, } --- Kills a unit by removing blood and vanishing them. -local function killUnit(unit) +-- removes the unit from existence, leaving no corpse if the unit hasn't died +-- by the time the vanish countdown expires +local function vaporizeUnit(unit, target_value) + target_value = target_value or 1 + unit.animal.vanish_countdown = target_value +end + +-- Kills a unit by removing blood and also setting a vanish countdown as a failsafe. +local function destroyUnit(unit) unit.body.blood_count = 0 - unit.animal.vanish_countdown = 2 + vaporizeUnit(unit, 2) end -- Marks a unit for slaughter at the butcher's shop. @@ -57,44 +63,50 @@ local function drownUnit(unit, liquid_type) local function createLiquid() spawnLiquid(unit.pos, 7, liquid_type) - if not same_xyz(previousPositions[unit.id], unit.pos) then spawnLiquid(previousPositions[unit.id], 0, nil, false) previousPositions[unit.id] = copyall(unit.pos) end - if unit.flags2.killed then spawnLiquid(previousPositions[unit.id], 0, nil, false) else dfhack.timeout(1, 'ticks', createLiquid) end end - createLiquid() end +local function killUnit(unit, method) + if method == killMethod.BUTCHER then + butcherUnit(unit) + elseif method == killMethod.MAGMA then + drownUnit(unit, df.tile_liquid.Magma) + elseif method == killMethod.DROWN then + drownUnit(unit, df.tile_liquid.Water) + elseif method == killMethod.VAPORIZE then + vaporizeUnit(unit) + else + destroyUnit(unit) + end +end + local function getRaceCastes(race_id) local unit_castes = {} - for _, caste in pairs(df.creature_raw.find(race_id).caste) do unit_castes[caste.caste_id] = {} end - return unit_castes end local function getMapRaces(only_visible, include_friendly) local map_races = {} - for _, unit in pairs(df.global.world.units.active) do if only_visible and not dfhack.units.isVisible(unit) then goto skipunit end - if not include_friendly and isUnitFriendly(unit) then goto skipunit end - if dfhack.units.isActive(unit) and checkUnit(unit) then local unit_race_name = dfhack.units.isUndead(unit) and "UNDEAD" or df.creature_raw.find(unit.race).creature_id @@ -103,7 +115,6 @@ local function getMapRaces(only_visible, include_friendly) race.name = unit_race_name race.count = (race.count or 0) + 1 end - :: skipunit :: end @@ -119,7 +130,7 @@ local options, args = { local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler = function() options.help = true end}, - {'m', 'method', handler = function(arg) options.method = killMethod[arg] end, hasArg = true}, + {'m', 'method', handler = function(arg) options.method = killMethod[arg:upper()] end, hasArg = true}, {'o', 'only-visible', handler = function() options.only_visible = true end}, {'f', 'include-friendly', handler = function() options.include_friendly = true end}, }) @@ -130,50 +141,46 @@ end if positionals[1] == "help" or options.help then print(dfhack.script_help()) + return end if positionals[1] == "this" then local selected_unit = dfhack.gui.getSelectedUnit() - if not selected_unit then qerror("Select a unit and run the script again.") end - - killUnit(selected_unit) + killUnit(selected_unit, options.method) + print('Unit exterminated.') return end -if positionals[1] == nil then - local map_races = getMapRaces(options.only_visible, options.include_friendly) +local map_races = getMapRaces(options.only_visible, options.include_friendly) +if not positionals[1] then local sorted_races = {} for race, value in pairs(map_races) do table.insert(sorted_races, { name = race, count = value.count }) end - table.sort(sorted_races, function(a, b) return a.count > b.count end) - - for _, race in pairs(sorted_races) do + for _, race in ipairs(sorted_races) do print(([[%4s %s]]):format(race.count, race.name)) end - return end -local map_races = getMapRaces(options.only_visible, options.include_friendly) - -if string.find(positionals[1], "UNDEAD") then - if map_races.UNDEAD then - for _, unit in pairs(df.global.world.units.active) do - if dfhack.units.isUndead(unit) and checkUnit(unit) then - killUnit(unit) - end - end - else +local count = 0 +if positionals[1]:lower() == 'undead' then + if not map_races.UNDEAD then qerror("No undead found on the map.") end + for _, unit in pairs(df.global.world.units.active) do + if dfhack.units.isUndead(unit) and checkUnit(unit) then + killUnit(unit, options.method) + count = count + 1 + end + end else local selected_race, selected_caste = positionals[1], nil @@ -192,40 +199,27 @@ else qerror("Invalid caste.") end - local count = 0 for _, unit in pairs(df.global.world.units.active) do - if not dfhack.units.isActive(unit) or not checkUnit(unit) then + if not checkUnit(unit) then goto skipunit end - if options.only_visible and not dfhack.units.isVisible(unit) then goto skipunit end - if not options.include_friendly and isUnitFriendly(unit) then goto skipunit end - if selected_caste and selected_caste ~= df.creature_raw.find(unit.race).caste[unit.caste].caste_id then goto skipunit end if selected_race == df.creature_raw.find(unit.race).creature_id then - if options.method == killMethod.BUTCHER then - butcherUnit(unit) - elseif options.method == killMethod.MAGMA then - drownUnit(unit, df.tile_liquid.Magma) - elseif options.method == killMethod.DROWN then - drownUnit(unit, df.tile_liquid.Water) - else - killUnit(unit) - end - + killUnit(unit, options.method) count = count + 1 end :: skipunit :: end - - print(([[Exterminated %s creatures.]]):format(count)) end + +print(([[Exterminated %d creatures.]]):format(count)) From 7c1fa391112c38deb42a3650b08217683294e95e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Mar 2023 03:09:22 -0700 Subject: [PATCH 073/732] unforbid dumped items and allow forbidden items to be ignored --- changelog.txt | 2 ++ docs/stripcaged.rst | 48 +++++++++++++++------------ stripcaged.lua | 79 ++++++++++++++++++--------------------------- 3 files changed, 61 insertions(+), 68 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1e1ba6847f..1cf3234aa7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,8 @@ that repo. - `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. - `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor - `combine`: added ``--quiet`` option for no output when there are no changes +- `stripcaged`: added ``--skip-forbidden`` option for greater control over which items are marked for dumping +- `stripcaged`: items that are marked for dumping are now automatically unforbidden (unless ``--skip-forbidden`` is set) - `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles - `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button diff --git a/docs/stripcaged.rst b/docs/stripcaged.rst index 98fd6c4d58..0691fafea7 100644 --- a/docs/stripcaged.rst +++ b/docs/stripcaged.rst @@ -7,10 +7,10 @@ stripcaged This tool helps with the tedious task of going through all your cages and marking the items inside for dumping. This lets you get leftover seeds out of -cages after you tamed the animals inside. The most popular use of this tool, -though, is to strip the weapons and armor from caged prisoners. After you run -this tool, your dwarves will come and take the items to the garbage dump, -leaving your cages clean and your prisoners stripped bare. +cages after you tamed the animals inside. The most popular use, though, is to +strip the weapons and armor from caged prisoners. After you run ``stripcaged``, +your dwarves will come and take the items to the garbage dump, leaving your +cages clean and your prisoners stripped bare. If you don't want to wait for your dwarves to dump all the items, you can use `autodump` to speed the process along. @@ -18,32 +18,40 @@ If you don't want to wait for your dwarves to dump all the items, you can use Usage ----- -``stripcaged list`` - Display a list of all cages and their item contents. -``stripcaged items|weapons|armor|all [here| ...]`` - Dump the given type of item. If ``here`` is specified, only act on the - in-game selected cage (or the cage under the keyboard cursor). Alternately, - you can specify the item ids of specific cages that you want to target. +:: - Note: Live vermin and tame vermin (pets) are considered items by the game. - Stripcaged excludes them from the ``all`` or ``items`` targets by default - as dumping them risks them escaping or dying from your cats. - - Use ``--include-vermin`` for untamed vermin and ``--include-pets`` for - tame vermin with the relevant targets to include them anyways. + stripcaged list + stripcaged items|weapons|armor|all [here| ...] [] Examples -------- +``stripcaged list`` + Display a list of all cages and their item contents. ``stripcaged all`` - For all cages, dump all items, equipped by a creature or not. + Dump all items in all cages, equipped by a creature or not. ``stripcaged items`` Dump loose items in all cages, such as seeds left over from animal training. ``stripcaged weapons`` Dump weapons equipped by caged creatures. -``stripcaged armor here`` - Dumps the armor equipped by the caged creature in the selected cage. +``stripcaged armor here --skip-forbidden`` + Dumps unforbidden armor equipped by the caged creature in the selected cage. ``stripcaged all 25321 34228`` Dumps all items out of the specified cages. -``stripcaged items here --include-vermin --include-pets`` +``stripcaged items here --include-pets --include-vermin`` Dumps loose items in the selected cage, including any tamed/untamed vermin. + +Options +------- + +``--include-pets``, ``--include-vermin`` + Live tame (pets) and untamed vermin are considered items by the game. They + are normally excluded from dumping since that risks them escaping or dying + from your cats. Use these options to dump them anyway. + +``-f``, ``--skip-forbidden`` + Items to be marked for dumping are unforbidden by default. Use this option + to instead only act on unforbidden items, and leave forbidden items + forbidden. This allows you to, for example, manually unforbid high-value + items from the stocks menu (like steel) and then have ``stripcaged`` just + act on the unforbidden items. diff --git a/stripcaged.lua b/stripcaged.lua index 0bb9f49aa7..2585057c56 100644 --- a/stripcaged.lua +++ b/stripcaged.lua @@ -1,11 +1,10 @@ local argparse = require('argparse') local opts = {} -local positionals = argparse.processArgsGetopt({...}, - {{'h', 'help', handler = function() opts.help = true end}, - {nil, 'include-vermin', - handler = function() opts.include_vermin = true end}, - {nil, 'include-pets', - handler = function() opts.include_pets = true end}, +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler = function() opts.help = true end}, + {nil, 'include-vermin', handler = function() opts.include_vermin = true end}, + {nil, 'include-pets', handler = function() opts.include_pets = true end}, + {'f', 'skip-forbidden', handler = function() opts.skip_forbidden = true end}, }) local function plural(nr, name) @@ -14,22 +13,23 @@ local function plural(nr, name) end -- Checks item against opts to see if it's a vermin type that we ignore -local function isexcludedvermin(item, opts) +local function isexcludedvermin(item) if df.item_verminst:is_instance(item) then - if opts.include_vermin then - return false - else - return true - end + return not opts.include_vermin elseif df.item_petst:is_instance(item) then - if opts.include_pets then - return false - else - return true - end - else - return false + return not opts.include_pets end + return false +end + +local function dump_item(item) + if item.flags.dump then return 0 end + if not item.flags.forbid or not opts.skip_forbidden then + item.flags.dump = true + item.flags.forbid = false + return 1 + end + return 0 end local function cage_dump_items(list) @@ -40,9 +40,8 @@ local function cage_dump_items(list) for _, ref in ipairs(cage.general_refs) do if df.general_ref_contains_itemst:is_instance(ref) then local item = df.item.find(ref.item_id) - if not item.flags.dump and not isexcludedvermin(item, opts) then - count = count + 1 - item.flags.dump = true + if not isexcludedvermin(item) then + count = count + dump_item(item) end end end @@ -61,10 +60,8 @@ local function cage_dump_armor(list) if df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if not it.item.flags.dump and - it.mode == df.unit_inventory_item.T_mode.Worn then - count = count + 1 - it.item.flags.dump = true + if it.mode == df.unit_inventory_item.T_mode.Worn then + count = count + dump_item(it.item) end end end @@ -84,10 +81,8 @@ local function cage_dump_weapons(list) if df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if not it.item.flags.dump and - it.mode == df.unit_inventory_item.T_mode.Weapon then - count = count + 1 - it.item.flags.dump = true + if it.mode == df.unit_inventory_item.T_mode.Weapon then + count = count + dump_item(it.item) end end end @@ -101,23 +96,19 @@ end local function cage_dump_all(list) local count = 0 local count_cage = 0 - for _, cage in ipairs(list) do local pre_count = count for _, ref in ipairs(cage.general_refs) do - if df.general_ref_contains_itemst:is_instance(ref) then local item = df.item.find(ref.item_id) - if not item.flags.dump and not isexcludedvermin(item, opts) then - count = count + 1 - item.flags.dump = true + if not isexcludedvermin(item) then + count = count + dump_item(item) end elseif df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if not it.item.flags.dump and not isexcludedvermin(it.item, opts) then - count = count + 1 - it.item.flags.dump = true + if not isexcludedvermin(it.item) then + count = count + dump_item(it.item) end end end @@ -137,26 +128,18 @@ local function cage_dump_list(list) for _, ref in ipairs(cage.general_refs) do if df.general_ref_contains_itemst:is_instance(ref) then local item = df.item.find(ref.item_id) - if not isexcludedvermin(item, opts) then + if not isexcludedvermin(item) then local classname = df.item_type.attrs[item:getType()].caption count[classname] = (count[classname] or 0) + 1 end elseif df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if not isexcludedvermin(it.item, opts) then + if not isexcludedvermin(it.item) then local classname = df.item_type.attrs[it.item:getType()].caption count[classname] = (count[classname] or 0) + 1 end end - - --[[ TODO: Determine how/if to handle a DEBUG flag. - - --Ruby: - else - puts "unhandled ref #{ref.inspect}" if $DEBUG - end - ]] end end From 8c0ef6695633f499eca60f655e04b7c5e20510a7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Mar 2023 07:48:42 -0700 Subject: [PATCH 074/732] slightly less garish configure button --- gui/civ-alert.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 0d41a0db9a..762a3fc040 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -167,7 +167,8 @@ function CivalertOverlay:init() 'Click to configure', NEWLINE, {gap=2, text='civilian alert'}, }, - text_pen=to_pen{fg=COLOR_BLACK, bg=COLOR_YELLOW}, + text_pen=to_pen{fg=COLOR_YELLOW, bg=COLOR_BLACK}, + text_hpen=to_pen{fg=COLOR_LIGHTRED, bg=COLOR_BLACK}, on_click=launch_config, }, }, From ba4d9e4ee1ba9320e82bd8972dafbbf50be5d99f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Mar 2023 10:38:56 -0700 Subject: [PATCH 075/732] make button behave more like other buttons --- gui/civ-alert.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 762a3fc040..75a288411b 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -165,10 +165,9 @@ function CivalertOverlay:init() widgets.Label{ text={ 'Click to configure', NEWLINE, - {gap=2, text='civilian alert'}, + ' civilian alert ', }, text_pen=to_pen{fg=COLOR_YELLOW, bg=COLOR_BLACK}, - text_hpen=to_pen{fg=COLOR_LIGHTRED, bg=COLOR_BLACK}, on_click=launch_config, }, }, From 03af898a38e770a2077a408441e420f18c6896d8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Mar 2023 12:51:26 -0700 Subject: [PATCH 076/732] fix (and simplify) logic for expanding and collapsing --- caravan.lua | 257 +++++++++++++++++++++++--------------------------- changelog.txt | 1 + 2 files changed, 121 insertions(+), 137 deletions(-) diff --git a/caravan.lua b/caravan.lua index 921cb7e5cc..b6bd30f70f 100644 --- a/caravan.lua +++ b/caravan.lua @@ -1,6 +1,11 @@ -- Adjusts properties of caravans and provides overlay for enhanced trading --@ module = true +-- TODO: the category checkbox that indicates whether all items in the category +-- are selected can be incorrect after the overlay adjusts the container +-- selection. the state is in trade.current_type_a_flag, but figuring out which +-- index to modify is non-trivial. + local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') @@ -28,161 +33,145 @@ local GOODFLAG = { CONTAINER_COLLAPSED_SELECTED = 5, } +local trade = df.global.game.main_interface.trade + function select_shift_clicked_container_items(new_state, old_state, list_index) -- if ctrl is also held, collapse the container too local also_collapse = dfhack.internal.getModifiers().ctrl - local collapsed_item_count = 0 + local collapsed_item_count, collapsing_container, in_container = 0, false, false for k, goodflag in ipairs(new_state) do - if old_state[k] ~= goodflag then - local next_item_flag = new_state[k + 1] - local this_item_is_container = df.item_binst:is_instance(df.global.game.main_interface.trade.good[list_index][k]) - if this_item_is_container then - local container_is_selected = goodflag == GOODFLAG.UNCONTAINED_SELECTED or goodflag == GOODFLAG.CONTAINER_COLLAPSED_SELECTED - if container_is_selected then - local collapsed_this_container = false - - if goodflag == GOODFLAG.UNCONTAINED_SELECTED and also_collapse then - collapsed_this_container = true - end - - new_state[k] = also_collapse and GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED or GOODFLAG.UNCONTAINED_UNSELECTED - local end_of_container_reached = false - local contained_item_index = k + 1 - - if contained_item_index > #new_state - 1 then - end_of_container_reached = true - end - - while not end_of_container_reached do - new_state[contained_item_index] = GOODFLAG.CONTAINED_SELECTED - - if collapsed_this_container then - collapsed_item_count = collapsed_item_count + 1 - end - - local next_item_index = contained_item_index + 1 - - if next_item_index > #new_state or new_state[next_item_index] < 2 or new_state[next_item_index] >= 4 then - end_of_container_reached = true - end - contained_item_index = contained_item_index + 1 - end - end + if in_container then + if goodflag <= GOODFLAG.UNCONTAINED_SELECTED + or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_SELECTED then + break + end + + new_state[k] = GOODFLAG.CONTAINED_SELECTED + + if collapsing_container then + collapsed_item_count = collapsed_item_count + 1 end + goto continue end + + if goodflag == old_state[k] then goto continue end + local is_container = df.item_binst:is_instance(trade.good[list_index][k]) + if not is_container then goto continue end + + -- deselect the container itself + if also_collapse then + collapsing_container = goodflag == GOODFLAG.UNCONTAINED_SELECTED + new_state[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED + else + new_state[k] = GOODFLAG.UNCONTAINED_UNSELECTED + end + in_container = true + + ::continue:: end if collapsed_item_count > 0 then - df.global.game.main_interface.trade.i_height[list_index] = df.global.game.main_interface.trade.i_height[list_index] - collapsed_item_count * 3 + trade.i_height[list_index] = trade.i_height[list_index] - collapsed_item_count * 3 end end -function collapse_ctrl_clicked_containers(new_state, old_state, list_index) +local CTRL_CLICK_STATE_MAP = { + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, +} + +-- collapses uncollapsed containers and restores the selection state for the container +-- and contained items +function toggle_ctrl_clicked_containers(new_state, old_state, list_index) + local toggled_item_count, in_container, is_collapsing = 0, false, false for k, goodflag in ipairs(new_state) do - if old_state[k] ~= goodflag then - local next_item_flag = new_state[k + 1] - if next_item_flag == GOODFLAG.CONTAINED_UNSELECTED or next_item_flag == GOODFLAG.CONTAINED_SELECTED then - local target_goodflag - if goodflag == GOODFLAG.UNCONTAINED_SELECTED then - target_goodflag = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED - elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then - target_goodflag = GOODFLAG.CONTAINER_COLLAPSED_SELECTED - end - - if target_goodflag ~= nil then - new_state[k] = target_goodflag - -- changed a container state, items inside will be reset, return contained items to state before collapse - local end_of_container_reached = false - local contained_item_index = k + 1 - - if contained_item_index > #new_state - 1 then - end_of_container_reached = true - end - - local num_items_collapsed = 0 - - while not end_of_container_reached do - num_items_collapsed = num_items_collapsed + 1 - new_state[contained_item_index] = old_state[contained_item_index] - - local next_item_index = contained_item_index + 1 - - if next_item_index > #new_state or new_state[next_item_index] < 2 or new_state[next_item_index] >= 4 then - end_of_container_reached = true - end - contained_item_index = contained_item_index + 1 - end - - if num_items_collapsed > 0 then - df.global.game.main_interface.trade.i_height[list_index] = df.global.game.main_interface.trade.i_height[list_index] - num_items_collapsed * 3 - end - end + if in_container then + if goodflag <= GOODFLAG.UNCONTAINED_SELECTED + or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_SELECTED then + break end + toggled_item_count = toggled_item_count + 1 + new_state[k] = old_state[k] + goto continue end + + if goodflag == old_state[k] then goto continue end + local is_contained = goodflag == GOODFLAG.CONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINED_SELECTED + if is_contained then goto continue end + local is_container = df.item_binst:is_instance(trade.good[list_index][k]) + if not is_container then goto continue end + + new_state[k] = CTRL_CLICK_STATE_MAP[old_state[k]] + in_container = true + is_collapsing = goodflag == GOODFLAG.UNCONTAINED_UNSELECTED or goodflag == GOODFLAG.UNCONTAINED_SELECTED + + ::continue:: end -end + if toggled_item_count > 0 then + trade.i_height[list_index] = trade.i_height[list_index] - toggled_item_count * 3 * (is_collapsing and 1 or -1) + end +end function collapseTypes(types_list, list_index) local type_on_count = 0 - for k, type_open in ipairs(types_list) do - local type_on = df.global.game.main_interface.trade.current_type_a_on[list_index][k] + for k in ipairs(types_list) do + local type_on = trade.current_type_a_on[list_index][k] if type_on then type_on_count = type_on_count + 1 end types_list[k] = false end - df.global.game.main_interface.trade.i_height[list_index] = type_on_count * 3 + trade.i_height[list_index] = type_on_count * 3 end function collapseAllTypes() - collapseTypes(df.global.game.main_interface.trade.current_type_a_expanded[0], 0) - collapseTypes(df.global.game.main_interface.trade.current_type_a_expanded[1], 1) + collapseTypes(trade.current_type_a_expanded[0], 0) + collapseTypes(trade.current_type_a_expanded[1], 1) -- reset scroll to top when collapsing types - df.global.game.main_interface.trade.scroll_position_item[0] = 0 - df.global.game.main_interface.trade.scroll_position_item[1] = 0 + trade.scroll_position_item[0] = 0 + trade.scroll_position_item[1] = 0 end function collapseContainers(item_list, list_index) local num_items_collapsed = 0 for k, goodflag in ipairs(item_list) do - if goodflag ~= GOODFLAG.CONTAINED_UNSELECTED and goodflag ~= GOODFLAG.CONTAINED_SELECTED then - local next_item_index = k + 1 - if next_item_index > #item_list - 1 then - goto skip - end + if goodflag == GOODFLAG.CONTAINED_UNSELECTED + or goodflag == GOODFLAG.CONTAINED_SELECTED then + goto continue + end - local next_item = item_list[next_item_index] - local this_item_is_container = df.item_binst:is_instance(df.global.game.main_interface.trade.good[list_index][k]) - local collapsed_this_container = false - if this_item_is_container then - if goodflag == GOODFLAG.UNCONTAINED_SELECTED then - item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED - collapsed_this_container = true - elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then - item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED - collapsed_this_container = true - end - - if collapsed_this_container then - num_items_collapsed = num_items_collapsed + #dfhack.items.getContainedItems(df.global.game.main_interface.trade.good[list_index][k]) - end - end + local item = trade.good[list_index][k] + local is_container = df.item_binst:is_instance(item) + if not is_container then goto continue end + + local collapsed_this_container = false + if goodflag == GOODFLAG.UNCONTAINED_SELECTED then + item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED + collapsed_this_container = true + elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then + item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED + collapsed_this_container = true + end - ::skip:: + if collapsed_this_container then + num_items_collapsed = num_items_collapsed + #dfhack.items.getContainedItems(item) end + ::continue:: end if num_items_collapsed > 0 then - df.global.game.main_interface.trade.i_height[list_index] = df.global.game.main_interface.trade.i_height[list_index] - num_items_collapsed * 3 + trade.i_height[list_index] = trade.i_height[list_index] - num_items_collapsed * 3 end end function collapseAllContainers() - collapseContainers(df.global.game.main_interface.trade.goodflag[0], 0) - collapseContainers(df.global.game.main_interface.trade.goodflag[1], 1) + collapseContainers(trade.goodflag[0], 0) + collapseContainers(trade.goodflag[1], 1) end function collapseEverything() @@ -191,8 +180,8 @@ function collapseEverything() end function copyGoodflagState() - trader_selected_state = copyall(df.global.game.main_interface.trade.goodflag[0]) - broker_selected_state = copyall(df.global.game.main_interface.trade.goodflag[1]) + trader_selected_state = copyall(trade.goodflag[0]) + broker_selected_state = copyall(trade.goodflag[1]) end CaravanTradeOverlay = defclass(CaravanTradeOverlay, overlay.OverlayWidget) @@ -210,20 +199,18 @@ function CaravanTradeOverlay:init() widgets.Label{ frame={t=0, l=0}, text={ - {text='Shift+Click checkbox:', pen=COLOR_LIGHTGREEN}, + {text='Shift+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', NEWLINE, - {text='select items inside bin', pen=COLOR_WHITE}, + ' select items inside bin', }, - text_pen=COLOR_LIGHTGREEN, }, widgets.Label{ frame={t=3, l=0}, text={ - {text='Ctrl+Click checkbox:', pen=COLOR_LIGHTGREEN}, + {text='Ctrl+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', NEWLINE, - {text='collapse single bin', pen=COLOR_WHITE}, + ' collapse/expand bin', }, - text_pen=COLOR_LIGHTGREEN, }, widgets.HotkeyLabel{ frame={t=6, l=0}, @@ -239,43 +226,42 @@ function CaravanTradeOverlay:init() }, widgets.Label{ frame={t=9, l=0}, - text = 'Shift+Scroll:', + text = 'Shift+Scroll', text_pen=COLOR_LIGHTGREEN, }, widgets.Label{ - frame={t=9, l=14}, - text = 'fast scroll', + frame={t=9, l=12}, + text = ': fast scroll', }, } end -function CaravanTradeOverlay:render(dc) - CaravanTradeOverlay.super.render(self, dc) +-- do our alterations *after* the vanilla response to the click has registered. otherwise +-- it's very difficult to figure out which item has been clicked +function CaravanTradeOverlay:onRenderBody(dc) if handle_shift_click_on_render then handle_shift_click_on_render = false - select_shift_clicked_container_items(df.global.game.main_interface.trade.goodflag[0], trader_selected_state, 0) - select_shift_clicked_container_items(df.global.game.main_interface.trade.goodflag[1], broker_selected_state, 1) + select_shift_clicked_container_items(trade.goodflag[0], trader_selected_state, 0) + select_shift_clicked_container_items(trade.goodflag[1], broker_selected_state, 1) elseif handle_ctrl_click_on_render then handle_ctrl_click_on_render = false - collapse_ctrl_clicked_containers(df.global.game.main_interface.trade.goodflag[0], trader_selected_state, 0) - collapse_ctrl_clicked_containers(df.global.game.main_interface.trade.goodflag[1], broker_selected_state, 1) + toggle_ctrl_clicked_containers(trade.goodflag[0], trader_selected_state, 0) + toggle_ctrl_clicked_containers(trade.goodflag[1], broker_selected_state, 1) end end function CaravanTradeOverlay:onInput(keys) + if CaravanTradeOverlay.super.onInput(self, keys) then return true end + if keys._MOUSE_L_DOWN then if dfhack.internal.getModifiers().shift then handle_shift_click_on_render = true + copyGoodflagState() elseif dfhack.internal.getModifiers().ctrl then handle_ctrl_click_on_render = true - end - - if handle_ctrl_click_on_render or handle_shift_click_on_render then copyGoodflagState() end end - CaravanTradeOverlay.super.onInput(self, keys) - return false end OVERLAY_WIDGETS = { @@ -393,7 +379,7 @@ local function rejoin_pack_animals() end end -function commands.unload(...) +function commands.unload() rejoin_pack_animals() end @@ -401,8 +387,7 @@ function commands.help() print(dfhack.script_help()) end -function main(...) - local args = {...} +function main(args) local command = table.remove(args, 1) if commands[command] then commands[command](table.unpack(args)) @@ -410,12 +395,10 @@ function main(...) commands.help() if command then qerror("No such subcommand: " .. command) - else - qerror("Missing subcommand") end end end if not dfhack_flags.module then - main(...) + main{...} end diff --git a/changelog.txt b/changelog.txt index 1e1ba6847f..3f06eed5ef 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. - `gui/seedwatch`: GUI config and status panel interface for `seedwatch` ## Fixes +- `caravan`: item list length now correct when expanding and collapsing containers - `suspendmanager`: does not suspend non blocking jobs such as floor bars or bridges anymore - `suspendmanager`: fix occasional bad identification of buildingplan jobs From 9ca0af25fd9fa39dd7242b4cc8fb9a6138352d7d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Mar 2023 13:07:02 -0700 Subject: [PATCH 077/732] refresh docs --- caravan.lua | 7 ++--- docs/caravan.rst | 71 +++++++++++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/caravan.lua b/caravan.lua index b6bd30f70f..6232e572c3 100644 --- a/caravan.lua +++ b/caravan.lua @@ -388,14 +388,13 @@ function commands.help() end function main(args) - local command = table.remove(args, 1) + local command = table.remove(args, 1) or 'list' if commands[command] then commands[command](table.unpack(args)) else commands.help() - if command then - qerror("No such subcommand: " .. command) - end + print() + qerror("No such command: " .. command) end end diff --git a/docs/caravan.rst b/docs/caravan.rst index 98cd4f0810..4be6e00940 100644 --- a/docs/caravan.rst +++ b/docs/caravan.rst @@ -8,21 +8,40 @@ caravan This tool can help with caravans that are leaving too quickly, refuse to unload, or are just plain unhappy that you are such a poor negotiator. +Also see `force` for creating caravans. + Usage ----- :: - caravan + caravan [list] + caravan extend [ []] + caravan happy [] + caravan leave [] + caravan unload -Also see `force` for creating caravans. +Commands listed with the argument ``[]`` can take multiple +(space-separated) caravan IDs (see ``caravan list`` to get the IDs). If no IDs +are specified, then the commands apply to all caravans on the map. Examples -------- +``caravan`` + List IDs and information about all caravans on the map. ``caravan extend`` Force a caravan that is leaving to return to the depot and extend their stay another 7 days. +``caravan extend 30 0 1`` + Extend the time that caravans 0 and 1 stay at the depot by 30 days. If the + caravans have already started leaving, they will return to the depot. +``caravan happy`` + Make the active caravans willing to trade again (after seizing goods, + annoying merchants, etc.). If the caravan has already started leaving in a + huff, they will return to the depot. +``caravan leave`` + Makes caravans pack up and leave immediately. ``caravan unload`` Fix a caravan that got spooked by wildlife and refuses to fully unload. @@ -31,35 +50,19 @@ Overlay Additional functionality is provided when the trade screen is open via an `overlay` widget: -- ``Shift+Click checkbox``: Select all items inside a bin without selecting the bin itself -- ``Ctrl+Click checkbox``: Collapse a single bin (as is possible in the "Move goods to/from depot" screen) -- ``Ctrl+c``: Collapses all bins. The hotkey hint can also be clicked as though it were a button. -- ``Ctrl+x``: Collapses everything (all item categories and anything collapsible within each category). - The hotkey hint can also be clicked as though it were a button. - -There is also a reminder of the fast scroll functionality provided by the vanilla game when you hold shift -while scrolling (this works everywhere). - -The overlay is named ``caravan.tradeScreenExtension`` in ``gui/overlay``. - -Commands --------- - -Commands listed with the argument ``[]`` can take multiple -(space-separated) caravan IDs (see ``caravan list``). If no IDs are specified, -then the commands apply to all caravans on the map. - -``list`` - List IDs and information about all caravans on the map. -``extend [ []]`` - Extend the time that caravans stay at the depot by the specified number of - days (defaults to 7). Also causes caravans to return to the depot if - applicable. -``happy []`` - Make caravans willing to trade again (after seizing goods, annoying - merchants, etc.). Also causes caravans to return to the depot if applicable. -``leave []`` - Makes caravans pack up and leave immediately. -``unload`` - Fix endless unloading at the depot. Run this if merchant pack animals were - startled and now refuse to come to the trade depot. +- ``Shift+Click checkbox``: Select all items inside a bin without selecting the + bin itself +- ``Ctrl+Click checkbox``: Collapse or expand a single bin (as is possible in + the "Move goods to/from depot" screen) +- ``Ctrl+c``: Collapses all bins. The hotkey hint can also be clicked as though + it were a button. +- ``Ctrl+x``: Collapses everything (all item categories and anything + collapsible within each category). The hotkey hint can also be clicked as + though it were a button. + +There is also a reminder of the fast scroll functionality provided by the +vanilla game when you hold shift while scrolling (this works everywhere). + +You can turn the overlay on and off in `gui/control-panel`, or you can +reposition it to your liking with `gui/overlay`. The overlay is named +``caravan.tradeScreenExtension``. From 27089b81be6f9de9486118a97a15be4dde09b1b3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Mar 2023 16:11:54 -0700 Subject: [PATCH 078/732] edit pass for gui/confirm docs --- docs/gui/confirm.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/gui/confirm.rst b/docs/gui/confirm.rst index f6a77e16bc..644581cd88 100644 --- a/docs/gui/confirm.rst +++ b/docs/gui/confirm.rst @@ -3,11 +3,10 @@ gui/confirm .. dfhack-tool:: :summary: Configure which confirmation dialogs are enabled. - :tags: fort productivity interface + :tags: fort interface This tool is a basic configuration interface for the `confirm` plugin. You can -see current state, and you can interactively choose which confirmation dialogs -are enabled. +see and modify which confirmation dialogs are enabled. Usage ----- From 0e86dfaa630da93b52208ab80df5168309f3de88 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Mar 2023 17:09:07 -0700 Subject: [PATCH 079/732] work around json converting numbers to strings why, json, why? --- changelog.txt | 1 + prioritize.lua | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/changelog.txt b/changelog.txt index afadfa604b..d2dcbf0009 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ that repo. ## Fixes - `caravan`: item list length now correct when expanding and collapsing containers +- `prioritize`: fixed all watched job type names showing as ``nil`` - `suspendmanager`: does not suspend non blocking jobs such as floor bars or bridges anymore - `suspendmanager`: fix occasional bad identification of buildingplan jobs diff --git a/prioritize.lua b/prioritize.lua index 0102089e0c..cef9327d24 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -588,6 +588,13 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) return end local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') + -- sometimes the keys come back as strings; fix that up + for k,v in pairs(persisted_data) do + if type(k) == 'string' then + persisted_data[tonumber(k)] = v + persisted_data[k] = nil + end + end g_watched_job_matchers = persisted_data or {} update_handlers() end From 71c58248e915b0ccf8ad090fd9a27bc5b9641883 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 28 Mar 2023 22:53:19 -0700 Subject: [PATCH 080/732] handle case where there is no persisted data --- prioritize.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prioritize.lua b/prioritize.lua index cef9327d24..abfa59c2bc 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -587,7 +587,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then return end - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') + local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') or {} -- sometimes the keys come back as strings; fix that up for k,v in pairs(persisted_data) do if type(k) == 'string' then @@ -595,7 +595,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) persisted_data[k] = nil end end - g_watched_job_matchers = persisted_data or {} + g_watched_job_matchers = persisted_data update_handlers() end From 247ccecb12030d170496234b8f27268cc82a2b30 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 29 Mar 2023 00:28:07 -0700 Subject: [PATCH 081/732] sync tags from spreadsheet --- docs/modtools/skill-change.rst | 2 +- docs/suspend.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modtools/skill-change.rst b/docs/modtools/skill-change.rst index 348ae38128..23d41b6fdb 100644 --- a/docs/modtools/skill-change.rst +++ b/docs/modtools/skill-change.rst @@ -3,7 +3,7 @@ modtools/skill-change .. dfhack-tool:: :summary: Modify unit skills. - :tags: unavailable dev + :tags: dev Sets or modifies a skill of a unit. diff --git a/docs/suspend.rst b/docs/suspend.rst index f7fb9a3cd4..6c50ab0f37 100644 --- a/docs/suspend.rst +++ b/docs/suspend.rst @@ -3,7 +3,7 @@ suspend .. dfhack-tool:: :summary: Suspends building construction jobs. - :tags: auto graphics military + :tags: fort productivity jobs This tool will suspend jobs. It can either suspend all the current jobs, or only construction jobs that are likely to block other jobs. When building walls, it's From 753b9f9c9ea449a098055af748763662907c851a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 29 Mar 2023 08:23:59 -0700 Subject: [PATCH 082/732] no reports for non-fort-controlled units this also skips reports for residents, which is debatable --- changelog.txt | 1 + docs/warn-starving.rst | 5 ++--- warn-starving.lua | 17 ++++++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index 61c9e94782..3cfee6a6da 100644 --- a/changelog.txt +++ b/changelog.txt @@ -22,6 +22,7 @@ that repo. - `prioritize`: fixed all watched job type names showing as ``nil`` - `suspendmanager`: does not suspend non blocking jobs such as floor bars or bridges anymore - `suspendmanager`: fix occasional bad identification of buildingplan jobs +- `warn-starving`: no longer warns for enemy and neutral units ## Misc Improvements - `gui/control-panel`: Now detects overlays from scripts named with capital letters diff --git a/docs/warn-starving.rst b/docs/warn-starving.rst index 50a6ce5280..8e411ff285 100644 --- a/docs/warn-starving.rst +++ b/docs/warn-starving.rst @@ -10,6 +10,8 @@ pause and you'll get a warning dialog telling you which units are in danger. This gives you a chance to rescue them (or take them out of their cages) before they die. +You can enable ``warn-starving`` notifications in `gui/control-panel` on the "Maintenance" tab. + Usage ----- @@ -23,9 +25,6 @@ Examples ``warn-starving all sane`` Report on all currently distressed units, excluding insane units that you wouldn't be able to save anyway. -``repeat --time 10 --timeUnits days --command [ warn-starving sane ]`` - Every 10 days, report any (sane) distressed units that haven't already been - reported. Options ------- diff --git a/warn-starving.lua b/warn-starving.lua index dde654856c..d43e65c299 100644 --- a/warn-starving.lua +++ b/warn-starving.lua @@ -34,7 +34,7 @@ warning.ATTRS = { pass_mouse_clicks=false, } -function warning:init(args) +function warning:init(info) local main = widgets.Window{ frame={w=80, h=18}, frame_title='Warning', @@ -44,7 +44,7 @@ function warning:init(args) main:addviews{ widgets.WrappedLabel{ - text_to_wrap=table.concat(args.messages, NEWLINE), + text_to_wrap=table.concat(info.messages, NEWLINE), } } @@ -98,13 +98,12 @@ function doCheck() for i=#units-1, 0, -1 do local unit = units[i] local rraw = findRaceCaste(unit) - if rraw and dfhack.units.isActive(unit) and not dfhack.units.isOpposedToLife(unit) then - if not checkOnlySane or dfhack.units.isSane(unit) then - table.insert(messages, checkVariable(unit.counters2.hunger_timer, 75000, 'starving', starvingUnits, unit)) - table.insert(messages, checkVariable(unit.counters2.thirst_timer, 50000, 'dehydrated', dehydratedUnits, unit)) - table.insert(messages, checkVariable(unit.counters2.sleepiness_timer, 150000, 'very drowsy', sleepyUnits, unit)) - end - end + if not rraw or not dfhack.units.isFortControlled(unit) then goto continue end + if checkOnlySane and not dfhack.units.isSane(unit) then goto continue end + table.insert(messages, checkVariable(unit.counters2.hunger_timer, 75000, 'starving', starvingUnits, unit)) + table.insert(messages, checkVariable(unit.counters2.thirst_timer, 50000, 'dehydrated', dehydratedUnits, unit)) + table.insert(messages, checkVariable(unit.counters2.sleepiness_timer, 150000, 'very drowsy', sleepyUnits, unit)) + ::continue:: end if #messages > 0 then dfhack.color(COLOR_LIGHTMAGENTA) From 7d94c0756867b7fcae60eb31496eab4a059e685c Mon Sep 17 00:00:00 2001 From: Cubittus Date: Thu, 30 Mar 2023 14:30:20 +0100 Subject: [PATCH 083/732] gm-editor: Don't clobber frame pos of previous window Make a copy of the frame stored in config.data so that it's not a reference to the frame of the last opened window, otherwise when updating the l/r/t/b during GMEditorUi:init the previous windows frame is updated too. --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index e609749f70..b09f86219a 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -72,7 +72,7 @@ end GmEditorUi = defclass(GmEditorUi, widgets.Window) GmEditorUi.ATTRS{ - frame=config.data, + frame={ w=config.data.w, h=config.data.h, l=config.data.l, r=config.data.r, t=config.data.t, b=config.data.b }, frame_title="GameMaster's editor", frame_inset=0, resizable=true, From 04dcb30b3357a4ea4260ae3468e9c5f0789538ad Mon Sep 17 00:00:00 2001 From: Cubittus Date: Thu, 30 Mar 2023 14:38:52 +0100 Subject: [PATCH 084/732] gm-editor: Enable jump to material ref-target When a ref-target jump is made on a field named 'mat_type' then look for a 'mat_index' field to go with it and look up the matinfo. --- gui/gm-editor.lua | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index b09f86219a..36bb99cb51 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -181,11 +181,22 @@ function GmEditorUi:find_id(force_dialog) ref_target = field.ref_target end if ref_target and not force_dialog then + local obj if not ref_target.find then - dialog.showMessage("Error!", ("Cannot look up %s by ID"):format(getmetatable(ref_target)), COLOR_LIGHTRED) - return + if key == 'mat_type' then + local ok, mi = pcall(function() + return self:currentTarget().target['mat_index'] + end) + if ok then + obj = dfhack.matinfo.decode(id, mi) + end + end + if not obj then + dialog.showMessage("Error!", ("Cannot look up %s by ID"):format(getmetatable(ref_target)), COLOR_LIGHTRED) + return + end end - local obj = ref_target.find(id) + obj = obj or ref_target.find(id) if obj then self:pushTarget(obj) else From cec0b19a390f14b627c19118dba7f6c8e963a536 Mon Sep 17 00:00:00 2001 From: Cubittus Date: Thu, 30 Mar 2023 15:11:04 +0100 Subject: [PATCH 085/732] gm-editor: auto-fit-width the key column Sets the width of the key column to fit the longest key --- gui/gm-editor.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 36bb99cb51..3c6bda5ded 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -488,7 +488,9 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) if reindex then trg.keys={} + trg.kw=10 for k,v in pairs(trg.target) do + if #tostring(k)>trg.kw then trg.kw=#tostring(k) end if filter~= "" then local ok,ret=dfhack.pcall(string.match,tostring(k):lower(),filter) if not ok then @@ -504,7 +506,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) self.subviews.lbl_current_item:itemById('name').text=tostring(trg.target) local t={} for k,v in pairs(trg.keys) do - table.insert(t,{text={{text=string.format("%-25s",tostring(v))},{gap=1,text=getStringValue(trg,v)}}}) + table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=1,text=getStringValue(trg,v)}}}) end local last_pos if preserve_pos then @@ -521,6 +523,7 @@ function GmEditorUi:pushTarget(target_to_push) local new_tbl={} new_tbl.target=target_to_push new_tbl.keys={} + new_tbl.kw=10 new_tbl.selected=1 new_tbl.filter="" if self:currentTarget()~=nil then @@ -528,6 +531,7 @@ function GmEditorUi:pushTarget(target_to_push) self.stack[#self.stack].filter=self.subviews.filter_input.text end for k,v in pairs(target_to_push) do + if #tostring(k)>new_tbl.kw then new_tbl.kw=#tostring(k) end table.insert(new_tbl.keys,k) end new_tbl.item_count=#new_tbl.keys From 0e238ed3637124e1aaa67669ab47d96f758f4009 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 30 Mar 2023 17:27:21 -0500 Subject: [PATCH 086/732] Update general-strike.lua only update seeds in farms --- fix/general-strike.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fix/general-strike.lua b/fix/general-strike.lua index eabc42881f..c07c738c2e 100644 --- a/fix/general-strike.lua +++ b/fix/general-strike.lua @@ -6,10 +6,12 @@ local argparse = require('argparse') local function fix_seeds(quiet) local count = 0 for _,v in ipairs(df.global.world.items.other.SEEDS) do - if not v.flags.in_building and - (dfhack.items.getGeneralRef(v, df.general_ref_type.BUILDING_HOLDER)) then - v.flags.in_building = true - count = count + 1 + if not v.flags.in_building then + local bld = dfhack.items.getHolderBuilding(v) + if bld and bld:isFarmPlot() then + v.flags.in_building = true + count = count + 1 + end end end if not quiet or count > 0 then From 0b55c5418970917310bfd437499adfac0be80e0e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 31 Mar 2023 04:56:17 -0700 Subject: [PATCH 087/732] update changelog for 50.07-beta2 --- changelog.txt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 3cfee6a6da..ffcfceb4a9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -13,14 +13,24 @@ that repo. # Future +## New Scripts + +## Fixes + +## Misc Improvements + +## Removed + +# 50.07-beta2 + ## New Scripts - `fix/general-strike`: fix known causes of the general strike bug (contributed by Putnam) - `gui/seedwatch`: GUI config and status panel interface for `seedwatch` ## Fixes - `caravan`: item list length now correct when expanding and collapsing containers -- `prioritize`: fixed all watched job type names showing as ``nil`` -- `suspendmanager`: does not suspend non blocking jobs such as floor bars or bridges anymore +- `prioritize`: fixed all watched job type names showing as ``nil`` after a game load +- `suspendmanager`: does not suspend non-blocking jobs such as floor bars or bridges anymore - `suspendmanager`: fix occasional bad identification of buildingplan jobs - `warn-starving`: no longer warns for enemy and neutral units @@ -34,6 +44,7 @@ that repo. - `stripcaged`: added ``--skip-forbidden`` option for greater control over which items are marked for dumping - `stripcaged`: items that are marked for dumping are now automatically unforbidden (unless ``--skip-forbidden`` is set) - `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles +- `gui/control-panel`: added ``general-strike`` maintenance option for automatic fixing of (at least one cause of) the general strike bug - `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button ## Removed From 298d9c00c7df81fc4ca363366dbaebf7f47be489 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 31 Mar 2023 12:30:57 -0700 Subject: [PATCH 088/732] add missing changelog entry for gui/civ-alert --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index ffcfceb4a9..f416e18f7c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,7 @@ that repo. ## New Scripts - `fix/general-strike`: fix known causes of the general strike bug (contributed by Putnam) - `gui/seedwatch`: GUI config and status panel interface for `seedwatch` +- `gui/civ-alert`: configure and trigger civilian alerts ## Fixes - `caravan`: item list length now correct when expanding and collapsing containers From 5f9adb9fceeff99841f50c31823a4756afc367a0 Mon Sep 17 00:00:00 2001 From: Cubittus Date: Sat, 1 Apr 2023 08:34:38 +0100 Subject: [PATCH 089/732] gm-editor: Use copyall to clone config Co-authored-by: Myk --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 3c6bda5ded..68ce5fe37e 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -72,7 +72,7 @@ end GmEditorUi = defclass(GmEditorUi, widgets.Window) GmEditorUi.ATTRS{ - frame={ w=config.data.w, h=config.data.h, l=config.data.l, r=config.data.r, t=config.data.t, b=config.data.b }, + frame=copyall(config.data), frame_title="GameMaster's editor", frame_inset=0, resizable=true, From bee945157327970be7ce6dada0920bffc7698ddd Mon Sep 17 00:00:00 2001 From: Cubittus Date: Sat, 1 Apr 2023 08:42:37 +0100 Subject: [PATCH 090/732] gm-editor: changelog for tweaks --- changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog.txt b/changelog.txt index 3cfee6a6da..60ce3a62a6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -23,6 +23,7 @@ that repo. - `suspendmanager`: does not suspend non blocking jobs such as floor bars or bridges anymore - `suspendmanager`: fix occasional bad identification of buildingplan jobs - `warn-starving`: no longer warns for enemy and neutral units +- `gui/gm-editor`: no longer nudges last open window when opening a new one ## Misc Improvements - `gui/control-panel`: Now detects overlays from scripts named with capital letters @@ -35,6 +36,8 @@ that repo. - `stripcaged`: items that are marked for dumping are now automatically unforbidden (unless ``--skip-forbidden`` is set) - `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles - `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button +- `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` +- `gui/gm-editor`: they key column now auto-fits to the widest key ## Removed - `autounsuspend`: replaced by `suspendmanager` From aa9d7c2d9b015a681662aa4b6674b2c678df5176 Mon Sep 17 00:00:00 2001 From: Cubittus Date: Sat, 1 Apr 2023 08:54:10 +0100 Subject: [PATCH 091/732] gm-editor: Move changelog entries to future version --- changelog.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5c3b36179e..b82265da9e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,8 +16,11 @@ that repo. ## New Scripts ## Fixes +- `gui/gm-editor`: no longer nudges last open window when opening a new one ## Misc Improvements +- `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` +- `gui/gm-editor`: they key column now auto-fits to the widest key ## Removed @@ -34,7 +37,6 @@ that repo. - `suspendmanager`: does not suspend non-blocking jobs such as floor bars or bridges anymore - `suspendmanager`: fix occasional bad identification of buildingplan jobs - `warn-starving`: no longer warns for enemy and neutral units -- `gui/gm-editor`: no longer nudges last open window when opening a new one ## Misc Improvements - `gui/control-panel`: Now detects overlays from scripts named with capital letters @@ -48,8 +50,6 @@ that repo. - `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles - `gui/control-panel`: added ``general-strike`` maintenance option for automatic fixing of (at least one cause of) the general strike bug - `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button -- `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` -- `gui/gm-editor`: they key column now auto-fits to the widest key ## Removed - `autounsuspend`: replaced by `suspendmanager` From a28843113b45cad1947430f9926511a2103e521e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Apr 2023 12:07:15 -0700 Subject: [PATCH 092/732] don't warn about uncomfortable dead units --- changelog.txt | 1 + warn-starving.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index b82265da9e..43d2dbfd26 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ that repo. ## Fixes - `gui/gm-editor`: no longer nudges last open window when opening a new one +- `warn-starving`: no longer warns for dead units ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` diff --git a/warn-starving.lua b/warn-starving.lua index d43e65c299..2b08042030 100644 --- a/warn-starving.lua +++ b/warn-starving.lua @@ -98,7 +98,7 @@ function doCheck() for i=#units-1, 0, -1 do local unit = units[i] local rraw = findRaceCaste(unit) - if not rraw or not dfhack.units.isFortControlled(unit) then goto continue end + if not rraw or not dfhack.units.isFortControlled(unit) or dfhack.units.isDead(unit) then goto continue end if checkOnlySane and not dfhack.units.isSane(unit) then goto continue end table.insert(messages, checkVariable(unit.counters2.hunger_timer, 75000, 'starving', starvingUnits, unit)) table.insert(messages, checkVariable(unit.counters2.thirst_timer, 50000, 'dehydrated', dehydratedUnits, unit)) From a35b00b4eb84744635cf5e9e3c782650bf99cc7b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Apr 2023 17:38:16 -0700 Subject: [PATCH 093/732] revise default prioritized job list --- changelog.txt | 1 + prioritize.lua | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/changelog.txt b/changelog.txt index b82265da9e..2f7dfc4e6b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -21,6 +21,7 @@ that repo. ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` - `gui/gm-editor`: they key column now auto-fits to the widest key +- `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) ## Removed diff --git a/prioritize.lua b/prioritize.lua index abfa59c2bc..ed04094d50 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -10,25 +10,25 @@ local persist = require('persist-table') local GLOBAL_KEY = 'prioritize' -- used for state change hooks and persistence local DEFAULT_HAUL_LABORS = {'Food', 'Body', 'Animals'} -local DEFAULT_REACTION_NAMES = {'TAN_A_HIDE'} +local DEFAULT_REACTION_NAMES = {'TAN_A_HIDE', 'ADAMANTINE_WAFERS'} local DEFAULT_JOB_TYPES = { -- take care of rottables before they rot - 'StoreItemInStockpile', 'CustomReaction', 'PrepareRawFish', + 'StoreItemInStockpile', 'CustomReaction', 'StoreItemInBarrel', + 'PrepareRawFish', 'PlaceItemInTomb', -- ensure medical, hygiene, and hospice tasks get done - 'CleanSelf', 'RecoverWounded', 'ApplyCast', 'BringCrutch', 'CleanPatient', - 'DiagnosePatient', 'DressWound', 'GiveFood', 'GiveWater', 'ImmobilizeBreak', - 'PlaceInTraction', 'SetBone', 'Surgery', 'Suture', - -- organize items efficiently so new items can be brought to the stockpiles - 'StoreItemInVehicle', 'StoreItemInBag', 'StoreItemInBarrel', - 'StoreItemInLocation', 'StoreItemInBin', 'PushTrackVehicle', + 'ApplyCast', 'BringCrutch', 'CleanPatient', 'CleanSelf', + 'DiagnosePatient', 'DressWound', 'GiveFood', 'GiveWater', + 'ImmobilizeBreak', 'PlaceInTraction', 'RecoverWounded', + 'SeekInfant', 'SetBone', 'Surgery', 'Suture', -- ensure prisoners and animals are tended to quickly - 'TameAnimal', 'TrainAnimal', 'TrainHuntingAnimal', 'TrainWarAnimal', - 'PenLargeAnimal', 'PitLargeAnimal', 'SlaughterAnimal', - -- when these things come up, get them done ASAP - 'ManageWorkOrders', 'TradeAtDepot', 'BringItemToDepot', 'DumpItem', - 'DestroyBuilding', 'RemoveConstruction', 'PullLever', 'FellTree', - 'FireBallista', 'FireCatapult', 'OperatePump', 'CollectSand', 'MakeArmor', - 'MakeWeapon', + -- (Animal/prisoner storage already covered by 'StoreItemInStockpile' above) + 'SlaughterAnimal', + -- ensure noble tasks never get starved + 'InterrogateSubject', 'ManageWorkOrders', 'ReportCrime', 'TradeAtDepot', + -- get tasks done quickly that might block the player from getting on to + -- the next thing they want to do + 'BringItemToDepot', 'DestroyBuilding', 'DumpItem', 'FellTree', + 'RemoveConstruction', } -- set of job types that we are watching. maps job_type (as a number) to From 3482fad621e640847c916957bfd49fae3c5fe992 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Apr 2023 18:34:13 -0700 Subject: [PATCH 094/732] fix off-by-one error when detecting end of bin --- caravan.lua | 4 ++-- changelog.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/caravan.lua b/caravan.lua index 6232e572c3..48c6402210 100644 --- a/caravan.lua +++ b/caravan.lua @@ -42,7 +42,7 @@ function select_shift_clicked_container_items(new_state, old_state, list_index) for k, goodflag in ipairs(new_state) do if in_container then if goodflag <= GOODFLAG.UNCONTAINED_SELECTED - or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_SELECTED then + or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then break end @@ -89,7 +89,7 @@ function toggle_ctrl_clicked_containers(new_state, old_state, list_index) for k, goodflag in ipairs(new_state) do if in_container then if goodflag <= GOODFLAG.UNCONTAINED_SELECTED - or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_SELECTED then + or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then break end toggled_item_count = toggled_item_count + 1 diff --git a/changelog.txt b/changelog.txt index b82265da9e..06f59f8fdc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,7 @@ that repo. ## New Scripts ## Fixes +- `caravan`: fix trade good list sometimes disappearing when you collapse a bin - `gui/gm-editor`: no longer nudges last open window when opening a new one ## Misc Improvements From a56413fab0d6df84c9350043b81321ffeae21abf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Apr 2023 19:20:42 -0700 Subject: [PATCH 095/732] set the scroll position properly when the height shrinks --- caravan.lua | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/caravan.lua b/caravan.lua index 48c6402210..e11ad71f31 100644 --- a/caravan.lua +++ b/caravan.lua @@ -35,6 +35,19 @@ local GOODFLAG = { local trade = df.global.game.main_interface.trade +local MARGIN_HEIGHT = 26 -- screen height *other* than the list + +function set_height(list_index, delta) + trade.i_height[list_index] = trade.i_height[list_index] + delta + if delta >= 0 then return end + _,screen_height = dfhack.screen.getWindowSize() + -- list only increments in three tiles at a time + local page_height = ((screen_height - 26) // 3) * 3 + trade.scroll_position_item[list_index] = math.max(0, + math.min(trade.scroll_position_item[list_index], + trade.i_height[list_index] - page_height)) +end + function select_shift_clicked_container_items(new_state, old_state, list_index) -- if ctrl is also held, collapse the container too local also_collapse = dfhack.internal.getModifiers().ctrl @@ -59,7 +72,9 @@ function select_shift_clicked_container_items(new_state, old_state, list_index) if not is_container then goto continue end -- deselect the container itself - if also_collapse then + if also_collapse or + old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED or + old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_SELECTED then collapsing_container = goodflag == GOODFLAG.UNCONTAINED_SELECTED new_state[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED else @@ -71,7 +86,7 @@ function select_shift_clicked_container_items(new_state, old_state, list_index) end if collapsed_item_count > 0 then - trade.i_height[list_index] = trade.i_height[list_index] - collapsed_item_count * 3 + set_height(list_index, collapsed_item_count * -3) end end @@ -111,7 +126,7 @@ function toggle_ctrl_clicked_containers(new_state, old_state, list_index) end if toggled_item_count > 0 then - trade.i_height[list_index] = trade.i_height[list_index] - toggled_item_count * 3 * (is_collapsing and 1 or -1) + set_height(list_index, toggled_item_count * 3 * (is_collapsing and -1 or 1)) end end @@ -127,14 +142,12 @@ function collapseTypes(types_list, list_index) end trade.i_height[list_index] = type_on_count * 3 + trade.scroll_position_item[list_index] = 0 end function collapseAllTypes() collapseTypes(trade.current_type_a_expanded[0], 0) collapseTypes(trade.current_type_a_expanded[1], 1) - -- reset scroll to top when collapsing types - trade.scroll_position_item[0] = 0 - trade.scroll_position_item[1] = 0 end function collapseContainers(item_list, list_index) @@ -165,7 +178,7 @@ function collapseContainers(item_list, list_index) end if num_items_collapsed > 0 then - trade.i_height[list_index] = trade.i_height[list_index] - num_items_collapsed * 3 + set_height(list_index, num_items_collapsed * -3) end end From 067dd91e7561b858b6cc7ebcd42af2783e6035a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 02:55:07 +0000 Subject: [PATCH 096/732] [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.21.0 → 0.22.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.21.0...0.22.0) - [github.com/Lucas-C/pre-commit-hooks: v1.4.2 → v1.5.1](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.4.2...v1.5.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95cac1e22d..59dac64677 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.21.0 + rev: 0.22.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.4.2 + rev: v1.5.1 hooks: - id: forbid-tabs exclude_types: From 94f870712138de1b685a1ec8c1dc1089ed36e5a7 Mon Sep 17 00:00:00 2001 From: Cubittus Date: Tue, 4 Apr 2023 14:17:12 +0100 Subject: [PATCH 097/732] Add ``g`` shortcut to locate a target or selected value on the map When a fort is loaded jump to the first of: - the coord if the current target or the selected value is a coord - the `pos` of the current target - the `centerx`, `centery`, `z` of the current target - the `pos` of the selected value - the `centerx`, `centery`, `z` of the selected value The last two allow browsing lists like world.units.active or world.buildings.all and pressing g to jump to each item in the list without opening them. Items in containers/being carried won't be located currently - I can add a (recursive) holder/container ref lookup if there is interest. --- changelog.txt | 3 ++- gui/gm-editor.lua | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index b82265da9e..88bed3ee3b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,7 +20,8 @@ that repo. ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` -- `gui/gm-editor`: they key column now auto-fits to the widest key +- `gui/gm-editor`: the key column now auto-fits to the widest key +- `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building ## Removed diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 68ce5fe37e..064d9e6a27 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -29,6 +29,7 @@ local keybindings_raw = { {name='delete', key="CUSTOM_ALT_D",desc="Delete selected entry"}, {name='reinterpret', key="CUSTOM_ALT_R",desc="Open selected entry as something else"}, {name='start_filter', key="CUSTOM_S",desc="Start typing filter, Enter to finish"}, + {name='gotopos', key="CUSTOM_G",desc="Move map view to location of target"}, {name='help', key="STRING_A063",desc="Show this help"}, {name='displace', key="STRING_A093",desc="Open reference offseted by index"}, --{name='NOT_USED', key="SEC_SELECT",desc="Edit selected entry as a number (for enums)"}, --not a binding... @@ -345,6 +346,35 @@ function GmEditorUi:openOffseted(index,choice) self:pushTarget(trg.target[trg_key]:_displace(tonumber(choice))) end) end +function GmEditorUi:locate(t) + if getmetatable(t) == 'coord' then return t end + local ok, pos = pcall(function() return t.pos end) + if getmetatable(pos) == 'coord' then return pos end + local x,y,z -- locate buildings + ok, x = pcall(function() return t.centerx end) + ok, y = pcall(function() return t.centery end) + ok, z = pcall(function() return t.z end) + if type(x)=='number' and type(y)=='number' and type(z)=='number' then + return { x=x, y=y, z=z } + end + return nil +end +function GmEditorUi:gotoPos() + if not dfhack.isMapLoaded() then return end + -- if the selected value is a coord then use it + local pos = self:getSelectedValue() + if getmetatable(pos) ~= 'coord' then + -- otherwise locate the current target + pos = GmEditorUi:locate(self:currentTarget().target) + if not pos then + -- otherwise locate the current selected value + pos = GmEditorUi:locate(self:getSelectedValue()) + end + end + if pos then + dfhack.gui.revealInDwarfmodeMap(pos,true) + end +end function GmEditorUi:editSelectedRaw(index,choice) self:editSelected(index, choice, {raw=true}) end @@ -453,6 +483,9 @@ function GmEditorUi:onInput(keys) elseif keys[keybindings.reinterpret.key] then self:openReinterpret(self:getSelectedKey()) return true + elseif keys[keybindings.gotopos.key] then + self:gotoPos() + return true elseif keys[keybindings.help.key] then self.subviews.pages:setSelected(2) return true From 555f91a5beb91f2ac0b7b1c735f3cbbf112b440f Mon Sep 17 00:00:00 2001 From: Cubittus Date: Wed, 5 Apr 2023 13:41:32 +0100 Subject: [PATCH 098/732] gm-editor: goto now uses getPosition for units, items and buildings For everything else it looks for position-like fields --- gui/gm-editor.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 064d9e6a27..b240d0134e 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -348,14 +348,22 @@ function GmEditorUi:openOffseted(index,choice) end function GmEditorUi:locate(t) if getmetatable(t) == 'coord' then return t end + if df.unit:is_instance(t) then return xyz2pos(dfhack.units.getPosition(t)) end + if df.item:is_instance(t) then return xyz2pos(dfhack.items.getPosition(t)) end + if df.building:is_instance(t) then return xyz2pos(t.centerx, t.centery, t.z) end + -- anything else - look for pos or x/y/z fields local ok, pos = pcall(function() return t.pos end) if getmetatable(pos) == 'coord' then return pos end - local x,y,z -- locate buildings - ok, x = pcall(function() return t.centerx end) - ok, y = pcall(function() return t.centery end) + local x,y,z + ok, x = pcall(function() return t.x end) + if type(x)~='number' then ok, x = pcall(function() return t.centerx end) end + if type(x)~='number' then ok, x = pcall(function() return t.x1 end) end + ok, y = pcall(function() return t.y end) + if type(y)~='number' then ok, y = pcall(function() return t.centery end) end + if type(y)~='number' then ok, y = pcall(function() return t.y1 end) end ok, z = pcall(function() return t.z end) if type(x)=='number' and type(y)=='number' and type(z)=='number' then - return { x=x, y=y, z=z } + return xyz2pos(x,y,z) end return nil end From 1b9d28deec0f5500beb9fe46f631e33e56e2890e Mon Sep 17 00:00:00 2001 From: Cubittus Date: Wed, 5 Apr 2023 18:15:51 +0100 Subject: [PATCH 099/732] `gm-editor`: add read-only mode state and config - new global `read_only` var - read only state is saved in `config.data.read_only` - frame position now saved in `config.data.frame` - config saving moved to `save_config` fn - keybinding `Ctrl-D` added to toggle `read_only` - titles of all views updated on change - added `dev`/`edit` and `safe`/`read` args to clear/set `read_only` on start - insert/delete/set fns do nothing when `read_only` set - `editSelected` allows only navigation when `read_only` set - one outstanding issue is that views opened via the `dialog` argument are not added to the `views` global array, so cannot have their `frame_title` updated by other views. --- gui/gm-editor.lua | 52 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 68ce5fe37e..96fadc73a6 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -9,6 +9,18 @@ local utils = require 'utils' config = config or json.open('dfhack-config/gm-editor.json') +function save_config(data) + for k,v in pairs(data) do + config.data[k] = v + end + config:write() +end + +read_only = true +if config.data.read_only ~= nil then + read_only = config.data.read_only +end + find_funcs = find_funcs or (function() local t = {} for k in pairs(df) do @@ -20,6 +32,7 @@ find_funcs = find_funcs or (function() end)() local keybindings_raw = { + {name='toggle_ro', key="CUSTOM_CTRL_D",desc="Toggle between read-only and read-write"}, {name='offset', key="CUSTOM_ALT_O",desc="Show current items offset"}, {name='find', key="CUSTOM_F",desc="Find a value by entering a predicate"}, {name='find_id', key="CUSTOM_I",desc="Find object with this ID, using ref-target if available"}, @@ -72,7 +85,7 @@ end GmEditorUi = defclass(GmEditorUi, widgets.Window) GmEditorUi.ATTRS{ - frame=copyall(config.data), + frame=copyall(config.data.frame or {}), frame_title="GameMaster's editor", frame_inset=0, resizable=true, @@ -242,6 +255,7 @@ function GmEditorUi:find_id(force_dialog) end) end function GmEditorUi:insertNew(typename) + if read_only then return end local tp=typename if typename == nil then dialog.showInputPrompt("Class type","You can:\n * Enter type name (without 'df.')\n * Leave empty for default type and 'nil' value\n * Enter '*' for default type and 'new' constructed pointer value",COLOR_WHITE,"",self:callback("insertNew")) @@ -266,6 +280,7 @@ function GmEditorUi:insertNew(typename) end end function GmEditorUi:deleteSelected(key) + if read_only then return end local trg=self:currentTarget() if trg.target and trg.target._kind and trg.target._kind=="container" then trg.target:erase(key) @@ -353,14 +368,17 @@ function GmEditorUi:editSelected(index,choice,opts) local trg=self:currentTarget() local trg_key=trg.keys[index] if trg.target and trg.target._kind and trg.target._kind=="bitfield" then + if read_only then return end trg.target[trg_key]= not trg.target[trg_key] self:updateTarget(true) else --print(type(trg.target[trg.keys[trg.selected]]),trg.target[trg.keys[trg.selected]]._kind or "") local trg_type=type(trg.target[trg_key]) if self:getSelectedEnumType() and not opts.raw then + if read_only then return end self:editSelectedEnum() elseif trg_type=='number' or trg_type=='string' then --ugly TODO: add metatable get selected + if read_only then return end local prompt = "Enter new value:" if self:getSelectedEnumType() then prompt = "Enter new " .. getTypeName(trg.target:_field(trg_key)._type) .. " value" @@ -369,6 +387,7 @@ function GmEditorUi:editSelected(index,choice,opts) tostring(trg.target[trg_key]), self:callback("commitEdit",trg_key)) elseif trg_type == 'boolean' then + if read_only then return end trg.target[trg_key] = not trg.target[trg_key] self:updateTarget(true) elseif trg_type == 'userdata' or trg_type == 'table' then @@ -383,6 +402,7 @@ function GmEditorUi:editSelected(index,choice,opts) end function GmEditorUi:commitEdit(key,value) + if read_only then return end local trg=self:currentTarget() if type(trg.target[key])=='number' then trg.target[key]=tonumber(value) @@ -393,6 +413,7 @@ function GmEditorUi:commitEdit(key,value) end function GmEditorUi:set(key,input) + if read_only then return end local trg=self:currentTarget() if input== nil then @@ -423,7 +444,11 @@ function GmEditorUi:onInput(keys) return false end - if keys[keybindings.offset.key] then + if keys[keybindings.toggle_ro.key] then + read_only = not read_only + self:updateTitles() + return true + elseif keys[keybindings.offset.key] then local trg=self:currentTarget() local _,stoff=df.sizeof(trg.target) local size,off=df.sizeof(trg.target:_field(self:getSelectedKey())) @@ -482,6 +507,18 @@ function getStringValue(trg,field) end) return text end + +function GmEditorUi:updateTitles() + local title = "GameMaster's Editor" + if read_only then + title = title.." (Read Only)" + end + for view,_ in pairs(views) do + view.subviews[1].frame_title = title + end + self.frame_title = title + save_config({read_only = read_only}) +end function GmEditorUi:updateTarget(preserve_pos,reindex) local trg=self:currentTarget() local filter=self.subviews.filter_input.text:lower() @@ -518,6 +555,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) else self.subviews.list_main:setSelected(trg.selected) end + self:updateTitles() end function GmEditorUi:pushTarget(target_to_push) local new_tbl={} @@ -555,7 +593,7 @@ function eval(s) return f() end function GmEditorUi:postUpdateLayout() - config:write(self.frame) + save_config({frame = self.frame}) end GmScreen = defclass(GmScreen, gui.ZScreen) @@ -577,6 +615,14 @@ end local function get_editor(args) if #args~=0 then + if args[1]=='dev' or args[1]=='edit' then + read_only = false + table.remove(args,1) + end + if args[1]=='safe' or args[1]=='read' then + read_only = true + table.remove(args,1) + end if args[1]=="dialog" then dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, "", function(entry) From 6e4ac420c860d82dfee9457116fa6793735962c1 Mon Sep 17 00:00:00 2001 From: Cubittus Date: Wed, 5 Apr 2023 18:24:06 +0100 Subject: [PATCH 100/732] gm-editor: readonly changelog --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index b82265da9e..510f34ecac 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,7 +20,8 @@ that repo. ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` -- `gui/gm-editor`: they key column now auto-fits to the widest key +- `gui/gm-editor`: the key column now auto-fits to the widest key +- `gui/gm-editor`: Now starts in read-only mode. Press Ctrl-D to switch to editing mode. This state persists across sessions ## Removed From e3bfec09ddc5ec733e8baccde070110f90103e83 Mon Sep 17 00:00:00 2001 From: Cubittus Date: Thu, 6 Apr 2023 16:48:24 +0100 Subject: [PATCH 101/732] gm-editor: Turn on the recenter indicator when using goto --- gui/gm-editor.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index b240d0134e..e5f09423d9 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -381,6 +381,9 @@ function GmEditorUi:gotoPos() end if pos then dfhack.gui.revealInDwarfmodeMap(pos,true) + df.global.game.main_interface.recenter_indicator_m.x = pos.x + df.global.game.main_interface.recenter_indicator_m.y = pos.y + df.global.game.main_interface.recenter_indicator_m.z = pos.z end end function GmEditorUi:editSelectedRaw(index,choice) From c270180dc7d22dcd90c043839f3468b66d0256ce Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 6 Apr 2023 23:17:46 -0700 Subject: [PATCH 102/732] add faststart to system services and persist setting --- changelog.txt | 1 + gui/control-panel.lua | 65 ++++++++++++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5434ed5926..8414510772 100644 --- a/changelog.txt +++ b/changelog.txt @@ -24,6 +24,7 @@ that repo. - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` - `gui/gm-editor`: they key column now auto-fits to the widest key - `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) +- `gui/control-panel`: add `faststart` to the system services ## Removed diff --git a/gui/control-panel.lua b/gui/control-panel.lua index e5b9f7db22..efef055c14 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -6,11 +6,13 @@ local repeatUtil = require('repeat-util') local utils = require('utils') local widgets = require('gui.widgets') +-- init files +local SYSTEM_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-system.init' local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init' local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' --- eventually this should be queryable from Core/script-manager +-- service and command lists local FORT_SERVICES = { 'autobutcher', 'autochop', @@ -35,21 +37,27 @@ local FORT_SERVICES = { local FORT_AUTOSTART = { 'ban-cooking all', - --'buildingplan set boulders false', - --'buildingplan set logs false', + 'buildingplan set boulders false', + 'buildingplan set logs false', } for _,v in ipairs(FORT_SERVICES) do table.insert(FORT_AUTOSTART, v) end table.sort(FORT_AUTOSTART) --- eventually this should be queryable from Core/script-manager local SYSTEM_SERVICES = { 'automelt', -- TODO needs dynamic detection of configurability 'buildingplan', 'confirm', 'overlay', } +local SYSTEM_USER_SERVICES = { + 'faststart', +} +for _,v in ipairs(SYSTEM_USER_SERVICES) do + table.insert(SYSTEM_SERVICES, v) +end +table.sort(SYSTEM_SERVICES) local PREFERENCES = { ['gui']={ @@ -109,32 +117,35 @@ local function save_file(path, save_fn) f:close() end - local function get_icon_pens() local start = dfhack.textures.getControlPanelTexposStart() local valid = start > 0 start = start + 10 + local function tp(offset) + return valid and start + offset or nil + end + local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+0) or nil, ch=string.byte('[')} + tile=tp(0), ch=string.byte('[')} local enabled_pen_center = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, - tile=valid and (start+1) or nil, ch=251} -- check + tile=tp(1) or nil, ch=251} -- check local enabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+2) or nil, ch=string.byte(']')} + tile=tp(2) or nil, ch=string.byte(']')} local disabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+3) or nil, ch=string.byte('[')} + tile=tp(3) or nil, ch=string.byte('[')} local disabled_pen_center = dfhack.pen.parse{fg=COLOR_RED, - tile=valid and (start+4) or nil, ch=string.byte('x')} + tile=tp(4) or nil, ch=string.byte('x')} local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+5) or nil, ch=string.byte(']')} + tile=tp(5) or nil, ch=string.byte(']')} local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+6) or nil, ch=string.byte('[')} + tile=tp(6) or nil, ch=string.byte('[')} local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+7) or nil, ch=string.byte(']')} + tile=tp(7) or nil, ch=string.byte(']')} local help_pen_center = dfhack.pen.parse{ - tile=valid and (start+8) or nil, ch=string.byte('?')} + tile=tp(8) or nil, ch=string.byte('?')} local configure_pen_center = dfhack.pen.parse{ - tile=valid and (start+9) or nil, ch=15} -- gear/masterwork symbol + tile=tp(9) or nil, ch=15} -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, button_pen_left, button_pen_right, @@ -376,12 +387,13 @@ function FortServicesAutostart:init() local enabled_map = {} local ok, f = pcall(io.open, AUTOSTART_FILE) if ok and f then + local services_set = utils.invert(FORT_AUTOSTART) for line in f:lines() do line = line:trim() if #line == 0 or line:startswith('#') then goto continue end local service = line:match('^on%-new%-fortress enable ([%S]+)$') or line:match('^on%-new%-fortress (.+)') - if service then + if service and services_set[service] then enabled_map[service] = true end ::continue:: @@ -423,12 +435,27 @@ SystemServices.ATTRS{ title='System', is_enableable=true, is_configurable=true, - intro_text='These are DFHack system services that should generally not'.. - ' be turned off. If you do turn them off, they may'.. - ' automatically re-enable themselves when you restart DF.', + intro_text='These are DFHack system services that are not bound to' .. + ' a specific fort. Some of these are critical DFHack services' .. + ' that can be manually disabled, but will re-enable themselves' .. + ' when DF restarts.', services_list=SYSTEM_SERVICES, } +function SystemServices:on_submit() + SystemServices.super.on_submit(self) + + local enabled_map = self:get_enabled_map() + local save_fn = function(f) + for _,service in ipairs(SYSTEM_USER_SERVICES) do + if enabled_map[service] then + f:write(('enable %s\n'):format(service)) + end + end + end + save_file(SYSTEM_INIT_FILE, save_fn) +end + -- -- Overlays -- From d8557ba6f1777c1a6d1b92a364e4ee22ec829873 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Apr 2023 11:05:52 -0700 Subject: [PATCH 103/732] update prioritize docs --- docs/prioritize.rst | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/prioritize.rst b/docs/prioritize.rst index 16fb57cd46..852e4c1e37 100644 --- a/docs/prioritize.rst +++ b/docs/prioritize.rst @@ -23,9 +23,6 @@ the same problem that ``prioritize`` is designed to solve. The script provides a good default set of job types to prioritize that have been suggested and playtested by the DF community. -Also see the `do-job-now tweak ` and the `do-job-now` script for boosting -the priority of specific individual jobs (as opposed to entire classes of jobs). - Usage ----- @@ -113,14 +110,14 @@ Default list of job types to prioritize The community has assembled a good default list of job types that most players will benefit from. They have been playtested across a wide variety of fort -types. It is a good idea to enable prioritize for all your forts. +types. It is a good idea to enable `prioritize` with at least these defaults +for all your forts. The default prioritize list includes: - Handling items that can rot - Medical, hygiene, and hospice tasks -- Putting items in bins/barrels/pots/minecarts - Interactions with animals and prisoners -- Dumping items, pulling levers, felling trees, and other tasks that you, as a - player, might stare at and internally scream "why why why isn't this getting - done??". +- Noble-specific tasks (like managing workorders) +- Dumping items, felling trees, and other tasks that you, as a player, might + stare at and internally scream "why why why isn't this getting done??". From a69b46580bc4f33270e2f55ca5c214b46167d017 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Apr 2023 00:15:21 -0700 Subject: [PATCH 104/732] update python build action to non-deprecated version --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bf2f9a853..919b3242e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Set up Python 3 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 - name: Install dependencies @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Set up Python 3 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 - name: Install dependencies From 94c8f7143fd51ff532e3884801394a6a8d0d162e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Apr 2023 03:16:55 -0700 Subject: [PATCH 105/732] display a message if there are no burrows and ensure nameless burrows are named the same as in the Burrows screen --- gui/civ-alert.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 75a288411b..75b58bb4c2 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -190,6 +190,8 @@ Civalert.ATTRS{ } function Civalert:init() + local choices = self:get_burrow_choices() + self:addviews{ widgets.Panel{ frame={t=0, l=0, r=12}, @@ -219,16 +221,23 @@ function Civalert:init() }, widgets.FilteredList{ frame={t=6, l=0, b=0, r=0}, - choices=self:get_burrow_choices(), + choices=choices, icon_width=2, on_submit=self:callback('select_burrow'), + visible=#choices > 0, + }, + widgets.WrappedLabel{ + frame={t=7, l=0, r=0}, + text_to_wrap='No burrows defined. Please define one to use for the civalert.', + text_pen=COLOR_RED, + visible=#choices == 0, }, } end local function get_burrow_name(burrow) if #burrow.name > 0 then return burrow.name end - return ('Burrow %d'):format(burrow.id) + return ('Burrow %d'):format(burrow.id+1) end local SELECTED_ICON = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} From d25ef792884b84980b8052fb707b87aa0a79b276 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Apr 2023 03:39:59 -0700 Subject: [PATCH 106/732] don't offer gui/automelt when not in fort mode --- changelog.txt | 1 + gui/control-panel.lua | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8414510772..ad7b280b62 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ that repo. - `caravan`: fix trade good list sometimes disappearing when you collapse a bin - `gui/gm-editor`: no longer nudges last open window when opening a new one - `warn-starving`: no longer warns for dead units +- `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` diff --git a/gui/control-panel.lua b/gui/control-panel.lua index efef055c14..f9e8000e3c 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -246,7 +246,7 @@ function ConfigPanel:refresh() local command = choice.target or choice.command command = command:match(COMMAND_REGEX) local gui_config = 'gui/' .. command - local want_gui_config = utils.getval(self.is_configurable) + local want_gui_config = utils.getval(self.is_configurable, gui_config) and helpdb.is_entry(gui_config) local enabled = choice.enabled local function get_enabled_pen(enabled_pen, disabled_pen) @@ -360,7 +360,7 @@ end FortServices = defclass(FortServices, Services) FortServices.ATTRS{ is_enableable=dfhack.world.isFortressMode, - is_configurable=dfhack.world.isFortressMode, + is_configurable=function() return dfhack.world.isFortressMode() end, intro_text='These tools can only be enabled when you have a fort loaded,'.. ' but once you enable them, they will stay enabled when you'.. ' save and reload your fort. If you want them to be'.. @@ -430,11 +430,15 @@ end -- SystemServices -- +local function system_service_is_configurable(gui_config) + return gui_config ~= 'gui/automelt' or dfhack.world.isFortressMode() +end + SystemServices = defclass(SystemServices, Services) SystemServices.ATTRS{ title='System', is_enableable=true, - is_configurable=true, + is_configurable=system_service_is_configurable, intro_text='These are DFHack system services that are not bound to' .. ' a specific fort. Some of these are critical DFHack services' .. ' that can be manually disabled, but will re-enable themselves' .. From 855d1cbf6432c91c0b5e6f6c2d30ce04d2765abf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Apr 2023 03:57:44 -0700 Subject: [PATCH 107/732] output a better error message when orders are attempted --- internal/quickfort/command.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/quickfort/command.lua b/internal/quickfort/command.lua index ed9701375a..ce73888f32 100644 --- a/internal/quickfort/command.lua +++ b/internal/quickfort/command.lua @@ -22,7 +22,7 @@ end local command_switch = { run='do_run', - orders='do_orders', + -- orders='do_orders', -- until we get stockflow working undo='do_undo', } @@ -234,6 +234,9 @@ end function do_command(args) for _,command in ipairs(args.commands) do if not command or not command_switch[command] then + if command == 'orders' then + qerror('orders functionality not updated yet') + end qerror(string.format('invalid command: "%s"', command)) end end From f15e199c401b4b46f543e1016bc108e481c5c953 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 11 Apr 2023 21:29:57 +1000 Subject: [PATCH 108/732] Version for review --- combine.lua | 462 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 327 insertions(+), 135 deletions(-) diff --git a/combine.lua b/combine.lua index a6045981d3..d72d126f03 100644 --- a/combine.lua +++ b/combine.lua @@ -8,25 +8,35 @@ local opts, args = { here = nil, dry_run = false, types = nil, - verbose = false + verbose = 0 }, {...} -- default max stack size of 30 -local DEF_MAX=30 +local MAX_ITEM_STACK=30 +local MAX_CONT_ITEMS=500 + +-- list of types that use race and caste +local typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true} -- list of valid item types for merging +-- Notes: 1. mergeable stacks are ones with the same type_id+race+caste or type_id+mat_type+mat_index +-- 2. the maximum stack size is calcuated at run time: the highest value of MAX_ITEM_STACK or largest current stack size. +-- 3. even though powders are specified, sand and plaster types items are excluded from merging. +-- 4. seeds cannot be combined in stacks > 1. local valid_types_map = { - ['all'] = { }, - ['drink'] = {[df.item_type.DRINK]={type_id=df.item_type.DRINK, type_name='DRINK',type_caste=false,max_stack_size=DEF_MAX}}, - ['fat'] = {[df.item_type.GLOB]={type_id=df.item_type.GLOB, type_name='GLOB',type_caste=false,max_stack_size=DEF_MAX}, - [df.item_type.CHEESE]={type_id=df.item_type.CHEESE, type_name='CHEESE',type_caste=false,max_stack_size=DEF_MAX}}, - ['fish'] = {[df.item_type.FISH]={type_id=df.item_type.FISH, type_name='FISH',type_caste=true,max_stack_size=DEF_MAX}, - [df.item_type.FISH_RAW]={type_id=df.item_type.FISH_RAW, type_name='FISH_RAW',type_caste=true,max_stack_size=DEF_MAX}, - [df.item_type.EGG]={type_id=df.item_type.EGG, type_name='EGG',type_caste=true,max_stack_size=DEF_MAX}}, - ['food'] = {[df.item_type.FOOD]={type_id=df.item_type.FOOD, type_name='FOOD',type_caste=false,max_stack_size=DEF_MAX}}, - ['meat'] = {[df.item_type.MEAT]={type_id=df.item_type.MEAT, type_name='MEAT',type_caste=false,max_stack_size=DEF_MAX}}, - ['plant'] = {[df.item_type.PLANT]={type_id=df.item_type.PLANT, type_name='PLANT',type_caste=false,max_stack_size=DEF_MAX}, - [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, type_name='PLANT_GROWTH',type_caste=false,max_stack_size=DEF_MAX}} + ['all'] = { }, + ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=MAX_ITEM_STACK}}, + ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=MAX_ITEM_STACK}, + [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=MAX_ITEM_STACK}}, + ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=MAX_ITEM_STACK}, + [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=MAX_ITEM_STACK}, + [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=MAX_ITEM_STACK}}, + ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=MAX_ITEM_STACK}}, + ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=MAX_ITEM_STACK}}, + ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=MAX_ITEM_STACK}, + [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=MAX_ITEM_STACK}}, + ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=MAX_ITEM_STACK}}, + ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_size=1}}, } -- populate all types entry @@ -41,9 +51,9 @@ for k1,v1 in pairs(valid_types_map) do end end -function log(...) +function log(level, ...) -- if verbose is specified, then print the arguments, or don't. - if opts.verbose then dfhack.print(string.format(...)) end + if opts.verbose >= level then dfhack.print(string.format(...)) end end -- CList class @@ -59,47 +69,49 @@ function CList:new(o) return o end -local function comp_item_new(comp_key, max_stack_size) +local function comp_item_new(comp_key, max_size) -- create a new comp_item entry to be added to a comp_items table. local comp_item = {} if not comp_key then qerror('new_comp_item: comp_key is nil') end - comp_item.comp_key = comp_key - comp_item.item_qty = 0 - comp_item.max_stack_size = max_stack_size or 0 - comp_item.before_stacks = 0 - comp_item.after_stacks = 0 - comp_item.before_stack_size = CList:new(nil) -- key:item.id, val:item.stack_size - comp_item.after_stack_size = CList:new(nil) -- key:item.id, val:item.stack_size - comp_item.items = CList:new(nil) -- key:item.id, val:item - comp_item.sorted_items = CList:new(nil) -- key:-1*item.id | item.id, val:item_id + comp_item.comp_key = comp_key -- key used to index comparable items for merging + comp_item.description = '' -- description of the comp item for output + comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack + -- item info + comp_item.items = CList:new(nil) -- key:item.id, val:{ item, before_size, after_size, before_cont_id, after_cont_id} + comp_item.item_qty = 0 -- total quantity of items + comp_item.before_stacks = 0 -- the number of stacks of the items before... + comp_item.after_stacks = 0 -- ...and after the merge + --container info + comp_item.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id + comp_item.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id return comp_item end -local function comp_item_add_item(comp_item, item) +local function comp_item_add_item(comp_item, item, container) -- add an item into the comp_items table, setting the comp_item attributes. if not comp_item.items[item.id] then - - comp_item.items[item.id] = item comp_item.item_qty = comp_item.item_qty + item.stack_size - if item.stack_size > comp_item.max_stack_size then - comp_item.max_stack_size = item.stack_size - end - comp_item.before_stack_size[item.id] = item.stack_size - comp_item.after_stack_size[item.id] = item.stack_size comp_item.before_stacks = comp_item.before_stacks + 1 - comp_item.after_stacks = comp_item.after_stacks + 1 + comp_item.description = utils.getItemDescription(item, 1) - local contained_item = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) + if item.stack_size > comp_item.max_size then + comp_item.max_size = item.stack_size + end - -- used to merge contained items before loose items - if contained_item then - table.insert(comp_item.sorted_items, -1*item.id) - else - table.insert(comp_item.sorted_items, item.id) + local new_item = {} + new_item.item = item + new_item.before_size = item.stack_size + + -- item is in a container + if container then + new_item.before_cont_id = container.id + comp_item.before_cont_ids[container.id] = container.id end + + comp_item.items[item.id] = new_item return comp_item.items[item.id] else - -- this case should not happen, unless an item is contained by more than one container. + -- this case should not happen, unless an item id is duplicated. -- in which case, only allow one instance for the merge. return nil end @@ -108,62 +120,152 @@ end local function stack_type_new(type_vals) -- create a new stack type entry to be added to the stacks table. local stack_type = {} + + -- attributes from the type val table for k,v in pairs(type_vals) do stack_type[k] = v end - stack_type.item_qty = 0 - stack_type.before_stacks = 0 - stack_type.after_stacks = 0 - stack_type.comp_items = CList:new(nil) -- key:comp_key, val=comp_item + + -- item info + stack_type.comp_items = CList:new(nil) -- key:comp_key, val:comp_item + stack_type.item_qty = 0 -- total quantity of items types + stack_type.before_stacks = 0 -- the number of stacks of the item types before ... + stack_type.after_stacks = 0 -- ...and after the merge + + --container info + stack_type.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id + stack_type.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id return stack_type end -local function stacks_type_add_item(stacks_type, item) +local function stacks_add_item(stacks, stack_type, item, container, contained_count) -- add an item to the matching comp_items table; based on comp_key. local comp_key = '' - if stacks_type.type_caste then - comp_key = tostring(stacks_type.type_id) .. tostring(item.race) .. tostring(item.caste) + if typesThatUseCreatures[df.item_type[stack_type.type_id]] then + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) .. "+" .. tostring(item.caste) else - comp_key = tostring(stacks_type.type_id) .. tostring(item.mat_type) .. tostring(item.mat_index) + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.mat_type) .. "+" .. tostring(item.mat_index) end - if not stacks_type.comp_items[comp_key] then - stacks_type.comp_items[comp_key] = comp_item_new(comp_key, stacks_type.max_stack_size) + if not stack_type.comp_items[comp_key] then + stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_size) end - if comp_item_add_item(stacks_type.comp_items[comp_key], item) then - stacks_type.before_stacks = stacks_type.before_stacks + 1 - stacks_type.after_stacks = stacks_type.after_stacks + 1 - stacks_type.item_qty = stacks_type.item_qty + item.stack_size - if item.stack_size > stacks_type.max_stack_size then - stacks_type.max_stack_size = item.stack_size + if comp_item_add_item(stack_type.comp_items[comp_key], item, container, contained_count) then + stack_type.before_stacks = stack_type.before_stacks + 1 + stack_type.item_qty = stack_type.item_qty + item.stack_size + + stacks.before_stacks = stacks.before_stacks + 1 + stacks.item_qty = stacks.item_qty + item.stack_size + + if item.stack_size > stack_type.max_size then + stack_type.max_size = item.stack_size + end + + -- item is in a container + if container then + + -- add it to the stack type list + stack_type.before_cont_ids[container.id] = container.id + + -- add it to the before stacks container list + stacks.before_cont_ids[container.id] = container.id end end end -local function print_stacks_details(stacks) - -- print stacks details - log(('Details #types:%5d\n'):format(#stacks)) - for _, stacks_type in pairs(stacks) do - log((' type: <%12s> <%d> comp item types#:%5d #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(stacks_type.type_name, stacks_type.type_id, stacks_type.item_qty, #stacks_type.comp_items, stacks_type.max_stack_size, stacks_type.before_stacks, stacks_type.after_stacks)) - for _, comp_item in pairs(stacks_type.comp_items) do - log((' compare key:%12s #item qty:%5d #comp item stacks:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_size, comp_item.before_stacks, comp_item.after_stacks)) - for _, item in pairs(comp_item.items) do - log((' item:%40s <%6d> before:%5d after:%5d\n'):format(utils.getItemDescription(item), item.id, comp_item.before_stack_size[item.id], comp_item.after_stack_size[item.id])) +local function sorted_items(tab) + -- used to sort the comp_items by contained, then size. Important for combining containers. + local tmp = {} + for id, val in pairs(tab) do + local val = {id=id, before_cont_id=val.before_cont_id, before_size=val.before_size} + table.insert(tmp, val) + end + + table.sort(tmp, + function(a, b) + if not a.before_cont_id and not b.before_cont_id or a.before_cont_id and b.before_cont_id then + return a.before_size > b.before_size + else + return a.before_cont_id and not b.before_cont_id + end + end + ) + + local i = 0 + local iter = + function() + i = i + 1 + if tmp[i] == nil then + return nil + else + return tmp[i].id, tab[tmp[i].id] end end + return iter +end + +local function sorted_desc(tab, ids) + -- used to sort the lists by description + local tmp = {} + for id, val in pairs(tab) do + if ids[id] then + local val = {id=id, description=val.description} + table.insert(tmp, val) + end end + + table.sort(tmp, function(a, b) return a.description < b.description end) + + local i = 0 + local iter = + function() + i = i + 1 + if tmp[i] == nil then + return nil + else + return tmp[i].id, tab[tmp[i].id] + end + end + return iter end -local function print_stacks_summary(stacks) - -- print stacks summary to the console - dfhack.print('Summary:\n') - for _, stacks_type in pairs(stacks) do - dfhack.print((' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(stacks_type.type_name, stacks_type.type_id, stacks_type.item_qty, stacks_type.max_stack_size, stacks_type.before_stacks, stacks_type.after_stacks)) +local function print_stacks(stacks) + -- print stacks details + log(0, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) + for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do + log(1, (' container: %50s <%6d> before:%5d after:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size)) + end + log(0, ('Items: #qty: %6d sizes: before:%5d after:%5d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks)) + for key, stack_type in pairs(stacks.stack_types) do + log(0, (' Type: %12s <%d> #qty:%6d sizes: max:%5d before:%6d after:%6d containers: before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids)) + for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do + log(1, (' Comp item:%40s <%12s> #qty:%6d #stacks:%5d sizes: max:%5d before:%6d after:%6d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + for _, item in sorted_items(comp_item.items) do + log(2, (' Item:%40s <%6d> before:%6d after:%6d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + log(3, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) + log(2, ('\n')) + end + end end end +local function stacks_new() + local stacks = {} + + stacks.stack_types = CList:new(nil) -- key=type_id, val=stack_type + stacks.containers = CList:new(nil) -- key=container.id, val={container, description, before_size, after_size} + stacks.before_cont_ids = CList:new(nil) -- key=container.id, val=container.id + stacks.after_cont_ids = CList:new(nil) -- key=container.id, val=container.id + stacks.item_qty = 0 + stacks.before_stacks = 0 + stacks.after_stacks = 0 + + return stacks + +end + local function isRestrictedItem(item) -- is the item restricted from merging? local flags = item.flags @@ -172,37 +274,50 @@ local function isRestrictedItem(item) or flags.removed or flags.encased or flags.spider_web or #item.specific_refs > 0 end - -function stacks_add_items(stacks, items, ind) +function stacks_add_items(stacks, items, container, contained_count, ind) -- loop through each item and add it to the matching stack[type_id].comp_items table -- recursively calls itself to add contained items if not ind then ind = '' end for _, item in pairs(items) do local type_id = item:getType() - local stacks_type = stacks[type_id] + local subtype_id = item:getSubtype() + local stack_type = stacks.stack_types[type_id] -- item type in list of included types? - if stacks_type then + if stack_type and not item:isSand() and not item:isPlaster() then if not isRestrictedItem(item) then - stacks_type_add_item(stacks_type, item) + stacks_add_item(stacks, stack_type, item, container, contained_count) + + if typesThatUseCreatures[df.item_type[type_id]] then + local raceRaw = df.global.world.raws.creatures.all[item.race] + local casteRaw = raceRaw.caste[item.caste] + log(3, (' %sitem:%40s <%6d> is incl, type:%d, race:%s, caste:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, raceRaw.creature_id, casteRaw.caste_id)) + else + local mat_info = dfhack.matinfo.decode(item.mat_type, item.mat_index) + log(3, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, sand:%s, plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(),item:isSand(), item:isPlaster())) + end - log((' %sitem:%40s <%6d> is incl, type %d\n'):format(ind, utils.getItemDescription(item), item.id, type_id)) else -- restricted; such as marked for action or dump. - log((' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) + log(3, (' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) end -- add contained items elseif dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINS_ITEM) then local contained_items = dfhack.items.getContainedItems(item) - log((' %sContainer:%s <%6d> #items:%5d\n'):format(ind, utils.getItemDescription(item), item.id, #contained_items)) - stacks_add_items(stacks, contained_items, ind .. ' ') + local count = #contained_items + stacks.containers[item.id] = {} + stacks.containers[item.id].container = item + stacks.containers[item.id].before_size = #contained_items + stacks.containers[item.id].description = utils.getItemDescription(item, 1) + log(3, (' %sContainer:%s <%6d> #items:%5d Sandbearing:%s\n'):format(ind, utils.getItemDescription(item), item.id, count, item:isSandBearing())) + stacks_add_items(stacks, contained_items, item, count, ind .. ' ') -- excluded item types else - log((' %sitem:%40s <%6d> is excl, type %d\n'):format(ind, utils.getItemDescription(item), item.id, type_id)) + log(3, (' %sitem:%40s <%6d> is excl, type %d, sand:%s plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, item:isSand(), item:isPlaster())) end end end @@ -212,85 +327,159 @@ local function populate_stacks(stacks, stockpiles, types) -- 2. loop through the table of stockpiles, get each item in the stockpile, then add them to stacks if the type_id matches -- an item is stored at the bottom of the structure: stacks[type_id].comp_items[comp_key].item -- comp_key is a compound key comprised of type_id+race+caste or type_id+mat_type+mat_index - log('Populating phase\n') + log(3, 'Populating phase\n') -- iterate across the types - log('stack types\n') + log(3, 'stack types\n') for type_id, type_vals in pairs(types) do - if not stacks[type_id] then - stacks[type_id] = stack_type_new(type_vals) - local stacks_type = stacks[type_id] - log((' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(stacks_type.type_name, stacks_type.type_id, stacks_type.item_qty, stacks_type.max_stack_size, stacks_type.before_stacks, stacks_type.after_stacks)) + if not stacks.stack_types[type_id] then + stacks.stack_types[type_id] = stack_type_new(type_vals) + local stack_type = stacks.stack_types[type_id] + log(3, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end -- iterate across the stockpiles, get the list of items and call the add function to check/add as needed - log(('stockpiles\n')) + log(3, ('stockpiles\n')) for _, stockpile in pairs(stockpiles) do local items = dfhack.buildings.getStockpileContents(stockpile) - log((' stockpile:%30s <%6d> pos:(%3d,%3d,%3d) #items:%5d\n'):format(stockpile.name, stockpile.id, stockpile.centerx, stockpile.centery, stockpile.z, #items)) + log(3, (' stockpile:%30s <%6d> pos:(%3d,%3d,%3d) #items:%5d\n'):format(stockpile.name, stockpile.id, stockpile.centerx, stockpile.centery, stockpile.z, #items)) if #items > 0 then stacks_add_items(stacks, items) else - log(' skipping stockpile: no items\n') + log(3, ' skipping stockpile: no items\n') end end end local function preview_stacks(stacks) - -- calculate the stacks sizes and store in after_stack_size + -- calculate the stacks sizes and store in after_item_stack_size -- the max stack size for each comp item is determined as the maximum stack size for it's type - log('\nPreview phase\n') - for _, stacks_type in pairs(stacks) do - for comp_key, comp_item in pairs(stacks_type.comp_items) do - -- sort the items. - table.sort(comp_item.sorted_items) - - if stacks_type.max_stack_size > comp_item.max_stack_size then - comp_item.max_stack_size = stacks_type.max_stack_size + log(3, '\nPreview phase\n') + + for _, stack_type in pairs(stacks.stack_types) do + log(3, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + + for comp_key, comp_item in pairs(stack_type.comp_items) do + log(3, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d before:%5d after:%5d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + + -- sort the items, according to contained first, then stack size second + if stack_type.max_size > comp_item.max_size then + comp_item.max_size = stack_type.max_size end - -- how many stacks are needed ? - local max_stacks_needed = math.floor(comp_item.item_qty / comp_item.max_stack_size) + -- how many stacks are needed? + local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) -- how many items are left over after the max stacks are allocated? - local stack_remainder = comp_item.item_qty - max_stacks_needed * comp_item.max_stack_size - - -- update the after stack sizes. use the sorted items list to get the items. - for _, s_item in ipairs(comp_item.sorted_items) do - local item_id = s_item - if s_item < 0 then item_id = s_item * -1 end - local item = comp_item.items[item_id] - if max_stacks_needed > 0 then - max_stacks_needed = max_stacks_needed - 1 - comp_item.after_stack_size[item.id] = comp_item.max_stack_size + local stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + + if stack_remainder > 0 then + comp_item.after_stacks = stacks_needed + 1 + else + comp_item.after_stacks = stacks_needed + end + + stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks + stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks + + -- Update the after stack sizes. + for _, item in sorted_items(comp_item.items) do + if stacks_needed > 0 then + stacks_needed = stacks_needed - 1 + item.after_size = comp_item.max_size elseif stack_remainder > 0 then - comp_item.after_stack_size[item.id] = stack_remainder + item.after_size = stack_remainder stack_remainder = 0 - elseif stack_remainder == 0 then - comp_item.after_stack_size[item.id] = stack_remainder - comp_item.after_stacks = comp_item.after_stacks - 1 - stacks_type.after_stacks = stacks_type.after_stacks - 1 + else + item.after_size = 0 + end + end + + -- Container loop; combine item stacks in containers. + local curr_cont = nil + local curr_size = 0 + + for item_id, item in sorted_items(comp_item.items) do + + -- non-zero quantity? + if item.after_size > 0 then + + -- in a container before merge? + if item.before_cont_id then + + local before_cont = stacks.containers[item.before_cont_id] + + -- first contained item or current container full? + if not curr_cont or curr_size >= MAX_CONT_ITEMS then + + curr_cont = before_cont + curr_size = curr_cont.before_size + stacks.after_cont_ids[item.before_cont_id] = item.before_cont_id + stack_type.after_cont_ids[item.before_cont_id] = item.before_cont_id + comp_item.after_cont_ids[item.before_cont_id] = item.before_cont_id + + -- enough room in current container + else + curr_size = curr_size + 1 + before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 + end + + curr_cont.after_size = curr_size + item.after_cont_id = curr_cont.container.id + + -- not in a container before merge, container exists, and has space + elseif curr_cont and curr_size < MAX_CONT_ITEMS then + + curr_size = curr_size + 1 + curr_cont.after_size = curr_size + item.after_cont_id = curr_cont.container.id + + -- not in a container, no container exists or no space in container + else + -- do nothing + end + + -- zero after size, reduce the number of stacks in the container + elseif item.before_cont_id then + local before_cont = stacks.containers[item.before_cont_id] + before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 end end + log(3, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d before:%5d after:%5d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) end + log(3, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end local function merge_stacks(stacks) - -- apply the stack size changes in the after_stack_size - -- if the after_stack_size is zero, then remove the item - log('Merge phase\n') - for _, stacks_type in pairs(stacks) do - for comp_key, comp_item in pairs(stacks_type.comp_items) do - for _, item in pairs(comp_item.items) do - if comp_item.after_stack_size[item.id] == 0 then - local remove_item = df.item.find(item.id) - dfhack.items.remove(remove_item) - elseif item.stack_size ~= comp_item.after_stack_size[item.id] then - item.stack_size = comp_item.after_stack_size[item.id] + -- apply the stack size changes in the after_item_stack_size + -- if the after_item_stack_size is zero, then remove the item + log(3, 'Merge phase\n') + for _, stack_type in pairs(stacks.stack_types) do + for comp_key, comp_item in pairs(stack_type.comp_items) do + + for item_id, item in pairs(comp_item.items) do + + -- no items left in stack? + if item.after_size == 0 then + log(3, (' removing item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + dfhack.items.remove(item.item) + + -- some items left in stack + elseif item.before_size ~= item.after_size then + log(3, (' updating item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + item.item.stack_size = item.after_size + end + + -- move to a container? + if item.after_cont_id then + if (item.before_cont_id or 0) ~= item.after_cont_id then + log(3, (' moving item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + dfhack.items.moveToContainer(item.item, stacks.containers[item.after_cont_id].container) + end end end end @@ -300,6 +489,7 @@ end local function get_stockpile_all() -- attempt to get all the stockpiles for the fort, or exit with error -- return the stockpiles as a table + log(3, 'get_stockpile_all\n') local stockpiles = {} for _, building in pairs(df.global.world.buildings.all) do if building:getType() == df.building_type.Stockpile then @@ -313,19 +503,21 @@ end local function get_stockpile_here() -- attempt to get the stockpile located at the game cursor, or exit with error -- return the stockpile as a table + log(3, 'get_stockpile_here\n') local stockpiles = {} local pos = argparse.coords('here', 'here') local building = dfhack.buildings.findAtTile(pos) if not building or building:getType() ~= df.building_type.Stockpile then qerror('Stockpile not found at game cursor position.') end table.insert(stockpiles, building) local items = dfhack.buildings.getStockpileContents(building) - dfhack.print(('Stockpile(here): %s <%d> #items:%d\n'):format(building.name, building.id, #items)) + log(0, ('Stockpile(here): %s <%d> #items:%d\n'):format(building.name, building.id, #items)) return stockpiles end local function parse_types_opts(arg) -- check the types specified on the command line, or exit with error -- return the selected types as a table + log(3, 'parse_types_opts\n') local types = {} local div = '' local types_output = '' @@ -347,24 +539,25 @@ local function parse_types_opts(arg) for k3, v3 in pairs(v2) do types[k2][k3]=v3 end - types_output = types_output .. div .. types[k2].type_name + types_output = types_output .. div .. df.item_type[types[k2].type_id] div=', ' else qerror(('Expected: only one value for %s'):format(t)) end end end - dfhack.print(types_output .. '\n') + log(0, types_output .. '\n') return types end local function parse_commandline(opts, args) -- check the command line/exit on error, and set the defaults + log(3, 'parse_commandline\n') local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, {'d', 'dry-run', handler=function(optarg) opts.dry_run = true end}, - {'v', 'verbose', handler=function(optarg) opts.verbose = true end}, + {'v', 'verbose', hasArg=true, handler=function(optarg) opts.verbose = math.tointeger(optarg) or 0 end}, }) -- if stockpile option is not specificed, then default to all @@ -398,7 +591,7 @@ local function main() return end - local stacks = CList:new() + local stacks = stacks_new() populate_stacks(stacks, opts.all or opts.here, opts.types) @@ -408,9 +601,8 @@ local function main() merge_stacks(stacks) end - print_stacks_details(stacks) - print_stacks_summary(stacks) - + print_stacks(stacks) + end if not dfhack_flags.module then From 8c7e7c32e3ba200bfe0daf3451fea8a91a85e4ff Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 11 Apr 2023 21:52:27 +1000 Subject: [PATCH 109/732] Doc update --- changelog.txt | 1 + docs/combine.rst | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 136847112f..a646daa36b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ that repo. ## Misc Improvements - `gui/design`: ``gui/dig`` renamed to ``gui/design`` - `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. +- `combine`: Now supports powders and seeds, and combines into containers. ## Removed diff --git a/docs/combine.rst b/docs/combine.rst index be5724edf6..bb53b14444 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -55,5 +55,9 @@ Options ``plant``: PLANT and PLANT_GROWTH -``-v``, ``--verbose`` - Print verbose output. + ``powders``: POWDERS_MISC + + ``seeds``: SEEDS + +``-v``, ``--verbose [0-3]`` + Print verbose output, level from 0 to 3. From 41ec989ec2312c8ff1ddfd896750f06c8856f49c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 11 Apr 2023 17:41:23 -0700 Subject: [PATCH 110/732] parse our new interim release candidate naming scheme --- gui/prerelease-warning.lua | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/gui/prerelease-warning.lua b/gui/prerelease-warning.lua index f51b06e6f6..c5bae7dfa6 100644 --- a/gui/prerelease-warning.lua +++ b/gui/prerelease-warning.lua @@ -1,6 +1,3 @@ --- Shows the warning about missing configuration file. - -local gui = require 'gui' local dlg = require 'gui.dialogs' local json = require 'json' local utils = require 'utils' @@ -19,13 +16,12 @@ if dfhack.internal.getAddress('gametype') and df.global.gametype == df.game_type return end -local state = dfhack.getDFHackRelease():lower():match('[a-z]+') +local state = dfhack.getDFHackRelease():lower():match('([a-z]+)%d*$') if not utils.invert{'alpha', 'beta', 'rc', 'r'}[state] then dfhack.printerr('warning: unknown release state: ' .. state) state = 'unknown' end - message = ({ alpha = { 'Warning', @@ -55,13 +51,13 @@ message = ({ COLOR_LIGHTRED, 'This release is flagged as a prerelease but named as a', NEWLINE, 'stable release.', NEWLINE, - {pen=COLOR_LIGHTMAGENTA, text='Please report this to the DFHack team or a pack maintainer.'} + {pen=COLOR_LIGHTMAGENTA, text='Please report this to the DFHack team.'} }, unknown = { 'Error', COLOR_LIGHTMAGENTA, 'Unknown prerelease DFHack build. This should never happen!', NEWLINE, - 'Please report this to the DFHack team or a pack maintainer.' + 'Please report this to the DFHack team.' } })[state] From ec1a69788fd6329008672523b622fd8b390fea73 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Apr 2023 01:37:00 -0700 Subject: [PATCH 111/732] clean changelog and bump to 50.07-r1 --- changelog.txt | 52 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/changelog.txt b/changelog.txt index ad7b280b62..94b9ad2731 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,16 +16,26 @@ that repo. ## New Scripts ## Fixes -- `caravan`: fix trade good list sometimes disappearing when you collapse a bin -- `gui/gm-editor`: no longer nudges last open window when opening a new one + +## Misc Improvements + +## Removed + +# 50.07-r1 + +## New Scripts + +## Fixes +-@ `caravan`: fix trade good list sometimes disappearing when you collapse a bin +-@ `gui/gm-editor`: no longer nudges last open window when opening a new one - `warn-starving`: no longer warns for dead units -- `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode +-@ `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` - `gui/gm-editor`: they key column now auto-fits to the widest key - `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) -- `gui/control-panel`: add `faststart` to the system services +-@ `gui/control-panel`: add `faststart` to the system services ## Removed @@ -37,28 +47,28 @@ that repo. - `gui/civ-alert`: configure and trigger civilian alerts ## Fixes -- `caravan`: item list length now correct when expanding and collapsing containers -- `prioritize`: fixed all watched job type names showing as ``nil`` after a game load -- `suspendmanager`: does not suspend non-blocking jobs such as floor bars or bridges anymore -- `suspendmanager`: fix occasional bad identification of buildingplan jobs +-@ `caravan`: item list length now correct when expanding and collapsing containers +-@ `prioritize`: fixed all watched job type names showing as ``nil`` after a game load +-@ `suspendmanager`: does not suspend non-blocking jobs such as floor bars or bridges anymore +-@ `suspendmanager`: fix occasional bad identification of buildingplan jobs - `warn-starving`: no longer warns for enemy and neutral units ## Misc Improvements - `gui/control-panel`: Now detects overlays from scripts named with capital letters - `gui/cp437-table`: now has larger key buttons and clickable backspace/submit/cancel buttons, making it fully usable on the Steam Deck and other systems that don't have an accessible keyboard -- `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. +-@ `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. - `exterminate`: add support for ``vaporize`` kill method for when you don't want to leave a corpse - `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor - `combine`: added ``--quiet`` option for no output when there are no changes - `stripcaged`: added ``--skip-forbidden`` option for greater control over which items are marked for dumping - `stripcaged`: items that are marked for dumping are now automatically unforbidden (unless ``--skip-forbidden`` is set) -- `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles -- `gui/control-panel`: added ``general-strike`` maintenance option for automatic fixing of (at least one cause of) the general strike bug +-@ `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles +-@ `gui/control-panel`: added ``general-strike`` maintenance option for automatic fixing of (at least one cause of) the general strike bug - `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button ## Removed - `autounsuspend`: replaced by `suspendmanager` -- `gui/dig`: renamed to `gui/design` +-@ `gui/dig`: renamed to `gui/design` # 50.07-beta1 @@ -68,7 +78,7 @@ that repo. - `suspend`: suspends building construction jobs ## Fixes -- `quicksave`: now reliably triggers an autosave, even if one has been performed recently +-@ `quicksave`: now reliably triggers an autosave, even if one has been performed recently - `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" ## Misc Improvements @@ -92,8 +102,8 @@ that repo. - `combine`: combines stacks of food and plant items. ## Fixes -- `troubleshoot-item`: fix printing of job details for chosen item -- `makeown`: fixes errors caused by using makeown on an invader +-@ `troubleshoot-item`: fix printing of job details for chosen item +-@ `makeown`: fixes errors caused by using makeown on an invader -@ `gui/blueprint`: correctly use setting presets passed on the commandline -@ `gui/quickfort`: correctly use settings presets passed on the commandline - `devel/query`: can now properly index vectors in the --table argument @@ -107,7 +117,7 @@ that repo. - `devel/visualize-structure`: now automatically inspects the contents of most pointer fields, rather than inspecting the pointers themselves - `devel/query`: will now search for jobs at the map coordinate highlighted, if no explicit job is highlighted and there is a map tile highlighted - `caravan`: add trade screen overlay that assists with seleting groups of items and collapsing groups in the UI -- `gui/gm-editor` will now inspect a selected building itself if the building has no current jobs +- `gui/gm-editor`: will now inspect a selected building itself if the building has no current jobs ## Removed - `combine-drinks`: replaced by `combine` @@ -116,8 +126,8 @@ that repo. # 50.07-alpha1 ## New Scripts -- `gui/dig`: digging designation tool for shapes and patterns -- `makeown`: adds the selected unit as a member of your fortress +- `gui/design`: digging and construction designation tool with shapes and patterns +- `makeown`: makes the selected unit a citizen of your fortress ## Fixes -@ `gui/unit-syndromes`: allow the window widgets to be interacted with @@ -129,7 +139,7 @@ that repo. - `gui/gm-editor`: now supports multiple independent data inspection windows - `gui/gm-editor`: now prints out contents of coordinate vars instead of just the type - `rejuvenate`: now takes an --age parameter to choose a desired age. -- `gui/dig` : Added 'Line' shape that also can draw curves, added draggable center handle +-@ `gui/dig` : Added 'Line' shape that also can draw curves, added draggable center handle # 50.05-alpha3.1 @@ -172,7 +182,7 @@ that repo. - `gui/quickfort`: don't close the window when applying a blueprint so players can apply the same blueprint multiple times more easily - `locate-ore`: now only searches revealed tiles by default - `modtools/spawn-liquid`: sets tile temperature to stable levels when spawning water or magma -- `prioritize`: pushing minecarts is now included in the default priortization list +-@ `prioritize`: pushing minecarts is now included in the default priortization list - `prioritize`: now automatically starts boosting the default list of job types when enabled - `unforbid`: avoids unforbidding unreachable and underwater items by default - `gui/create-item`: added whole corpse spawning alongside corpsepieces. (under "corpse") @@ -192,7 +202,7 @@ that repo. # 50.05-alpha1 ## New Scripts -- `gui/autochop`: configuration frontend for the `autochop` plugin. you can pin the window and leave it up on the screen somewhere for live monitoring of your logging industry. +- `gui/autochop`: configuration frontend and status monitor for the `autochop` plugin - `devel/tile-browser`: page through available textures and see their texture ids - `allneeds`: list all unmet needs sorted by how many dwarves suffer from them. From 960693cd463d9b8629024e956bbdb012b1c42030 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Thu, 13 Apr 2023 15:37:04 +1000 Subject: [PATCH 112/732] Issue: 3221 --- changelog.txt | 1 + deteriorate.lua | 68 +++++++++++++++++++++++++++++++++++--------- docs/deteriorate.rst | 13 ++++----- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/changelog.txt b/changelog.txt index ad7b280b62..263dcc1b38 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,7 @@ that repo. - `gui/gm-editor`: they key column now auto-fits to the widest key - `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) - `gui/control-panel`: add `faststart` to the system services +- `deteriorate`: check for residents corpses. Added option to exclude useable parts from deterioration. ## Removed diff --git a/deteriorate.lua b/deteriorate.lua index e05719ba36..ee9e16b4e8 100644 --- a/deteriorate.lua +++ b/deteriorate.lua @@ -3,6 +3,7 @@ local argparse = require('argparse') local utils = require('utils') +local item_stockpiles = {} local function get_clothes_vectors() return {df.global.world.items.other.GLOVES, @@ -35,11 +36,48 @@ local function is_valid_clothing(item) and item.wear > 0 end -local function is_valid_corpse(item) - return not item.flags.dead_dwarf +local function is_not_useable(opts, item) + local not_useable = + not(opts.useable and ( + not item.corpse_flags.unbutchered and ( + item.corpse_flags.bone or + item.corpse_flags.horn or + item.corpse_flags.leather or + item.corpse_flags.skull1 or + item.corpse_flags.skull2 or + item.corpse_flags.tooth) or ( + item.corpse_flags.hair_wool or + item.corpse_flags.pearl or + item.corpse_flags.plant or + item.corpse_flags.shell or + item.corpse_flags.silk or + item.corpse_flags.yarn) ) ) + return not_useable +end + +local function is_valid_corpse(opts, item) + -- check if the corpse is a resident of the fortress and is not useable + local unit = df.unit.find(item.unit_id) + if not unit then + return is_not_useable(opts, item) + end + local hf = df.historical_figure.find(unit.hist_figure_id) + if not hf then + return is_not_useable(opts, item) + end + for _,link in ipairs(hf.entity_links) do + if link.entity_id == df.global.plotinfo.group_id and df.histfig_entity_link_type[link:getType()] == 'MEMBER' then + return false + end + end + return is_not_useable(opts, item) +end + +local function is_valid_remains(opts, item) + return true end -local function is_valid_food(item) +local function is_valid_food(opts, item) return true end @@ -69,11 +107,11 @@ local function increment_food_wear(item) return increment_generic_wear(item, 24) end -local function deteriorate(get_item_vectors_fn, is_valid_fn, increment_wear_fn) +local function deteriorate(opts, get_item_vectors_fn, is_valid_fn, increment_wear_fn) local count = 0 for _,v in ipairs(get_item_vectors_fn()) do for _,item in ipairs(v) do - if is_valid_fn(item) and increment_wear_fn(item) + if is_valid_fn(opts, item) and increment_wear_fn(item) and not item.flags.garbage_collect then item.flags.garbage_collect = true item.flags.hidden = true @@ -88,20 +126,20 @@ local function always_worn() return true end -local function deteriorate_clothes(now) - return deteriorate(get_clothes_vectors, is_valid_clothing, +local function deteriorate_clothes(opts, now) + return deteriorate(opts, get_clothes_vectors, is_valid_clothing, now and always_worn or increment_clothes_wear) end -local function deteriorate_corpses(now) - return deteriorate(get_corpse_vectors, is_valid_corpse, +local function deteriorate_corpses(opts, now) + return deteriorate(opts, get_corpse_vectors, is_valid_corpse, now and always_worn or increment_corpse_wear) - + deteriorate(get_remains_vectors, is_valid_corpse, + + deteriorate(opts, get_remains_vectors, is_valid_remains, now and always_worn or increment_remains_wear) end -local function deteriorate_food(now) - return deteriorate(get_food_vectors, is_valid_food, +local function deteriorate_food(opts, now) + return deteriorate(opts, get_food_vectors, is_valid_food, now and always_worn or increment_food_wear) end @@ -141,7 +179,7 @@ local function make_timeout_cb(item_type, opts) return end if not first_time then - local count = type_fns[item_type]() + local count = type_fns[item_type](opts) if count > 0 then print(('Deteriorated %d %s'):format(count, item_type)) end @@ -186,7 +224,7 @@ end local function now(opts) for _,v in ipairs(opts.types) do - local count = type_fns[v](true) + local count = type_fns[v](opts, true) if not opts.quiet then print(('Deteriorated %d %s'):format(count, v)) end @@ -245,6 +283,7 @@ local opts = { mode = 'days', quiet = false, types = {}, + useable = false, help = false, } @@ -253,6 +292,7 @@ local nonoptions = argparse.processArgsGetopt({...}, { handler=function(optarg) opts.time,opts.mode = parse_freq(optarg) end}, {'h', 'help', handler=function() opts.help = true end}, {'q', 'quiet', handler=function() opts.quiet = true end}, + {'u', 'useable', handler=function() opts.useable = true end}, {'t', 'types', hasArg=true, handler=function(optarg) opts.types = parse_types(optarg) end}}) diff --git a/docs/deteriorate.rst b/docs/deteriorate.rst index 1e0b49f944..ded49560a2 100644 --- a/docs/deteriorate.rst +++ b/docs/deteriorate.rst @@ -18,16 +18,16 @@ dwarves feel, your FPS does not like it! Usage ----- -``deteriorate start --types [--freq ] [--quiet]`` - Starts deteriorating the specified item types while you play. +``deteriorate start --types [--freq ] [--quiet] [--useable]`` + Starts deteriorating the specified item types while you play, keeping useable corpse pieces. ``deteriorate stop --types `` Stops deteriorating the specified item types. ``deteriorate status`` Shows the item types that are currently being monitored and their deterioration frequencies. -``deteriorate now --types [--quiet]`` +``deteriorate now --types [--quiet] [--useable]`` Causes all items (of the specified item types) to rot away within a few - ticks. + ticks, keeping useable corpse pieces. You can have different types of items rotting away at different rates by running ``deteriorate start`` multiple times with different options. @@ -68,8 +68,7 @@ Types :clothes: All clothing pieces that have an armor rating of 0 and are lying on the ground. -:corpses: All non-dwarf corpses and body parts. This includes potentially - useful remains such as hair, wool, hooves, bones, and skulls. Use - them before you lose them! +:corpses: All resident corpses and body parts. To keep useable remains such as + hair, wool, hooves, bones, and skulls, specify --useable. :food: All food and plants, regardless of whether they are in barrels or stockpiles. Seeds are left untouched. From c9963ae44028983c13d097edd5f5d5c363137e29 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Thu, 13 Apr 2023 15:47:05 +1000 Subject: [PATCH 113/732] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 94b9ad2731..b495629437 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,7 @@ that repo. -@ `gui/gm-editor`: no longer nudges last open window when opening a new one - `warn-starving`: no longer warns for dead units -@ `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode +- `deteriorate`: check for residents corpses. Added option to exclude useable parts from deterioration. ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` From af6c5fab0be1da5fb0312a4391fa7023150477e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Apr 2023 05:49:13 +0000 Subject: [PATCH 114/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deteriorate.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deteriorate.lua b/deteriorate.lua index ee9e16b4e8..b04e99adf5 100644 --- a/deteriorate.lua +++ b/deteriorate.lua @@ -54,16 +54,16 @@ local function is_not_useable(opts, item) item.corpse_flags.yarn) ) ) return not_useable end - + local function is_valid_corpse(opts, item) -- check if the corpse is a resident of the fortress and is not useable local unit = df.unit.find(item.unit_id) if not unit then - return is_not_useable(opts, item) + return is_not_useable(opts, item) end local hf = df.historical_figure.find(unit.hist_figure_id) if not hf then - return is_not_useable(opts, item) + return is_not_useable(opts, item) end for _,link in ipairs(hf.entity_links) do if link.entity_id == df.global.plotinfo.group_id and df.histfig_entity_link_type[link:getType()] == 'MEMBER' then From 419f070707c1e9254b6892926447038c362ef01e Mon Sep 17 00:00:00 2001 From: silverflyone Date: Thu, 13 Apr 2023 18:21:01 +1000 Subject: [PATCH 115/732] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit ec1a69788fd6329008672523b622fd8b390fea73 Author: Myk Taylor Date: Wed Apr 12 01:37:00 2023 -0700 clean changelog and bump to 50.07-r1 commit 973b8ef191cc92cff62d96aa591c7631818d19b6 Merge: af3cff5e d25ef792 Author: Myk Date: Mon Apr 10 04:31:57 2023 -0700 Merge pull request #674 from myk002/myk_configurable [gui/control-panel] don't offer gui/automelt when not in fort mode commit af3cff5e85371130500b49592d55c87312474eea Merge: 94c8f714 855d1cbf Author: Myk Date: Mon Apr 10 04:31:32 2023 -0700 Merge pull request #675 from myk002/myk_no_orders [quickfort] output a better error message when orders are attempted commit 855d1cbf6432c91c0b5e6f6c2d30ce04d2765abf Author: Myk Taylor Date: Mon Apr 10 03:57:44 2023 -0700 output a better error message when orders are attempted commit d25ef792884b84980b8052fb707b87aa0a79b276 Author: Myk Taylor Date: Mon Apr 10 03:39:59 2023 -0700 don't offer gui/automelt when not in fort mode commit 94c8f7143fd51ff532e3884801394a6a8d0d162e Author: Myk Taylor Date: Mon Apr 10 03:16:55 2023 -0700 display a message if there are no burrows and ensure nameless burrows are named the same as in the Burrows screen commit 99aa1c9e9f79b13843d301fe1270a1702edff8d0 Merge: d8557ba6 a69b4658 Author: Myk Date: Mon Apr 10 00:19:39 2023 -0700 Merge pull request #673 from myk002/myk_nodejs update python build action to non-deprecated version commit a69b46580bc4f33270e2f55ca5c214b46167d017 Author: Myk Taylor Date: Mon Apr 10 00:15:21 2023 -0700 update python build action to non-deprecated version commit d8557ba6f1777c1a6d1b92a364e4ee22ec829873 Author: Myk Taylor Date: Fri Apr 7 11:05:52 2023 -0700 update prioritize docs commit a4e5d4514ec33462ee0c0bd25e02d4a9c3e2ce01 Merge: e6216cc2 c270180d Author: Myk Date: Thu Apr 6 23:25:18 2023 -0700 Merge pull request #672 from myk002/myk_control add faststart to system services and persist setting commit c270180dc7d22dcd90c043839f3468b66d0256ce Author: Myk Taylor Date: Thu Apr 6 23:17:46 2023 -0700 add faststart to system services and persist setting commit e6216cc28e4315df5fb128411d0ca57fe78ccb2b Merge: d0f1ee12 a2884311 Author: Myk Date: Wed Apr 5 18:14:05 2023 -0700 Merge pull request #666 from myk002/myk_warn_dead [warn-starving] don't warn about uncomfortable dead units commit d0f1ee129657002743d6b5c68acc1995c7519a3a Merge: 2eda84a6 a35b00b4 Author: Myk Date: Wed Apr 5 18:13:56 2023 -0700 Merge pull request #667 from myk002/myk_prioritize revise default prioritized job list commit 2eda84a64aadc7097d821f0c1cd5156eefa38ebe Merge: 53f0aedf a56413fa Author: Myk Date: Wed Apr 5 18:13:46 2023 -0700 Merge pull request #668 from myk002/myk_caravan [caravan] fix list height and position issues commit 53f0aedf9f7df33a4f79246ba46de02794619d09 Merge: d2dad272 067dd91e Author: Myk Date: Mon Apr 3 21:57:45 2023 -0700 Merge pull request #669 from DFHack/pre-commit-ci-update-config [pre-commit.ci] pre-commit autoupdate commit 067dd91e7561b858b6cc7ebcd42af2783e6035a5 Author: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue Apr 4 02:55:07 2023 +0000 [pre-commit.ci] pre-commit autoupdate updates: - [github.com/python-jsonschema/check-jsonschema: 0.21.0 → 0.22.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.21.0...0.22.0) - [github.com/Lucas-C/pre-commit-hooks: v1.4.2 → v1.5.1](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.4.2...v1.5.1) commit a56413fab0d6df84c9350043b81321ffeae21abf Author: Myk Taylor Date: Mon Apr 3 19:20:42 2023 -0700 set the scroll position properly when the height shrinks commit 3482fad621e640847c916957bfd49fae3c5fe992 Author: Myk Taylor Date: Mon Apr 3 18:34:13 2023 -0700 fix off-by-one error when detecting end of bin commit a35b00b4eb84744635cf5e9e3c782650bf99cc7b Author: Myk Taylor Date: Mon Apr 3 17:38:16 2023 -0700 revise default prioritized job list commit a28843113b45cad1947430f9926511a2103e521e Author: Myk Taylor Date: Mon Apr 3 12:07:15 2023 -0700 don't warn about uncomfortable dead units commit d2dad272e4b24c043ca62f843511c763fb1f67b5 Merge: 298d9c00 aa9d7c2d Author: Myk Date: Sat Apr 1 00:56:12 2023 -0700 Merge pull request #662 from Cubittus/editor-tweaks gm-editor: Small usability tweaks commit aa9d7c2d9b015a681662aa4b6674b2c678df5176 Author: Cubittus Date: Sat Apr 1 08:54:10 2023 +0100 gm-editor: Move changelog entries to future version commit d5055250b106aedbff9e4fd8b15d564f0a7b3070 Merge: bee94515 298d9c00 Author: Cubittus Date: Sat Apr 1 08:51:58 2023 +0100 Merge branch 'DFHack:master' into editor-tweaks commit bee945157327970be7ce6dada0920bffc7698ddd Author: Cubittus Date: Sat Apr 1 08:42:37 2023 +0100 gm-editor: changelog for tweaks commit 5f9adb9fceeff99841f50c31823a4756afc367a0 Author: Cubittus Date: Sat Apr 1 08:34:38 2023 +0100 gm-editor: Use copyall to clone config Co-authored-by: Myk commit 298d9c00c7df81fc4ca363366dbaebf7f47be489 Author: Myk Taylor Date: Fri Mar 31 12:30:57 2023 -0700 add missing changelog entry for gui/civ-alert commit 0b55c5418970917310bfd437499adfac0be80e0e Author: Myk Taylor Date: Fri Mar 31 04:56:17 2023 -0700 update changelog for 50.07-beta2 commit 0b5fe3458ab20241121ca911888d261671d75df2 Merge: cec0b19a 2cb58469 Author: Cubittus Date: Fri Mar 31 12:40:26 2023 +0100 Merge branch 'DFHack:master' into editor-tweaks commit 2cb58469e0696111d8a29a2d3ce0ca585d29b3f5 Merge: f6c4bc97 0e238ed3 Author: Myk Date: Thu Mar 30 15:42:55 2023 -0700 Merge pull request #663 from ab9rf/fix-fixgs Update general-strike.lua commit 0e238ed3637124e1aaa67669ab47d96f758f4009 Author: Kelly Kinkade Date: Thu Mar 30 17:27:21 2023 -0500 Update general-strike.lua only update seeds in farms commit cec0b19a390f14b627c19118dba7f6c8e963a536 Author: Cubittus Date: Thu Mar 30 15:11:04 2023 +0100 gm-editor: auto-fit-width the key column Sets the width of the key column to fit the longest key commit 04dcb30b3357a4ea4260ae3468e9c5f0789538ad Author: Cubittus Date: Thu Mar 30 14:38:52 2023 +0100 gm-editor: Enable jump to material ref-target When a ref-target jump is made on a field named 'mat_type' then look for a 'mat_index' field to go with it and look up the matinfo. commit 7d94c0756867b7fcae60eb31496eab4a059e685c Author: Cubittus Date: Thu Mar 30 14:30:20 2023 +0100 gm-editor: Don't clobber frame pos of previous window Make a copy of the frame stored in config.data so that it's not a reference to the frame of the last opened window, otherwise when updating the l/r/t/b during GMEditorUi:init the previous windows frame is updated too. commit f6c4bc97ec50646b4dc813b0130025e0e0012382 Merge: 247ccecb 753b9f9c Author: Myk Date: Wed Mar 29 20:13:45 2023 -0700 Merge pull request #661 from myk002/myk_warn_starving [warn-starving] no reports for non-fort-controlled units commit 753b9f9c9ea449a098055af748763662907c851a Author: Myk Taylor Date: Wed Mar 29 08:23:59 2023 -0700 no reports for non-fort-controlled units this also skips reports for residents, which is debatable commit 247ccecb12030d170496234b8f27268cc82a2b30 Author: Myk Taylor Date: Wed Mar 29 00:28:07 2023 -0700 sync tags from spreadsheet commit 71c58248e915b0ccf8ad090fd9a27bc5b9641883 Author: Myk Taylor Date: Tue Mar 28 22:53:19 2023 -0700 handle case where there is no persisted data commit 1abfa6c6fadd63eca99b77824405490525ee2216 Merge: cfce783a 0e86dfaa Author: Myk Date: Tue Mar 28 06:47:00 2023 -0700 Merge pull request #660 from myk002/myk_prioritize [prioritize] work around json converting numbers to strings commit cfce783a65263dbe9f54d6eb0c83ccb21d6a97a9 Merge: 27089b81 f2f74dc7 Author: Myk Date: Tue Mar 28 06:46:35 2023 -0700 Merge pull request #656 from myk002/myk_vaporize [exterminate] add vaporize kill method commit 0e86dfaa630da93b52208ab80df5168309f3de88 Author: Myk Taylor Date: Mon Mar 27 17:09:07 2023 -0700 work around json converting numbers to strings why, json, why? commit 27089b81be6f9de9486118a97a15be4dde09b1b3 Author: Myk Taylor Date: Mon Mar 27 16:11:54 2023 -0700 edit pass for gui/confirm docs commit 96d653b1325d98836745c4e083f33609d28cfe62 Merge: 76e12c12 7c1fa391 Author: Myk Date: Mon Mar 27 13:53:13 2023 -0700 Merge pull request #657 from myk002/myk_stripcaged_forbidden [stripcaged] unforbid dumped items and allow forbidden items to be ignored commit 76e12c1204dd7a44f3b7d1c3b9fc7501d7519faa Merge: ba4d9e4e 9ca0af25 Author: Myk Date: Mon Mar 27 13:53:00 2023 -0700 Merge pull request #659 from myk002/myk_caravan [caravan] fix logic for expanding and collapsing commit 9ca0af25fd9fa39dd7242b4cc8fb9a6138352d7d Author: Myk Taylor Date: Mon Mar 27 13:07:02 2023 -0700 refresh docs commit 03af898a38e770a2077a408441e420f18c6896d8 Author: Myk Taylor Date: Mon Mar 27 12:51:26 2023 -0700 fix (and simplify) logic for expanding and collapsing commit ba4d9e4ee1ba9320e82bd8972dafbbf50be5d99f Author: Myk Taylor Date: Mon Mar 27 10:38:56 2023 -0700 make button behave more like other buttons commit 8c0ef6695633f499eca60f655e04b7c5e20510a7 Author: Myk Taylor Date: Mon Mar 27 07:48:42 2023 -0700 slightly less garish configure button commit 7c1fa391112c38deb42a3650b08217683294e95e Author: Myk Taylor Date: Mon Mar 27 03:09:22 2023 -0700 unforbid dumped items and allow forbidden items to be ignored commit f2f74dc77a47400c1c1396ecc4d5422dade0569f Author: Myk Taylor Date: Mon Mar 27 01:31:19 2023 -0700 add vaporize kill method for exterminate commit bb3dfc0d3e2bdebc4ba6fd814371662457b18917 Merge: 2568b79c 6e7727dc Author: Myk Date: Sun Mar 26 21:41:39 2023 -0700 Merge pull request #655 from myk002/myk_civ_alert add gui/civ-alert commit 6e7727dcf14c3bf29f78c6404922d219323a31fe Author: Myk Taylor Date: Sun Mar 26 21:38:47 2023 -0700 give configure text a highlighted background commit 2b607144c014351a7b7a8b3368da3edab8b1434a Author: Myk Taylor Date: Sun Mar 26 10:25:55 2023 -0700 revise overlay position and layout Thanks for the feedback, Taxiservice! commit f9e9a4d9094f7ff8f7ecf51459db04a03b2712a9 Author: Myk Taylor Date: Sun Mar 26 03:19:00 2023 -0700 add docs for gui/civ-alert commit 37f8fb777e12238d62986a3d513f4c65a71188a7 Author: Myk Taylor Date: Sun Mar 26 02:30:17 2023 -0700 add gui/civ-alert commit 2568b79c3a74128b649bab8fdd20ce67cd88790f Merge: f637dc4b 3a259a33 Author: Myk Date: Sat Mar 25 15:27:27 2023 -0700 Merge pull request #654 from myk002/myk_keyboard overhaul onscreen kbd to be usable on steam deck commit 3a259a3322fceba409ab0ff8c44ea87278c52c63 Author: Myk Taylor Date: Sat Mar 25 15:21:21 2023 -0700 overhaul onscreen kbd to be usable on steam deck commit f637dc4bf18a7ca4968f45e79790893c6d22c3f5 Merge: db24eca0 ff32c728 Author: Myk Date: Sat Mar 25 12:31:10 2023 -0700 Merge pull request #650 from myk002/myk_byebyeautounsuspend remove autounsuspend commit ff32c72896f0ba51b79936ee8729a4b13eafe340 Author: Myk Taylor Date: Fri Mar 24 23:22:58 2023 -0700 remove autounsuspend commit db24eca06ec934a643862a794c4acb45b0c868bc Merge: ba946e4c 9b857167 Author: Myk Date: Sat Mar 25 12:29:30 2023 -0700 Merge pull request #653 from plule/suspendmanager-fix-get-bld Fix suspendmanager building identification sometimes being wrong commit ba946e4ccdd1ff4a81b7e1f11b1d373e5986f761 Merge: 6e7e2951 0f5fc78e Author: Myk Date: Sat Mar 25 12:28:48 2023 -0700 Merge pull request #652 from myk002/myk_general_strike add fix/general-strike commit 6e7e29513c760a185cd09384477edc5f01d928ff Merge: 8f0fa375 797880be Author: Myk Date: Sat Mar 25 12:27:29 2023 -0700 Merge pull request #649 from myk002/myk_unavailable rename "untested" tag to "unavailable" commit 8f0fa3752df963d29badc6a5f25315b857804ca1 Merge: 0b37dcf9 17fb5775 Author: Myk Date: Sat Mar 25 10:27:34 2023 -0700 Merge pull request #651 from myk002/myk_keyboard [gui/cp437-table] widgetify the onscreen keyboard and add backspace button commit 9b857167a8070cd00309d8d15f0403d81bd27c57 Author: plule <630159+plule@users.noreply.github.com> Date: Sat Mar 25 16:56:49 2023 +0100 Fix suspendmanager building identification sometimes being wrong commit 0f5fc78e38c3dd782bb4bd2ad5c2949c1d0dd023 Author: Myk Taylor Date: Sat Mar 25 01:59:29 2023 -0700 only increment counter if seeds are fixed commit bc721bce8b1344574ee6f477399997da23f70380 Author: Myk Taylor Date: Sat Mar 25 01:34:32 2023 -0700 update changelog commit cd588eec2b811d8a657c637d2122ee5bd1ea3c0e Author: Myk Taylor Date: Sat Mar 25 01:33:24 2023 -0700 update changelog commit 8b2e2c007f71b575e8d7953995f0301e0ca4fe00 Author: Myk Taylor Date: Sat Mar 25 01:31:44 2023 -0700 add fix/general-strike commit 17fb57754f65e7a8b054d5246795a68b0d3e770e Author: Myk Taylor Date: Sat Mar 25 00:53:04 2023 -0700 change background to red for better visibility update docs commit 85c6b40e35b43604a5f75aa1f62bde52c8be35bd Author: Myk Taylor Date: Sat Mar 25 00:43:25 2023 -0700 widgetify the onscreen keyboard and add backspace button commit 797880be49045c4c61281f0e0b45b3c7b71f9909 Author: Myk Taylor Date: Fri Mar 24 22:31:46 2023 -0700 untested -> unavailable commit 0b37dcf996bf4b64423d2acb3e3ca224ad76c6bc Merge: 685d041b e4561283 Author: Myk Date: Thu Mar 23 15:50:34 2023 -0700 Merge pull request #648 from plule/suspendmanager-blocking-fix Suspendmanager: Fix suspending floor grates, bridges and others commit e456128382fc7803d17839503ab00bea59762be8 Author: plule <630159+plule@users.noreply.github.com> Date: Thu Mar 23 20:27:06 2023 +0100 changelog commit 43a5a4712da762017754f29bf98b198bed0dd19e Author: plule <630159+plule@users.noreply.github.com> Date: Thu Mar 23 20:25:11 2023 +0100 Suspendmanager: Don't suspend non blocking jobs commit 685d041b23cdce453cb283bc27fdd2d3fd0bbd2b Merge: 11c33384 12a4ec55 Author: Myk Date: Thu Mar 23 06:43:36 2023 -0700 Merge pull request #647 from johncosker/dig-layout-update-fix gui/design: Call updateLayout less, improve performance commit 12a4ec5567effd34fe6a8d0f7ee93943f0b4094b Author: John Cosker Date: Thu Mar 23 09:32:09 2023 -0400 call updateLayout less, improve performance commit 11c33384db25a5ea4880ee573d03b3b5cd94c680 Merge: 9214c0fe 4874d8c3 Author: Myk Date: Thu Mar 23 02:19:26 2023 -0700 Merge pull request #645 from myk002/myk_controlpanel support overlays from scripts with capital letters commit 9214c0fe60ac6cb313bc01585cbd836de3afa512 Merge: 7f388c0c f2580049 Author: Myk Date: Thu Mar 23 02:18:53 2023 -0700 Merge pull request #646 from myk002/myk_repeaters support quiet combine and add "combine all" maintenance script commit f258004924f77931ef9defaf28653cd910f32be2 Author: Myk Taylor Date: Thu Mar 23 01:32:53 2023 -0700 support quiet combine and add maintenace script commit 4874d8c331b7d19d9af1fdf749d5b9d4b73fed8a Author: Myk Taylor Date: Wed Mar 22 18:33:00 2023 -0700 support overlays from scripts with capital letters commit 7f388c0c1bf9ed5fa3f7abf1153da54921a93e15 Merge: f3a8d2df dc439c58 Author: Myk Date: Tue Mar 21 21:52:05 2023 -0700 Merge pull request #625 from silverflyone/seed-watch-gui [gui/seedwatch] initial version commit dc439c58ee9b5d9f4ee78e30f6d2888cf2b25bfb Author: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed Mar 22 04:49:46 2023 +0000 [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci commit 9890979d08a11a144504d53185910988f769daf3 Author: Myk Date: Tue Mar 21 21:49:03 2023 -0700 Update gui/seedwatch.lua remove leftover (seeming) code commit 5598dacf5d7e2463207e5222355cc6dbdb8cf172 Merge: ad3cd59b f3a8d2df Author: Myk Date: Tue Mar 21 21:42:35 2023 -0700 Merge branch 'master' into seed-watch-gui commit ad3cd59b795e00900ec1d7048defdc93da461cce Author: silverflyone Date: Tue Mar 21 14:33:19 2023 +1100 Latest change log commit b898c308f5032a7061cda24baa1a8e9e918782d2 Author: silverflyone Date: Tue Mar 21 13:00:53 2023 +1100 Update to feedback. commit 45642ea017e41b2db881bc98d9577c060241fcb4 Author: silverflyone Date: Tue Feb 21 22:24:16 2023 +1100 Update seedwatch.lua commit 0fb38c2290fd89750812388ffc52e0b52a1e2db1 Merge: 6da30186 22ff7fd5 Author: silverflyone Date: Tue Feb 21 20:51:13 2023 +1100 Merge branch 'seed-watch-gui' of https://github.com/silverflyone/dfhack-scripts into seed-watch-gui commit 6da30186ed6762d2f3bd96669f8ce6d89c639d04 Author: silverflyone Date: Tue Feb 21 20:51:07 2023 +1100 Update seedwatch.lua Use filtered list instead of list, in case of lots of seeds. Also, do not refresh data if the seed settings modal has focus, and if the focus is not on the all or search edit fields. commit 22ff7fd57ba0a8060627489bd0e30764cde41895 Author: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue Feb 21 04:27:50 2023 +0000 [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci commit 9661c0de83a9547a4d7225fbf0d5186523fd2b1e Author: silverflyone Date: Tue Feb 21 15:24:15 2023 +1100 Create seedwatch.rst commit 6a344d8ea09d76348b92c2bb9e1136889d0b32b6 Author: silverflyone Date: Tue Feb 21 15:23:57 2023 +1100 Update changelog.txt commit 51c1c09377120dc27a037a16c61fb9dbfbe3b0fd Author: silverflyone Date: Sun Feb 19 20:39:02 2023 +1100 initial version of seedwatch gui initial version of seedwatch gui --- .github/workflows/build.yml | 4 +- .pre-commit-config.yaml | 4 +- autounsuspend.lua | 66 --- caravan.lua | 279 +++++----- changelog.txt | 70 ++- combine.lua | 486 ++++++------------ docs/adaptation.rst | 2 +- docs/add-recipe.rst | 2 +- docs/add-thought.rst | 2 +- docs/adv-fix-sleepers.rst | 2 +- docs/adv-max-skills.rst | 2 +- docs/adv-rumors.rst | 2 +- docs/assign-minecarts.rst | 2 +- docs/assign-profile.rst | 2 +- docs/autolabor-artisans.rst | 2 +- docs/autounsuspend.rst | 18 - docs/binpatch.rst | 2 +- docs/bodyswap.rst | 2 +- docs/break-dance.rst | 2 +- docs/build-now.rst | 2 +- docs/burial.rst | 2 +- docs/cannibalism.rst | 2 +- docs/caravan.rst | 71 +-- docs/color-schemes.rst | 2 +- docs/combat-harden.rst | 2 +- docs/combine.rst | 13 +- docs/deteriorate.rst | 2 +- docs/devel/block-borders.rst | 2 +- docs/devel/cmptiles.rst | 2 +- docs/devel/dump-offsets.rst | 2 +- docs/devel/export-dt-ini.rst | 2 +- docs/devel/find-offsets.rst | 2 +- docs/devel/find-primitive.rst | 2 +- docs/devel/find-twbt.rst | 2 +- docs/devel/inject-raws.rst | 2 +- docs/devel/kill-hf.rst | 2 +- docs/devel/light.rst | 2 +- docs/devel/list-filters.rst | 2 +- docs/devel/lsmem.rst | 2 +- docs/devel/lua-example.rst | 2 +- docs/devel/luacov.rst | 2 +- docs/devel/nuke-items.rst | 2 +- docs/devel/prepare-save.rst | 2 +- docs/devel/print-args.rst | 2 +- docs/devel/print-args2.rst | 2 +- docs/devel/print-event.rst | 2 +- docs/devel/save-version.rst | 2 +- docs/devel/sc.rst | 2 +- docs/devel/test-perlin.rst | 2 +- docs/devel/unit-path.rst | 2 +- docs/devel/watch-minecarts.rst | 2 +- docs/do-job-now.rst | 2 +- docs/dwarf-op.rst | 2 +- docs/embark-skills.rst | 2 +- docs/exportlegends.rst | 2 +- docs/exterminate.rst | 57 +- docs/fix-ster.rst | 2 +- docs/fix/corrupt-equipment.rst | 2 +- docs/fix/general-strike.rst | 25 + docs/fix/item-occupancy.rst | 2 +- docs/fix/population-cap.rst | 2 +- docs/fix/tile-occupancy.rst | 2 +- docs/fixnaked.rst | 2 +- docs/flashstep.rst | 2 +- docs/forget-dead-body.rst | 2 +- docs/forum-dwarves.rst | 2 +- docs/gaydar.rst | 2 +- docs/ghostly.rst | 2 +- docs/growcrops.rst | 2 +- docs/gui/advfort.rst | 2 +- docs/gui/autogems.rst | 2 +- docs/gui/choose-weapons.rst | 2 +- docs/gui/civ-alert.rst | 59 +++ docs/gui/clone-uniform.rst | 2 +- docs/gui/color-schemes.rst | 2 +- docs/gui/companion-order.rst | 2 +- docs/gui/confirm.rst | 5 +- docs/gui/cp437-table.rst | 2 +- docs/gui/create-tree.rst | 2 +- docs/gui/design.rst | 2 +- docs/gui/dfstatus.rst | 2 +- docs/gui/extended-status.rst | 2 +- docs/gui/family-affairs.rst | 2 +- docs/gui/guide-path.rst | 2 +- docs/gui/kitchen-info.rst | 2 +- docs/gui/launcher.rst | 2 +- docs/gui/load-screen.rst | 2 +- docs/gui/manager-quantity.rst | 2 +- docs/gui/mechanisms.rst | 2 +- docs/gui/mod-manager.rst | 2 +- docs/gui/petitions.rst | 2 +- docs/gui/power-meter.rst | 2 +- docs/gui/quantum.rst | 2 +- docs/gui/rename.rst | 2 +- docs/gui/room-list.rst | 2 +- docs/gui/seedwatch.rst | 19 + docs/gui/settings-manager.rst | 2 +- docs/gui/siege-engine.rst | 2 +- docs/gui/stamper.rst | 2 +- docs/gui/stockpiles.rst | 2 +- docs/gui/suspendmanager.rst | 2 +- docs/gui/teleport.rst | 2 +- docs/gui/unit-info-viewer.rst | 2 +- docs/gui/workflow.rst | 2 +- docs/gui/workorder-details.rst | 2 +- docs/gui/workshop-job.rst | 2 +- docs/hotkey-notes.rst | 2 +- docs/launch.rst | 2 +- docs/light-aquifers-only.rst | 2 +- docs/linger.rst | 2 +- docs/list-waves.rst | 2 +- docs/load-save.rst | 2 +- docs/make-legendary.rst | 2 +- docs/markdown.rst | 2 +- docs/max-wave.rst | 2 +- docs/modtools/add-syndrome.rst | 2 +- docs/modtools/anonymous-script.rst | 2 +- docs/modtools/change-build-menu.rst | 2 +- docs/modtools/create-item.rst | 2 +- docs/modtools/create-tree.rst | 2 +- docs/modtools/create-unit.rst | 2 +- docs/modtools/equip-item.rst | 2 +- docs/modtools/extra-gamelog.rst | 2 +- docs/modtools/fire-rate.rst | 2 +- docs/modtools/if-entity.rst | 2 +- docs/modtools/interaction-trigger.rst | 2 +- docs/modtools/invader-item-destroyer.rst | 2 +- docs/modtools/item-trigger.rst | 2 +- docs/modtools/moddable-gods.rst | 2 +- docs/modtools/outside-only.rst | 2 +- docs/modtools/pref-edit.rst | 2 +- docs/modtools/projectile-trigger.rst | 2 +- docs/modtools/random-trigger.rst | 2 +- docs/modtools/raw-lint.rst | 2 +- docs/modtools/reaction-product-trigger.rst | 2 +- docs/modtools/reaction-trigger-transition.rst | 2 +- docs/modtools/reaction-trigger.rst | 2 +- docs/modtools/set-belief.rst | 2 +- docs/modtools/set-need.rst | 2 +- docs/modtools/set-personality.rst | 2 +- docs/modtools/skill-change.rst | 2 +- docs/modtools/spawn-flow.rst | 2 +- docs/modtools/syndrome-trigger.rst | 2 +- docs/modtools/transform-unit.rst | 2 +- docs/names.rst | 2 +- docs/open-legends.rst | 2 +- docs/points.rst | 2 +- docs/pop-control.rst | 2 +- docs/prefchange.rst | 2 +- docs/prioritize.rst | 13 +- docs/putontable.rst | 2 +- docs/questport.rst | 2 +- docs/region-pops.rst | 2 +- docs/resurrect-adv.rst | 2 +- docs/reveal-adv-map.rst | 2 +- docs/season-palette.rst | 2 +- docs/set-orientation.rst | 2 +- docs/siren.rst | 2 +- docs/spawnunit.rst | 2 +- docs/startdwarf.rst | 2 +- docs/stripcaged.rst | 48 +- docs/tidlers.rst | 2 +- docs/timestream.rst | 2 +- docs/undump-buildings.rst | 2 +- docs/uniform-unstick.rst | 2 +- docs/unretire-anyone.rst | 2 +- docs/view-item-info.rst | 2 +- docs/view-unit-reports.rst | 2 +- docs/warn-starving.rst | 5 +- docs/warn-stealers.rst | 2 +- docs/workorder-recheck.rst | 2 +- exterminate.lua | 96 ++-- fix/general-strike.lua | 38 ++ gui/civ-alert.lua | 294 +++++++++++ gui/control-panel.lua | 89 +++- gui/cp437-table.lua | 113 ++-- gui/design.lua | 9 +- gui/gm-editor.lua | 25 +- gui/launcher.lua | 2 +- gui/seedwatch.lua | 291 +++++++++++ internal/quickfort/command.lua | 5 +- prioritize.lua | 41 +- stripcaged.lua | 79 ++- suspendmanager.lua | 32 +- warn-starving.lua | 17 +- 185 files changed, 1657 insertions(+), 1024 deletions(-) delete mode 100644 autounsuspend.lua delete mode 100644 docs/autounsuspend.rst create mode 100644 docs/fix/general-strike.rst create mode 100644 docs/gui/civ-alert.rst create mode 100644 docs/gui/seedwatch.rst create mode 100644 fix/general-strike.lua create mode 100644 gui/civ-alert.lua create mode 100644 gui/seedwatch.lua diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bf2f9a853..919b3242e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Set up Python 3 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 - name: Install dependencies @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Set up Python 3 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95cac1e22d..59dac64677 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.21.0 + rev: 0.22.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.4.2 + rev: v1.5.1 hooks: - id: forbid-tabs exclude_types: diff --git a/autounsuspend.lua b/autounsuspend.lua deleted file mode 100644 index 2be18ff5f8..0000000000 --- a/autounsuspend.lua +++ /dev/null @@ -1,66 +0,0 @@ --- Automate periodic running of the unsuspend script ---@module = true ---@enable = true - -local json = require('json') -local persist = require('persist-table') - -local GLOBAL_KEY = 'autounsuspend' -- used for state change hooks and persistence - -enabled = enabled or false - -function isEnabled() - return enabled -end - -local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled}) -end - -local function event_loop() - if enabled then - dfhack.run_script('unsuspend', '--quiet') - dfhack.timeout(1, 'days', event_loop) - end -end - -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - enabled = false - return - end - - if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return - end - - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') - enabled = (persisted_data or {enabled=false})['enabled'] - event_loop() -end - -if dfhack_flags.module then - return -end - -if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then - dfhack.printerr('autounsuspend needs a loaded fortress map to work') - return -end - -local args = {...} -if dfhack_flags and dfhack_flags.enable then - args = {dfhack_flags.enable_state and 'enable' or 'disable'} -end - -local command = args[1] -if command == "enable" then - enabled = true -elseif command == "disable" then - enabled = false -else - return -end - -event_loop() -persist_state() diff --git a/caravan.lua b/caravan.lua index 921cb7e5cc..e11ad71f31 100644 --- a/caravan.lua +++ b/caravan.lua @@ -1,6 +1,11 @@ -- Adjusts properties of caravans and provides overlay for enhanced trading --@ module = true +-- TODO: the category checkbox that indicates whether all items in the category +-- are selected can be incorrect after the overlay adjusts the container +-- selection. the state is in trade.current_type_a_flag, but figuring out which +-- index to modify is non-trivial. + local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') @@ -28,161 +33,158 @@ local GOODFLAG = { CONTAINER_COLLAPSED_SELECTED = 5, } +local trade = df.global.game.main_interface.trade + +local MARGIN_HEIGHT = 26 -- screen height *other* than the list + +function set_height(list_index, delta) + trade.i_height[list_index] = trade.i_height[list_index] + delta + if delta >= 0 then return end + _,screen_height = dfhack.screen.getWindowSize() + -- list only increments in three tiles at a time + local page_height = ((screen_height - 26) // 3) * 3 + trade.scroll_position_item[list_index] = math.max(0, + math.min(trade.scroll_position_item[list_index], + trade.i_height[list_index] - page_height)) +end + function select_shift_clicked_container_items(new_state, old_state, list_index) -- if ctrl is also held, collapse the container too local also_collapse = dfhack.internal.getModifiers().ctrl - local collapsed_item_count = 0 + local collapsed_item_count, collapsing_container, in_container = 0, false, false for k, goodflag in ipairs(new_state) do - if old_state[k] ~= goodflag then - local next_item_flag = new_state[k + 1] - local this_item_is_container = df.item_binst:is_instance(df.global.game.main_interface.trade.good[list_index][k]) - if this_item_is_container then - local container_is_selected = goodflag == GOODFLAG.UNCONTAINED_SELECTED or goodflag == GOODFLAG.CONTAINER_COLLAPSED_SELECTED - if container_is_selected then - local collapsed_this_container = false - - if goodflag == GOODFLAG.UNCONTAINED_SELECTED and also_collapse then - collapsed_this_container = true - end - - new_state[k] = also_collapse and GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED or GOODFLAG.UNCONTAINED_UNSELECTED - local end_of_container_reached = false - local contained_item_index = k + 1 - - if contained_item_index > #new_state - 1 then - end_of_container_reached = true - end - - while not end_of_container_reached do - new_state[contained_item_index] = GOODFLAG.CONTAINED_SELECTED - - if collapsed_this_container then - collapsed_item_count = collapsed_item_count + 1 - end - - local next_item_index = contained_item_index + 1 - - if next_item_index > #new_state or new_state[next_item_index] < 2 or new_state[next_item_index] >= 4 then - end_of_container_reached = true - end - contained_item_index = contained_item_index + 1 - end - end + if in_container then + if goodflag <= GOODFLAG.UNCONTAINED_SELECTED + or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then + break end + + new_state[k] = GOODFLAG.CONTAINED_SELECTED + + if collapsing_container then + collapsed_item_count = collapsed_item_count + 1 + end + goto continue end + + if goodflag == old_state[k] then goto continue end + local is_container = df.item_binst:is_instance(trade.good[list_index][k]) + if not is_container then goto continue end + + -- deselect the container itself + if also_collapse or + old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED or + old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_SELECTED then + collapsing_container = goodflag == GOODFLAG.UNCONTAINED_SELECTED + new_state[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED + else + new_state[k] = GOODFLAG.UNCONTAINED_UNSELECTED + end + in_container = true + + ::continue:: end if collapsed_item_count > 0 then - df.global.game.main_interface.trade.i_height[list_index] = df.global.game.main_interface.trade.i_height[list_index] - collapsed_item_count * 3 + set_height(list_index, collapsed_item_count * -3) end end -function collapse_ctrl_clicked_containers(new_state, old_state, list_index) +local CTRL_CLICK_STATE_MAP = { + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, +} + +-- collapses uncollapsed containers and restores the selection state for the container +-- and contained items +function toggle_ctrl_clicked_containers(new_state, old_state, list_index) + local toggled_item_count, in_container, is_collapsing = 0, false, false for k, goodflag in ipairs(new_state) do - if old_state[k] ~= goodflag then - local next_item_flag = new_state[k + 1] - if next_item_flag == GOODFLAG.CONTAINED_UNSELECTED or next_item_flag == GOODFLAG.CONTAINED_SELECTED then - local target_goodflag - if goodflag == GOODFLAG.UNCONTAINED_SELECTED then - target_goodflag = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED - elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then - target_goodflag = GOODFLAG.CONTAINER_COLLAPSED_SELECTED - end - - if target_goodflag ~= nil then - new_state[k] = target_goodflag - -- changed a container state, items inside will be reset, return contained items to state before collapse - local end_of_container_reached = false - local contained_item_index = k + 1 - - if contained_item_index > #new_state - 1 then - end_of_container_reached = true - end - - local num_items_collapsed = 0 - - while not end_of_container_reached do - num_items_collapsed = num_items_collapsed + 1 - new_state[contained_item_index] = old_state[contained_item_index] - - local next_item_index = contained_item_index + 1 - - if next_item_index > #new_state or new_state[next_item_index] < 2 or new_state[next_item_index] >= 4 then - end_of_container_reached = true - end - contained_item_index = contained_item_index + 1 - end - - if num_items_collapsed > 0 then - df.global.game.main_interface.trade.i_height[list_index] = df.global.game.main_interface.trade.i_height[list_index] - num_items_collapsed * 3 - end - end + if in_container then + if goodflag <= GOODFLAG.UNCONTAINED_SELECTED + or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then + break end + toggled_item_count = toggled_item_count + 1 + new_state[k] = old_state[k] + goto continue end + + if goodflag == old_state[k] then goto continue end + local is_contained = goodflag == GOODFLAG.CONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINED_SELECTED + if is_contained then goto continue end + local is_container = df.item_binst:is_instance(trade.good[list_index][k]) + if not is_container then goto continue end + + new_state[k] = CTRL_CLICK_STATE_MAP[old_state[k]] + in_container = true + is_collapsing = goodflag == GOODFLAG.UNCONTAINED_UNSELECTED or goodflag == GOODFLAG.UNCONTAINED_SELECTED + + ::continue:: end -end + if toggled_item_count > 0 then + set_height(list_index, toggled_item_count * 3 * (is_collapsing and -1 or 1)) + end +end function collapseTypes(types_list, list_index) local type_on_count = 0 - for k, type_open in ipairs(types_list) do - local type_on = df.global.game.main_interface.trade.current_type_a_on[list_index][k] + for k in ipairs(types_list) do + local type_on = trade.current_type_a_on[list_index][k] if type_on then type_on_count = type_on_count + 1 end types_list[k] = false end - df.global.game.main_interface.trade.i_height[list_index] = type_on_count * 3 + trade.i_height[list_index] = type_on_count * 3 + trade.scroll_position_item[list_index] = 0 end function collapseAllTypes() - collapseTypes(df.global.game.main_interface.trade.current_type_a_expanded[0], 0) - collapseTypes(df.global.game.main_interface.trade.current_type_a_expanded[1], 1) - -- reset scroll to top when collapsing types - df.global.game.main_interface.trade.scroll_position_item[0] = 0 - df.global.game.main_interface.trade.scroll_position_item[1] = 0 + collapseTypes(trade.current_type_a_expanded[0], 0) + collapseTypes(trade.current_type_a_expanded[1], 1) end function collapseContainers(item_list, list_index) local num_items_collapsed = 0 for k, goodflag in ipairs(item_list) do - if goodflag ~= GOODFLAG.CONTAINED_UNSELECTED and goodflag ~= GOODFLAG.CONTAINED_SELECTED then - local next_item_index = k + 1 - if next_item_index > #item_list - 1 then - goto skip - end + if goodflag == GOODFLAG.CONTAINED_UNSELECTED + or goodflag == GOODFLAG.CONTAINED_SELECTED then + goto continue + end - local next_item = item_list[next_item_index] - local this_item_is_container = df.item_binst:is_instance(df.global.game.main_interface.trade.good[list_index][k]) - local collapsed_this_container = false - if this_item_is_container then - if goodflag == GOODFLAG.UNCONTAINED_SELECTED then - item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED - collapsed_this_container = true - elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then - item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED - collapsed_this_container = true - end - - if collapsed_this_container then - num_items_collapsed = num_items_collapsed + #dfhack.items.getContainedItems(df.global.game.main_interface.trade.good[list_index][k]) - end - end + local item = trade.good[list_index][k] + local is_container = df.item_binst:is_instance(item) + if not is_container then goto continue end + + local collapsed_this_container = false + if goodflag == GOODFLAG.UNCONTAINED_SELECTED then + item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED + collapsed_this_container = true + elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then + item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED + collapsed_this_container = true + end - ::skip:: + if collapsed_this_container then + num_items_collapsed = num_items_collapsed + #dfhack.items.getContainedItems(item) end + ::continue:: end if num_items_collapsed > 0 then - df.global.game.main_interface.trade.i_height[list_index] = df.global.game.main_interface.trade.i_height[list_index] - num_items_collapsed * 3 + set_height(list_index, num_items_collapsed * -3) end end function collapseAllContainers() - collapseContainers(df.global.game.main_interface.trade.goodflag[0], 0) - collapseContainers(df.global.game.main_interface.trade.goodflag[1], 1) + collapseContainers(trade.goodflag[0], 0) + collapseContainers(trade.goodflag[1], 1) end function collapseEverything() @@ -191,8 +193,8 @@ function collapseEverything() end function copyGoodflagState() - trader_selected_state = copyall(df.global.game.main_interface.trade.goodflag[0]) - broker_selected_state = copyall(df.global.game.main_interface.trade.goodflag[1]) + trader_selected_state = copyall(trade.goodflag[0]) + broker_selected_state = copyall(trade.goodflag[1]) end CaravanTradeOverlay = defclass(CaravanTradeOverlay, overlay.OverlayWidget) @@ -210,20 +212,18 @@ function CaravanTradeOverlay:init() widgets.Label{ frame={t=0, l=0}, text={ - {text='Shift+Click checkbox:', pen=COLOR_LIGHTGREEN}, + {text='Shift+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', NEWLINE, - {text='select items inside bin', pen=COLOR_WHITE}, + ' select items inside bin', }, - text_pen=COLOR_LIGHTGREEN, }, widgets.Label{ frame={t=3, l=0}, text={ - {text='Ctrl+Click checkbox:', pen=COLOR_LIGHTGREEN}, + {text='Ctrl+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', NEWLINE, - {text='collapse single bin', pen=COLOR_WHITE}, + ' collapse/expand bin', }, - text_pen=COLOR_LIGHTGREEN, }, widgets.HotkeyLabel{ frame={t=6, l=0}, @@ -239,43 +239,42 @@ function CaravanTradeOverlay:init() }, widgets.Label{ frame={t=9, l=0}, - text = 'Shift+Scroll:', + text = 'Shift+Scroll', text_pen=COLOR_LIGHTGREEN, }, widgets.Label{ - frame={t=9, l=14}, - text = 'fast scroll', + frame={t=9, l=12}, + text = ': fast scroll', }, } end -function CaravanTradeOverlay:render(dc) - CaravanTradeOverlay.super.render(self, dc) +-- do our alterations *after* the vanilla response to the click has registered. otherwise +-- it's very difficult to figure out which item has been clicked +function CaravanTradeOverlay:onRenderBody(dc) if handle_shift_click_on_render then handle_shift_click_on_render = false - select_shift_clicked_container_items(df.global.game.main_interface.trade.goodflag[0], trader_selected_state, 0) - select_shift_clicked_container_items(df.global.game.main_interface.trade.goodflag[1], broker_selected_state, 1) + select_shift_clicked_container_items(trade.goodflag[0], trader_selected_state, 0) + select_shift_clicked_container_items(trade.goodflag[1], broker_selected_state, 1) elseif handle_ctrl_click_on_render then handle_ctrl_click_on_render = false - collapse_ctrl_clicked_containers(df.global.game.main_interface.trade.goodflag[0], trader_selected_state, 0) - collapse_ctrl_clicked_containers(df.global.game.main_interface.trade.goodflag[1], broker_selected_state, 1) + toggle_ctrl_clicked_containers(trade.goodflag[0], trader_selected_state, 0) + toggle_ctrl_clicked_containers(trade.goodflag[1], broker_selected_state, 1) end end function CaravanTradeOverlay:onInput(keys) + if CaravanTradeOverlay.super.onInput(self, keys) then return true end + if keys._MOUSE_L_DOWN then if dfhack.internal.getModifiers().shift then handle_shift_click_on_render = true + copyGoodflagState() elseif dfhack.internal.getModifiers().ctrl then handle_ctrl_click_on_render = true - end - - if handle_ctrl_click_on_render or handle_shift_click_on_render then copyGoodflagState() end end - CaravanTradeOverlay.super.onInput(self, keys) - return false end OVERLAY_WIDGETS = { @@ -393,7 +392,7 @@ local function rejoin_pack_animals() end end -function commands.unload(...) +function commands.unload() rejoin_pack_animals() end @@ -401,21 +400,17 @@ function commands.help() print(dfhack.script_help()) end -function main(...) - local args = {...} - local command = table.remove(args, 1) +function main(args) + local command = table.remove(args, 1) or 'list' if commands[command] then commands[command](table.unpack(args)) else commands.help() - if command then - qerror("No such subcommand: " .. command) - else - qerror("Missing subcommand") - end + print() + qerror("No such command: " .. command) end end if not dfhack_flags.module then - main(...) + main{...} end diff --git a/changelog.txt b/changelog.txt index a646daa36b..94b9ad2731 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,12 +18,58 @@ that repo. ## Fixes ## Misc Improvements -- `gui/design`: ``gui/dig`` renamed to ``gui/design`` -- `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. -- `combine`: Now supports powders and seeds, and combines into containers. ## Removed +# 50.07-r1 + +## New Scripts + +## Fixes +-@ `caravan`: fix trade good list sometimes disappearing when you collapse a bin +-@ `gui/gm-editor`: no longer nudges last open window when opening a new one +- `warn-starving`: no longer warns for dead units +-@ `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode + +## Misc Improvements +- `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` +- `gui/gm-editor`: they key column now auto-fits to the widest key +- `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) +-@ `gui/control-panel`: add `faststart` to the system services + +## Removed + +# 50.07-beta2 + +## New Scripts +- `fix/general-strike`: fix known causes of the general strike bug (contributed by Putnam) +- `gui/seedwatch`: GUI config and status panel interface for `seedwatch` +- `gui/civ-alert`: configure and trigger civilian alerts + +## Fixes +-@ `caravan`: item list length now correct when expanding and collapsing containers +-@ `prioritize`: fixed all watched job type names showing as ``nil`` after a game load +-@ `suspendmanager`: does not suspend non-blocking jobs such as floor bars or bridges anymore +-@ `suspendmanager`: fix occasional bad identification of buildingplan jobs +- `warn-starving`: no longer warns for enemy and neutral units + +## Misc Improvements +- `gui/control-panel`: Now detects overlays from scripts named with capital letters +- `gui/cp437-table`: now has larger key buttons and clickable backspace/submit/cancel buttons, making it fully usable on the Steam Deck and other systems that don't have an accessible keyboard +-@ `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. +- `exterminate`: add support for ``vaporize`` kill method for when you don't want to leave a corpse +- `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor +- `combine`: added ``--quiet`` option for no output when there are no changes +- `stripcaged`: added ``--skip-forbidden`` option for greater control over which items are marked for dumping +- `stripcaged`: items that are marked for dumping are now automatically unforbidden (unless ``--skip-forbidden`` is set) +-@ `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles +-@ `gui/control-panel`: added ``general-strike`` maintenance option for automatic fixing of (at least one cause of) the general strike bug +- `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button + +## Removed +- `autounsuspend`: replaced by `suspendmanager` +-@ `gui/dig`: renamed to `gui/design` + # 50.07-beta1 ## New Scripts @@ -32,7 +78,7 @@ that repo. - `suspend`: suspends building construction jobs ## Fixes -- `quicksave`: now reliably triggers an autosave, even if one has been performed recently +-@ `quicksave`: now reliably triggers an autosave, even if one has been performed recently - `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" ## Misc Improvements @@ -56,8 +102,8 @@ that repo. - `combine`: combines stacks of food and plant items. ## Fixes -- `troubleshoot-item`: fix printing of job details for chosen item -- `makeown`: fixes errors caused by using makeown on an invader +-@ `troubleshoot-item`: fix printing of job details for chosen item +-@ `makeown`: fixes errors caused by using makeown on an invader -@ `gui/blueprint`: correctly use setting presets passed on the commandline -@ `gui/quickfort`: correctly use settings presets passed on the commandline - `devel/query`: can now properly index vectors in the --table argument @@ -71,7 +117,7 @@ that repo. - `devel/visualize-structure`: now automatically inspects the contents of most pointer fields, rather than inspecting the pointers themselves - `devel/query`: will now search for jobs at the map coordinate highlighted, if no explicit job is highlighted and there is a map tile highlighted - `caravan`: add trade screen overlay that assists with seleting groups of items and collapsing groups in the UI -- `gui/gm-editor` will now inspect a selected building itself if the building has no current jobs +- `gui/gm-editor`: will now inspect a selected building itself if the building has no current jobs ## Removed - `combine-drinks`: replaced by `combine` @@ -80,8 +126,8 @@ that repo. # 50.07-alpha1 ## New Scripts -- `gui/dig`: digging designation tool for shapes and patterns -- `makeown`: adds the selected unit as a member of your fortress +- `gui/design`: digging and construction designation tool with shapes and patterns +- `makeown`: makes the selected unit a citizen of your fortress ## Fixes -@ `gui/unit-syndromes`: allow the window widgets to be interacted with @@ -93,7 +139,7 @@ that repo. - `gui/gm-editor`: now supports multiple independent data inspection windows - `gui/gm-editor`: now prints out contents of coordinate vars instead of just the type - `rejuvenate`: now takes an --age parameter to choose a desired age. -- `gui/dig` : Added 'Line' shape that also can draw curves, added draggable center handle +-@ `gui/dig` : Added 'Line' shape that also can draw curves, added draggable center handle # 50.05-alpha3.1 @@ -136,7 +182,7 @@ that repo. - `gui/quickfort`: don't close the window when applying a blueprint so players can apply the same blueprint multiple times more easily - `locate-ore`: now only searches revealed tiles by default - `modtools/spawn-liquid`: sets tile temperature to stable levels when spawning water or magma -- `prioritize`: pushing minecarts is now included in the default priortization list +-@ `prioritize`: pushing minecarts is now included in the default priortization list - `prioritize`: now automatically starts boosting the default list of job types when enabled - `unforbid`: avoids unforbidding unreachable and underwater items by default - `gui/create-item`: added whole corpse spawning alongside corpsepieces. (under "corpse") @@ -156,7 +202,7 @@ that repo. # 50.05-alpha1 ## New Scripts -- `gui/autochop`: configuration frontend for the `autochop` plugin. you can pin the window and leave it up on the screen somewhere for live monitoring of your logging industry. +- `gui/autochop`: configuration frontend and status monitor for the `autochop` plugin - `devel/tile-browser`: page through available textures and see their texture ids - `allneeds`: list all unmet needs sorted by how many dwarves suffer from them. diff --git a/combine.lua b/combine.lua index d72d126f03..b1b4037474 100644 --- a/combine.lua +++ b/combine.lua @@ -8,35 +8,26 @@ local opts, args = { here = nil, dry_run = false, types = nil, - verbose = 0 + quiet = false, + verbose = false, }, {...} -- default max stack size of 30 -local MAX_ITEM_STACK=30 -local MAX_CONT_ITEMS=500 - --- list of types that use race and caste -local typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true} +local DEF_MAX=30 -- list of valid item types for merging --- Notes: 1. mergeable stacks are ones with the same type_id+race+caste or type_id+mat_type+mat_index --- 2. the maximum stack size is calcuated at run time: the highest value of MAX_ITEM_STACK or largest current stack size. --- 3. even though powders are specified, sand and plaster types items are excluded from merging. --- 4. seeds cannot be combined in stacks > 1. local valid_types_map = { - ['all'] = { }, - ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=MAX_ITEM_STACK}}, - ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=MAX_ITEM_STACK}, - [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=MAX_ITEM_STACK}}, - ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=MAX_ITEM_STACK}, - [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=MAX_ITEM_STACK}, - [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=MAX_ITEM_STACK}}, - ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=MAX_ITEM_STACK}}, - ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=MAX_ITEM_STACK}}, - ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=MAX_ITEM_STACK}, - [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=MAX_ITEM_STACK}}, - ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=MAX_ITEM_STACK}}, - ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_size=1}}, + ['all'] = { }, + ['drink'] = {[df.item_type.DRINK]={type_id=df.item_type.DRINK, type_name='DRINK',type_caste=false,max_stack_size=DEF_MAX}}, + ['fat'] = {[df.item_type.GLOB]={type_id=df.item_type.GLOB, type_name='GLOB',type_caste=false,max_stack_size=DEF_MAX}, + [df.item_type.CHEESE]={type_id=df.item_type.CHEESE, type_name='CHEESE',type_caste=false,max_stack_size=DEF_MAX}}, + ['fish'] = {[df.item_type.FISH]={type_id=df.item_type.FISH, type_name='FISH',type_caste=true,max_stack_size=DEF_MAX}, + [df.item_type.FISH_RAW]={type_id=df.item_type.FISH_RAW, type_name='FISH_RAW',type_caste=true,max_stack_size=DEF_MAX}, + [df.item_type.EGG]={type_id=df.item_type.EGG, type_name='EGG',type_caste=true,max_stack_size=DEF_MAX}}, + ['food'] = {[df.item_type.FOOD]={type_id=df.item_type.FOOD, type_name='FOOD',type_caste=false,max_stack_size=DEF_MAX}}, + ['meat'] = {[df.item_type.MEAT]={type_id=df.item_type.MEAT, type_name='MEAT',type_caste=false,max_stack_size=DEF_MAX}}, + ['plant'] = {[df.item_type.PLANT]={type_id=df.item_type.PLANT, type_name='PLANT',type_caste=false,max_stack_size=DEF_MAX}, + [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, type_name='PLANT_GROWTH',type_caste=false,max_stack_size=DEF_MAX}} } -- populate all types entry @@ -51,9 +42,9 @@ for k1,v1 in pairs(valid_types_map) do end end -function log(level, ...) +function log(...) -- if verbose is specified, then print the arguments, or don't. - if opts.verbose >= level then dfhack.print(string.format(...)) end + if opts.verbose then dfhack.print(string.format(...)) end end -- CList class @@ -69,49 +60,47 @@ function CList:new(o) return o end -local function comp_item_new(comp_key, max_size) +local function comp_item_new(comp_key, max_stack_size) -- create a new comp_item entry to be added to a comp_items table. local comp_item = {} if not comp_key then qerror('new_comp_item: comp_key is nil') end - comp_item.comp_key = comp_key -- key used to index comparable items for merging - comp_item.description = '' -- description of the comp item for output - comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack - -- item info - comp_item.items = CList:new(nil) -- key:item.id, val:{ item, before_size, after_size, before_cont_id, after_cont_id} - comp_item.item_qty = 0 -- total quantity of items - comp_item.before_stacks = 0 -- the number of stacks of the items before... - comp_item.after_stacks = 0 -- ...and after the merge - --container info - comp_item.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id - comp_item.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id + comp_item.comp_key = comp_key + comp_item.item_qty = 0 + comp_item.max_stack_size = max_stack_size or 0 + comp_item.before_stacks = 0 + comp_item.after_stacks = 0 + comp_item.before_stack_size = CList:new(nil) -- key:item.id, val:item.stack_size + comp_item.after_stack_size = CList:new(nil) -- key:item.id, val:item.stack_size + comp_item.items = CList:new(nil) -- key:item.id, val:item + comp_item.sorted_items = CList:new(nil) -- key:-1*item.id | item.id, val:item_id return comp_item end -local function comp_item_add_item(comp_item, item, container) +local function comp_item_add_item(comp_item, item) -- add an item into the comp_items table, setting the comp_item attributes. if not comp_item.items[item.id] then - comp_item.item_qty = comp_item.item_qty + item.stack_size - comp_item.before_stacks = comp_item.before_stacks + 1 - comp_item.description = utils.getItemDescription(item, 1) - if item.stack_size > comp_item.max_size then - comp_item.max_size = item.stack_size + comp_item.items[item.id] = item + comp_item.item_qty = comp_item.item_qty + item.stack_size + if item.stack_size > comp_item.max_stack_size then + comp_item.max_stack_size = item.stack_size end + comp_item.before_stack_size[item.id] = item.stack_size + comp_item.after_stack_size[item.id] = item.stack_size + comp_item.before_stacks = comp_item.before_stacks + 1 + comp_item.after_stacks = comp_item.after_stacks + 1 - local new_item = {} - new_item.item = item - new_item.before_size = item.stack_size + local contained_item = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) - -- item is in a container - if container then - new_item.before_cont_id = container.id - comp_item.before_cont_ids[container.id] = container.id + -- used to merge contained items before loose items + if contained_item then + table.insert(comp_item.sorted_items, -1*item.id) + else + table.insert(comp_item.sorted_items, item.id) end - - comp_item.items[item.id] = new_item return comp_item.items[item.id] else - -- this case should not happen, unless an item id is duplicated. + -- this case should not happen, unless an item is contained by more than one container. -- in which case, only allow one instance for the merge. return nil end @@ -120,150 +109,67 @@ end local function stack_type_new(type_vals) -- create a new stack type entry to be added to the stacks table. local stack_type = {} - - -- attributes from the type val table for k,v in pairs(type_vals) do stack_type[k] = v end - - -- item info - stack_type.comp_items = CList:new(nil) -- key:comp_key, val:comp_item - stack_type.item_qty = 0 -- total quantity of items types - stack_type.before_stacks = 0 -- the number of stacks of the item types before ... - stack_type.after_stacks = 0 -- ...and after the merge - - --container info - stack_type.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id - stack_type.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id + stack_type.item_qty = 0 + stack_type.before_stacks = 0 + stack_type.after_stacks = 0 + stack_type.comp_items = CList:new(nil) -- key:comp_key, val=comp_item return stack_type end -local function stacks_add_item(stacks, stack_type, item, container, contained_count) +local function stacks_type_add_item(stacks_type, item) -- add an item to the matching comp_items table; based on comp_key. local comp_key = '' - if typesThatUseCreatures[df.item_type[stack_type.type_id]] then - comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) .. "+" .. tostring(item.caste) + if stacks_type.type_caste then + comp_key = tostring(stacks_type.type_id) .. tostring(item.race) .. tostring(item.caste) else - comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.mat_type) .. "+" .. tostring(item.mat_index) + comp_key = tostring(stacks_type.type_id) .. tostring(item.mat_type) .. tostring(item.mat_index) end - if not stack_type.comp_items[comp_key] then - stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_size) + if not stacks_type.comp_items[comp_key] then + stacks_type.comp_items[comp_key] = comp_item_new(comp_key, stacks_type.max_stack_size) end - if comp_item_add_item(stack_type.comp_items[comp_key], item, container, contained_count) then - stack_type.before_stacks = stack_type.before_stacks + 1 - stack_type.item_qty = stack_type.item_qty + item.stack_size - - stacks.before_stacks = stacks.before_stacks + 1 - stacks.item_qty = stacks.item_qty + item.stack_size - - if item.stack_size > stack_type.max_size then - stack_type.max_size = item.stack_size - end - - -- item is in a container - if container then - - -- add it to the stack type list - stack_type.before_cont_ids[container.id] = container.id - - -- add it to the before stacks container list - stacks.before_cont_ids[container.id] = container.id + if comp_item_add_item(stacks_type.comp_items[comp_key], item) then + stacks_type.before_stacks = stacks_type.before_stacks + 1 + stacks_type.after_stacks = stacks_type.after_stacks + 1 + stacks_type.item_qty = stacks_type.item_qty + item.stack_size + if item.stack_size > stacks_type.max_stack_size then + stacks_type.max_stack_size = item.stack_size end end end -local function sorted_items(tab) - -- used to sort the comp_items by contained, then size. Important for combining containers. - local tmp = {} - for id, val in pairs(tab) do - local val = {id=id, before_cont_id=val.before_cont_id, before_size=val.before_size} - table.insert(tmp, val) - end - - table.sort(tmp, - function(a, b) - if not a.before_cont_id and not b.before_cont_id or a.before_cont_id and b.before_cont_id then - return a.before_size > b.before_size - else - return a.before_cont_id and not b.before_cont_id - end - end - ) - - local i = 0 - local iter = - function() - i = i + 1 - if tmp[i] == nil then - return nil - else - return tmp[i].id, tab[tmp[i].id] +local function print_stacks_details(stacks) + -- print stacks details + log(('Details #types:%5d\n'):format(#stacks)) + for _, stacks_type in pairs(stacks) do + log((' type: <%12s> <%d> comp item types#:%5d #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(stacks_type.type_name, stacks_type.type_id, stacks_type.item_qty, #stacks_type.comp_items, stacks_type.max_stack_size, stacks_type.before_stacks, stacks_type.after_stacks)) + for _, comp_item in pairs(stacks_type.comp_items) do + log((' compare key:%12s #item qty:%5d #comp item stacks:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_size, comp_item.before_stacks, comp_item.after_stacks)) + for _, item in pairs(comp_item.items) do + log((' item:%40s <%6d> before:%5d after:%5d\n'):format(utils.getItemDescription(item), item.id, comp_item.before_stack_size[item.id], comp_item.after_stack_size[item.id])) end end - return iter -end - -local function sorted_desc(tab, ids) - -- used to sort the lists by description - local tmp = {} - for id, val in pairs(tab) do - if ids[id] then - local val = {id=id, description=val.description} - table.insert(tmp, val) - end end - - table.sort(tmp, function(a, b) return a.description < b.description end) - - local i = 0 - local iter = - function() - i = i + 1 - if tmp[i] == nil then - return nil - else - return tmp[i].id, tab[tmp[i].id] - end - end - return iter end -local function print_stacks(stacks) - -- print stacks details - log(0, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) - for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do - log(1, (' container: %50s <%6d> before:%5d after:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size)) - end - log(0, ('Items: #qty: %6d sizes: before:%5d after:%5d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks)) - for key, stack_type in pairs(stacks.stack_types) do - log(0, (' Type: %12s <%d> #qty:%6d sizes: max:%5d before:%6d after:%6d containers: before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids)) - for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do - log(1, (' Comp item:%40s <%12s> #qty:%6d #stacks:%5d sizes: max:%5d before:%6d after:%6d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) - for _, item in sorted_items(comp_item.items) do - log(2, (' Item:%40s <%6d> before:%6d after:%6d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) - log(3, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) - log(2, ('\n')) - end +local function print_stacks_summary(stacks, quiet) + -- print stacks summary to the console + local printed = 0 + for _, s in pairs(stacks) do + if s.before_stacks ~= s.after_stacks then + printed = printed + 1 + print(('combined %d %s items from %d stacks into %d') + :format(s.item_qty, s.type_name, s.before_stacks, s.after_stacks)) end end -end - -local function stacks_new() - local stacks = {} - - stacks.stack_types = CList:new(nil) -- key=type_id, val=stack_type - stacks.containers = CList:new(nil) -- key=container.id, val={container, description, before_size, after_size} - stacks.before_cont_ids = CList:new(nil) -- key=container.id, val=container.id - stacks.after_cont_ids = CList:new(nil) -- key=container.id, val=container.id - stacks.item_qty = 0 - stacks.before_stacks = 0 - stacks.after_stacks = 0 - - return stacks - + if printed == 0 and not quiet then + print('All stacks already optimally combined.') + end end local function isRestrictedItem(item) @@ -274,50 +180,37 @@ local function isRestrictedItem(item) or flags.removed or flags.encased or flags.spider_web or #item.specific_refs > 0 end -function stacks_add_items(stacks, items, container, contained_count, ind) + +function stacks_add_items(stacks, items, ind) -- loop through each item and add it to the matching stack[type_id].comp_items table -- recursively calls itself to add contained items if not ind then ind = '' end for _, item in pairs(items) do local type_id = item:getType() - local subtype_id = item:getSubtype() - local stack_type = stacks.stack_types[type_id] + local stacks_type = stacks[type_id] -- item type in list of included types? - if stack_type and not item:isSand() and not item:isPlaster() then + if stacks_type then if not isRestrictedItem(item) then - stacks_add_item(stacks, stack_type, item, container, contained_count) - - if typesThatUseCreatures[df.item_type[type_id]] then - local raceRaw = df.global.world.raws.creatures.all[item.race] - local casteRaw = raceRaw.caste[item.caste] - log(3, (' %sitem:%40s <%6d> is incl, type:%d, race:%s, caste:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, raceRaw.creature_id, casteRaw.caste_id)) - else - local mat_info = dfhack.matinfo.decode(item.mat_type, item.mat_index) - log(3, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, sand:%s, plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(),item:isSand(), item:isPlaster())) - end + stacks_type_add_item(stacks_type, item) + log((' %sitem:%40s <%6d> is incl, type %d\n'):format(ind, utils.getItemDescription(item), item.id, type_id)) else -- restricted; such as marked for action or dump. - log(3, (' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) + log((' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) end -- add contained items elseif dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINS_ITEM) then local contained_items = dfhack.items.getContainedItems(item) - local count = #contained_items - stacks.containers[item.id] = {} - stacks.containers[item.id].container = item - stacks.containers[item.id].before_size = #contained_items - stacks.containers[item.id].description = utils.getItemDescription(item, 1) - log(3, (' %sContainer:%s <%6d> #items:%5d Sandbearing:%s\n'):format(ind, utils.getItemDescription(item), item.id, count, item:isSandBearing())) - stacks_add_items(stacks, contained_items, item, count, ind .. ' ') + log((' %sContainer:%s <%6d> #items:%5d\n'):format(ind, utils.getItemDescription(item), item.id, #contained_items)) + stacks_add_items(stacks, contained_items, ind .. ' ') -- excluded item types else - log(3, (' %sitem:%40s <%6d> is excl, type %d, sand:%s plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, item:isSand(), item:isPlaster())) + log((' %sitem:%40s <%6d> is excl, type %d\n'):format(ind, utils.getItemDescription(item), item.id, type_id)) end end end @@ -327,159 +220,85 @@ local function populate_stacks(stacks, stockpiles, types) -- 2. loop through the table of stockpiles, get each item in the stockpile, then add them to stacks if the type_id matches -- an item is stored at the bottom of the structure: stacks[type_id].comp_items[comp_key].item -- comp_key is a compound key comprised of type_id+race+caste or type_id+mat_type+mat_index - log(3, 'Populating phase\n') + log('Populating phase\n') -- iterate across the types - log(3, 'stack types\n') + log('stack types\n') for type_id, type_vals in pairs(types) do - if not stacks.stack_types[type_id] then - stacks.stack_types[type_id] = stack_type_new(type_vals) - local stack_type = stacks.stack_types[type_id] - log(3, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + if not stacks[type_id] then + stacks[type_id] = stack_type_new(type_vals) + local stacks_type = stacks[type_id] + log((' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(stacks_type.type_name, stacks_type.type_id, stacks_type.item_qty, stacks_type.max_stack_size, stacks_type.before_stacks, stacks_type.after_stacks)) end end -- iterate across the stockpiles, get the list of items and call the add function to check/add as needed - log(3, ('stockpiles\n')) + log(('stockpiles\n')) for _, stockpile in pairs(stockpiles) do local items = dfhack.buildings.getStockpileContents(stockpile) - log(3, (' stockpile:%30s <%6d> pos:(%3d,%3d,%3d) #items:%5d\n'):format(stockpile.name, stockpile.id, stockpile.centerx, stockpile.centery, stockpile.z, #items)) + log((' stockpile:%30s <%6d> pos:(%3d,%3d,%3d) #items:%5d\n'):format(stockpile.name, stockpile.id, stockpile.centerx, stockpile.centery, stockpile.z, #items)) if #items > 0 then stacks_add_items(stacks, items) else - log(3, ' skipping stockpile: no items\n') + log(' skipping stockpile: no items\n') end end end local function preview_stacks(stacks) - -- calculate the stacks sizes and store in after_item_stack_size + -- calculate the stacks sizes and store in after_stack_size -- the max stack size for each comp item is determined as the maximum stack size for it's type - log(3, '\nPreview phase\n') - - for _, stack_type in pairs(stacks.stack_types) do - log(3, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) - - for comp_key, comp_item in pairs(stack_type.comp_items) do - log(3, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d before:%5d after:%5d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) - - -- sort the items, according to contained first, then stack size second - if stack_type.max_size > comp_item.max_size then - comp_item.max_size = stack_type.max_size + log('\nPreview phase\n') + for _, stacks_type in pairs(stacks) do + for comp_key, comp_item in pairs(stacks_type.comp_items) do + -- sort the items. + table.sort(comp_item.sorted_items) + + if stacks_type.max_stack_size > comp_item.max_stack_size then + comp_item.max_stack_size = stacks_type.max_stack_size end - -- how many stacks are needed? - local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) + -- how many stacks are needed ? + local max_stacks_needed = math.floor(comp_item.item_qty / comp_item.max_stack_size) -- how many items are left over after the max stacks are allocated? - local stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size - - if stack_remainder > 0 then - comp_item.after_stacks = stacks_needed + 1 - else - comp_item.after_stacks = stacks_needed - end - - stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks - stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks - - -- Update the after stack sizes. - for _, item in sorted_items(comp_item.items) do - if stacks_needed > 0 then - stacks_needed = stacks_needed - 1 - item.after_size = comp_item.max_size + local stack_remainder = comp_item.item_qty - max_stacks_needed * comp_item.max_stack_size + + -- update the after stack sizes. use the sorted items list to get the items. + for _, s_item in ipairs(comp_item.sorted_items) do + local item_id = s_item + if s_item < 0 then item_id = s_item * -1 end + local item = comp_item.items[item_id] + if max_stacks_needed > 0 then + max_stacks_needed = max_stacks_needed - 1 + comp_item.after_stack_size[item.id] = comp_item.max_stack_size elseif stack_remainder > 0 then - item.after_size = stack_remainder + comp_item.after_stack_size[item.id] = stack_remainder stack_remainder = 0 - else - item.after_size = 0 - end - end - - -- Container loop; combine item stacks in containers. - local curr_cont = nil - local curr_size = 0 - - for item_id, item in sorted_items(comp_item.items) do - - -- non-zero quantity? - if item.after_size > 0 then - - -- in a container before merge? - if item.before_cont_id then - - local before_cont = stacks.containers[item.before_cont_id] - - -- first contained item or current container full? - if not curr_cont or curr_size >= MAX_CONT_ITEMS then - - curr_cont = before_cont - curr_size = curr_cont.before_size - stacks.after_cont_ids[item.before_cont_id] = item.before_cont_id - stack_type.after_cont_ids[item.before_cont_id] = item.before_cont_id - comp_item.after_cont_ids[item.before_cont_id] = item.before_cont_id - - -- enough room in current container - else - curr_size = curr_size + 1 - before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 - end - - curr_cont.after_size = curr_size - item.after_cont_id = curr_cont.container.id - - -- not in a container before merge, container exists, and has space - elseif curr_cont and curr_size < MAX_CONT_ITEMS then - - curr_size = curr_size + 1 - curr_cont.after_size = curr_size - item.after_cont_id = curr_cont.container.id - - -- not in a container, no container exists or no space in container - else - -- do nothing - end - - -- zero after size, reduce the number of stacks in the container - elseif item.before_cont_id then - local before_cont = stacks.containers[item.before_cont_id] - before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 + elseif stack_remainder == 0 then + comp_item.after_stack_size[item.id] = stack_remainder + comp_item.after_stacks = comp_item.after_stacks - 1 + stacks_type.after_stacks = stacks_type.after_stacks - 1 end end - log(3, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d before:%5d after:%5d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) end - log(3, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end local function merge_stacks(stacks) - -- apply the stack size changes in the after_item_stack_size - -- if the after_item_stack_size is zero, then remove the item - log(3, 'Merge phase\n') - for _, stack_type in pairs(stacks.stack_types) do - for comp_key, comp_item in pairs(stack_type.comp_items) do - - for item_id, item in pairs(comp_item.items) do - - -- no items left in stack? - if item.after_size == 0 then - log(3, (' removing item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) - dfhack.items.remove(item.item) - - -- some items left in stack - elseif item.before_size ~= item.after_size then - log(3, (' updating item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) - item.item.stack_size = item.after_size - end - - -- move to a container? - if item.after_cont_id then - if (item.before_cont_id or 0) ~= item.after_cont_id then - log(3, (' moving item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) - dfhack.items.moveToContainer(item.item, stacks.containers[item.after_cont_id].container) - end + -- apply the stack size changes in the after_stack_size + -- if the after_stack_size is zero, then remove the item + log('Merge phase\n') + for _, stacks_type in pairs(stacks) do + for comp_key, comp_item in pairs(stacks_type.comp_items) do + for _, item in pairs(comp_item.items) do + if comp_item.after_stack_size[item.id] == 0 then + local remove_item = df.item.find(item.id) + dfhack.items.remove(remove_item) + elseif item.stack_size ~= comp_item.after_stack_size[item.id] then + item.stack_size = comp_item.after_stack_size[item.id] end end end @@ -489,35 +308,35 @@ end local function get_stockpile_all() -- attempt to get all the stockpiles for the fort, or exit with error -- return the stockpiles as a table - log(3, 'get_stockpile_all\n') local stockpiles = {} for _, building in pairs(df.global.world.buildings.all) do if building:getType() == df.building_type.Stockpile then table.insert(stockpiles, building) end end - dfhack.print(('Stockpile(all): %d found\n'):format(#stockpiles)) + if opts.verbose then + print(('Stockpile(all): %d found'):format(#stockpiles)) + end return stockpiles end local function get_stockpile_here() - -- attempt to get the stockpile located at the game cursor, or exit with error + -- attempt to get the selected stockpile, or exit with error -- return the stockpile as a table - log(3, 'get_stockpile_here\n') local stockpiles = {} - local pos = argparse.coords('here', 'here') - local building = dfhack.buildings.findAtTile(pos) - if not building or building:getType() ~= df.building_type.Stockpile then qerror('Stockpile not found at game cursor position.') end + local building = dfhack.gui.getSelectedStockpile() + if not building then qerror('Please select a stockpile.') end table.insert(stockpiles, building) local items = dfhack.buildings.getStockpileContents(building) - log(0, ('Stockpile(here): %s <%d> #items:%d\n'):format(building.name, building.id, #items)) + if opts.verbose then + print(('Stockpile(here): %s <%d> #items:%d'):format(building.name, building.id, #items)) + end return stockpiles end local function parse_types_opts(arg) -- check the types specified on the command line, or exit with error -- return the selected types as a table - log(3, 'parse_types_opts\n') local types = {} local div = '' local types_output = '' @@ -539,25 +358,25 @@ local function parse_types_opts(arg) for k3, v3 in pairs(v2) do types[k2][k3]=v3 end - types_output = types_output .. div .. df.item_type[types[k2].type_id] + types_output = types_output .. div .. types[k2].type_name div=', ' else qerror(('Expected: only one value for %s'):format(t)) end end end - log(0, types_output .. '\n') + dfhack.print(types_output .. '\n') return types end local function parse_commandline(opts, args) -- check the command line/exit on error, and set the defaults - log(3, 'parse_commandline\n') local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, - {'d', 'dry-run', handler=function(optarg) opts.dry_run = true end}, - {'v', 'verbose', hasArg=true, handler=function(optarg) opts.verbose = math.tointeger(optarg) or 0 end}, + {'d', 'dry-run', handler=function() opts.dry_run = true end}, + {'q', 'quiet', handler=function() opts.quiet = true end}, + {'v', 'verbose', handler=function() opts.verbose = true end}, }) -- if stockpile option is not specificed, then default to all @@ -573,10 +392,8 @@ local function parse_commandline(opts, args) if not opts.types then opts.types = valid_types_map['all'] end - end - -- main program starts here local function main() @@ -591,7 +408,7 @@ local function main() return end - local stacks = stacks_new() + local stacks = CList:new() populate_stacks(stacks, opts.all or opts.here, opts.types) @@ -601,8 +418,9 @@ local function main() merge_stacks(stacks) end - print_stacks(stacks) - + print_stacks_details(stacks) + print_stacks_summary(stacks, opts.quiet) + end if not dfhack_flags.module then diff --git a/docs/adaptation.rst b/docs/adaptation.rst index a216a30e5a..140fa81a21 100644 --- a/docs/adaptation.rst +++ b/docs/adaptation.rst @@ -3,7 +3,7 @@ adaptation .. dfhack-tool:: :summary: Adjust a unit's cave adaptation level. - :tags: untested fort armok units + :tags: unavailable fort armok units View or set level of cavern adaptation for the selected unit or the whole fort. diff --git a/docs/add-recipe.rst b/docs/add-recipe.rst index 23f7b1ca12..2263dafe39 100644 --- a/docs/add-recipe.rst +++ b/docs/add-recipe.rst @@ -3,7 +3,7 @@ add-recipe .. dfhack-tool:: :summary: Add crafting recipes to a civ. - :tags: untested adventure fort gameplay + :tags: unavailable adventure fort gameplay Civilizations pick randomly from a pool of possible recipes, which means not all civs get high boots, for instance. This script can help fix that. Only weapons, diff --git a/docs/add-thought.rst b/docs/add-thought.rst index aee8ed84a4..b2d9e5e5c8 100644 --- a/docs/add-thought.rst +++ b/docs/add-thought.rst @@ -3,7 +3,7 @@ add-thought .. dfhack-tool:: :summary: Adds a thought to the selected unit. - :tags: untested fort armok units + :tags: unavailable fort armok units Usage ----- diff --git a/docs/adv-fix-sleepers.rst b/docs/adv-fix-sleepers.rst index 2a8fe80821..befa194dc0 100644 --- a/docs/adv-fix-sleepers.rst +++ b/docs/adv-fix-sleepers.rst @@ -3,7 +3,7 @@ adv-fix-sleepers .. dfhack-tool:: :summary: Fix units who refuse to awaken in adventure mode. - :tags: untested adventure bugfix units + :tags: unavailable adventure bugfix units Use this tool if you encounter sleeping units who refuse to awaken regardless of talking to them, hitting them, or waiting so long you die of thirst diff --git a/docs/adv-max-skills.rst b/docs/adv-max-skills.rst index e08caf5026..34c7ad25c5 100644 --- a/docs/adv-max-skills.rst +++ b/docs/adv-max-skills.rst @@ -3,7 +3,7 @@ adv-max-skills .. dfhack-tool:: :summary: Raises adventurer stats to max. - :tags: untested adventure embark armok + :tags: unavailable adventure embark armok When creating an adventurer, raises all changeable skills and attributes to their maximum level. diff --git a/docs/adv-rumors.rst b/docs/adv-rumors.rst index 24105109a7..f07cb324d8 100644 --- a/docs/adv-rumors.rst +++ b/docs/adv-rumors.rst @@ -3,7 +3,7 @@ adv-rumors .. dfhack-tool:: :summary: Improves the rumors menu in adventure mode. - :tags: untested adventure interface + :tags: unavailable adventure interface In adventure mode, start a conversation with someone and then run this tool to improve the "Bring up specific incident or rumor" menu. Specifically, this diff --git a/docs/assign-minecarts.rst b/docs/assign-minecarts.rst index 81c67fb27e..abd7ff856f 100644 --- a/docs/assign-minecarts.rst +++ b/docs/assign-minecarts.rst @@ -3,7 +3,7 @@ assign-minecarts .. dfhack-tool:: :summary: Assign minecarts to hauling routes. - :tags: untested fort productivity + :tags: unavailable fort productivity This script allows you to assign minecarts to hauling routes without having to use the in-game interface. diff --git a/docs/assign-profile.rst b/docs/assign-profile.rst index 30b05eb273..c491428180 100644 --- a/docs/assign-profile.rst +++ b/docs/assign-profile.rst @@ -3,7 +3,7 @@ assign-profile .. dfhack-tool:: :summary: Adjust characteristics of a unit according to saved profiles. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool can load a profile stored in a JSON file and apply the characteristics to a unit. diff --git a/docs/autolabor-artisans.rst b/docs/autolabor-artisans.rst index fb36a25f71..7e1b662123 100644 --- a/docs/autolabor-artisans.rst +++ b/docs/autolabor-artisans.rst @@ -3,7 +3,7 @@ autolabor-artisans .. dfhack-tool:: :summary: Configures autolabor to produce artisan dwarves. - :tags: untested fort labors + :tags: unavailable fort labors This script runs an `autolabor` command for all labors where skill level influences output quality (e.g. Carpentry, Stone detailing, Weaponsmithing, diff --git a/docs/autounsuspend.rst b/docs/autounsuspend.rst deleted file mode 100644 index 2b64c54585..0000000000 --- a/docs/autounsuspend.rst +++ /dev/null @@ -1,18 +0,0 @@ -autounsuspend -============= - -.. dfhack-tool:: - :summary: Keep construction jobs unsuspended. - :tags: fort auto jobs - -This tool will unsuspend jobs that have become suspended due to inaccessible -materials, items in the way, or worker dwarves getting scared by wildlife. - -Also see `unsuspend` for one-time use. - -Usage ------ - -:: - - enable autounsuspend diff --git a/docs/binpatch.rst b/docs/binpatch.rst index c790cc1576..517af00afc 100644 --- a/docs/binpatch.rst +++ b/docs/binpatch.rst @@ -3,7 +3,7 @@ binpatch .. dfhack-tool:: :summary: Applies or removes binary patches. - :tags: untested dev + :tags: unavailable dev See `binpatches` for more info. diff --git a/docs/bodyswap.rst b/docs/bodyswap.rst index ef17510d96..b9b880eead 100644 --- a/docs/bodyswap.rst +++ b/docs/bodyswap.rst @@ -3,7 +3,7 @@ bodyswap .. dfhack-tool:: :summary: Take direct control of any visible unit. - :tags: untested adventure armok units + :tags: unavailable adventure armok units This script allows the player to take direct control of any unit present in adventure mode whilst giving up control of their current player character. diff --git a/docs/break-dance.rst b/docs/break-dance.rst index d22641559d..9a58be4c73 100644 --- a/docs/break-dance.rst +++ b/docs/break-dance.rst @@ -3,7 +3,7 @@ break-dance .. dfhack-tool:: :summary: Fixes buggy tavern dances. - :tags: untested fort bugfix units + :tags: unavailable fort bugfix units Sometimes when a unit can't find a dance partner, the dance becomes stuck and never stops. This tool can get them unstuck. diff --git a/docs/build-now.rst b/docs/build-now.rst index 759de2b603..3a3d472a5d 100644 --- a/docs/build-now.rst +++ b/docs/build-now.rst @@ -3,7 +3,7 @@ build-now .. dfhack-tool:: :summary: Instantly completes building construction jobs. - :tags: untested fort armok buildings + :tags: unavailable fort armok buildings By default, all unsuspended buildings on the map are completed, but the area of effect is configurable. diff --git a/docs/burial.rst b/docs/burial.rst index fbb3b4b60c..19e58b9b87 100644 --- a/docs/burial.rst +++ b/docs/burial.rst @@ -3,7 +3,7 @@ burial .. dfhack-tool:: :summary: Configures all unowned coffins to allow burial. - :tags: untested fort productivity buildings + :tags: unavailable fort productivity buildings Usage ----- diff --git a/docs/cannibalism.rst b/docs/cannibalism.rst index 21939991b0..75a63df0ea 100644 --- a/docs/cannibalism.rst +++ b/docs/cannibalism.rst @@ -3,7 +3,7 @@ cannibalism .. dfhack-tool:: :summary: Allows a player character to consume sapient corpses. - :tags: untested adventure gameplay + :tags: unavailable adventure gameplay This tool clears the flag from items that mark them as being from a sapient creature. Use from an adventurer's inventory screen or an individual item's diff --git a/docs/caravan.rst b/docs/caravan.rst index 98cd4f0810..4be6e00940 100644 --- a/docs/caravan.rst +++ b/docs/caravan.rst @@ -8,21 +8,40 @@ caravan This tool can help with caravans that are leaving too quickly, refuse to unload, or are just plain unhappy that you are such a poor negotiator. +Also see `force` for creating caravans. + Usage ----- :: - caravan + caravan [list] + caravan extend [ []] + caravan happy [] + caravan leave [] + caravan unload -Also see `force` for creating caravans. +Commands listed with the argument ``[]`` can take multiple +(space-separated) caravan IDs (see ``caravan list`` to get the IDs). If no IDs +are specified, then the commands apply to all caravans on the map. Examples -------- +``caravan`` + List IDs and information about all caravans on the map. ``caravan extend`` Force a caravan that is leaving to return to the depot and extend their stay another 7 days. +``caravan extend 30 0 1`` + Extend the time that caravans 0 and 1 stay at the depot by 30 days. If the + caravans have already started leaving, they will return to the depot. +``caravan happy`` + Make the active caravans willing to trade again (after seizing goods, + annoying merchants, etc.). If the caravan has already started leaving in a + huff, they will return to the depot. +``caravan leave`` + Makes caravans pack up and leave immediately. ``caravan unload`` Fix a caravan that got spooked by wildlife and refuses to fully unload. @@ -31,35 +50,19 @@ Overlay Additional functionality is provided when the trade screen is open via an `overlay` widget: -- ``Shift+Click checkbox``: Select all items inside a bin without selecting the bin itself -- ``Ctrl+Click checkbox``: Collapse a single bin (as is possible in the "Move goods to/from depot" screen) -- ``Ctrl+c``: Collapses all bins. The hotkey hint can also be clicked as though it were a button. -- ``Ctrl+x``: Collapses everything (all item categories and anything collapsible within each category). - The hotkey hint can also be clicked as though it were a button. - -There is also a reminder of the fast scroll functionality provided by the vanilla game when you hold shift -while scrolling (this works everywhere). - -The overlay is named ``caravan.tradeScreenExtension`` in ``gui/overlay``. - -Commands --------- - -Commands listed with the argument ``[]`` can take multiple -(space-separated) caravan IDs (see ``caravan list``). If no IDs are specified, -then the commands apply to all caravans on the map. - -``list`` - List IDs and information about all caravans on the map. -``extend [ []]`` - Extend the time that caravans stay at the depot by the specified number of - days (defaults to 7). Also causes caravans to return to the depot if - applicable. -``happy []`` - Make caravans willing to trade again (after seizing goods, annoying - merchants, etc.). Also causes caravans to return to the depot if applicable. -``leave []`` - Makes caravans pack up and leave immediately. -``unload`` - Fix endless unloading at the depot. Run this if merchant pack animals were - startled and now refuse to come to the trade depot. +- ``Shift+Click checkbox``: Select all items inside a bin without selecting the + bin itself +- ``Ctrl+Click checkbox``: Collapse or expand a single bin (as is possible in + the "Move goods to/from depot" screen) +- ``Ctrl+c``: Collapses all bins. The hotkey hint can also be clicked as though + it were a button. +- ``Ctrl+x``: Collapses everything (all item categories and anything + collapsible within each category). The hotkey hint can also be clicked as + though it were a button. + +There is also a reminder of the fast scroll functionality provided by the +vanilla game when you hold shift while scrolling (this works everywhere). + +You can turn the overlay on and off in `gui/control-panel`, or you can +reposition it to your liking with `gui/overlay`. The overlay is named +``caravan.tradeScreenExtension``. diff --git a/docs/color-schemes.rst b/docs/color-schemes.rst index a3375487f0..13f13ee145 100644 --- a/docs/color-schemes.rst +++ b/docs/color-schemes.rst @@ -3,7 +3,7 @@ color-schemes .. dfhack-tool:: :summary: Modify the colors used by the DF UI. - :tags: untested fort gameplay graphics + :tags: unavailable fort gameplay graphics This tool allows you to set exactly which shades of colors should be used in the DF interface color palette. diff --git a/docs/combat-harden.rst b/docs/combat-harden.rst index 6331e95b80..f77ff1f979 100644 --- a/docs/combat-harden.rst +++ b/docs/combat-harden.rst @@ -3,7 +3,7 @@ combat-harden .. dfhack-tool:: :summary: Set the combat-hardened value on a unit. - :tags: untested fort armok military units + :tags: unavailable fort armok military units This tool can make a unit care more/less about seeing corpses. diff --git a/docs/combine.rst b/docs/combine.rst index bb53b14444..e9ca96aa39 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -23,14 +23,14 @@ Examples ``combine all --types=meat,plant`` Merge ``meat`` and ``plant`` type stacks in all stockpiles. ``combine here`` - Merge stacks in stockpile located at game cursor. + Merge stacks in the selected stockpile. Commands -------- ``all`` Search all stockpiles. ``here`` - Search the stockpile under the game cursor. + Search the currently selected stockpile. Options ------- @@ -55,9 +55,8 @@ Options ``plant``: PLANT and PLANT_GROWTH - ``powders``: POWDERS_MISC +``-q``, ``--quiet`` + Only print changes instead of a summary of all processed stockpiles. - ``seeds``: SEEDS - -``-v``, ``--verbose [0-3]`` - Print verbose output, level from 0 to 3. +``-v``, ``--verbose`` + Print verbose output. diff --git a/docs/deteriorate.rst b/docs/deteriorate.rst index 4a7e663e9c..1e0b49f944 100644 --- a/docs/deteriorate.rst +++ b/docs/deteriorate.rst @@ -3,7 +3,7 @@ deteriorate .. dfhack-tool:: :summary: Cause corpses, clothes, and/or food to rot away over time. - :tags: untested fort auto fps gameplay items plants + :tags: unavailable fort auto fps gameplay items plants When enabled, this script will cause the specified item types to slowly rot away. By default, items disappear after a few months, but you can choose to slow diff --git a/docs/devel/block-borders.rst b/docs/devel/block-borders.rst index 8e87a00ce4..b13f5b074a 100644 --- a/docs/devel/block-borders.rst +++ b/docs/devel/block-borders.rst @@ -3,7 +3,7 @@ devel/block-borders .. dfhack-tool:: :summary: Outline map blocks on the map screen. - :tags: untested dev map + :tags: unavailable dev map This tool displays an overlay that highlights the borders of map blocks. See :doc:`/docs/api/Maps` for details on map blocks. diff --git a/docs/devel/cmptiles.rst b/docs/devel/cmptiles.rst index c98c0b7c08..c10949e9e7 100644 --- a/docs/devel/cmptiles.rst +++ b/docs/devel/cmptiles.rst @@ -3,7 +3,7 @@ devel/cmptiles .. dfhack-tool:: :summary: List or compare two tiletype material groups. - :tags: untested dev + :tags: unavailable dev Lists and/or compares two tiletype material groups. You can see the list of valid material groups by running:: diff --git a/docs/devel/dump-offsets.rst b/docs/devel/dump-offsets.rst index 63ad92307e..80748e0d32 100644 --- a/docs/devel/dump-offsets.rst +++ b/docs/devel/dump-offsets.rst @@ -3,7 +3,7 @@ devel/dump-offsets .. dfhack-tool:: :summary: Dump the contents of the table of global addresses. - :tags: untested dev + :tags: unavailable dev .. warning:: diff --git a/docs/devel/export-dt-ini.rst b/docs/devel/export-dt-ini.rst index 81ba6d9ea0..34f6f05e88 100644 --- a/docs/devel/export-dt-ini.rst +++ b/docs/devel/export-dt-ini.rst @@ -3,7 +3,7 @@ devel/export-dt-ini .. dfhack-tool:: :summary: Export memory addresses for Dwarf Therapist configuration. - :tags: untested dev + :tags: unavailable dev This tool exports an ini file containing memory addresses for Dwarf Therapist. diff --git a/docs/devel/find-offsets.rst b/docs/devel/find-offsets.rst index 9034c135b4..160253ebf8 100644 --- a/docs/devel/find-offsets.rst +++ b/docs/devel/find-offsets.rst @@ -3,7 +3,7 @@ devel/find-offsets .. dfhack-tool:: :summary: Find memory offsets of DF data structures. - :tags: untested dev + :tags: unavailable dev .. warning:: diff --git a/docs/devel/find-primitive.rst b/docs/devel/find-primitive.rst index 2887d073b1..d162560e07 100644 --- a/docs/devel/find-primitive.rst +++ b/docs/devel/find-primitive.rst @@ -3,7 +3,7 @@ devel/find-primitive .. dfhack-tool:: :summary: Discover memory offsets for new variables. - :tags: untested dev + :tags: unavailable dev This tool helps find a primitive variable in DF's data section, relying on the user to change its value and then scanning for memory that has changed to that diff --git a/docs/devel/find-twbt.rst b/docs/devel/find-twbt.rst index f0eded1450..1d10c12c1e 100644 --- a/docs/devel/find-twbt.rst +++ b/docs/devel/find-twbt.rst @@ -3,7 +3,7 @@ devel/find-twbt .. dfhack-tool:: :summary: Display the memory offsets of some important TWBT functions. - :tags: untested dev + :tags: unavailable dev Finds some TWBT-related offsets - currently just ``twbt_render_map``. diff --git a/docs/devel/inject-raws.rst b/docs/devel/inject-raws.rst index e1ef56708e..42dd30ce3a 100644 --- a/docs/devel/inject-raws.rst +++ b/docs/devel/inject-raws.rst @@ -3,7 +3,7 @@ devel/inject-raws .. dfhack-tool:: :summary: Add objects and reactions into an existing world. - :tags: untested dev + :tags: unavailable dev WARNING: THIS SCRIPT CAN PERMANENTLY DAMAGE YOUR SAVE. diff --git a/docs/devel/kill-hf.rst b/docs/devel/kill-hf.rst index d9435c7bc0..9e9a29f2d2 100644 --- a/docs/devel/kill-hf.rst +++ b/docs/devel/kill-hf.rst @@ -3,7 +3,7 @@ devel/kill-hf .. dfhack-tool:: :summary: Kill a historical figure. - :tags: untested dev + :tags: unavailable dev This tool can kill the specified historical figure, even if off-site, or terminate a pregnancy. Useful for working around :bug:`11549`. diff --git a/docs/devel/light.rst b/docs/devel/light.rst index 095b127d7a..ce175767cd 100644 --- a/docs/devel/light.rst +++ b/docs/devel/light.rst @@ -3,7 +3,7 @@ devel/light .. dfhack-tool:: :summary: Experiment with lighting overlays. - :tags: untested dev graphics + :tags: unavailable dev graphics This is an experimental lighting engine for DF, using the `rendermax` plugin. diff --git a/docs/devel/list-filters.rst b/docs/devel/list-filters.rst index 893b6a6281..36642018f6 100644 --- a/docs/devel/list-filters.rst +++ b/docs/devel/list-filters.rst @@ -3,7 +3,7 @@ devel/list-filters .. dfhack-tool:: :summary: List input items for the selected building type. - :tags: untested dev + :tags: unavailable dev This tool lists input items for the building that is currently being built. You must be in build mode and have a building type selected for placement. This is diff --git a/docs/devel/lsmem.rst b/docs/devel/lsmem.rst index e1a897062b..f046e4f41d 100644 --- a/docs/devel/lsmem.rst +++ b/docs/devel/lsmem.rst @@ -3,7 +3,7 @@ devel/lsmem .. dfhack-tool:: :summary: Print memory ranges of the DF process. - :tags: untested dev + :tags: unavailable dev Useful for checking whether a pointer is valid, whether a certain library/plugin is loaded, etc. diff --git a/docs/devel/lua-example.rst b/docs/devel/lua-example.rst index 9df8ef3d9b..63634a9931 100644 --- a/docs/devel/lua-example.rst +++ b/docs/devel/lua-example.rst @@ -3,7 +3,7 @@ devel/lua-example .. dfhack-tool:: :summary: An example lua script. - :tags: untested dev + :tags: unavailable dev This is an example Lua script which just reports the number of times it has been called. Useful for testing environment persistence. diff --git a/docs/devel/luacov.rst b/docs/devel/luacov.rst index b948bc9fcb..e3af3e7899 100644 --- a/docs/devel/luacov.rst +++ b/docs/devel/luacov.rst @@ -3,7 +3,7 @@ devel/luacov .. dfhack-tool:: :summary: Lua script coverage report generator. - :tags: untested dev + :tags: unavailable dev This script generates a coverage report from collected statistics. By default it reports on every Lua file in all of DFHack. To filter filenames, specify one or diff --git a/docs/devel/nuke-items.rst b/docs/devel/nuke-items.rst index 21e7dc07cf..26c6dd6a84 100644 --- a/docs/devel/nuke-items.rst +++ b/docs/devel/nuke-items.rst @@ -3,7 +3,7 @@ devel/nuke-items .. dfhack-tool:: :summary: Deletes all free items in the game. - :tags: untested dev fps items + :tags: unavailable dev fps items This tool deletes **ALL** items not referred to by units, buildings, or jobs. Intended solely for lag investigation. diff --git a/docs/devel/prepare-save.rst b/docs/devel/prepare-save.rst index a795392c81..7498e92840 100644 --- a/docs/devel/prepare-save.rst +++ b/docs/devel/prepare-save.rst @@ -3,7 +3,7 @@ devel/prepare-save .. dfhack-tool:: :summary: Set internal game state to known values for memory analysis. - :tags: untested dev + :tags: unavailable dev .. warning:: diff --git a/docs/devel/print-args.rst b/docs/devel/print-args.rst index f4be5130d5..f5ad8af85b 100644 --- a/docs/devel/print-args.rst +++ b/docs/devel/print-args.rst @@ -3,7 +3,7 @@ devel/print-args .. dfhack-tool:: :summary: Echo parameters to the output. - :tags: untested dev + :tags: unavailable dev Prints all the arguments you supply to the script, one per line. diff --git a/docs/devel/print-args2.rst b/docs/devel/print-args2.rst index baf4d35534..aac9b27f94 100644 --- a/docs/devel/print-args2.rst +++ b/docs/devel/print-args2.rst @@ -3,7 +3,7 @@ devel/print-args2 .. dfhack-tool:: :summary: Echo parameters to the output. - :tags: untested dev + :tags: unavailable dev Prints all the arguments you supply to the script, one per line, with quotes around them. diff --git a/docs/devel/print-event.rst b/docs/devel/print-event.rst index 450b019383..7d1bb97945 100644 --- a/docs/devel/print-event.rst +++ b/docs/devel/print-event.rst @@ -3,7 +3,7 @@ devel/print-event .. dfhack-tool:: :summary: Show historical events. - :tags: untested dev + :tags: unavailable dev This tool displays the description of a historical event. diff --git a/docs/devel/save-version.rst b/docs/devel/save-version.rst index 01ce14ae74..5b33d6680f 100644 --- a/docs/devel/save-version.rst +++ b/docs/devel/save-version.rst @@ -3,7 +3,7 @@ devel/save-version .. dfhack-tool:: :summary: Display what DF version has handled the current save. - :tags: untested dev + :tags: unavailable dev This tool displays the DF version that created the game, the most recent DF version that has loaded and saved the game, and the current DF version. diff --git a/docs/devel/sc.rst b/docs/devel/sc.rst index b7e394459f..3a7df80547 100644 --- a/docs/devel/sc.rst +++ b/docs/devel/sc.rst @@ -3,7 +3,7 @@ devel/sc .. dfhack-tool:: :summary: Scan DF structures for errors. - :tags: untested dev + :tags: unavailable dev Size Check: scans structures for invalid vectors, misaligned structures, and unidentified enum values. diff --git a/docs/devel/test-perlin.rst b/docs/devel/test-perlin.rst index b17dfdb981..5f8deceef1 100644 --- a/docs/devel/test-perlin.rst +++ b/docs/devel/test-perlin.rst @@ -3,7 +3,7 @@ devel/test-perlin .. dfhack-tool:: :summary: Generate an image based on perlin noise. - :tags: untested dev + :tags: unavailable dev Generates an image using multiple octaves of perlin noise. diff --git a/docs/devel/unit-path.rst b/docs/devel/unit-path.rst index 76f58fc99d..8bca45cc46 100644 --- a/docs/devel/unit-path.rst +++ b/docs/devel/unit-path.rst @@ -3,7 +3,7 @@ devel/unit-path .. dfhack-tool:: :summary: Inspect where a unit is going and how it's getting there. - :tags: untested dev + :tags: unavailable dev When run with a unit selected, the path that the unit is currently following is highlighted on the map. You can jump between the unit and the destination tile. diff --git a/docs/devel/watch-minecarts.rst b/docs/devel/watch-minecarts.rst index 11933d8ecb..6b915a5aa1 100644 --- a/docs/devel/watch-minecarts.rst +++ b/docs/devel/watch-minecarts.rst @@ -3,7 +3,7 @@ devel/watch-minecarts .. dfhack-tool:: :summary: Inspect minecart coordinates and speeds. - :tags: untested dev + :tags: unavailable dev When running, this tool will log minecart coordinates and speeds to the console. diff --git a/docs/do-job-now.rst b/docs/do-job-now.rst index ca85121da9..291defd133 100644 --- a/docs/do-job-now.rst +++ b/docs/do-job-now.rst @@ -3,7 +3,7 @@ do-job-now .. dfhack-tool:: :summary: Mark the job related to what you're looking at as high priority. - :tags: untested fort productivity jobs + :tags: unavailable fort productivity jobs The script will try its best to find a job related to the selected entity (which can be a job, dwarf, animal, item, building, plant or work order) and then mark diff --git a/docs/dwarf-op.rst b/docs/dwarf-op.rst index ac3dcd41b3..4b60a88802 100644 --- a/docs/dwarf-op.rst +++ b/docs/dwarf-op.rst @@ -3,7 +3,7 @@ dwarf-op .. dfhack-tool:: :summary: Tune units to perform underrepresented job roles in your fortress. - :tags: untested fort armok units + :tags: unavailable fort armok units ``dwarf-op`` examines the distribution of skills and attributes across the dwarves in your fortress and can rewrite the characteristics of a dwarf (or diff --git a/docs/embark-skills.rst b/docs/embark-skills.rst index 7e9d832efe..a43c5b2672 100644 --- a/docs/embark-skills.rst +++ b/docs/embark-skills.rst @@ -3,7 +3,7 @@ embark-skills .. dfhack-tool:: :summary: Adjust dwarves' skills when embarking. - :tags: untested embark fort armok units + :tags: unavailable embark fort armok units When selecting starting skills for your dwarves on the embark screen, this tool can manipulate the skill values or adjust the number of points you have diff --git a/docs/exportlegends.rst b/docs/exportlegends.rst index 6121c177e8..33bf20afd7 100644 --- a/docs/exportlegends.rst +++ b/docs/exportlegends.rst @@ -3,7 +3,7 @@ exportlegends .. dfhack-tool:: :summary: Exports legends data for external viewing. - :tags: untested legends inspection + :tags: unavailable legends inspection When run from legends mode, you can export detailed data about your world so that it can be browsed with external programs like diff --git a/docs/exterminate.rst b/docs/exterminate.rst index a84e27d65a..1e2609a81c 100644 --- a/docs/exterminate.rst +++ b/docs/exterminate.rst @@ -2,29 +2,22 @@ exterminate =========== .. dfhack-tool:: - :summary: Kills creatures. + :summary: Kill things. :tags: fort armok units -Kills any unit, or all units of a given race. You can target any unit on a -revealed tile of the map, including ambushers, but caged/chained creatures are -ignored. - -If ``method`` is specified, ``exterminate`` will kill the selected units -using the provided method. ``Instant`` will instantly kill the units. -``Butcher`` will mark the units for butchering, not kill them, useful for pets -and not for armed enemies. ``Drown`` and ``Magma`` will spawn a 7/7 column of -water or magma on the units respectively, cleaning up the liquid as they move -and die. Magma not recommended for magma-safe creatures... +Kills any unit, or all undead, or all units of a given race. You can target any +unit on a revealed tile of the map, including ambushers, but caged/chained +creatures cannot be killed with this tool. Usage ----- -``exterminate`` - List the available targets. -``exterminate this []`` - Kills the selected unit, instantly by default. -``exterminate [:] []`` - Kills all available units of the specified race, or all undead units. +:: + + exterminate + exterminate this [] + exterminate undead [] + exterminate [:] [] Examples -------- @@ -35,27 +28,43 @@ Examples List the targets on your map. ``exterminate BIRD_RAVEN:MALE`` Kill the ravens flying around the map (but only the male ones). -``exterminate GOBLIN --method MAGMA --only-visible --only-hostile`` - Kill all visible, hostile goblins on the map by drowning them in magma. +``exterminate GOBLIN --method magma --only-visible`` + Kill all visible, hostile goblins on the map by boiling them in magma. Options ------- ``-m``, ``--method `` - Specifies the "method" of killing units. + Specifies the "method" of killing units. See below for details. ``-o``, ``--only-visible`` - Specifies the tool should only kill units visible to the player + Specifies the tool should only kill units visible to the player. on the map. ``-f``, ``--include-friendly`` Specifies the tool should also kill units friendly to the player. +Methods +------- + +`exterminate` can kill units using any of the following methods: + +:instant: Kill by blood loss, and if this is ineffective, then kill by + vaporization (default). +:vaporize: Make the unit disappear in a puff of smoke. Note that units killed + this way will not leave a corpse behind, but any items they were carrying + will still drop. +:drown: Drown the unit in water. +:magma: Boil the unit in magma (not recommended for magma-safe creatures). +:butcher: Will mark the units for butchering instead of killing them. This is + more useful for pets than armed enemies. + Technical details ----------------- This tool kills by setting a unit's ``blood_count`` to 0, which means immediate death at the next game tick. For creatures where this is not enough, -such as vampires, it also sets animal.vanish_countdown to 2. +such as vampires, it also sets ``animal.vanish_countdown``, allowing the unit +to vanish in a puff of smoke if the blood loss doesn't kill them. -The script drowns units in the liquid of choice by modifying the tile with a -liquid level of 7 every tick. If the unit moves, the liquid moves along with +If the method of choice involves liquids, the tile is filled with a liquid +level of 7 every tick. If the target unit moves, the liquid moves along with it, leaving the vacated tiles clean. diff --git a/docs/fix-ster.rst b/docs/fix-ster.rst index b6e0ca7c65..d21e62f725 100644 --- a/docs/fix-ster.rst +++ b/docs/fix-ster.rst @@ -3,7 +3,7 @@ fix-ster .. dfhack-tool:: :summary: Toggle infertility for units. - :tags: untested fort armok animals + :tags: unavailable fort armok animals Now you can restore fertility to infertile creatures or inflict infertility on creatures that you do not want to breed. diff --git a/docs/fix/corrupt-equipment.rst b/docs/fix/corrupt-equipment.rst index 5e7db15051..e9cc0e16b9 100644 --- a/docs/fix/corrupt-equipment.rst +++ b/docs/fix/corrupt-equipment.rst @@ -3,7 +3,7 @@ fix/corrupt-equipment .. dfhack-tool:: :summary: Fixes some game crashes caused by corrupt military equipment. - :tags: untested fort bugfix military + :tags: unavailable fort bugfix military This fix corrects some kinds of corruption that can occur in equipment lists, as in :bug:`11014`. Run this script at least every time a squad comes back from a diff --git a/docs/fix/general-strike.rst b/docs/fix/general-strike.rst new file mode 100644 index 0000000000..7bdd5ccf2c --- /dev/null +++ b/docs/fix/general-strike.rst @@ -0,0 +1,25 @@ +fix/general-strike +================== + +.. dfhack-tool:: + :summary: Prevent dwarves from getting stuck and refusing to work. + :tags: fort bugfix + +This script attempts to fix known causes of the "general strike bug", where +dwarves just stop accepting work and stand around with "No job". + +You can enable automatic running of this fix in the "Maintenance" tab of +`gui/control-panel`. + +Usage +----- + +:: + + fix/general-strike [] + +Options +------- + +``-q``, ``--quiet`` + Only output status when something was actually fixed. diff --git a/docs/fix/item-occupancy.rst b/docs/fix/item-occupancy.rst index 635b67420c..193d3899aa 100644 --- a/docs/fix/item-occupancy.rst +++ b/docs/fix/item-occupancy.rst @@ -3,7 +3,7 @@ fix/item-occupancy .. dfhack-tool:: :summary: Fixes errors with phantom items occupying site. - :tags: untested fort bugfix map + :tags: unavailable fort bugfix map This tool diagnoses and fixes issues with nonexistent 'items occupying site', usually caused by hacking mishaps with items being improperly moved about. diff --git a/docs/fix/population-cap.rst b/docs/fix/population-cap.rst index 02039280ff..59fcbdb2ea 100644 --- a/docs/fix/population-cap.rst +++ b/docs/fix/population-cap.rst @@ -3,7 +3,7 @@ fix/population-cap .. dfhack-tool:: :summary: Ensure the population cap is respected. - :tags: untested fort bugfix units + :tags: unavailable fort bugfix units Run this after every migrant wave to ensure your population cap is not exceeded. diff --git a/docs/fix/tile-occupancy.rst b/docs/fix/tile-occupancy.rst index 0fc869f1c7..25210f6121 100644 --- a/docs/fix/tile-occupancy.rst +++ b/docs/fix/tile-occupancy.rst @@ -3,7 +3,7 @@ fix/tile-occupancy .. dfhack-tool:: :summary: Fix tile occupancy flags. - :tags: untested fort bugfix map + :tags: unavailable fort bugfix map This tool clears bad occupancy flags at the selected tile. It is useful for getting rid of phantom "building present" messages when trying to build diff --git a/docs/fixnaked.rst b/docs/fixnaked.rst index 3e1feab24a..c416c35694 100644 --- a/docs/fixnaked.rst +++ b/docs/fixnaked.rst @@ -3,7 +3,7 @@ fixnaked .. dfhack-tool:: :summary: Removes all unhappy thoughts due to lack of clothing. - :tags: untested fort armok units + :tags: unavailable fort armok units If you're having trouble keeping your dwarves properly clothed and the stress is mounting, this tool can help you calm things down. ``fixnaked`` will go through diff --git a/docs/flashstep.rst b/docs/flashstep.rst index 4a39847dcb..888ae36826 100644 --- a/docs/flashstep.rst +++ b/docs/flashstep.rst @@ -3,7 +3,7 @@ flashstep .. dfhack-tool:: :summary: Teleport your adventurer to the cursor. - :tags: untested adventure armok + :tags: unavailable adventure armok ``flashstep`` is a hotkey-friendly teleport that places your adventurer where your cursor is. diff --git a/docs/forget-dead-body.rst b/docs/forget-dead-body.rst index 2f42851a30..494555c0e7 100644 --- a/docs/forget-dead-body.rst +++ b/docs/forget-dead-body.rst @@ -3,7 +3,7 @@ forget-dead-body .. dfhack-tool:: :summary: Removes emotions associated with seeing a dead body. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool can help your dwarves recover from seeing a massacre. It removes all emotions associated with seeing a dead body. If your dwarves are traumatized and diff --git a/docs/forum-dwarves.rst b/docs/forum-dwarves.rst index a206ca06ec..fd3f34fcbc 100644 --- a/docs/forum-dwarves.rst +++ b/docs/forum-dwarves.rst @@ -3,7 +3,7 @@ forum-dwarves .. dfhack-tool:: :summary: Exports the text you see on the screen for posting to the forums. - :tags: untested dfhack + :tags: unavailable dfhack This tool saves a copy of a text screen, formatted in BBcode for posting to the Bay12 Forums. Text color and layout is preserved. See `markdown` if you want to diff --git a/docs/gaydar.rst b/docs/gaydar.rst index 863bf2af19..9067a19098 100644 --- a/docs/gaydar.rst +++ b/docs/gaydar.rst @@ -3,7 +3,7 @@ gaydar .. dfhack-tool:: :summary: Shows the sexual orientation of units. - :tags: untested fort inspection animals units + :tags: unavailable fort inspection animals units ``gaydar`` is useful for social engineering or checking the viability of livestock breeding programs. diff --git a/docs/ghostly.rst b/docs/ghostly.rst index 93464a080e..0afc2dd854 100644 --- a/docs/ghostly.rst +++ b/docs/ghostly.rst @@ -3,7 +3,7 @@ ghostly .. dfhack-tool:: :summary: Toggles an adventurer's ghost status. - :tags: untested adventure armok units + :tags: unavailable adventure armok units This is useful for walking through walls, avoiding attacks, or recovering after a death. diff --git a/docs/growcrops.rst b/docs/growcrops.rst index b7b1374d27..c53d4b5b67 100644 --- a/docs/growcrops.rst +++ b/docs/growcrops.rst @@ -3,7 +3,7 @@ growcrops .. dfhack-tool:: :summary: Instantly grow planted seeds into crops. - :tags: untested fort armok plants + :tags: unavailable fort armok plants With no parameters, this command lists the seed types currently planted in your farming plots. With a seed type, the script will grow those seeds, ready to be diff --git a/docs/gui/advfort.rst b/docs/gui/advfort.rst index 487d46900d..7575213b5b 100644 --- a/docs/gui/advfort.rst +++ b/docs/gui/advfort.rst @@ -3,7 +3,7 @@ gui/advfort .. dfhack-tool:: :summary: Perform fort-like jobs in adventure mode. - :tags: untested adventure gameplay + :tags: unavailable adventure gameplay This script allows performing jobs in adventure mode. For interactive help, press :kbd:`?` while the script is running. diff --git a/docs/gui/autogems.rst b/docs/gui/autogems.rst index 9eb91a4556..64f61c0fad 100644 --- a/docs/gui/autogems.rst +++ b/docs/gui/autogems.rst @@ -4,7 +4,7 @@ gui/autogems .. dfhack-tool:: :summary: Automatically cut rough gems. - :tags: untested fort auto workorders + :tags: unavailable fort auto workorders This is a frontend for the `autogems` plugin that allows interactively configuring the gem types that you want to be cut. diff --git a/docs/gui/choose-weapons.rst b/docs/gui/choose-weapons.rst index 522a4da0ea..acd197fb29 100644 --- a/docs/gui/choose-weapons.rst +++ b/docs/gui/choose-weapons.rst @@ -3,7 +3,7 @@ gui/choose-weapons .. dfhack-tool:: :summary: Ensure military dwarves choose appropriate weapons. - :tags: untested fort productivity military + :tags: unavailable fort productivity military Activate in the :guilabel:`Equip->View/Customize` page of the military screen. diff --git a/docs/gui/civ-alert.rst b/docs/gui/civ-alert.rst new file mode 100644 index 0000000000..cc87518550 --- /dev/null +++ b/docs/gui/civ-alert.rst @@ -0,0 +1,59 @@ +gui/civ-alert +============= + +.. dfhack-tool:: + :summary: Quickly get your civilians to safety. + :tags: fort gameplay interface military units + +Normally, assigning a unit to a burrow is treated more like a suggestion than a +command. This can be frustrating when you're assigning units to burrows in +order to get them out of danger. In contrast, triggering a civilian alert with +`gui/civ-alert` will cause all your non-military citizens to immediately rush +to a burrow ASAP and stay there. This gives you a way to keep your civilians +safe when there is danger about. + +Usage +----- + +:: + + gui/civ-alert + +How to set up and use a civilian alert +-------------------------------------- + +A civ alert needs a burrow to send civilians to. Go set one up if you haven't +already. If you have walls around a secure interior, you can include all your +below-ground area and the safe parts inside your walls. You can name the burrow +"Inside" or "Safety" or "Panic room" or whatever you like. + +Then, start up `gui/civ-alert` and select the burrow from the list. You can +activate the civ alert right away with the button in the upper right corner. +You can also access this button at any time from the squads panel. + +When danger appears, open up the squads menu and click on the new "Activate +civilian alert" button in the lower left corner. It's big and red; you can't +miss it. Your civilians will rush off to safety and you can concentrate on +dealing with the incursion without Urist McArmorsmith getting in the way. + +When the civ alert is active, the civilian alert button will stay on the +screen, even if the squads menu is closed. After the danger has passed, +remember to turn the civ alert off again by clicking the button. Otherwise, +your units will continue to be confined to their burrow and may eventually +become unhappy or starve. + +Overlay +------- + +The position of the "Activate civilian alert" button that appears when the +squads panel is open is configurable via `gui/overlay`. The overlay panel also +gives you a way to launch `gui/civ-alert` if you need to change which burrow +civilians should be gathering at. + +Technical notes +--------------- + +The functionality for civilian alerts is actually already inside the vanilla +game. The ability to configure civilian alerts was lost when the DF UI was +updated for the v50 release. This tool simply provides an interface layer for +the vanilla functionality. diff --git a/docs/gui/clone-uniform.rst b/docs/gui/clone-uniform.rst index ba24e21b1d..f3bbe94da4 100644 --- a/docs/gui/clone-uniform.rst +++ b/docs/gui/clone-uniform.rst @@ -3,7 +3,7 @@ gui/clone-uniform .. dfhack-tool:: :summary: Duplicate an existing military uniform. - :tags: untested fort productivity military + :tags: unavailable fort productivity military When invoked, this tool duplicates the currently selected uniform template and selects the newly created copy. Activate in the :guilabel:`Uniforms` page of the diff --git a/docs/gui/color-schemes.rst b/docs/gui/color-schemes.rst index f7d1eb7193..83e99a021b 100644 --- a/docs/gui/color-schemes.rst +++ b/docs/gui/color-schemes.rst @@ -3,7 +3,7 @@ gui/color-schemes .. dfhack-tool:: :summary: Modify the colors in the DF UI. - :tags: untested graphics + :tags: unavailable graphics This is an in-game interface for `color-schemes`, which allows you to modify the colors in the Dwarf Fortress interface. This script must be called from either diff --git a/docs/gui/companion-order.rst b/docs/gui/companion-order.rst index 0bffeaef49..c55ebf038b 100644 --- a/docs/gui/companion-order.rst +++ b/docs/gui/companion-order.rst @@ -3,7 +3,7 @@ gui/companion-order .. dfhack-tool:: :summary: Issue orders to companions. - :tags: untested adventure interface + :tags: unavailable adventure interface This tool allows you to issue orders to your adventurer's companions. Select which companions to issue orders to with lower case letters (green when diff --git a/docs/gui/confirm.rst b/docs/gui/confirm.rst index f6a77e16bc..644581cd88 100644 --- a/docs/gui/confirm.rst +++ b/docs/gui/confirm.rst @@ -3,11 +3,10 @@ gui/confirm .. dfhack-tool:: :summary: Configure which confirmation dialogs are enabled. - :tags: fort productivity interface + :tags: fort interface This tool is a basic configuration interface for the `confirm` plugin. You can -see current state, and you can interactively choose which confirmation dialogs -are enabled. +see and modify which confirmation dialogs are enabled. Usage ----- diff --git a/docs/gui/cp437-table.rst b/docs/gui/cp437-table.rst index 65ca7995be..c092c34e0d 100644 --- a/docs/gui/cp437-table.rst +++ b/docs/gui/cp437-table.rst @@ -9,7 +9,7 @@ This tool provides an in-game virtual keyboard. You can choose from all the characters that DF supports (code page 437). Just click on the characters to build the text that you want to send to the parent screen. The text is sent as soon as you hit :kbd:`Enter`, so make sure there is a text field selected -before starting this UI! +in the parent window before starting this UI! Usage ----- diff --git a/docs/gui/create-tree.rst b/docs/gui/create-tree.rst index 7a20d8d04c..78ef616998 100644 --- a/docs/gui/create-tree.rst +++ b/docs/gui/create-tree.rst @@ -3,7 +3,7 @@ gui/create-tree .. dfhack-tool:: :summary: Create a tree. - :tags: untested fort armok plants + :tags: unavailable fort armok plants This tool provides a graphical interface for creating trees. diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 2c2e2feb8b..5bbbce4ecc 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -4,7 +4,7 @@ gui/design .. dfhack-tool:: :summary: Design designation utility with shapes. - :tags: fort design productivity map + This tool provides a point and click interface to make designating shapes and patterns easier. Supports both digging designations and placing constructions. diff --git a/docs/gui/dfstatus.rst b/docs/gui/dfstatus.rst index 085c4a9137..be45390ca7 100644 --- a/docs/gui/dfstatus.rst +++ b/docs/gui/dfstatus.rst @@ -3,7 +3,7 @@ gui/dfstatus .. dfhack-tool:: :summary: Show a quick overview of critical stock quantities. - :tags: untested fort inspection + :tags: unavailable fort inspection This tool show a quick overview of stock quantities for: diff --git a/docs/gui/extended-status.rst b/docs/gui/extended-status.rst index fc5c6380f1..52e2a086c5 100644 --- a/docs/gui/extended-status.rst +++ b/docs/gui/extended-status.rst @@ -3,7 +3,7 @@ gui/extended-status .. dfhack-tool:: :summary: Add information on beds and bedrooms to the status screen. - :tags: untested fort inspection interface + :tags: unavailable fort inspection interface Adds an additional page to the ``z`` status screen where you can see information about beds, bedrooms, and whether your dwarves have bedrooms of their own. diff --git a/docs/gui/family-affairs.rst b/docs/gui/family-affairs.rst index 1661622af6..170e2a1a9d 100644 --- a/docs/gui/family-affairs.rst +++ b/docs/gui/family-affairs.rst @@ -3,7 +3,7 @@ gui/family-affairs .. dfhack-tool:: :summary: Inspect or meddle with romantic relationships. - :tags: untested fort armok inspection units + :tags: unavailable fort armok inspection units This tool provides a user-friendly interface to view romantic relationships, with the ability to add, remove, or otherwise change them at your whim - diff --git a/docs/gui/guide-path.rst b/docs/gui/guide-path.rst index 65335d5fc4..5d76993cb8 100644 --- a/docs/gui/guide-path.rst +++ b/docs/gui/guide-path.rst @@ -3,7 +3,7 @@ gui/guide-path .. dfhack-tool:: :summary: Visualize minecart guide paths. - :tags: untested fort inspection map + :tags: unavailable fort inspection map This tool displays the cached path that will be used by the minecart guide order. The game computes this path when the order is executed for the first diff --git a/docs/gui/kitchen-info.rst b/docs/gui/kitchen-info.rst index 7ea5338b2a..86584a88b7 100644 --- a/docs/gui/kitchen-info.rst +++ b/docs/gui/kitchen-info.rst @@ -3,7 +3,7 @@ gui/kitchen-info .. dfhack-tool:: :summary: Show food item uses in the kitchen status screen. - :tags: untested fort inspection + :tags: unavailable fort inspection This tool is an overlay that adds more info to the Kitchen screen, such as the potential alternate uses of the items that you could mark for cooking. diff --git a/docs/gui/launcher.rst b/docs/gui/launcher.rst index 5c56b384b4..04d81b41f3 100644 --- a/docs/gui/launcher.rst +++ b/docs/gui/launcher.rst @@ -104,5 +104,5 @@ Dev mode -------- By default, commands intended for developers and modders are filtered out of the -autocomplete list. This includes any tools tagged with ``untested``. You can +autocomplete list. This includes any tools tagged with ``unavailable``. You can toggle this filtering by hitting :kbd:`Ctrl`:kbd:`D` at any time. diff --git a/docs/gui/load-screen.rst b/docs/gui/load-screen.rst index f00aa49495..1301ab09f3 100644 --- a/docs/gui/load-screen.rst +++ b/docs/gui/load-screen.rst @@ -3,7 +3,7 @@ gui/load-screen .. dfhack-tool:: :summary: Replace DF's continue game screen with a searchable list. - :tags: untested dfhack + :tags: unavailable dfhack If you tend to have many ongoing games, this tool can make it much easier to load the one you're looking for. It replaces DF's "continue game" screen with diff --git a/docs/gui/manager-quantity.rst b/docs/gui/manager-quantity.rst index ff72b47132..5eb9a44c36 100644 --- a/docs/gui/manager-quantity.rst +++ b/docs/gui/manager-quantity.rst @@ -3,7 +3,7 @@ gui/manager-quantity .. dfhack-tool:: :summary: Set the quantity of the selected manager workorder. - :tags: untested fort workorders + :tags: unavailable fort workorders There is no way in the base DF game to change the quantity for an existing manager workorder. Select a workorder on the j-m or u-m screens and run this diff --git a/docs/gui/mechanisms.rst b/docs/gui/mechanisms.rst index 28129fb157..266fb280d2 100644 --- a/docs/gui/mechanisms.rst +++ b/docs/gui/mechanisms.rst @@ -3,7 +3,7 @@ gui/mechanisms .. dfhack-tool:: :summary: List mechanisms and links connected to a building. - :tags: untested fort inspection buildings + :tags: unavailable fort inspection buildings This convenient tool lists the mechanisms connected to the building and the buildings linked via the mechanisms. Navigating the list centers the view on the diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index c6d0dcee63..6eb56b2937 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -3,7 +3,7 @@ gui/mod-manager .. dfhack-tool:: :summary: Easily install and uninstall mods. - :tags: untested dfhack + :tags: unavailable dfhack This tool provides a simple way to install and remove small mods that you have downloaded from the internet -- or have created yourself! Several mods are diff --git a/docs/gui/petitions.rst b/docs/gui/petitions.rst index 8f1ab91348..0f48bc90a4 100644 --- a/docs/gui/petitions.rst +++ b/docs/gui/petitions.rst @@ -3,7 +3,7 @@ gui/petitions .. dfhack-tool:: :summary: Show information about your fort's petitions. - :tags: untested fort inspection + :tags: unavailable fort inspection Show your fort's petitions, both pending and fulfilled. diff --git a/docs/gui/power-meter.rst b/docs/gui/power-meter.rst index d966d25dc7..fc7e59bff1 100644 --- a/docs/gui/power-meter.rst +++ b/docs/gui/power-meter.rst @@ -3,7 +3,7 @@ gui/power-meter .. dfhack-tool:: :summary: Allow pressure plates to measure power. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings If you run this tool after selecting :guilabel:`Pressure Plate` in the build menu, you will build a power meter building instead of a regular pressure plate. diff --git a/docs/gui/quantum.rst b/docs/gui/quantum.rst index 9072690857..534f35fcbd 100644 --- a/docs/gui/quantum.rst +++ b/docs/gui/quantum.rst @@ -3,7 +3,7 @@ gui/quantum .. dfhack-tool:: :summary: Quickly and easily create quantum stockpiles. - :tags: untested fort productivity stockpiles + :tags: unavailable fort productivity stockpiles This tool provides a visual, interactive interface for creating quantum stockpiles. diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index 3d1ce9e26b..b23aa8c783 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -3,7 +3,7 @@ gui/rename .. dfhack-tool:: :summary: Give buildings and units new names, optionally with special chars. - :tags: untested fort productivity buildings stockpiles units + :tags: unavailable fort productivity buildings stockpiles units Once you select a target on the game map, this tool allows you to rename it. It is more powerful than the in-game rename functionality since it allows you to diff --git a/docs/gui/room-list.rst b/docs/gui/room-list.rst index 4973ccc2e6..6a5749215e 100644 --- a/docs/gui/room-list.rst +++ b/docs/gui/room-list.rst @@ -3,7 +3,7 @@ gui/room-list .. dfhack-tool:: :summary: Manage rooms owned by a dwarf. - :tags: untested fort inspection + :tags: unavailable fort inspection When invoked in :kbd:`q` mode with the cursor over an owned room, this tool lists other rooms owned by the same owner, or by the unit selected in the assign diff --git a/docs/gui/seedwatch.rst b/docs/gui/seedwatch.rst new file mode 100644 index 0000000000..61412a1c51 --- /dev/null +++ b/docs/gui/seedwatch.rst @@ -0,0 +1,19 @@ +gui/seedwatch +============= + +.. dfhack-tool:: + :summary: Manages seed and plant cooking based on seed stock levels. + + +This is the configuration interface for the `seedwatch` plugin. You can configure +a target stock amount for each seed type. If the number of seeds of that type falls +below the target, then the plants and seeds of that type will be protected from +cookery. If the number rises above the target + 20, then cooking will be allowed +again. + +Usage +----- + +:: + + gui/seedwatch diff --git a/docs/gui/settings-manager.rst b/docs/gui/settings-manager.rst index 6bf10c6f63..6ea2a623fe 100644 --- a/docs/gui/settings-manager.rst +++ b/docs/gui/settings-manager.rst @@ -3,7 +3,7 @@ gui/settings-manager .. dfhack-tool:: :summary: Dynamically adjust global DF settings. - :tags: untested dfhack + :tags: unavailable dfhack This tool is an in-game editor for settings defined in :file:`data/init/init.txt` and :file:`data/init/d_init.txt`. Changes are written diff --git a/docs/gui/siege-engine.rst b/docs/gui/siege-engine.rst index 634f19e522..704fd8c931 100644 --- a/docs/gui/siege-engine.rst +++ b/docs/gui/siege-engine.rst @@ -3,7 +3,7 @@ gui/siege-engine .. dfhack-tool:: :summary: Extend the functionality and usability of siege engines. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings This tool is an in-game interface for `siege-engine`, which allows you to link siege engines to stockpiles, restrict operation to certain dwarves, fire a diff --git a/docs/gui/stamper.rst b/docs/gui/stamper.rst index b9a965e876..779e57b6e2 100644 --- a/docs/gui/stamper.rst +++ b/docs/gui/stamper.rst @@ -3,7 +3,7 @@ gui/stamper .. dfhack-tool:: :summary: Copy, paste, and transform dig designations. - :tags: untested fort design map + :tags: unavailable fort design map This tool allows you to copy and paste blocks of dig designations. You can also transform what you have copied by shifting it, reflecting it, rotating it, diff --git a/docs/gui/stockpiles.rst b/docs/gui/stockpiles.rst index 2b8d234d20..80405f6c37 100644 --- a/docs/gui/stockpiles.rst +++ b/docs/gui/stockpiles.rst @@ -3,7 +3,7 @@ gui/stockpiles .. dfhack-tool:: :summary: Import and export stockpile settings. - :tags: untested fort design stockpiles + :tags: unavailable fort design stockpiles With a stockpile selected in :kbd:`q` mode, you can use this tool to load stockpile settings from a file or save them to a file for later loading, in diff --git a/docs/gui/suspendmanager.rst b/docs/gui/suspendmanager.rst index 7ff9dbb3c1..4940dc7c30 100644 --- a/docs/gui/suspendmanager.rst +++ b/docs/gui/suspendmanager.rst @@ -3,7 +3,7 @@ gui/suspendmanager .. dfhack-tool:: :summary: Intelligently suspend and unsuspend jobs. - :tags: fort auto jobs + This is the graphical configuration interface for the `suspendmanager` automation tool. diff --git a/docs/gui/teleport.rst b/docs/gui/teleport.rst index c6b6b67749..e2cd9229e4 100644 --- a/docs/gui/teleport.rst +++ b/docs/gui/teleport.rst @@ -3,7 +3,7 @@ gui/teleport .. dfhack-tool:: :summary: Teleport a unit anywhere. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool is a front-end for the `teleport` tool. It allows you to interactively choose a unit to teleport and a destination tile using the in-game cursor. diff --git a/docs/gui/unit-info-viewer.rst b/docs/gui/unit-info-viewer.rst index 7fa6c334c0..dac54e87e5 100644 --- a/docs/gui/unit-info-viewer.rst +++ b/docs/gui/unit-info-viewer.rst @@ -3,7 +3,7 @@ gui/unit-info-viewer .. dfhack-tool:: :summary: Display detailed information about a unit. - :tags: untested fort inspection units + :tags: unavailable fort inspection units Displays information about age, birth, maxage, shearing, milking, grazing, egg laying, body size, and death for the selected unit. diff --git a/docs/gui/workflow.rst b/docs/gui/workflow.rst index 1af67a7879..d4724efb9a 100644 --- a/docs/gui/workflow.rst +++ b/docs/gui/workflow.rst @@ -3,7 +3,7 @@ gui/workflow .. dfhack-tool:: :summary: Manage automated item production rules. - :tags: untested fort auto jobs + :tags: unavailable fort auto jobs This tool provides a simple interface to item production constraints managed by `workflow`. When a workshop job is selected in :kbd:`q` mode and this tool is diff --git a/docs/gui/workorder-details.rst b/docs/gui/workorder-details.rst index cf7f30a122..ff3639f436 100644 --- a/docs/gui/workorder-details.rst +++ b/docs/gui/workorder-details.rst @@ -3,7 +3,7 @@ gui/workorder-details .. dfhack-tool:: :summary: Adjust input materials and traits for workorders. - :tags: untested fort inspection workorders + :tags: unavailable fort inspection workorders This tool allows you to adjust item types, materials, and/or traits for items used in manager workorders. The jobs created from those workorders will inherit diff --git a/docs/gui/workshop-job.rst b/docs/gui/workshop-job.rst index e1a8ed93af..3caaeef591 100644 --- a/docs/gui/workshop-job.rst +++ b/docs/gui/workshop-job.rst @@ -3,7 +3,7 @@ gui/workshop-job .. dfhack-tool:: :summary: Adjust the input materials used for a job at a workshop. - :tags: untested fort inspection jobs + :tags: unavailable fort inspection jobs This tool allows you to inspect or change the input reagents for the selected workshop job (in :kbd:`q` mode). diff --git a/docs/hotkey-notes.rst b/docs/hotkey-notes.rst index a418fca10c..b0f2be8184 100644 --- a/docs/hotkey-notes.rst +++ b/docs/hotkey-notes.rst @@ -3,7 +3,7 @@ hotkey-notes .. dfhack-tool:: :summary: Show info on DF map location hotkeys. - :tags: untested fort inspection + :tags: unavailable fort inspection This command lists the key (e.g. :kbd:`F1`), name, and jump position of the map location hotkeys you set in the :kbd:`H` menu. diff --git a/docs/launch.rst b/docs/launch.rst index c364e56e20..d6d0f88096 100644 --- a/docs/launch.rst +++ b/docs/launch.rst @@ -3,7 +3,7 @@ launch .. dfhack-tool:: :summary: Thrash your enemies with a flying suplex. - :tags: untested adventure armok units + :tags: unavailable adventure armok units Attack another unit and then run this command to grab them and fly in a glorious parabolic arc to where you have placed the cursor. You'll land safely and your diff --git a/docs/light-aquifers-only.rst b/docs/light-aquifers-only.rst index 53e5296bf8..f3a77206d2 100644 --- a/docs/light-aquifers-only.rst +++ b/docs/light-aquifers-only.rst @@ -3,7 +3,7 @@ light-aquifers-only .. dfhack-tool:: :summary: Change heavy and varied aquifers to light aquifers. - :tags: untested embark fort armok map + :tags: unavailable embark fort armok map This script behaves differently depending on whether it's called pre-embark or post-embark. Pre-embark, it changes all aquifers in the world to light ones, diff --git a/docs/linger.rst b/docs/linger.rst index ea85f9c630..c985ac95a0 100644 --- a/docs/linger.rst +++ b/docs/linger.rst @@ -3,7 +3,7 @@ linger .. dfhack-tool:: :summary: Take control of your adventurer's killer. - :tags: untested adventure armok + :tags: unavailable adventure armok Run this script after being presented with the "You are deceased." message to abandon your dead adventurer and take control of your adventurer's killer. diff --git a/docs/list-waves.rst b/docs/list-waves.rst index e3228d0bc4..574315bbb8 100644 --- a/docs/list-waves.rst +++ b/docs/list-waves.rst @@ -3,7 +3,7 @@ list-waves .. dfhack-tool:: :summary: Show migration wave information for your dwarves. - :tags: untested fort inspection units + :tags: unavailable fort inspection units This script displays information about migration waves or identifies which wave a particular dwarf came from. diff --git a/docs/load-save.rst b/docs/load-save.rst index 00ccdd02b0..5d3e281273 100644 --- a/docs/load-save.rst +++ b/docs/load-save.rst @@ -3,7 +3,7 @@ load-save .. dfhack-tool:: :summary: Load a savegame. - :tags: untested dfhack + :tags: unavailable dfhack When run on the Dwarf Fortress title screen or "load game" screen, this script will load the save with the given folder name without requiring interaction. diff --git a/docs/make-legendary.rst b/docs/make-legendary.rst index 6dd2278c57..4964947aff 100644 --- a/docs/make-legendary.rst +++ b/docs/make-legendary.rst @@ -3,7 +3,7 @@ make-legendary .. dfhack-tool:: :summary: Boost skills of the selected dwarf. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool can make the selected dwarf legendary in one skill, a group of skills, or all skills. diff --git a/docs/markdown.rst b/docs/markdown.rst index 5a4df7c3ca..ae4be55a94 100644 --- a/docs/markdown.rst +++ b/docs/markdown.rst @@ -3,7 +3,7 @@ markdown .. dfhack-tool:: :summary: Exports the text you see on the screen for posting online. - :tags: untested dfhack + :tags: unavailable dfhack This tool saves a copy of a text screen, formatted in markdown, for posting to Reddit (among other places). See `forum-dwarves` if you want to export BBCode diff --git a/docs/max-wave.rst b/docs/max-wave.rst index f738c46fbf..6b53e7c2b1 100644 --- a/docs/max-wave.rst +++ b/docs/max-wave.rst @@ -3,7 +3,7 @@ max-wave .. dfhack-tool:: :summary: Dynamically limit the next immigration wave. - :tags: untested fort gameplay + :tags: unavailable fort gameplay Limit the number of migrants that can arrive in the next wave by overriding the population cap value from data/init/d_init.txt. diff --git a/docs/modtools/add-syndrome.rst b/docs/modtools/add-syndrome.rst index fc4427d7c7..5492db23c7 100644 --- a/docs/modtools/add-syndrome.rst +++ b/docs/modtools/add-syndrome.rst @@ -3,7 +3,7 @@ modtools/add-syndrome .. dfhack-tool:: :summary: Add and remove syndromes from units. - :tags: untested dev + :tags: unavailable dev This allows adding and removing syndromes from units. diff --git a/docs/modtools/anonymous-script.rst b/docs/modtools/anonymous-script.rst index 8df4dd023c..9c2074fcb4 100644 --- a/docs/modtools/anonymous-script.rst +++ b/docs/modtools/anonymous-script.rst @@ -3,7 +3,7 @@ modtools/anonymous-script .. dfhack-tool:: :summary: Run dynamically generated script code. - :tags: untested dev + :tags: unavailable dev This allows running a short simple Lua script passed as an argument instead of running a script from a file. This is useful when you want to do something too diff --git a/docs/modtools/change-build-menu.rst b/docs/modtools/change-build-menu.rst index 522c4a50ca..2fffec91a7 100644 --- a/docs/modtools/change-build-menu.rst +++ b/docs/modtools/change-build-menu.rst @@ -3,7 +3,7 @@ modtools/change-build-menu .. dfhack-tool:: :summary: Add or remove items from the build sidebar menus. - :tags: untested dev + :tags: unavailable dev Change the build sidebar menus. diff --git a/docs/modtools/create-item.rst b/docs/modtools/create-item.rst index 58ea8af99f..4704502c08 100644 --- a/docs/modtools/create-item.rst +++ b/docs/modtools/create-item.rst @@ -3,7 +3,7 @@ modtools/create-item .. dfhack-tool:: :summary: Create arbitrary items. - :tags: untested dev + :tags: unavailable dev Replaces the `createitem` plugin, with standard arguments. The other versions will be phased out in a later version. diff --git a/docs/modtools/create-tree.rst b/docs/modtools/create-tree.rst index 3a4b422819..e425c02229 100644 --- a/docs/modtools/create-tree.rst +++ b/docs/modtools/create-tree.rst @@ -3,7 +3,7 @@ modtools/create-tree .. dfhack-tool:: :summary: Spawn trees. - :tags: untested dev + :tags: unavailable dev Spawns a tree. diff --git a/docs/modtools/create-unit.rst b/docs/modtools/create-unit.rst index 44bb9c868a..9c338bde20 100644 --- a/docs/modtools/create-unit.rst +++ b/docs/modtools/create-unit.rst @@ -3,7 +3,7 @@ modtools/create-unit .. dfhack-tool:: :summary: Create arbitrary units. - :tags: untested dev + :tags: unavailable dev Creates a unit. Usage:: diff --git a/docs/modtools/equip-item.rst b/docs/modtools/equip-item.rst index c01fc764a8..f590b4a4ad 100644 --- a/docs/modtools/equip-item.rst +++ b/docs/modtools/equip-item.rst @@ -3,7 +3,7 @@ modtools/equip-item .. dfhack-tool:: :summary: Force a unit to equip an item. - :tags: untested dev + :tags: unavailable dev Force a unit to equip an item with a particular body part; useful in conjunction with the ``create`` scripts above. See also `forceequip`. diff --git a/docs/modtools/extra-gamelog.rst b/docs/modtools/extra-gamelog.rst index f4c2ebe136..2c79594771 100644 --- a/docs/modtools/extra-gamelog.rst +++ b/docs/modtools/extra-gamelog.rst @@ -3,7 +3,7 @@ modtools/extra-gamelog .. dfhack-tool:: :summary: Write info to the gamelog for Soundsense. - :tags: untested dev + :tags: unavailable dev This script writes extra information to the gamelog. This is useful for tools like :forums:`Soundsense <60287>`. diff --git a/docs/modtools/fire-rate.rst b/docs/modtools/fire-rate.rst index ff427e3054..35310d873d 100644 --- a/docs/modtools/fire-rate.rst +++ b/docs/modtools/fire-rate.rst @@ -3,7 +3,7 @@ modtools/fire-rate .. dfhack-tool:: :summary: Alter the fire rate of ranged weapons. - :tags: untested dev + :tags: unavailable dev Allows altering the fire rates of ranged weapons. Each are defined on a per-item basis. As this is done in an on-world basis, commands for this should be placed diff --git a/docs/modtools/if-entity.rst b/docs/modtools/if-entity.rst index c38dfafe69..9846842b31 100644 --- a/docs/modtools/if-entity.rst +++ b/docs/modtools/if-entity.rst @@ -3,7 +3,7 @@ modtools/if-entity .. dfhack-tool:: :summary: Run DFHack commands based on current civ id. - :tags: untested dev + :tags: unavailable dev Run a command if the current entity matches a given ID. diff --git a/docs/modtools/interaction-trigger.rst b/docs/modtools/interaction-trigger.rst index 6ab287ad8e..f1ea4b61df 100644 --- a/docs/modtools/interaction-trigger.rst +++ b/docs/modtools/interaction-trigger.rst @@ -3,7 +3,7 @@ modtools/interaction-trigger .. dfhack-tool:: :summary: Run DFHack commands when a unit attacks or defends. - :tags: untested dev + :tags: unavailable dev This triggers events when a unit uses an interaction on another. It works by scanning the announcements for the correct attack verb, so the attack verb diff --git a/docs/modtools/invader-item-destroyer.rst b/docs/modtools/invader-item-destroyer.rst index 7ad0aa3a59..20f300109d 100644 --- a/docs/modtools/invader-item-destroyer.rst +++ b/docs/modtools/invader-item-destroyer.rst @@ -3,7 +3,7 @@ modtools/invader-item-destroyer .. dfhack-tool:: :summary: Destroy invader items when they die. - :tags: untested dev + :tags: unavailable dev This tool can destroy invader items to prevent clutter or to prevent the player from getting tools exclusive to certain races. diff --git a/docs/modtools/item-trigger.rst b/docs/modtools/item-trigger.rst index c5211a03b6..8db00cef7d 100644 --- a/docs/modtools/item-trigger.rst +++ b/docs/modtools/item-trigger.rst @@ -3,7 +3,7 @@ modtools/item-trigger .. dfhack-tool:: :summary: Run DFHack commands when a unit uses an item. - :tags: untested dev + :tags: unavailable dev This powerful tool triggers DFHack commands when a unit equips, unequips, or attacks another unit with specified item types, specified item materials, or diff --git a/docs/modtools/moddable-gods.rst b/docs/modtools/moddable-gods.rst index a5d4872800..763082762b 100644 --- a/docs/modtools/moddable-gods.rst +++ b/docs/modtools/moddable-gods.rst @@ -3,7 +3,7 @@ modtools/moddable-gods .. dfhack-tool:: :summary: Create deities. - :tags: untested dev + :tags: unavailable dev This is a standardized version of Putnam's moddableGods script. It allows you to create gods on the command-line. diff --git a/docs/modtools/outside-only.rst b/docs/modtools/outside-only.rst index 8c753180e0..15c9751c61 100644 --- a/docs/modtools/outside-only.rst +++ b/docs/modtools/outside-only.rst @@ -3,7 +3,7 @@ modtools/outside-only .. dfhack-tool:: :summary: Set building inside/outside restrictions. - :tags: untested dev + :tags: unavailable dev This allows you to specify certain custom buildings as outside only, or inside only. If the player attempts to build a building in an inappropriate location, diff --git a/docs/modtools/pref-edit.rst b/docs/modtools/pref-edit.rst index 84a0fe5e16..220ebf70b8 100644 --- a/docs/modtools/pref-edit.rst +++ b/docs/modtools/pref-edit.rst @@ -3,7 +3,7 @@ modtools/pref-edit .. dfhack-tool:: :summary: Modify unit preferences. - :tags: untested dev + :tags: unavailable dev Add, remove, or edit the preferences of a unit. Requires a modifier, a unit argument, and filters. diff --git a/docs/modtools/projectile-trigger.rst b/docs/modtools/projectile-trigger.rst index e697d9c6f5..d50c434dec 100644 --- a/docs/modtools/projectile-trigger.rst +++ b/docs/modtools/projectile-trigger.rst @@ -3,7 +3,7 @@ modtools/projectile-trigger .. dfhack-tool:: :summary: Run DFHack commands when projectiles hit their targets. - :tags: untested dev + :tags: unavailable dev This triggers dfhack commands when projectiles hit their targets. Usage:: diff --git a/docs/modtools/random-trigger.rst b/docs/modtools/random-trigger.rst index 463c4cec18..56b6c21868 100644 --- a/docs/modtools/random-trigger.rst +++ b/docs/modtools/random-trigger.rst @@ -3,7 +3,7 @@ modtools/random-trigger .. dfhack-tool:: :summary: Randomly select DFHack scripts to run. - :tags: untested dev + :tags: unavailable dev Trigger random dfhack commands with specified probabilities. Register a few scripts, then tell it to "go" and it will pick one diff --git a/docs/modtools/raw-lint.rst b/docs/modtools/raw-lint.rst index 75ff257426..13311b1f06 100644 --- a/docs/modtools/raw-lint.rst +++ b/docs/modtools/raw-lint.rst @@ -3,6 +3,6 @@ modtools/raw-lint .. dfhack-tool:: :summary: Check for errors in raw files. - :tags: untested dev + :tags: unavailable dev Checks for simple issues with raw files. Can be run automatically. diff --git a/docs/modtools/reaction-product-trigger.rst b/docs/modtools/reaction-product-trigger.rst index 92c44076a3..32ede32949 100644 --- a/docs/modtools/reaction-product-trigger.rst +++ b/docs/modtools/reaction-product-trigger.rst @@ -3,7 +3,7 @@ modtools/reaction-product-trigger .. dfhack-tool:: :summary: Call DFHack commands when reaction products are produced. - :tags: untested dev + :tags: unavailable dev This triggers dfhack commands when reaction products are produced, once per product. Usage:: diff --git a/docs/modtools/reaction-trigger-transition.rst b/docs/modtools/reaction-trigger-transition.rst index 2674e295e2..bc3c3e45c3 100644 --- a/docs/modtools/reaction-trigger-transition.rst +++ b/docs/modtools/reaction-trigger-transition.rst @@ -3,7 +3,7 @@ modtools/reaction-trigger-transition .. dfhack-tool:: :summary: Help create reaction triggers. - :tags: untested dev + :tags: unavailable dev Prints useful things to the console and a file to help modders transition from ``autoSyndrome`` to `modtools/reaction-trigger`. diff --git a/docs/modtools/reaction-trigger.rst b/docs/modtools/reaction-trigger.rst index 3b94637a70..bf35bcf0aa 100644 --- a/docs/modtools/reaction-trigger.rst +++ b/docs/modtools/reaction-trigger.rst @@ -3,7 +3,7 @@ modtools/reaction-trigger .. dfhack-tool:: :summary: Run DFHack commands when custom reactions complete. - :tags: untested dev + :tags: unavailable dev Triggers dfhack commands when custom reactions complete, regardless of whether it produced anything, once per completion. Arguments:: diff --git a/docs/modtools/set-belief.rst b/docs/modtools/set-belief.rst index 4ad997c590..4d010a0cd5 100644 --- a/docs/modtools/set-belief.rst +++ b/docs/modtools/set-belief.rst @@ -3,7 +3,7 @@ modtools/set-belief .. dfhack-tool:: :summary: Change the beliefs/values of a unit. - :tags: untested dev + :tags: unavailable dev Changes the beliefs (values) of units. Requires a belief, modifier, and a target. diff --git a/docs/modtools/set-need.rst b/docs/modtools/set-need.rst index e753ed6403..b4beea169b 100644 --- a/docs/modtools/set-need.rst +++ b/docs/modtools/set-need.rst @@ -3,7 +3,7 @@ modtools/set-need .. dfhack-tool:: :summary: Change the needs of a unit. - :tags: untested dev + :tags: unavailable dev Sets and edits unit needs. diff --git a/docs/modtools/set-personality.rst b/docs/modtools/set-personality.rst index 7e3969d27b..3e34c7db0c 100644 --- a/docs/modtools/set-personality.rst +++ b/docs/modtools/set-personality.rst @@ -3,7 +3,7 @@ modtools/set-personality .. dfhack-tool:: :summary: Change a unit's personality. - :tags: untested dev + :tags: unavailable dev Changes the personality of units. diff --git a/docs/modtools/skill-change.rst b/docs/modtools/skill-change.rst index 474df9e673..23d41b6fdb 100644 --- a/docs/modtools/skill-change.rst +++ b/docs/modtools/skill-change.rst @@ -3,7 +3,7 @@ modtools/skill-change .. dfhack-tool:: :summary: Modify unit skills. - :tags: untested dev + :tags: dev Sets or modifies a skill of a unit. diff --git a/docs/modtools/spawn-flow.rst b/docs/modtools/spawn-flow.rst index eccf3efef0..1565e75a6d 100644 --- a/docs/modtools/spawn-flow.rst +++ b/docs/modtools/spawn-flow.rst @@ -3,7 +3,7 @@ modtools/spawn-flow .. dfhack-tool:: :summary: Creates flows at the specified location. - :tags: untested dev + :tags: unavailable dev Creates flows at the specified location. diff --git a/docs/modtools/syndrome-trigger.rst b/docs/modtools/syndrome-trigger.rst index 48ef5f00e2..24e15ef8f5 100644 --- a/docs/modtools/syndrome-trigger.rst +++ b/docs/modtools/syndrome-trigger.rst @@ -3,7 +3,7 @@ modtools/syndrome-trigger .. dfhack-tool:: :summary: Trigger DFHack commands when units acquire syndromes. - :tags: untested dev + :tags: unavailable dev This script helps you set up commands that trigger when syndromes are applied to units. diff --git a/docs/modtools/transform-unit.rst b/docs/modtools/transform-unit.rst index 1137c590dc..9fe68975c3 100644 --- a/docs/modtools/transform-unit.rst +++ b/docs/modtools/transform-unit.rst @@ -3,7 +3,7 @@ modtools/transform-unit .. dfhack-tool:: :summary: Transform a unit into another unit type. - :tags: untested dev + :tags: unavailable dev This tool transforms a unit into another unit type, either temporarily or permanently. diff --git a/docs/names.rst b/docs/names.rst index b7a8c2933d..40b9ed38b3 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -3,7 +3,7 @@ names .. dfhack-tool:: :summary: Rename units or items with the DF name generator. - :tags: untested fort productivity units + :tags: unavailable fort productivity units This tool allows you to rename the selected unit or item (including artifacts) with the native Dwarf Fortress name generation interface. diff --git a/docs/open-legends.rst b/docs/open-legends.rst index 593da992a1..85f258f8f2 100644 --- a/docs/open-legends.rst +++ b/docs/open-legends.rst @@ -3,7 +3,7 @@ open-legends .. dfhack-tool:: :summary: Open a legends screen from fort or adventure mode. - :tags: untested adventure fort legends + :tags: unavailable adventure fort legends You can use this tool to open legends mode from a world loaded in fortress or adventure mode. You can browse around, or even run `exportlegends` while you're diff --git a/docs/points.rst b/docs/points.rst index 41c1f2c562..0016f779ef 100644 --- a/docs/points.rst +++ b/docs/points.rst @@ -3,7 +3,7 @@ points .. dfhack-tool:: :summary: Sets available points at the embark screen. - :tags: untested embark fort armok + :tags: unavailable embark fort armok Run at the embark screen when you are choosing items to bring with you and skills to assign to your dwarves. You can set the available points to any diff --git a/docs/pop-control.rst b/docs/pop-control.rst index cb9a916598..d648451241 100644 --- a/docs/pop-control.rst +++ b/docs/pop-control.rst @@ -3,7 +3,7 @@ pop-control .. dfhack-tool:: :summary: Controls population and migration caps persistently per-fort. - :tags: untested fort auto gameplay + :tags: unavailable fort auto gameplay This script controls `hermit` and the various population caps per-fortress. It is intended to be run from ``dfhack-config/init/onMapLoad.init`` as diff --git a/docs/prefchange.rst b/docs/prefchange.rst index 7ce46ae486..11eebd6efd 100644 --- a/docs/prefchange.rst +++ b/docs/prefchange.rst @@ -3,7 +3,7 @@ prefchange .. dfhack-tool:: :summary: Set strange mood preferences. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool sets preferences for strange moods to include a weapon type, equipment type, and material. If you also wish to trigger a mood, see `strangemood`. diff --git a/docs/prioritize.rst b/docs/prioritize.rst index 16fb57cd46..852e4c1e37 100644 --- a/docs/prioritize.rst +++ b/docs/prioritize.rst @@ -23,9 +23,6 @@ the same problem that ``prioritize`` is designed to solve. The script provides a good default set of job types to prioritize that have been suggested and playtested by the DF community. -Also see the `do-job-now tweak ` and the `do-job-now` script for boosting -the priority of specific individual jobs (as opposed to entire classes of jobs). - Usage ----- @@ -113,14 +110,14 @@ Default list of job types to prioritize The community has assembled a good default list of job types that most players will benefit from. They have been playtested across a wide variety of fort -types. It is a good idea to enable prioritize for all your forts. +types. It is a good idea to enable `prioritize` with at least these defaults +for all your forts. The default prioritize list includes: - Handling items that can rot - Medical, hygiene, and hospice tasks -- Putting items in bins/barrels/pots/minecarts - Interactions with animals and prisoners -- Dumping items, pulling levers, felling trees, and other tasks that you, as a - player, might stare at and internally scream "why why why isn't this getting - done??". +- Noble-specific tasks (like managing workorders) +- Dumping items, felling trees, and other tasks that you, as a player, might + stare at and internally scream "why why why isn't this getting done??". diff --git a/docs/putontable.rst b/docs/putontable.rst index 566c4402a4..99c3f3778b 100644 --- a/docs/putontable.rst +++ b/docs/putontable.rst @@ -3,7 +3,7 @@ putontable .. dfhack-tool:: :summary: Make an item appear on a table. - :tags: untested fort armok items + :tags: unavailable fort armok items To use this tool, move an item to the ground on the same tile as a built table. Then, place the cursor over the table and item and run this command. The item diff --git a/docs/questport.rst b/docs/questport.rst index ea52bdbb02..c2c2712e1a 100644 --- a/docs/questport.rst +++ b/docs/questport.rst @@ -3,7 +3,7 @@ questport .. dfhack-tool:: :summary: Teleport to your quest log map cursor. - :tags: untested adventure armok + :tags: unavailable adventure armok If you open the quest log map and move the cursor to your target location, you can run this command to teleport straight there. This can be done both within diff --git a/docs/region-pops.rst b/docs/region-pops.rst index 8c7db255df..9627b7fd18 100644 --- a/docs/region-pops.rst +++ b/docs/region-pops.rst @@ -3,7 +3,7 @@ region-pops .. dfhack-tool:: :summary: Change regional animal populations. - :tags: untested fort inspection animals + :tags: unavailable fort inspection animals This tool can show or modify the populations of animals in the region. diff --git a/docs/resurrect-adv.rst b/docs/resurrect-adv.rst index 744779197c..6bd10393af 100644 --- a/docs/resurrect-adv.rst +++ b/docs/resurrect-adv.rst @@ -3,7 +3,7 @@ resurrect-adv .. dfhack-tool:: :summary: Bring a dead adventurer back to life. - :tags: untested adventure armok + :tags: unavailable adventure armok Have you ever died, but wish you hadn't? This tool can help : ) When you see the "You are deceased" message, run this command to be resurrected and fully healed. diff --git a/docs/reveal-adv-map.rst b/docs/reveal-adv-map.rst index dd5ba4c0c2..af92f06b39 100644 --- a/docs/reveal-adv-map.rst +++ b/docs/reveal-adv-map.rst @@ -3,7 +3,7 @@ reveal-adv-map .. dfhack-tool:: :summary: Reveal or hide the world map. - :tags: untested adventure armok map + :tags: unavailable adventure armok map This tool can be used to either reveal or hide all tiles on the world map in adventure mode (visible when viewing the quest log or fast traveling). diff --git a/docs/season-palette.rst b/docs/season-palette.rst index 92f147636d..5b7dcc000d 100644 --- a/docs/season-palette.rst +++ b/docs/season-palette.rst @@ -3,7 +3,7 @@ season-palette .. dfhack-tool:: :summary: Swap color palettes when the seasons change. - :tags: untested fort auto graphics + :tags: unavailable fort auto graphics For this tool to work you need to add *at least* one color palette file to your save raw directory. These files must be in the same format as diff --git a/docs/set-orientation.rst b/docs/set-orientation.rst index 37bac9dcd2..c1c549252a 100644 --- a/docs/set-orientation.rst +++ b/docs/set-orientation.rst @@ -3,7 +3,7 @@ set-orientation .. dfhack-tool:: :summary: Alter a unit's romantic inclinations. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool lets you tinker with the interest levels your dwarves have towards dwarves of the same/different sex. diff --git a/docs/siren.rst b/docs/siren.rst index 7488e5f34e..c62668a2cb 100644 --- a/docs/siren.rst +++ b/docs/siren.rst @@ -3,7 +3,7 @@ siren .. dfhack-tool:: :summary: Wake up sleeping units and stop parties. - :tags: untested fort armok units + :tags: unavailable fort armok units Sound the alarm! This tool can shake your sleeping units awake and knock some sense into your party animal military dwarves so they can address a siege. diff --git a/docs/spawnunit.rst b/docs/spawnunit.rst index 3f8cbfb223..42a0687108 100644 --- a/docs/spawnunit.rst +++ b/docs/spawnunit.rst @@ -3,7 +3,7 @@ spawnunit .. dfhack-tool:: :summary: Create a unit. - :tags: untested fort armok units + :tags: unavailable fort armok units This tool allows you to easily spawn a unit of your choice. It is a simplified interface to `modtools/create-unit`, which this tool uses to actually create diff --git a/docs/startdwarf.rst b/docs/startdwarf.rst index f650be7d5e..4f63442a99 100644 --- a/docs/startdwarf.rst +++ b/docs/startdwarf.rst @@ -3,7 +3,7 @@ startdwarf .. dfhack-tool:: :summary: Increase the number of dwarves you embark with. - :tags: untested embark fort armok + :tags: unavailable embark fort armok You must use this tool before embarking (e.g. at the site selection screen or any time before) to change the number of dwarves you embark with from the diff --git a/docs/stripcaged.rst b/docs/stripcaged.rst index 98fd6c4d58..0691fafea7 100644 --- a/docs/stripcaged.rst +++ b/docs/stripcaged.rst @@ -7,10 +7,10 @@ stripcaged This tool helps with the tedious task of going through all your cages and marking the items inside for dumping. This lets you get leftover seeds out of -cages after you tamed the animals inside. The most popular use of this tool, -though, is to strip the weapons and armor from caged prisoners. After you run -this tool, your dwarves will come and take the items to the garbage dump, -leaving your cages clean and your prisoners stripped bare. +cages after you tamed the animals inside. The most popular use, though, is to +strip the weapons and armor from caged prisoners. After you run ``stripcaged``, +your dwarves will come and take the items to the garbage dump, leaving your +cages clean and your prisoners stripped bare. If you don't want to wait for your dwarves to dump all the items, you can use `autodump` to speed the process along. @@ -18,32 +18,40 @@ If you don't want to wait for your dwarves to dump all the items, you can use Usage ----- -``stripcaged list`` - Display a list of all cages and their item contents. -``stripcaged items|weapons|armor|all [here| ...]`` - Dump the given type of item. If ``here`` is specified, only act on the - in-game selected cage (or the cage under the keyboard cursor). Alternately, - you can specify the item ids of specific cages that you want to target. +:: - Note: Live vermin and tame vermin (pets) are considered items by the game. - Stripcaged excludes them from the ``all`` or ``items`` targets by default - as dumping them risks them escaping or dying from your cats. - - Use ``--include-vermin`` for untamed vermin and ``--include-pets`` for - tame vermin with the relevant targets to include them anyways. + stripcaged list + stripcaged items|weapons|armor|all [here| ...] [] Examples -------- +``stripcaged list`` + Display a list of all cages and their item contents. ``stripcaged all`` - For all cages, dump all items, equipped by a creature or not. + Dump all items in all cages, equipped by a creature or not. ``stripcaged items`` Dump loose items in all cages, such as seeds left over from animal training. ``stripcaged weapons`` Dump weapons equipped by caged creatures. -``stripcaged armor here`` - Dumps the armor equipped by the caged creature in the selected cage. +``stripcaged armor here --skip-forbidden`` + Dumps unforbidden armor equipped by the caged creature in the selected cage. ``stripcaged all 25321 34228`` Dumps all items out of the specified cages. -``stripcaged items here --include-vermin --include-pets`` +``stripcaged items here --include-pets --include-vermin`` Dumps loose items in the selected cage, including any tamed/untamed vermin. + +Options +------- + +``--include-pets``, ``--include-vermin`` + Live tame (pets) and untamed vermin are considered items by the game. They + are normally excluded from dumping since that risks them escaping or dying + from your cats. Use these options to dump them anyway. + +``-f``, ``--skip-forbidden`` + Items to be marked for dumping are unforbidden by default. Use this option + to instead only act on unforbidden items, and leave forbidden items + forbidden. This allows you to, for example, manually unforbid high-value + items from the stocks menu (like steel) and then have ``stripcaged`` just + act on the unforbidden items. diff --git a/docs/tidlers.rst b/docs/tidlers.rst index 03fea7e24d..295bd8d998 100644 --- a/docs/tidlers.rst +++ b/docs/tidlers.rst @@ -3,7 +3,7 @@ tidlers .. dfhack-tool:: :summary: Change where the idlers count is displayed. - :tags: untested interface + :tags: unavailable interface This tool simply cycles the idlers count among the possible positions where the idlers count can be placed, including making it disappear entirely. diff --git a/docs/timestream.rst b/docs/timestream.rst index de4e65a5cb..07e359278c 100644 --- a/docs/timestream.rst +++ b/docs/timestream.rst @@ -3,7 +3,7 @@ timestream .. dfhack-tool:: :summary: Fix FPS death. - :tags: untested fort auto fps + :tags: unavailable fort auto fps Do you remember when you first start a new fort, your initial 7 dwarves zip around the screen and get things done so quickly? As a player, you never had diff --git a/docs/undump-buildings.rst b/docs/undump-buildings.rst index f3e90439a2..cf76ca7234 100644 --- a/docs/undump-buildings.rst +++ b/docs/undump-buildings.rst @@ -3,7 +3,7 @@ undump-buildings .. dfhack-tool:: :summary: Undesignate building base materials for dumping. - :tags: untested fort productivity buildings + :tags: unavailable fort productivity buildings If you designate a bunch of tiles in dump mode, all the items on those tiles will be marked for dumping. Unfortunately, if there are buildings on any of diff --git a/docs/uniform-unstick.rst b/docs/uniform-unstick.rst index 3e164d977d..d9b08d7a1a 100644 --- a/docs/uniform-unstick.rst +++ b/docs/uniform-unstick.rst @@ -3,7 +3,7 @@ uniform-unstick .. dfhack-tool:: :summary: Make military units reevaluate their uniforms. - :tags: untested fort bugfix military + :tags: unavailable fort bugfix military This tool prompts military units to reevaluate their uniform, making them remove and drop potentially conflicting worn items. diff --git a/docs/unretire-anyone.rst b/docs/unretire-anyone.rst index 0df984dd25..9adce5ad0d 100644 --- a/docs/unretire-anyone.rst +++ b/docs/unretire-anyone.rst @@ -3,7 +3,7 @@ unretire-anyone .. dfhack-tool:: :summary: Adventure as any living historical figure. - :tags: untested adventure embark armok + :tags: unavailable adventure embark armok This tool allows you to play as any living (or undead) historical figure (except for deities) in adventure mode. diff --git a/docs/view-item-info.rst b/docs/view-item-info.rst index c91601b19b..84b6eb47a3 100644 --- a/docs/view-item-info.rst +++ b/docs/view-item-info.rst @@ -3,7 +3,7 @@ view-item-info .. dfhack-tool:: :summary: Extend item and unit descriptions with more information. - :tags: untested adventure fort interface + :tags: unavailable adventure fort interface This tool extends the item or unit description viewscreen with additional information, including a custom description of each item (when available), and diff --git a/docs/view-unit-reports.rst b/docs/view-unit-reports.rst index 963f03137b..b649038ded 100644 --- a/docs/view-unit-reports.rst +++ b/docs/view-unit-reports.rst @@ -3,7 +3,7 @@ view-unit-reports .. dfhack-tool:: :summary: Show combat reports for a unit. - :tags: untested fort inspection military + :tags: unavailable fort inspection military Show combat reports specifically for the selected unit. You can select a unit with the cursor in :kbd:`v` mode, from the list in :kbd:`u` mode, or from the diff --git a/docs/warn-starving.rst b/docs/warn-starving.rst index 50a6ce5280..8e411ff285 100644 --- a/docs/warn-starving.rst +++ b/docs/warn-starving.rst @@ -10,6 +10,8 @@ pause and you'll get a warning dialog telling you which units are in danger. This gives you a chance to rescue them (or take them out of their cages) before they die. +You can enable ``warn-starving`` notifications in `gui/control-panel` on the "Maintenance" tab. + Usage ----- @@ -23,9 +25,6 @@ Examples ``warn-starving all sane`` Report on all currently distressed units, excluding insane units that you wouldn't be able to save anyway. -``repeat --time 10 --timeUnits days --command [ warn-starving sane ]`` - Every 10 days, report any (sane) distressed units that haven't already been - reported. Options ------- diff --git a/docs/warn-stealers.rst b/docs/warn-stealers.rst index f90544a4c0..bf81f0b602 100644 --- a/docs/warn-stealers.rst +++ b/docs/warn-stealers.rst @@ -3,7 +3,7 @@ warn-stealers .. dfhack-tool:: :summary: Watch for and warn about units that like to steal your stuff. - :tags: untested fort armok auto units + :tags: unavailable fort armok auto units This script will watch for new units entering the map and will make a zoomable announcement whenever a creature that can eat food, guzzle drinks, or steal diff --git a/docs/workorder-recheck.rst b/docs/workorder-recheck.rst index 21da3622b7..70f18fdf48 100644 --- a/docs/workorder-recheck.rst +++ b/docs/workorder-recheck.rst @@ -3,7 +3,7 @@ workorder-recheck .. dfhack-tool:: :summary: Recheck start conditions for a manager workorder. - :tags: untested fort workorders + :tags: unavailable fort workorders Sets the status to ``Checking`` (from ``Active``) of the selected work order (in the ``j-m`` or ``u-m`` screens). This makes the manager reevaluate its diff --git a/exterminate.lua b/exterminate.lua index 37b7524913..f179b28648 100644 --- a/exterminate.lua +++ b/exterminate.lua @@ -1,5 +1,3 @@ --- Exterminate creatures based on criteria - local argparse = require('argparse') local function spawnLiquid(position, liquid_level, liquid_type, update_liquids) @@ -38,12 +36,20 @@ local killMethod = { BUTCHER = 1, MAGMA = 2, DROWN = 3, + VAPORIZE = 4, } --- Kills a unit by removing blood and vanishing them. -local function killUnit(unit) +-- removes the unit from existence, leaving no corpse if the unit hasn't died +-- by the time the vanish countdown expires +local function vaporizeUnit(unit, target_value) + target_value = target_value or 1 + unit.animal.vanish_countdown = target_value +end + +-- Kills a unit by removing blood and also setting a vanish countdown as a failsafe. +local function destroyUnit(unit) unit.body.blood_count = 0 - unit.animal.vanish_countdown = 2 + vaporizeUnit(unit, 2) end -- Marks a unit for slaughter at the butcher's shop. @@ -57,44 +63,50 @@ local function drownUnit(unit, liquid_type) local function createLiquid() spawnLiquid(unit.pos, 7, liquid_type) - if not same_xyz(previousPositions[unit.id], unit.pos) then spawnLiquid(previousPositions[unit.id], 0, nil, false) previousPositions[unit.id] = copyall(unit.pos) end - if unit.flags2.killed then spawnLiquid(previousPositions[unit.id], 0, nil, false) else dfhack.timeout(1, 'ticks', createLiquid) end end - createLiquid() end +local function killUnit(unit, method) + if method == killMethod.BUTCHER then + butcherUnit(unit) + elseif method == killMethod.MAGMA then + drownUnit(unit, df.tile_liquid.Magma) + elseif method == killMethod.DROWN then + drownUnit(unit, df.tile_liquid.Water) + elseif method == killMethod.VAPORIZE then + vaporizeUnit(unit) + else + destroyUnit(unit) + end +end + local function getRaceCastes(race_id) local unit_castes = {} - for _, caste in pairs(df.creature_raw.find(race_id).caste) do unit_castes[caste.caste_id] = {} end - return unit_castes end local function getMapRaces(only_visible, include_friendly) local map_races = {} - for _, unit in pairs(df.global.world.units.active) do if only_visible and not dfhack.units.isVisible(unit) then goto skipunit end - if not include_friendly and isUnitFriendly(unit) then goto skipunit end - if dfhack.units.isActive(unit) and checkUnit(unit) then local unit_race_name = dfhack.units.isUndead(unit) and "UNDEAD" or df.creature_raw.find(unit.race).creature_id @@ -103,7 +115,6 @@ local function getMapRaces(only_visible, include_friendly) race.name = unit_race_name race.count = (race.count or 0) + 1 end - :: skipunit :: end @@ -119,7 +130,7 @@ local options, args = { local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler = function() options.help = true end}, - {'m', 'method', handler = function(arg) options.method = killMethod[arg] end, hasArg = true}, + {'m', 'method', handler = function(arg) options.method = killMethod[arg:upper()] end, hasArg = true}, {'o', 'only-visible', handler = function() options.only_visible = true end}, {'f', 'include-friendly', handler = function() options.include_friendly = true end}, }) @@ -130,50 +141,46 @@ end if positionals[1] == "help" or options.help then print(dfhack.script_help()) + return end if positionals[1] == "this" then local selected_unit = dfhack.gui.getSelectedUnit() - if not selected_unit then qerror("Select a unit and run the script again.") end - - killUnit(selected_unit) + killUnit(selected_unit, options.method) + print('Unit exterminated.') return end -if positionals[1] == nil then - local map_races = getMapRaces(options.only_visible, options.include_friendly) +local map_races = getMapRaces(options.only_visible, options.include_friendly) +if not positionals[1] then local sorted_races = {} for race, value in pairs(map_races) do table.insert(sorted_races, { name = race, count = value.count }) end - table.sort(sorted_races, function(a, b) return a.count > b.count end) - - for _, race in pairs(sorted_races) do + for _, race in ipairs(sorted_races) do print(([[%4s %s]]):format(race.count, race.name)) end - return end -local map_races = getMapRaces(options.only_visible, options.include_friendly) - -if string.find(positionals[1], "UNDEAD") then - if map_races.UNDEAD then - for _, unit in pairs(df.global.world.units.active) do - if dfhack.units.isUndead(unit) and checkUnit(unit) then - killUnit(unit) - end - end - else +local count = 0 +if positionals[1]:lower() == 'undead' then + if not map_races.UNDEAD then qerror("No undead found on the map.") end + for _, unit in pairs(df.global.world.units.active) do + if dfhack.units.isUndead(unit) and checkUnit(unit) then + killUnit(unit, options.method) + count = count + 1 + end + end else local selected_race, selected_caste = positionals[1], nil @@ -192,40 +199,27 @@ else qerror("Invalid caste.") end - local count = 0 for _, unit in pairs(df.global.world.units.active) do - if not dfhack.units.isActive(unit) or not checkUnit(unit) then + if not checkUnit(unit) then goto skipunit end - if options.only_visible and not dfhack.units.isVisible(unit) then goto skipunit end - if not options.include_friendly and isUnitFriendly(unit) then goto skipunit end - if selected_caste and selected_caste ~= df.creature_raw.find(unit.race).caste[unit.caste].caste_id then goto skipunit end if selected_race == df.creature_raw.find(unit.race).creature_id then - if options.method == killMethod.BUTCHER then - butcherUnit(unit) - elseif options.method == killMethod.MAGMA then - drownUnit(unit, df.tile_liquid.Magma) - elseif options.method == killMethod.DROWN then - drownUnit(unit, df.tile_liquid.Water) - else - killUnit(unit) - end - + killUnit(unit, options.method) count = count + 1 end :: skipunit :: end - - print(([[Exterminated %s creatures.]]):format(count)) end + +print(([[Exterminated %d creatures.]]):format(count)) diff --git a/fix/general-strike.lua b/fix/general-strike.lua new file mode 100644 index 0000000000..c07c738c2e --- /dev/null +++ b/fix/general-strike.lua @@ -0,0 +1,38 @@ +local argparse = require('argparse') + +-- sometimes, planted seeds lose their 'in_building' flag. This causes massive +-- amounts of job cancellation spam as everyone tries, then fails, to re-plant +-- the seeds. +local function fix_seeds(quiet) + local count = 0 + for _,v in ipairs(df.global.world.items.other.SEEDS) do + if not v.flags.in_building then + local bld = dfhack.items.getHolderBuilding(v) + if bld and bld:isFarmPlot() then + v.flags.in_building = true + count = count + 1 + end + end + end + if not quiet or count > 0 then + print(('fixed %d seed(s)'):format(count)) + end +end + +local function main(args) + local help = false + local quiet = false + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() help = true end}, + {'q', 'quiet', handler=function() quiet = true end}, + }) + + if help or positionals[1] == 'help' then + print(dfhack.script_help()) + return + end + + fix_seeds(quiet) +end + +main{...} diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua new file mode 100644 index 0000000000..75b58bb4c2 --- /dev/null +++ b/gui/civ-alert.lua @@ -0,0 +1,294 @@ +--@ module=true + +local gui = require('gui') +local widgets = require('gui.widgets') +local overlay = require('plugins.overlay') + +local function get_civ_alert() + local list = df.global.plotinfo.alerts.list + while #list < 2 do + local list_item = df.plotinfost.T_alerts.T_list:new() + list_item.id = df.global.plotinfo.alerts.next_id + df.global.plotinfo.alerts.next_id = df.global.plotinfo.alerts.next_id + 1 + list_item.name = 'civ-alert' + list:insert('#', list_item) + end + return list[1] +end + +local function can_sound_alarm() + return df.global.plotinfo.alerts.civ_alert_idx == 0 and + #get_civ_alert().burrows > 0 +end + +local function sound_alarm() + if not can_sound_alarm() then return end + df.global.plotinfo.alerts.civ_alert_idx = 1 +end + +local function can_clear_alarm() + return df.global.plotinfo.alerts.civ_alert_idx ~= 0 +end + +local function clear_alarm() + df.global.plotinfo.alerts.civ_alert_idx = 0 +end + +local function toggle_civalert_burrow(id) + local burrows = get_civ_alert().burrows + if #burrows == 0 then + burrows:insert('#', id) + elseif burrows[0] == id then + burrows:resize(0) + clear_alarm() + else + burrows[0] = id + end +end + +-- +-- BigRedButton +-- + +local to_pen = dfhack.pen.parse +local BUTTON_TEXT_ON = to_pen{fg=COLOR_BLACK, bg=COLOR_LIGHTRED} +local BUTTON_TEXT_OFF = to_pen{fg=COLOR_WHITE, bg=COLOR_RED} + +BigRedButton = defclass(BigRedButton, widgets.Panel) +BigRedButton.ATTRS{ +} + +function BigRedButton:init() + self.frame = self.frame or {} + self.frame.w = 10 + self.frame.h = 3 + + self:addviews{ + widgets.Label{ + text={ + ' Activate ', NEWLINE, + ' civilian ', NEWLINE, + ' alert ', + }, + text_pen=BUTTON_TEXT_ON, + text_hpen=BUTTON_TEXT_OFF, + visible=can_sound_alarm, + on_click=sound_alarm, + }, + widgets.Label{ + text={ + ' Clear ', NEWLINE, + ' civilian ', NEWLINE, + ' alert ', + }, + text_pen=BUTTON_TEXT_OFF, + text_hpen=BUTTON_TEXT_ON, + visible=can_clear_alarm, + on_click=clear_alarm, + }, + } +end + +-- +-- CivalertOverlay +-- + +CivalertOverlay = defclass(CivalertOverlay, overlay.OverlayWidget) +CivalertOverlay.ATTRS{ + default_pos={x=-15,y=-1}, + default_enabled=true, + viewscreens='dwarfmode', + frame={w=20, h=5}, +} + +local function should_show_alert_button() + return can_clear_alarm() or + (df.global.game.main_interface.squads.open and can_sound_alarm()) +end + +local function should_show_configure_button() + return df.global.game.main_interface.squads.open + and not can_sound_alarm() and not can_clear_alarm() +end + +local function launch_config() + dfhack.run_script('gui/civ-alert') +end + +last_tp_start = last_tp_start or 0 +CONFIG_BUTTON_PENS = CONFIG_BUTTON_PENS or {} +local function get_button_pen(idx) + local start = dfhack.textures.getControlPanelTexposStart() + if last_tp_start == start then return CONFIG_BUTTON_PENS[idx] end + last_tp_start = start + + local tp = function(offset) + if start == -1 then return nil end + return start + offset + end + + CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=tp(6), ch=string.byte('[')} + CONFIG_BUTTON_PENS[2] = to_pen{tile=tp(9), ch=15} -- gear/masterwork symbol + CONFIG_BUTTON_PENS[3] = to_pen{fg=COLOR_CYAN, tile=tp(7), ch=string.byte(']')} + + return CONFIG_BUTTON_PENS[idx] +end + +function CivalertOverlay:init() + self:addviews{ + widgets.Panel{ + frame={t=0, r=0, w=16, h=5}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + visible=should_show_alert_button, + subviews={ + BigRedButton{ + frame={t=0, l=0}, + }, + widgets.Label{ + frame={t=1, r=0, w=3}, + text={ + {tile=curry(get_button_pen, 1)}, + {tile=curry(get_button_pen, 2)}, + {tile=curry(get_button_pen, 3)}, + }, + on_click=launch_config, + }, + }, + }, + widgets.Panel{ + frame={b=0, r=0, w=20, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + visible=should_show_configure_button, + subviews={ + widgets.Label{ + text={ + 'Click to configure', NEWLINE, + ' civilian alert ', + }, + text_pen=to_pen{fg=COLOR_YELLOW, bg=COLOR_BLACK}, + on_click=launch_config, + }, + }, + }, + } +end + +OVERLAY_WIDGETS = {big_red_button=CivalertOverlay} + +-- +-- Civalert +-- + +Civalert = defclass(Civalert, widgets.Window) +Civalert.ATTRS{ + frame_title='Civilian alert', + frame={w=60, h=20}, + resizable=true, + resize_min={h=15}, +} + +function Civalert:init() + local choices = self:get_burrow_choices() + + self:addviews{ + widgets.Panel{ + frame={t=0, l=0, r=12}, + subviews={ + widgets.WrappedLabel{ + frame={t=0, r=0, h=2}, + text_to_wrap='Choose a burrow where you want your civilians to hide during danger.', + }, + widgets.HotkeyLabel{ + frame={t=3, l=0}, + key='CUSTOM_CTRL_W', + label='Sound alarm! Citizens run to safety!', + on_activate=sound_alarm, + enabled=can_sound_alarm, + }, + widgets.HotkeyLabel{ + frame={t=4, l=0}, + key='CUSTOM_CTRL_D', + label='All clear! Citizens return to normal', + on_activate=clear_alarm, + enabled=can_clear_alarm, + }, + }, + }, + BigRedButton{ + frame={t=0, r=0}, + }, + widgets.FilteredList{ + frame={t=6, l=0, b=0, r=0}, + choices=choices, + icon_width=2, + on_submit=self:callback('select_burrow'), + visible=#choices > 0, + }, + widgets.WrappedLabel{ + frame={t=7, l=0, r=0}, + text_to_wrap='No burrows defined. Please define one to use for the civalert.', + text_pen=COLOR_RED, + visible=#choices == 0, + }, + } +end + +local function get_burrow_name(burrow) + if #burrow.name > 0 then return burrow.name end + return ('Burrow %d'):format(burrow.id+1) +end + +local SELECTED_ICON = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} + +function Civalert:get_burrow_icon(id) + local burrows = get_civ_alert().burrows + if #burrows == 0 or burrows[0] ~= id then return nil end + return SELECTED_ICON +end + +function Civalert:get_burrow_choices() + local choices = {} + for _,burrow in ipairs(df.global.plotinfo.burrows.list) do + local choice = { + text=get_burrow_name(burrow), + id=burrow.id, + icon=self:callback('get_burrow_icon', burrow.id), + } + table.insert(choices, choice) + end + return choices +end + +function Civalert:select_burrow(_, choice) + toggle_civalert_burrow(choice.id) + self:updateLayout() +end + +-- +-- CivalertScreen +-- + +CivalertScreen = defclass(CivalertScreen, gui.ZScreen) +CivalertScreen.ATTRS { + focus_path='civalert', +} + +function CivalertScreen:init() + self:addviews{Civalert{}} +end + +function CivalertScreen:onDismiss() + view = nil +end + +if dfhack_flags.module then + return +end + +if not dfhack.isMapLoaded() then + qerror('gui/civ-alert requires a map to be loaded') +end + +view = view and view:raise() or CivalertScreen{}:show() diff --git a/gui/control-panel.lua b/gui/control-panel.lua index da204a6cda..f9e8000e3c 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -6,11 +6,13 @@ local repeatUtil = require('repeat-util') local utils = require('utils') local widgets = require('gui.widgets') +-- init files +local SYSTEM_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-system.init' local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init' local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' --- eventually this should be queryable from Core/script-manager +-- service and command lists local FORT_SERVICES = { 'autobutcher', 'autochop', @@ -35,21 +37,27 @@ local FORT_SERVICES = { local FORT_AUTOSTART = { 'ban-cooking all', - --'buildingplan set boulders false', - --'buildingplan set logs false', + 'buildingplan set boulders false', + 'buildingplan set logs false', } for _,v in ipairs(FORT_SERVICES) do table.insert(FORT_AUTOSTART, v) end table.sort(FORT_AUTOSTART) --- eventually this should be queryable from Core/script-manager local SYSTEM_SERVICES = { 'automelt', -- TODO needs dynamic detection of configurability 'buildingplan', 'confirm', 'overlay', } +local SYSTEM_USER_SERVICES = { + 'faststart', +} +for _,v in ipairs(SYSTEM_USER_SERVICES) do + table.insert(SYSTEM_SERVICES, v) +end +table.sort(SYSTEM_SERVICES) local PREFERENCES = { ['gui']={ @@ -76,6 +84,12 @@ local REPEATS = { ['cleanowned']={ desc='Encourage dwarves to drop tattered clothing and grab new ones.', command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, + ['combine']={ + desc='Combine partial stacks in stockpiles into full stacks.', + command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, + ['general-strike']={ + desc='Prevent dwarves from getting stuck and refusing to work.', + command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, ['orders-sort']={ desc='Sort manager orders by repeat frequency so one-time orders can be completed.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, @@ -92,7 +106,7 @@ table.sort(REPEATS_LIST) -- save_fn takes the file as a param and should call f:write() to write data local function save_file(path, save_fn) local ok, f = pcall(io.open, path, 'w') - if not ok then + if not ok or not f then dialogs.showMessage('Error', ('Cannot open file for writing: "%s"'):format(path)) return @@ -103,32 +117,35 @@ local function save_file(path, save_fn) f:close() end - local function get_icon_pens() local start = dfhack.textures.getControlPanelTexposStart() local valid = start > 0 start = start + 10 + local function tp(offset) + return valid and start + offset or nil + end + local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+0) or nil, ch=string.byte('[')} + tile=tp(0), ch=string.byte('[')} local enabled_pen_center = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, - tile=valid and (start+1) or nil, ch=251} -- check + tile=tp(1) or nil, ch=251} -- check local enabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+2) or nil, ch=string.byte(']')} + tile=tp(2) or nil, ch=string.byte(']')} local disabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+3) or nil, ch=string.byte('[')} + tile=tp(3) or nil, ch=string.byte('[')} local disabled_pen_center = dfhack.pen.parse{fg=COLOR_RED, - tile=valid and (start+4) or nil, ch=string.byte('x')} + tile=tp(4) or nil, ch=string.byte('x')} local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+5) or nil, ch=string.byte(']')} + tile=tp(5) or nil, ch=string.byte(']')} local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+6) or nil, ch=string.byte('[')} + tile=tp(6) or nil, ch=string.byte('[')} local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=valid and (start+7) or nil, ch=string.byte(']')} + tile=tp(7) or nil, ch=string.byte(']')} local help_pen_center = dfhack.pen.parse{ - tile=valid and (start+8) or nil, ch=string.byte('?')} + tile=tp(8) or nil, ch=string.byte('?')} local configure_pen_center = dfhack.pen.parse{ - tile=valid and (start+9) or nil, ch=15} -- gear/masterwork symbol + tile=tp(9) or nil, ch=15} -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, button_pen_left, button_pen_right, @@ -221,13 +238,15 @@ function ConfigPanel:onInput(keys) return handled end +local COMMAND_REGEX = '^([%w/_-]+)' + function ConfigPanel:refresh() local choices = {} for _,choice in ipairs(self:get_choices()) do local command = choice.target or choice.command - command = command:match('^([%l/_-]+)') + command = command:match(COMMAND_REGEX) local gui_config = 'gui/' .. command - local want_gui_config = utils.getval(self.is_configurable) + local want_gui_config = utils.getval(self.is_configurable, gui_config) and helpdb.is_entry(gui_config) local enabled = choice.enabled local function get_enabled_pen(enabled_pen, disabled_pen) @@ -293,7 +312,7 @@ end function ConfigPanel:show_help() _,choice = self.subviews.list:getSelected() if not choice then return end - local command = choice.target:match('^([%l/_-]+)') + local command = choice.target:match(COMMAND_REGEX) dfhack.run_command('gui/launcher', command .. ' ') end @@ -341,7 +360,7 @@ end FortServices = defclass(FortServices, Services) FortServices.ATTRS{ is_enableable=dfhack.world.isFortressMode, - is_configurable=dfhack.world.isFortressMode, + is_configurable=function() return dfhack.world.isFortressMode() end, intro_text='These tools can only be enabled when you have a fort loaded,'.. ' but once you enable them, they will stay enabled when you'.. ' save and reload your fort. If you want them to be'.. @@ -368,12 +387,13 @@ function FortServicesAutostart:init() local enabled_map = {} local ok, f = pcall(io.open, AUTOSTART_FILE) if ok and f then + local services_set = utils.invert(FORT_AUTOSTART) for line in f:lines() do line = line:trim() if #line == 0 or line:startswith('#') then goto continue end local service = line:match('^on%-new%-fortress enable ([%S]+)$') or line:match('^on%-new%-fortress (.+)') - if service then + if service and services_set[service] then enabled_map[service] = true end ::continue:: @@ -410,17 +430,36 @@ end -- SystemServices -- +local function system_service_is_configurable(gui_config) + return gui_config ~= 'gui/automelt' or dfhack.world.isFortressMode() +end + SystemServices = defclass(SystemServices, Services) SystemServices.ATTRS{ title='System', is_enableable=true, - is_configurable=true, - intro_text='These are DFHack system services that should generally not'.. - ' be turned off. If you do turn them off, they may'.. - ' automatically re-enable themselves when you restart DF.', + is_configurable=system_service_is_configurable, + intro_text='These are DFHack system services that are not bound to' .. + ' a specific fort. Some of these are critical DFHack services' .. + ' that can be manually disabled, but will re-enable themselves' .. + ' when DF restarts.', services_list=SYSTEM_SERVICES, } +function SystemServices:on_submit() + SystemServices.super.on_submit(self) + + local enabled_map = self:get_enabled_map() + local save_fn = function(f) + for _,service in ipairs(SYSTEM_USER_SERVICES) do + if enabled_map[service] then + f:write(('enable %s\n'):format(service)) + end + end + end + save_file(SYSTEM_INIT_FILE, save_fn) +end + -- -- Overlays -- diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index dd29cf3279..e38ac7a401 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -4,12 +4,38 @@ local dialog = require('gui.dialogs') local gui = require('gui') local widgets = require('gui.widgets') +local to_pen = dfhack.pen.parse + +local tb_texpos = dfhack.textures.getThinBordersTexposStart() +local tp = function(offset) + if tb_texpos == -1 then return nil end + return tb_texpos + offset +end + +local function get_key_pens(ch) + return { + lt=to_pen{tile=tp(0), write_to_lower=true}, + t=to_pen{tile=tp(1), ch=ch, write_to_lower=true, top_of_text=true}, + t_ascii=to_pen{ch=32}, + rt=to_pen{tile=tp(2), write_to_lower=true}, + lb=to_pen{tile=tp(14), write_to_lower=true}, + b=to_pen{tile=tp(15), ch=ch, write_to_lower=true, bottom_of_text=true}, + rb=to_pen{tile=tp(16), write_to_lower=true}, + } +end + +local function get_key_hover_pens(ch) + return { + t=to_pen{tile=tp(1), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, top_of_text=true}, + t_ascii=to_pen{fg=COLOR_WHITE, bg=COLOR_RED, ch=ch == 0 and 0 or 32}, + b=to_pen{tile=tp(15), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, bottom_of_text=true}, + } +end + CPDialog = defclass(CPDialog, widgets.Window) CPDialog.ATTRS { - focus_path='cp437-table', frame_title='CP437 table', - drag_anchors={frame=true, body=true}, - frame={w=36, h=17}, + frame={w=100, h=26}, } function CPDialog:init(info) @@ -17,33 +43,67 @@ function CPDialog:init(info) widgets.EditField{ view_id='edit', frame={t=0, l=0}, - on_submit=self:callback('submit'), }, widgets.Panel{ view_id='board', - frame={t=2, l=0, w=32, h=9}, - on_render=self:callback('render_board'), + frame={t=2, l=0, w=96, h=18}, }, widgets.Label{ - frame={b=1, l=0}, + frame={b=2, l=0}, text='Click characters or type', }, - widgets.Label{ + widgets.HotkeyLabel{ frame={b=0, l=0}, - text={ - {key='LEAVESCREEN', text=': Cancel'}, - ' ', - {key='SELECT', text=': Done'}, - }, + key='SELECT', + label='Send text to parent', + auto_width=true, + on_activate=self:callback('submit'), + }, + widgets.HotkeyLabel{ + frame={b=0}, + key='STRING_A000', + label='Backspace', + auto_width=true, + on_activate=function() self.subviews.edit:onInput{_STRING=0} end, + }, + widgets.HotkeyLabel{ + frame={b=0, r=0}, + key='LEAVESCREEN', + label='Cancel', + auto_width=true, + on_activate=function() self.parent_view:dismiss() end, }, } -end -function CPDialog:render_board(dc) + local board = self.subviews.board + local edit = self.subviews.edit for ch = 0,255 do - if dfhack.screen.charToKey(ch) then - dc:seek(ch % 32, math.floor(ch / 32)):char(ch) + local xoff, yoff = (ch%32) * 3, (ch//32) * 2 + if not dfhack.screen.charToKey(ch) then ch = 0 end + local pens = get_key_pens(ch) + local hpens = get_key_hover_pens(ch) + local function get_top_tile() + return dfhack.screen.inGraphicsMode() and pens.t or pens.t_ascii end + local function get_top_htile() + return dfhack.screen.inGraphicsMode() and hpens.t or hpens.t_ascii + end + board:addviews{ + widgets.Label{ + frame={t=yoff, l=xoff, w=3, h=2}, + auto_height=false, + text={ + {tile=pens.lt}, + {tile=get_top_tile, htile=get_top_htile}, + {tile=pens.rt}, + NEWLINE, + {tile=pens.lb}, + {tile=pens.b, htile=hpens.b}, + {tile=pens.rb}, + }, + on_click=function() if ch ~= 0 then edit:insert(string.char(ch)) end end, + }, + } end end @@ -61,6 +121,11 @@ function CPDialog:submit() end keys[i] = k end + + -- ensure clicks on "submit" don't bleed through + df.global.enabler.mouse_lbut = 0 + df.global.enabler.mouse_lbut_down = 0 + local screen = self.parent_view local parent = screen._native.parent dfhack.screen.hideGuard(screen, function() @@ -71,25 +136,13 @@ function CPDialog:submit() screen:dismiss() end -function CPDialog:onInput(keys) - local x, y = self.subviews.board:getMousePos() - if keys._MOUSE_L_DOWN and x then - local ch = x + (32 * y) - if ch ~= 0 and dfhack.screen.charToKey(ch) then - self.subviews.edit:insert(string.char(ch)) - end - return true - end - return CPDialog.super.onInput(self, keys) -end - CPScreen = defclass(CPScreen, gui.ZScreen) CPScreen.ATTRS { focus_path='cp437-table', } function CPScreen:init() - self:addviews{CPDialog{view_id='main'}} + self:addviews{CPDialog{}} end function CPScreen:onDismiss() diff --git a/gui/design.lua b/gui/design.lua index bc0aec5bcd..ef2e0dcdd0 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -302,6 +302,7 @@ function MarksPanel:update_mark_labels() } } + self:updateLayout() end -- Panel to show the Mouse position/dimensions/etc @@ -469,7 +470,7 @@ function GenericOptionsPanel:init() active = true, enabled = true, initial_option = false, - on_change = nil + on_change = function() self.design_panel.needs_update = true end }, widgets.ResizingPanel { view_id = 'transform_panel_rotate', @@ -1513,10 +1514,10 @@ function Design:onRenderFrame(dc, rect) self.shape:update(points, self.extra_points) self.last_mouse_point = mouse_pos self.needs_update = false + self:add_shape_options() + self:updateLayout() end - self:add_shape_options() - -- Generate bounds based on the shape's dimensions local bounds = self:get_view_bounds() if self.shape and bounds then @@ -1565,8 +1566,6 @@ function Design:onRenderFrame(dc, rect) end guidm.renderMapOverlay(get_overlay_pen, bounds) - - self:updateLayout() end -- TODO function too long diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index e609749f70..68ce5fe37e 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -72,7 +72,7 @@ end GmEditorUi = defclass(GmEditorUi, widgets.Window) GmEditorUi.ATTRS{ - frame=config.data, + frame=copyall(config.data), frame_title="GameMaster's editor", frame_inset=0, resizable=true, @@ -181,11 +181,22 @@ function GmEditorUi:find_id(force_dialog) ref_target = field.ref_target end if ref_target and not force_dialog then + local obj if not ref_target.find then - dialog.showMessage("Error!", ("Cannot look up %s by ID"):format(getmetatable(ref_target)), COLOR_LIGHTRED) - return + if key == 'mat_type' then + local ok, mi = pcall(function() + return self:currentTarget().target['mat_index'] + end) + if ok then + obj = dfhack.matinfo.decode(id, mi) + end + end + if not obj then + dialog.showMessage("Error!", ("Cannot look up %s by ID"):format(getmetatable(ref_target)), COLOR_LIGHTRED) + return + end end - local obj = ref_target.find(id) + obj = obj or ref_target.find(id) if obj then self:pushTarget(obj) else @@ -477,7 +488,9 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) if reindex then trg.keys={} + trg.kw=10 for k,v in pairs(trg.target) do + if #tostring(k)>trg.kw then trg.kw=#tostring(k) end if filter~= "" then local ok,ret=dfhack.pcall(string.match,tostring(k):lower(),filter) if not ok then @@ -493,7 +506,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) self.subviews.lbl_current_item:itemById('name').text=tostring(trg.target) local t={} for k,v in pairs(trg.keys) do - table.insert(t,{text={{text=string.format("%-25s",tostring(v))},{gap=1,text=getStringValue(trg,v)}}}) + table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=1,text=getStringValue(trg,v)}}}) end local last_pos if preserve_pos then @@ -510,6 +523,7 @@ function GmEditorUi:pushTarget(target_to_push) local new_tbl={} new_tbl.target=target_to_push new_tbl.keys={} + new_tbl.kw=10 new_tbl.selected=1 new_tbl.filter="" if self:currentTarget()~=nil then @@ -517,6 +531,7 @@ function GmEditorUi:pushTarget(target_to_push) self.stack[#self.stack].filter=self.subviews.filter_input.text end for k,v in pairs(target_to_push) do + if #tostring(k)>new_tbl.kw then new_tbl.kw=#tostring(k) end table.insert(new_tbl.keys,k) end new_tbl.item_count=#new_tbl.keys diff --git a/gui/launcher.lua b/gui/launcher.lua index 1468c302ba..5a5a2a6d26 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -794,7 +794,7 @@ local function sort_by_freq(entries) table.sort(entries, stable_sort_by_frequency) end -local DEV_FILTER = {tag={'dev', 'untested'}} +local DEV_FILTER = {tag={'dev', 'unavailable'}} -- adds the n most closely affiliated peer entries for the given entry that -- aren't already in the entries list. affiliation is determined by how many diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua new file mode 100644 index 0000000000..30ae9dc8d0 --- /dev/null +++ b/gui/seedwatch.lua @@ -0,0 +1,291 @@ +-- config ui for seedwatch + +local gui = require('gui') +local widgets = require('gui.widgets') +local plugin = require('plugins.seedwatch') + +local PROPERTIES_HEADER = ' Quantity Target ' +local REFRESH_MS = 10000 +local MAX_TARGET = 2147483647 +-- +-- SeedSettings +-- +SeedSettings = defclass(SeedSettings, widgets.Window) +SeedSettings.ATTRS{ + frame={l=5, t=5, w=35, h=9}, +} + +function SeedSettings:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Seed: ', + }, + widgets.Label{ + view_id='name', + frame={t=0, l=6}, + text_pen=COLOR_GREEN, + }, + widgets.Label{ + frame={t=1, l=0}, + text='Quantity: ', + }, + widgets.Label{ + view_id='quantity', + frame={t=1, l=10}, + text_pen=COLOR_GREEN, + }, + widgets.EditField{ + view_id='target', + frame={t=2, l=0}, + label_text='Target: ', + key='CUSTOM_CTRL_T', + on_char=function(ch) return ch:match('%d') end, + on_submit=self:callback('commit'), + }, + widgets.HotkeyLabel{ + frame={t=4, l=0}, + key='SELECT', + label='Apply', + on_activate=self:callback('commit'), + }, + } +end + +function SeedSettings:show(choice, on_commit) + self.data = choice.data + self.on_commit = on_commit + self.subviews.name:setText(self.data.name) + self.subviews.quantity:setText(tostring(self.data.quantity)) + self.subviews.target:setText(tostring(self.data.target)) + self.visible = true + self:setFocus(true) + self:updateLayout() +end + +function SeedSettings:hide() + self:setFocus(false) + self.visible = false +end + +function SeedSettings:commit() + local target = math.tointeger(self.subviews.target.text) or 0 + target = math.min(MAX_TARGET, math.max(0, target)) + + plugin.seedwatch_setTarget(self.data.id, target) + self:hide() + self.on_commit() +end + +function SeedSettings:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + self:hide() + return true + end + SeedSettings.super.onInput(self, keys) + return true +end + +-- +-- Seedwatch +-- +Seedwatch = defclass(Seedwatch, widgets.Window) +Seedwatch.ATTRS { + frame_title='Seedwatch', + frame={w=60, h=27}, + resizable=true, + resize_min={h=25}, +} + +function Seedwatch:init() + local minimal = false + local saved_frame = {w=50, h=6, r=2, t=18} + local saved_resize_min = {w=saved_frame.w, h=saved_frame.h} + local function toggle_minimal() + minimal = not minimal + local swap = self.frame + self.frame = saved_frame + saved_frame = swap + swap = self.resize_min + self.resize_min = saved_resize_min + saved_resize_min = swap + self:updateLayout() + self:refresh_data() + end + local function is_minimal() + return minimal + end + local function is_not_minimal() + return not minimal + end + + self:addviews{ + widgets.ToggleHotkeyLabel{ + view_id='enable_toggle', + frame={t=0, l=0, w=31}, + label='Seedwatch is', + key='CUSTOM_CTRL_E', + options={{value=true, label='Enabled', pen=COLOR_GREEN}, + {value=false, label='Disabled', pen=COLOR_RED}}, + on_change=function(val) plugin.setEnabled(val) end, + }, + widgets.EditField{ + view_id='all', + frame={t=1, l=0}, + label_text='Target for all: ', + key='CUSTOM_CTRL_A', + on_char=function(ch) return ch:match('%d') end, + on_submit=function(text) + local target = math.tointeger(text) + if not target or target == '' then + target = 0 + elseif target > MAX_TARGET then + target = MAX_TARGET + end + plugin.seedwatch_setTarget('all', target) + self.subviews.list:setFilter('') + self:refresh_data() + self:update_choices() + end, + visible=is_not_minimal, + text='30', + }, + + widgets.HotkeyLabel{ + frame={r=0, t=0, w=10}, + key='CUSTOM_ALT_M', + label=string.char(31)..string.char(30), + on_activate=toggle_minimal}, + widgets.Label{ + view_id='minimal_summary', + frame={t=1, l=0, h=1}, + auto_height=false, + visible=is_minimal, + }, + widgets.Label{ + frame={t=3, l=0}, + text='Seed', + auto_width=true, + visible=is_not_minimal, + }, + widgets.Label{ + frame={t=3, r=0}, + text=PROPERTIES_HEADER, + auto_width=true, + visible=is_not_minimal, + }, + widgets.FilteredList{ + view_id='list', + frame={t=5, l=0, r=0, b=3}, + on_submit=self:callback('configure_seed'), + visible=is_not_minimal, + edit_key = 'CUSTOM_S', + }, + widgets.Label{ + view_id='summary', + frame={b=0, l=0}, + visible=is_not_minimal, + }, + SeedSettings{ + view_id='seed_settings', + visible=false, + }, + + } + + self:refresh_data() +end + +function Seedwatch:configure_seed(idx, choice) + self.subviews.seed_settings:show(choice, function() + self:refresh_data() + self:update_choices() + end) +end + +function Seedwatch:update_choices() + local list = self.subviews.list + local name_width = list.frame_body.width - #PROPERTIES_HEADER + local fmt = '%-'..tostring(name_width)..'s %10d %10d ' + local choices = {} + local prior_search=self.subviews.list.edit.text + for k, v in pairs(self.data.seeds) do + local text = (fmt):format(v.name:sub(1,name_width), v.quantity or 0, v.target or 0) + table.insert(choices, {text=text, data=v}) + end + + self.subviews.list:setChoices(choices) + if prior_search then self.subviews.list:setFilter(prior_search) end + self.subviews.list:updateLayout() +end + +function Seedwatch:refresh_data() + self.subviews.enable_toggle:setOption(plugin.isEnabled()) + local watch_map, seed_counts = plugin.seedwatch_getData() + self.data = {} + self.data.sum = 0 + self.data.seeds_qty = 0 + self.data.seeds_watched = 0 + self.data.seeds = {} + for k,v in pairs(seed_counts) do + local seed = {} + seed.id = df.global.world.raws.plants.all[k].id + seed.name = df.global.world.raws.plants.all[k].seed_singular + seed.quantity = v + seed.target = watch_map[k] or 0 + self.data.seeds[k] = seed + if self.data.seeds[k].target > 0 then + self.data.seeds_watched = self.data.seeds_watched + 1 + end + self.data.seeds_qty = self.data.seeds_qty + v + end + if self.subviews.all.text == '' then + self.subviews.all:setText('0') + end + local summary_text = ('Seeds quantity: %d watched: %d\n'):format(tostring(self.data.seeds_qty),tostring(self.data.seeds_watched)) + self.subviews.summary:setText(summary_text) + local minimal_summary_text = summary_text + self.subviews.minimal_summary:setText(minimal_summary_text) + + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + +end + + +function Seedwatch:postUpdateLayout() + self:update_choices() +end + +-- refreshes data every 10 seconds or so +function Seedwatch:onRenderBody() + if self.next_refresh_ms <= dfhack.getTickCount() + and self.subviews.seed_settings.visible == false + and not self.subviews.all.focus + and not self.subviews.list.edit.focus then + self:refresh_data() + self:update_choices() + end +end + +-- +-- SeedwatchScreen +-- + +SeedwatchScreen = defclass(SeedwatchScreen, gui.ZScreen) +SeedwatchScreen.ATTRS { + focus_path='seedwatch', +} + +function SeedwatchScreen:init() + self:addviews{Seedwatch{}} +end + +function SeedwatchScreen:onDismiss() + view = nil +end + +if not dfhack.isMapLoaded() then + qerror('seedwatch requires a map to be loaded') +end + +view = view and view:raise() or SeedwatchScreen{}:show() diff --git a/internal/quickfort/command.lua b/internal/quickfort/command.lua index ed9701375a..ce73888f32 100644 --- a/internal/quickfort/command.lua +++ b/internal/quickfort/command.lua @@ -22,7 +22,7 @@ end local command_switch = { run='do_run', - orders='do_orders', + -- orders='do_orders', -- until we get stockflow working undo='do_undo', } @@ -234,6 +234,9 @@ end function do_command(args) for _,command in ipairs(args.commands) do if not command or not command_switch[command] then + if command == 'orders' then + qerror('orders functionality not updated yet') + end qerror(string.format('invalid command: "%s"', command)) end end diff --git a/prioritize.lua b/prioritize.lua index 0102089e0c..ed04094d50 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -10,25 +10,25 @@ local persist = require('persist-table') local GLOBAL_KEY = 'prioritize' -- used for state change hooks and persistence local DEFAULT_HAUL_LABORS = {'Food', 'Body', 'Animals'} -local DEFAULT_REACTION_NAMES = {'TAN_A_HIDE'} +local DEFAULT_REACTION_NAMES = {'TAN_A_HIDE', 'ADAMANTINE_WAFERS'} local DEFAULT_JOB_TYPES = { -- take care of rottables before they rot - 'StoreItemInStockpile', 'CustomReaction', 'PrepareRawFish', + 'StoreItemInStockpile', 'CustomReaction', 'StoreItemInBarrel', + 'PrepareRawFish', 'PlaceItemInTomb', -- ensure medical, hygiene, and hospice tasks get done - 'CleanSelf', 'RecoverWounded', 'ApplyCast', 'BringCrutch', 'CleanPatient', - 'DiagnosePatient', 'DressWound', 'GiveFood', 'GiveWater', 'ImmobilizeBreak', - 'PlaceInTraction', 'SetBone', 'Surgery', 'Suture', - -- organize items efficiently so new items can be brought to the stockpiles - 'StoreItemInVehicle', 'StoreItemInBag', 'StoreItemInBarrel', - 'StoreItemInLocation', 'StoreItemInBin', 'PushTrackVehicle', + 'ApplyCast', 'BringCrutch', 'CleanPatient', 'CleanSelf', + 'DiagnosePatient', 'DressWound', 'GiveFood', 'GiveWater', + 'ImmobilizeBreak', 'PlaceInTraction', 'RecoverWounded', + 'SeekInfant', 'SetBone', 'Surgery', 'Suture', -- ensure prisoners and animals are tended to quickly - 'TameAnimal', 'TrainAnimal', 'TrainHuntingAnimal', 'TrainWarAnimal', - 'PenLargeAnimal', 'PitLargeAnimal', 'SlaughterAnimal', - -- when these things come up, get them done ASAP - 'ManageWorkOrders', 'TradeAtDepot', 'BringItemToDepot', 'DumpItem', - 'DestroyBuilding', 'RemoveConstruction', 'PullLever', 'FellTree', - 'FireBallista', 'FireCatapult', 'OperatePump', 'CollectSand', 'MakeArmor', - 'MakeWeapon', + -- (Animal/prisoner storage already covered by 'StoreItemInStockpile' above) + 'SlaughterAnimal', + -- ensure noble tasks never get starved + 'InterrogateSubject', 'ManageWorkOrders', 'ReportCrime', 'TradeAtDepot', + -- get tasks done quickly that might block the player from getting on to + -- the next thing they want to do + 'BringItemToDepot', 'DestroyBuilding', 'DumpItem', 'FellTree', + 'RemoveConstruction', } -- set of job types that we are watching. maps job_type (as a number) to @@ -587,8 +587,15 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then return end - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') - g_watched_job_matchers = persisted_data or {} + local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') or {} + -- sometimes the keys come back as strings; fix that up + for k,v in pairs(persisted_data) do + if type(k) == 'string' then + persisted_data[tonumber(k)] = v + persisted_data[k] = nil + end + end + g_watched_job_matchers = persisted_data update_handlers() end diff --git a/stripcaged.lua b/stripcaged.lua index 0bb9f49aa7..2585057c56 100644 --- a/stripcaged.lua +++ b/stripcaged.lua @@ -1,11 +1,10 @@ local argparse = require('argparse') local opts = {} -local positionals = argparse.processArgsGetopt({...}, - {{'h', 'help', handler = function() opts.help = true end}, - {nil, 'include-vermin', - handler = function() opts.include_vermin = true end}, - {nil, 'include-pets', - handler = function() opts.include_pets = true end}, +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler = function() opts.help = true end}, + {nil, 'include-vermin', handler = function() opts.include_vermin = true end}, + {nil, 'include-pets', handler = function() opts.include_pets = true end}, + {'f', 'skip-forbidden', handler = function() opts.skip_forbidden = true end}, }) local function plural(nr, name) @@ -14,22 +13,23 @@ local function plural(nr, name) end -- Checks item against opts to see if it's a vermin type that we ignore -local function isexcludedvermin(item, opts) +local function isexcludedvermin(item) if df.item_verminst:is_instance(item) then - if opts.include_vermin then - return false - else - return true - end + return not opts.include_vermin elseif df.item_petst:is_instance(item) then - if opts.include_pets then - return false - else - return true - end - else - return false + return not opts.include_pets end + return false +end + +local function dump_item(item) + if item.flags.dump then return 0 end + if not item.flags.forbid or not opts.skip_forbidden then + item.flags.dump = true + item.flags.forbid = false + return 1 + end + return 0 end local function cage_dump_items(list) @@ -40,9 +40,8 @@ local function cage_dump_items(list) for _, ref in ipairs(cage.general_refs) do if df.general_ref_contains_itemst:is_instance(ref) then local item = df.item.find(ref.item_id) - if not item.flags.dump and not isexcludedvermin(item, opts) then - count = count + 1 - item.flags.dump = true + if not isexcludedvermin(item) then + count = count + dump_item(item) end end end @@ -61,10 +60,8 @@ local function cage_dump_armor(list) if df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if not it.item.flags.dump and - it.mode == df.unit_inventory_item.T_mode.Worn then - count = count + 1 - it.item.flags.dump = true + if it.mode == df.unit_inventory_item.T_mode.Worn then + count = count + dump_item(it.item) end end end @@ -84,10 +81,8 @@ local function cage_dump_weapons(list) if df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if not it.item.flags.dump and - it.mode == df.unit_inventory_item.T_mode.Weapon then - count = count + 1 - it.item.flags.dump = true + if it.mode == df.unit_inventory_item.T_mode.Weapon then + count = count + dump_item(it.item) end end end @@ -101,23 +96,19 @@ end local function cage_dump_all(list) local count = 0 local count_cage = 0 - for _, cage in ipairs(list) do local pre_count = count for _, ref in ipairs(cage.general_refs) do - if df.general_ref_contains_itemst:is_instance(ref) then local item = df.item.find(ref.item_id) - if not item.flags.dump and not isexcludedvermin(item, opts) then - count = count + 1 - item.flags.dump = true + if not isexcludedvermin(item) then + count = count + dump_item(item) end elseif df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if not it.item.flags.dump and not isexcludedvermin(it.item, opts) then - count = count + 1 - it.item.flags.dump = true + if not isexcludedvermin(it.item) then + count = count + dump_item(it.item) end end end @@ -137,26 +128,18 @@ local function cage_dump_list(list) for _, ref in ipairs(cage.general_refs) do if df.general_ref_contains_itemst:is_instance(ref) then local item = df.item.find(ref.item_id) - if not isexcludedvermin(item, opts) then + if not isexcludedvermin(item) then local classname = df.item_type.attrs[item:getType()].caption count[classname] = (count[classname] or 0) + 1 end elseif df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if not isexcludedvermin(it.item, opts) then + if not isexcludedvermin(it.item) then local classname = df.item_type.attrs[it.item:getType()].caption count[classname] = (count[classname] or 0) + 1 end end - - --[[ TODO: Determine how/if to handle a DEBUG flag. - - --Ruby: - else - puts "unhandled ref #{ref.inspect}" if $DEBUG - end - ]] end end diff --git a/suspendmanager.lua b/suspendmanager.lua index 67370bfaf6..b4a56e4957 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -78,6 +78,32 @@ function foreach_construction_job(fn) end end +local CONSTRUCTION_IMPASSABLE = { + [df.construction_type.Wall]=true, + [df.construction_type.Fortification]=true, +} + +local BUILDING_IMPASSABLE = { + [df.building_type.Floodgate]=true, + [df.building_type.Statue]=true, + [df.building_type.WindowGlass]=true, + [df.building_type.WindowGem]=true, + [df.building_type.GrateWall]=true, + [df.building_type.BarsVertical]=true, +} + +--- Check if a building is blocking once constructed +---@param building building_constructionst|building +---@return boolean +local function isImpassable(building) + local type = building:getType() + if type == df.building_type.Construction then + return CONSTRUCTION_IMPASSABLE[building.type] + else + return BUILDING_IMPASSABLE[type] + end +end + --- True if there is a construction plan to build an unwalkable tile ---@param pos coord ---@return boolean @@ -89,7 +115,7 @@ local function plansToConstructImpassableAt(pos) -- The building is already created return false end - return building:isImpassableAtCreation() + return isImpassable(building) end --- Check if the tile can be walked on @@ -142,7 +168,7 @@ function isBlocking(job) local building = dfhack.job.getHolder(job) --- Not building a blocking construction, no risk - if not building or not building:isImpassableAtCreation() then return false end + if not building or not isImpassable(building) then return false end --- job.pos is sometimes off by one, get the building pos local pos = {x=building.centerx,y=building.centery,z=building.z} @@ -170,7 +196,7 @@ function shouldBeSuspended(job, accountblocking) return true, 'underwater' end - local bld = dfhack.buildings.findAtTile(job.pos) + local bld = dfhack.job.getHolder(job) if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then return true, 'buildingplan' end diff --git a/warn-starving.lua b/warn-starving.lua index dde654856c..2b08042030 100644 --- a/warn-starving.lua +++ b/warn-starving.lua @@ -34,7 +34,7 @@ warning.ATTRS = { pass_mouse_clicks=false, } -function warning:init(args) +function warning:init(info) local main = widgets.Window{ frame={w=80, h=18}, frame_title='Warning', @@ -44,7 +44,7 @@ function warning:init(args) main:addviews{ widgets.WrappedLabel{ - text_to_wrap=table.concat(args.messages, NEWLINE), + text_to_wrap=table.concat(info.messages, NEWLINE), } } @@ -98,13 +98,12 @@ function doCheck() for i=#units-1, 0, -1 do local unit = units[i] local rraw = findRaceCaste(unit) - if rraw and dfhack.units.isActive(unit) and not dfhack.units.isOpposedToLife(unit) then - if not checkOnlySane or dfhack.units.isSane(unit) then - table.insert(messages, checkVariable(unit.counters2.hunger_timer, 75000, 'starving', starvingUnits, unit)) - table.insert(messages, checkVariable(unit.counters2.thirst_timer, 50000, 'dehydrated', dehydratedUnits, unit)) - table.insert(messages, checkVariable(unit.counters2.sleepiness_timer, 150000, 'very drowsy', sleepyUnits, unit)) - end - end + if not rraw or not dfhack.units.isFortControlled(unit) or dfhack.units.isDead(unit) then goto continue end + if checkOnlySane and not dfhack.units.isSane(unit) then goto continue end + table.insert(messages, checkVariable(unit.counters2.hunger_timer, 75000, 'starving', starvingUnits, unit)) + table.insert(messages, checkVariable(unit.counters2.thirst_timer, 50000, 'dehydrated', dehydratedUnits, unit)) + table.insert(messages, checkVariable(unit.counters2.sleepiness_timer, 150000, 'very drowsy', sleepyUnits, unit)) + ::continue:: end if #messages > 0 then dfhack.color(COLOR_LIGHTMAGENTA) From 0e7b08b590ec9bc04997ae1bffc485cfad6f2678 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Thu, 13 Apr 2023 18:29:42 +1000 Subject: [PATCH 116/732] Updated for review. post merge --- changelog.txt | 1 + combine.lua | 454 ++++++++++++++++++++++++++++++++++------------- docs/combine.rst | 13 +- 3 files changed, 334 insertions(+), 134 deletions(-) diff --git a/changelog.txt b/changelog.txt index 94b9ad2731..4e9109b61c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes ## Misc Improvements +- `combine`: added seeds and powder types. ## Removed diff --git a/combine.lua b/combine.lua index b1b4037474..a74af144f7 100644 --- a/combine.lua +++ b/combine.lua @@ -9,25 +9,35 @@ local opts, args = { dry_run = false, types = nil, quiet = false, - verbose = false, + verbose = 0, }, {...} - -- default max stack size of 30 -local DEF_MAX=30 +-- default max stack size of 30 +local MAX_ITEM_STACK=30 +local MAX_CONT_ITEMS=500 + +-- list of types that use race and caste +local typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true} -- list of valid item types for merging +-- Notes: 1. mergeable stacks are ones with the same type_id+race+caste or type_id+mat_type+mat_index +-- 2. the maximum stack size is calcuated at run time: the highest value of MAX_ITEM_STACK or largest current stack size. +-- 3. even though powders are specified, sand and plaster types items are excluded from merging. +-- 4. seeds cannot be combined in stacks > 1. local valid_types_map = { - ['all'] = { }, - ['drink'] = {[df.item_type.DRINK]={type_id=df.item_type.DRINK, type_name='DRINK',type_caste=false,max_stack_size=DEF_MAX}}, - ['fat'] = {[df.item_type.GLOB]={type_id=df.item_type.GLOB, type_name='GLOB',type_caste=false,max_stack_size=DEF_MAX}, - [df.item_type.CHEESE]={type_id=df.item_type.CHEESE, type_name='CHEESE',type_caste=false,max_stack_size=DEF_MAX}}, - ['fish'] = {[df.item_type.FISH]={type_id=df.item_type.FISH, type_name='FISH',type_caste=true,max_stack_size=DEF_MAX}, - [df.item_type.FISH_RAW]={type_id=df.item_type.FISH_RAW, type_name='FISH_RAW',type_caste=true,max_stack_size=DEF_MAX}, - [df.item_type.EGG]={type_id=df.item_type.EGG, type_name='EGG',type_caste=true,max_stack_size=DEF_MAX}}, - ['food'] = {[df.item_type.FOOD]={type_id=df.item_type.FOOD, type_name='FOOD',type_caste=false,max_stack_size=DEF_MAX}}, - ['meat'] = {[df.item_type.MEAT]={type_id=df.item_type.MEAT, type_name='MEAT',type_caste=false,max_stack_size=DEF_MAX}}, - ['plant'] = {[df.item_type.PLANT]={type_id=df.item_type.PLANT, type_name='PLANT',type_caste=false,max_stack_size=DEF_MAX}, - [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, type_name='PLANT_GROWTH',type_caste=false,max_stack_size=DEF_MAX}} + ['all'] = { }, + ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=MAX_ITEM_STACK}}, + ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=MAX_ITEM_STACK}, + [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=MAX_ITEM_STACK}}, + ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=MAX_ITEM_STACK}, + [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=MAX_ITEM_STACK}, + [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=MAX_ITEM_STACK}}, + ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=MAX_ITEM_STACK}}, + ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=MAX_ITEM_STACK}}, + ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=MAX_ITEM_STACK}, + [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=MAX_ITEM_STACK}}, + ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=MAX_ITEM_STACK}}, + ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_size=1}}, } -- populate all types entry @@ -42,9 +52,9 @@ for k1,v1 in pairs(valid_types_map) do end end -function log(...) +function log(level, ...) -- if verbose is specified, then print the arguments, or don't. - if opts.verbose then dfhack.print(string.format(...)) end + if not opts.quiet and opts.verbose >= level then dfhack.print(string.format(...)) end end -- CList class @@ -60,47 +70,49 @@ function CList:new(o) return o end -local function comp_item_new(comp_key, max_stack_size) +local function comp_item_new(comp_key, max_size) -- create a new comp_item entry to be added to a comp_items table. local comp_item = {} if not comp_key then qerror('new_comp_item: comp_key is nil') end - comp_item.comp_key = comp_key - comp_item.item_qty = 0 - comp_item.max_stack_size = max_stack_size or 0 - comp_item.before_stacks = 0 - comp_item.after_stacks = 0 - comp_item.before_stack_size = CList:new(nil) -- key:item.id, val:item.stack_size - comp_item.after_stack_size = CList:new(nil) -- key:item.id, val:item.stack_size - comp_item.items = CList:new(nil) -- key:item.id, val:item - comp_item.sorted_items = CList:new(nil) -- key:-1*item.id | item.id, val:item_id + comp_item.comp_key = comp_key -- key used to index comparable items for merging + comp_item.description = '' -- description of the comp item for output + comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack + -- item info + comp_item.items = CList:new(nil) -- key:item.id, val:{ item, before_size, after_size, before_cont_id, after_cont_id} + comp_item.item_qty = 0 -- total quantity of items + comp_item.before_stacks = 0 -- the number of stacks of the items before... + comp_item.after_stacks = 0 -- ...and after the merge + --container info + comp_item.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id + comp_item.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id return comp_item end -local function comp_item_add_item(comp_item, item) +local function comp_item_add_item(comp_item, item, container) -- add an item into the comp_items table, setting the comp_item attributes. if not comp_item.items[item.id] then - - comp_item.items[item.id] = item comp_item.item_qty = comp_item.item_qty + item.stack_size - if item.stack_size > comp_item.max_stack_size then - comp_item.max_stack_size = item.stack_size - end - comp_item.before_stack_size[item.id] = item.stack_size - comp_item.after_stack_size[item.id] = item.stack_size comp_item.before_stacks = comp_item.before_stacks + 1 - comp_item.after_stacks = comp_item.after_stacks + 1 + comp_item.description = utils.getItemDescription(item, 1) - local contained_item = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) + if item.stack_size > comp_item.max_size then + comp_item.max_size = item.stack_size + end - -- used to merge contained items before loose items - if contained_item then - table.insert(comp_item.sorted_items, -1*item.id) - else - table.insert(comp_item.sorted_items, item.id) + local new_item = {} + new_item.item = item + new_item.before_size = item.stack_size + + -- item is in a container + if container then + new_item.before_cont_id = container.id + comp_item.before_cont_ids[container.id] = container.id end + + comp_item.items[item.id] = new_item return comp_item.items[item.id] else - -- this case should not happen, unless an item is contained by more than one container. + -- this case should not happen, unless an item id is duplicated. -- in which case, only allow one instance for the merge. return nil end @@ -109,49 +121,133 @@ end local function stack_type_new(type_vals) -- create a new stack type entry to be added to the stacks table. local stack_type = {} + + -- attributes from the type val table for k,v in pairs(type_vals) do stack_type[k] = v end - stack_type.item_qty = 0 - stack_type.before_stacks = 0 - stack_type.after_stacks = 0 - stack_type.comp_items = CList:new(nil) -- key:comp_key, val=comp_item + + -- item info + stack_type.comp_items = CList:new(nil) -- key:comp_key, val:comp_item + stack_type.item_qty = 0 -- total quantity of items types + stack_type.before_stacks = 0 -- the number of stacks of the item types before ... + stack_type.after_stacks = 0 -- ...and after the merge + + --container info + stack_type.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id + stack_type.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id return stack_type end -local function stacks_type_add_item(stacks_type, item) +local function stacks_add_item(stacks, stack_type, item, container, contained_count) -- add an item to the matching comp_items table; based on comp_key. local comp_key = '' - if stacks_type.type_caste then - comp_key = tostring(stacks_type.type_id) .. tostring(item.race) .. tostring(item.caste) + if typesThatUseCreatures[df.item_type[stack_type.type_id]] then + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) .. "+" .. tostring(item.caste) else - comp_key = tostring(stacks_type.type_id) .. tostring(item.mat_type) .. tostring(item.mat_index) + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.mat_type) .. "+" .. tostring(item.mat_index) end - if not stacks_type.comp_items[comp_key] then - stacks_type.comp_items[comp_key] = comp_item_new(comp_key, stacks_type.max_stack_size) + if not stack_type.comp_items[comp_key] then + stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_size) end - if comp_item_add_item(stacks_type.comp_items[comp_key], item) then - stacks_type.before_stacks = stacks_type.before_stacks + 1 - stacks_type.after_stacks = stacks_type.after_stacks + 1 - stacks_type.item_qty = stacks_type.item_qty + item.stack_size - if item.stack_size > stacks_type.max_stack_size then - stacks_type.max_stack_size = item.stack_size + if comp_item_add_item(stack_type.comp_items[comp_key], item, container, contained_count) then + stack_type.before_stacks = stack_type.before_stacks + 1 + stack_type.item_qty = stack_type.item_qty + item.stack_size + + stacks.before_stacks = stacks.before_stacks + 1 + stacks.item_qty = stacks.item_qty + item.stack_size + + if item.stack_size > stack_type.max_size then + stack_type.max_size = item.stack_size + end + + -- item is in a container + if container then + + -- add it to the stack type list + stack_type.before_cont_ids[container.id] = container.id + + -- add it to the before stacks container list + stacks.before_cont_ids[container.id] = container.id end end end -local function print_stacks_details(stacks) +local function sorted_items(tab) + -- used to sort the comp_items by contained, then size. Important for combining containers. + local tmp = {} + for id, val in pairs(tab) do + local val = {id=id, before_cont_id=val.before_cont_id, before_size=val.before_size} + table.insert(tmp, val) + end + + table.sort(tmp, + function(a, b) + if not a.before_cont_id and not b.before_cont_id or a.before_cont_id and b.before_cont_id then + return a.before_size > b.before_size + else + return a.before_cont_id and not b.before_cont_id + end + end + ) + + local i = 0 + local iter = + function() + i = i + 1 + if tmp[i] == nil then + return nil + else + return tmp[i].id, tab[tmp[i].id] + end + end + return iter +end + +local function sorted_desc(tab, ids) + -- used to sort the lists by description + local tmp = {} + for id, val in pairs(tab) do + if ids[id] then + local val = {id=id, description=val.description} + table.insert(tmp, val) + end + end + + table.sort(tmp, function(a, b) return a.description < b.description end) + + local i = 0 + local iter = + function() + i = i + 1 + if tmp[i] == nil then + return nil + else + return tmp[i].id, tab[tmp[i].id] + end + end + return iter +end + +local function print_stacks_details(stacks, quiet) -- print stacks details - log(('Details #types:%5d\n'):format(#stacks)) - for _, stacks_type in pairs(stacks) do - log((' type: <%12s> <%d> comp item types#:%5d #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(stacks_type.type_name, stacks_type.type_id, stacks_type.item_qty, #stacks_type.comp_items, stacks_type.max_stack_size, stacks_type.before_stacks, stacks_type.after_stacks)) - for _, comp_item in pairs(stacks_type.comp_items) do - log((' compare key:%12s #item qty:%5d #comp item stacks:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_size, comp_item.before_stacks, comp_item.after_stacks)) - for _, item in pairs(comp_item.items) do - log((' item:%40s <%6d> before:%5d after:%5d\n'):format(utils.getItemDescription(item), item.id, comp_item.before_stack_size[item.id], comp_item.after_stack_size[item.id])) + if quiet then return end + log(1, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) + for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do + log(2, (' container: %50s <%6d> before:%5d after:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size)) + end + log(1, ('Items: #qty: %6d sizes: before:%5d after:%5d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks)) + for key, stack_type in pairs(stacks.stack_types) do + log(1, (' Type: %12s <%d> #qty:%6d sizes: max:%5d before:%6d after:%6d containers: before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids)) + for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do + log(2, (' Comp item:%40s <%12s> #qty:%6d #stacks:%5d sizes: max:%5d before:%6d after:%6d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + for _, item in sorted_items(comp_item.items) do + log(3, (' Item:%40s <%6d> before:%6d after:%6d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) + log(3, ('\n')) end end end @@ -160,11 +256,11 @@ end local function print_stacks_summary(stacks, quiet) -- print stacks summary to the console local printed = 0 - for _, s in pairs(stacks) do + for _, s in pairs(stacks.stack_types) do if s.before_stacks ~= s.after_stacks then printed = printed + 1 print(('combined %d %s items from %d stacks into %d') - :format(s.item_qty, s.type_name, s.before_stacks, s.after_stacks)) + :format(s.item_qty, df.item_type[s.type_id], s.before_stacks, s.after_stacks)) end end if printed == 0 and not quiet then @@ -172,6 +268,21 @@ local function print_stacks_summary(stacks, quiet) end end +local function stacks_new() + local stacks = {} + + stacks.stack_types = CList:new(nil) -- key=type_id, val=stack_type + stacks.containers = CList:new(nil) -- key=container.id, val={container, description, before_size, after_size} + stacks.before_cont_ids = CList:new(nil) -- key=container.id, val=container.id + stacks.after_cont_ids = CList:new(nil) -- key=container.id, val=container.id + stacks.item_qty = 0 + stacks.before_stacks = 0 + stacks.after_stacks = 0 + + return stacks + +end + local function isRestrictedItem(item) -- is the item restricted from merging? local flags = item.flags @@ -180,37 +291,50 @@ local function isRestrictedItem(item) or flags.removed or flags.encased or flags.spider_web or #item.specific_refs > 0 end - -function stacks_add_items(stacks, items, ind) +function stacks_add_items(stacks, items, container, contained_count, ind) -- loop through each item and add it to the matching stack[type_id].comp_items table -- recursively calls itself to add contained items if not ind then ind = '' end for _, item in pairs(items) do local type_id = item:getType() - local stacks_type = stacks[type_id] + local subtype_id = item:getSubtype() + local stack_type = stacks.stack_types[type_id] -- item type in list of included types? - if stacks_type then + if stack_type and not item:isSand() and not item:isPlaster() then if not isRestrictedItem(item) then - stacks_type_add_item(stacks_type, item) + stacks_add_item(stacks, stack_type, item, container, contained_count) + + if typesThatUseCreatures[df.item_type[type_id]] then + local raceRaw = df.global.world.raws.creatures.all[item.race] + local casteRaw = raceRaw.caste[item.caste] + log(4, (' %sitem:%40s <%6d> is incl, type:%d, race:%s, caste:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, raceRaw.creature_id, casteRaw.caste_id)) + else + local mat_info = dfhack.matinfo.decode(item.mat_type, item.mat_index) + log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, sand:%s, plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(),item:isSand(), item:isPlaster())) + end - log((' %sitem:%40s <%6d> is incl, type %d\n'):format(ind, utils.getItemDescription(item), item.id, type_id)) else -- restricted; such as marked for action or dump. - log((' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) + log(4, (' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) end -- add contained items elseif dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINS_ITEM) then local contained_items = dfhack.items.getContainedItems(item) - log((' %sContainer:%s <%6d> #items:%5d\n'):format(ind, utils.getItemDescription(item), item.id, #contained_items)) - stacks_add_items(stacks, contained_items, ind .. ' ') + local count = #contained_items + stacks.containers[item.id] = {} + stacks.containers[item.id].container = item + stacks.containers[item.id].before_size = #contained_items + stacks.containers[item.id].description = utils.getItemDescription(item, 1) + log(4, (' %sContainer:%s <%6d> #items:%5d Sandbearing:%s\n'):format(ind, utils.getItemDescription(item), item.id, count, item:isSandBearing())) + stacks_add_items(stacks, contained_items, item, count, ind .. ' ') -- excluded item types else - log((' %sitem:%40s <%6d> is excl, type %d\n'):format(ind, utils.getItemDescription(item), item.id, type_id)) + log(4, (' %sitem:%40s <%6d> is excl, type %d, sand:%s plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, item:isSand(), item:isPlaster())) end end end @@ -220,85 +344,159 @@ local function populate_stacks(stacks, stockpiles, types) -- 2. loop through the table of stockpiles, get each item in the stockpile, then add them to stacks if the type_id matches -- an item is stored at the bottom of the structure: stacks[type_id].comp_items[comp_key].item -- comp_key is a compound key comprised of type_id+race+caste or type_id+mat_type+mat_index - log('Populating phase\n') + log(4, 'Populating phase\n') -- iterate across the types - log('stack types\n') + log(4, 'stack types\n') for type_id, type_vals in pairs(types) do - if not stacks[type_id] then - stacks[type_id] = stack_type_new(type_vals) - local stacks_type = stacks[type_id] - log((' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(stacks_type.type_name, stacks_type.type_id, stacks_type.item_qty, stacks_type.max_stack_size, stacks_type.before_stacks, stacks_type.after_stacks)) + if not stacks.stack_types[type_id] then + stacks.stack_types[type_id] = stack_type_new(type_vals) + local stack_type = stacks.stack_types[type_id] + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end -- iterate across the stockpiles, get the list of items and call the add function to check/add as needed - log(('stockpiles\n')) + log(4, ('stockpiles\n')) for _, stockpile in pairs(stockpiles) do local items = dfhack.buildings.getStockpileContents(stockpile) - log((' stockpile:%30s <%6d> pos:(%3d,%3d,%3d) #items:%5d\n'):format(stockpile.name, stockpile.id, stockpile.centerx, stockpile.centery, stockpile.z, #items)) + log(4, (' stockpile:%30s <%6d> pos:(%3d,%3d,%3d) #items:%5d\n'):format(stockpile.name, stockpile.id, stockpile.centerx, stockpile.centery, stockpile.z, #items)) if #items > 0 then stacks_add_items(stacks, items) else - log(' skipping stockpile: no items\n') + log(4, ' skipping stockpile: no items\n') end end end local function preview_stacks(stacks) - -- calculate the stacks sizes and store in after_stack_size + -- calculate the stacks sizes and store in after_item_stack_size -- the max stack size for each comp item is determined as the maximum stack size for it's type - log('\nPreview phase\n') - for _, stacks_type in pairs(stacks) do - for comp_key, comp_item in pairs(stacks_type.comp_items) do - -- sort the items. - table.sort(comp_item.sorted_items) - - if stacks_type.max_stack_size > comp_item.max_stack_size then - comp_item.max_stack_size = stacks_type.max_stack_size + log(4, '\nPreview phase\n') + + for _, stack_type in pairs(stacks.stack_types) do + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + + for comp_key, comp_item in pairs(stack_type.comp_items) do + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d before:%5d after:%5d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + + -- sort the items, according to contained first, then stack size second + if stack_type.max_size > comp_item.max_size then + comp_item.max_size = stack_type.max_size end - -- how many stacks are needed ? - local max_stacks_needed = math.floor(comp_item.item_qty / comp_item.max_stack_size) + -- how many stacks are needed? + local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) -- how many items are left over after the max stacks are allocated? - local stack_remainder = comp_item.item_qty - max_stacks_needed * comp_item.max_stack_size - - -- update the after stack sizes. use the sorted items list to get the items. - for _, s_item in ipairs(comp_item.sorted_items) do - local item_id = s_item - if s_item < 0 then item_id = s_item * -1 end - local item = comp_item.items[item_id] - if max_stacks_needed > 0 then - max_stacks_needed = max_stacks_needed - 1 - comp_item.after_stack_size[item.id] = comp_item.max_stack_size + local stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + + if stack_remainder > 0 then + comp_item.after_stacks = stacks_needed + 1 + else + comp_item.after_stacks = stacks_needed + end + + stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks + stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks + + -- Update the after stack sizes. + for _, item in sorted_items(comp_item.items) do + if stacks_needed > 0 then + stacks_needed = stacks_needed - 1 + item.after_size = comp_item.max_size elseif stack_remainder > 0 then - comp_item.after_stack_size[item.id] = stack_remainder + item.after_size = stack_remainder stack_remainder = 0 - elseif stack_remainder == 0 then - comp_item.after_stack_size[item.id] = stack_remainder - comp_item.after_stacks = comp_item.after_stacks - 1 - stacks_type.after_stacks = stacks_type.after_stacks - 1 + else + item.after_size = 0 end end + + -- Container loop; combine item stacks in containers. + local curr_cont = nil + local curr_size = 0 + + for item_id, item in sorted_items(comp_item.items) do + + -- non-zero quantity? + if item.after_size > 0 then + + -- in a container before merge? + if item.before_cont_id then + + local before_cont = stacks.containers[item.before_cont_id] + + -- first contained item or current container full? + if not curr_cont or curr_size >= MAX_CONT_ITEMS then + + curr_cont = before_cont + curr_size = curr_cont.before_size + stacks.after_cont_ids[item.before_cont_id] = item.before_cont_id + stack_type.after_cont_ids[item.before_cont_id] = item.before_cont_id + comp_item.after_cont_ids[item.before_cont_id] = item.before_cont_id + + -- enough room in current container + else + curr_size = curr_size + 1 + before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 + end + + curr_cont.after_size = curr_size + item.after_cont_id = curr_cont.container.id + + -- not in a container before merge, container exists, and has space + elseif curr_cont and curr_size < MAX_CONT_ITEMS then + + curr_size = curr_size + 1 + curr_cont.after_size = curr_size + item.after_cont_id = curr_cont.container.id + + -- not in a container, no container exists or no space in container + else + -- do nothing + end + + -- zero after size, reduce the number of stacks in the container + elseif item.before_cont_id then + local before_cont = stacks.containers[item.before_cont_id] + before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 + end + end + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d before:%5d after:%5d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) end + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end local function merge_stacks(stacks) - -- apply the stack size changes in the after_stack_size - -- if the after_stack_size is zero, then remove the item - log('Merge phase\n') - for _, stacks_type in pairs(stacks) do - for comp_key, comp_item in pairs(stacks_type.comp_items) do - for _, item in pairs(comp_item.items) do - if comp_item.after_stack_size[item.id] == 0 then - local remove_item = df.item.find(item.id) - dfhack.items.remove(remove_item) - elseif item.stack_size ~= comp_item.after_stack_size[item.id] then - item.stack_size = comp_item.after_stack_size[item.id] + -- apply the stack size changes in the after_item_stack_size + -- if the after_item_stack_size is zero, then remove the item + log(4, 'Merge phase\n') + for _, stack_type in pairs(stacks.stack_types) do + for comp_key, comp_item in pairs(stack_type.comp_items) do + + for item_id, item in pairs(comp_item.items) do + + -- no items left in stack? + if item.after_size == 0 then + log(4, (' removing item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + dfhack.items.remove(item.item) + + -- some items left in stack + elseif item.before_size ~= item.after_size then + log(4, (' updating item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + item.item.stack_size = item.after_size + end + + -- move to a container? + if item.after_cont_id then + if (item.before_cont_id or 0) ~= item.after_cont_id then + log(4, (' moving item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + dfhack.items.moveToContainer(item.item, stacks.containers[item.after_cont_id].container) + end end end end @@ -314,7 +512,7 @@ local function get_stockpile_all() table.insert(stockpiles, building) end end - if opts.verbose then + if opts.verbose > 0 then print(('Stockpile(all): %d found'):format(#stockpiles)) end return stockpiles @@ -328,7 +526,7 @@ local function get_stockpile_here() if not building then qerror('Please select a stockpile.') end table.insert(stockpiles, building) local items = dfhack.buildings.getStockpileContents(building) - if opts.verbose then + if opts.verbose > 0 then print(('Stockpile(here): %s <%d> #items:%d'):format(building.name, building.id, #items)) end return stockpiles @@ -376,7 +574,7 @@ local function parse_commandline(opts, args) {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, {'d', 'dry-run', handler=function() opts.dry_run = true end}, {'q', 'quiet', handler=function() opts.quiet = true end}, - {'v', 'verbose', handler=function() opts.verbose = true end}, + {'v', 'verbose', hasArg=true, handler=function(optarg) opts.verbose = math.tointeger(optarg) or 0 end}, }) -- if stockpile option is not specificed, then default to all @@ -408,7 +606,7 @@ local function main() return end - local stacks = CList:new() + local stacks = stacks_new() populate_stacks(stacks, opts.all or opts.here, opts.types) diff --git a/docs/combine.rst b/docs/combine.rst index e9ca96aa39..5c851e3eaf 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -23,14 +23,14 @@ Examples ``combine all --types=meat,plant`` Merge ``meat`` and ``plant`` type stacks in all stockpiles. ``combine here`` - Merge stacks in the selected stockpile. + Merge stacks in stockpile located at game cursor. Commands -------- ``all`` Search all stockpiles. ``here`` - Search the currently selected stockpile. + Search the stockpile under the game cursor. Options ------- @@ -55,8 +55,9 @@ Options ``plant``: PLANT and PLANT_GROWTH -``-q``, ``--quiet`` - Only print changes instead of a summary of all processed stockpiles. + ``powders``: POWDERS_MISC -``-v``, ``--verbose`` - Print verbose output. + ``seeds``: SEEDS + +``-v``, ``--verbose 1-4`` + Print verbose output, level from 1 to 4. From 7b6d20039efdf31d61483196351647d62d37cedc Mon Sep 17 00:00:00 2001 From: Cubittus Date: Fri, 14 Apr 2023 09:01:47 +0100 Subject: [PATCH 117/732] gm-editor: default to not read_only and removed args - moved ready_only into GmEditorUi and removed args - default is now false - I removed the `dev` / `safe` args as they would need to pass info through to the creation of the GmEditorUi class, which doesn't exist at time of their processing. They could be added in a subsequent PR if required, along with a getopt rewrite. --- gui/gm-editor.lua | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 96fadc73a6..18e6b60652 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -16,11 +16,6 @@ function save_config(data) config:write() end -read_only = true -if config.data.read_only ~= nil then - read_only = config.data.read_only -end - find_funcs = find_funcs or (function() local t = {} for k in pairs(df) do @@ -90,6 +85,7 @@ GmEditorUi.ATTRS{ frame_inset=0, resizable=true, resize_min={w=30, h=20}, + read_only=(config.data.read_only or false) } function burning_red(input) -- todo does not work! bug angavrilov that so that he would add this, very important!! @@ -255,7 +251,7 @@ function GmEditorUi:find_id(force_dialog) end) end function GmEditorUi:insertNew(typename) - if read_only then return end + if self.read_only then return end local tp=typename if typename == nil then dialog.showInputPrompt("Class type","You can:\n * Enter type name (without 'df.')\n * Leave empty for default type and 'nil' value\n * Enter '*' for default type and 'new' constructed pointer value",COLOR_WHITE,"",self:callback("insertNew")) @@ -280,7 +276,7 @@ function GmEditorUi:insertNew(typename) end end function GmEditorUi:deleteSelected(key) - if read_only then return end + if self.read_only then return end local trg=self:currentTarget() if trg.target and trg.target._kind and trg.target._kind=="container" then trg.target:erase(key) @@ -368,17 +364,17 @@ function GmEditorUi:editSelected(index,choice,opts) local trg=self:currentTarget() local trg_key=trg.keys[index] if trg.target and trg.target._kind and trg.target._kind=="bitfield" then - if read_only then return end + if self.read_only then return end trg.target[trg_key]= not trg.target[trg_key] self:updateTarget(true) else --print(type(trg.target[trg.keys[trg.selected]]),trg.target[trg.keys[trg.selected]]._kind or "") local trg_type=type(trg.target[trg_key]) if self:getSelectedEnumType() and not opts.raw then - if read_only then return end + if self.read_only then return end self:editSelectedEnum() elseif trg_type=='number' or trg_type=='string' then --ugly TODO: add metatable get selected - if read_only then return end + if self.read_only then return end local prompt = "Enter new value:" if self:getSelectedEnumType() then prompt = "Enter new " .. getTypeName(trg.target:_field(trg_key)._type) .. " value" @@ -387,7 +383,7 @@ function GmEditorUi:editSelected(index,choice,opts) tostring(trg.target[trg_key]), self:callback("commitEdit",trg_key)) elseif trg_type == 'boolean' then - if read_only then return end + if self.read_only then return end trg.target[trg_key] = not trg.target[trg_key] self:updateTarget(true) elseif trg_type == 'userdata' or trg_type == 'table' then @@ -402,7 +398,7 @@ function GmEditorUi:editSelected(index,choice,opts) end function GmEditorUi:commitEdit(key,value) - if read_only then return end + if self.read_only then return end local trg=self:currentTarget() if type(trg.target[key])=='number' then trg.target[key]=tonumber(value) @@ -413,7 +409,7 @@ function GmEditorUi:commitEdit(key,value) end function GmEditorUi:set(key,input) - if read_only then return end + if self.read_only then return end local trg=self:currentTarget() if input== nil then @@ -445,7 +441,7 @@ function GmEditorUi:onInput(keys) end if keys[keybindings.toggle_ro.key] then - read_only = not read_only + self.read_only = not self.read_only self:updateTitles() return true elseif keys[keybindings.offset.key] then @@ -510,14 +506,14 @@ end function GmEditorUi:updateTitles() local title = "GameMaster's Editor" - if read_only then + if self.read_only then title = title.." (Read Only)" end for view,_ in pairs(views) do view.subviews[1].frame_title = title end self.frame_title = title - save_config({read_only = read_only}) + save_config({read_only = self.read_only}) end function GmEditorUi:updateTarget(preserve_pos,reindex) local trg=self:currentTarget() @@ -615,14 +611,6 @@ end local function get_editor(args) if #args~=0 then - if args[1]=='dev' or args[1]=='edit' then - read_only = false - table.remove(args,1) - end - if args[1]=='safe' or args[1]=='read' then - read_only = true - table.remove(args,1) - end if args[1]=="dialog" then dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, "", function(entry) From 15469630978418745681c98646488c7e0ae210da Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 11 Apr 2023 21:29:57 +1000 Subject: [PATCH 118/732] Version for review --- combine.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/combine.lua b/combine.lua index a74af144f7..85ca8e6d9a 100644 --- a/combine.lua +++ b/combine.lua @@ -506,6 +506,7 @@ end local function get_stockpile_all() -- attempt to get all the stockpiles for the fort, or exit with error -- return the stockpiles as a table + log(3, 'get_stockpile_all\n') local stockpiles = {} for _, building in pairs(df.global.world.buildings.all) do if building:getType() == df.building_type.Stockpile then @@ -521,6 +522,7 @@ end local function get_stockpile_here() -- attempt to get the selected stockpile, or exit with error -- return the stockpile as a table + log(3, 'get_stockpile_here\n') local stockpiles = {} local building = dfhack.gui.getSelectedStockpile() if not building then qerror('Please select a stockpile.') end @@ -535,6 +537,7 @@ end local function parse_types_opts(arg) -- check the types specified on the command line, or exit with error -- return the selected types as a table + log(3, 'parse_types_opts\n') local types = {} local div = '' local types_output = '' @@ -556,19 +559,20 @@ local function parse_types_opts(arg) for k3, v3 in pairs(v2) do types[k2][k3]=v3 end - types_output = types_output .. div .. types[k2].type_name + types_output = types_output .. div .. df.item_type[types[k2].type_id] div=', ' else qerror(('Expected: only one value for %s'):format(t)) end end end - dfhack.print(types_output .. '\n') + log(0, types_output .. '\n') return types end local function parse_commandline(opts, args) -- check the command line/exit on error, and set the defaults + log(3, 'parse_commandline\n') local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, From 11eee8b4a814cd10924e3b02fc8e8e3fa0e38767 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Tue, 11 Apr 2023 21:52:27 +1000 Subject: [PATCH 119/732] Doc update --- combine.lua | 24 ++++++++++++++++-------- docs/combine.rst | 9 +++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/combine.lua b/combine.lua index 85ca8e6d9a..2c6dd1eb83 100644 --- a/combine.lua +++ b/combine.lua @@ -14,6 +14,7 @@ local opts, args = { -- default max stack size of 30 local MAX_ITEM_STACK=30 +local MAX_AMMO_STACK=25 local MAX_CONT_ITEMS=500 -- list of types that use race and caste @@ -26,6 +27,7 @@ local typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PE -- 4. seeds cannot be combined in stacks > 1. local valid_types_map = { ['all'] = { }, + ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_size=MAX_AMMO_STACK}}, ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=MAX_ITEM_STACK}}, ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=MAX_ITEM_STACK}, [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=MAX_ITEM_STACK}}, @@ -145,6 +147,12 @@ local function stacks_add_item(stacks, stack_type, item, container, contained_co if typesThatUseCreatures[df.item_type[stack_type.type_id]] then comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) .. "+" .. tostring(item.caste) + elseif item:isAmmo() then + if item:getQuality() == 5 then + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.mat_type) .. "+" .. tostring(item.mat_index) .. "+" .. tostring(item:getQuality() .. "+" .. item:getMaker()) + else + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.mat_type) .. "+" .. tostring(item.mat_index) .. "+" .. tostring(item:getQuality()) + end else comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.mat_type) .. "+" .. tostring(item.mat_index) end @@ -288,7 +296,8 @@ local function isRestrictedItem(item) local flags = item.flags return flags.rotten or flags.trader or flags.hostile or flags.forbid or flags.dump or flags.on_fire or flags.garbage_collect or flags.owned - or flags.removed or flags.encased or flags.spider_web or #item.specific_refs > 0 + or flags.removed or flags.encased or flags.spider_web or flags.melt + or flags.hidden or #item.specific_refs > 0 end function stacks_add_items(stacks, items, container, contained_count, ind) @@ -311,9 +320,12 @@ function stacks_add_items(stacks, items, container, contained_count, ind) local raceRaw = df.global.world.raws.creatures.all[item.race] local casteRaw = raceRaw.caste[item.caste] log(4, (' %sitem:%40s <%6d> is incl, type:%d, race:%s, caste:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, raceRaw.creature_id, casteRaw.caste_id)) + elseif item:isAmmo() then + local mat_info = dfhack.matinfo.decode(item.mat_type, item.mat_index) + log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, quality:%d, maker:%d\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(), item:getQuality(), item:getMaker())) else local mat_info = dfhack.matinfo.decode(item.mat_type, item.mat_index) - log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, sand:%s, plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(),item:isSand(), item:isPlaster())) + log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, sand:%s, plaster:%s quality:%d ovl quality:%d\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(), item:isSand(), item:isPlaster(), item:getQuality(), item:getOverallQuality())) end else @@ -506,7 +518,6 @@ end local function get_stockpile_all() -- attempt to get all the stockpiles for the fort, or exit with error -- return the stockpiles as a table - log(3, 'get_stockpile_all\n') local stockpiles = {} for _, building in pairs(df.global.world.buildings.all) do if building:getType() == df.building_type.Stockpile then @@ -522,7 +533,6 @@ end local function get_stockpile_here() -- attempt to get the selected stockpile, or exit with error -- return the stockpile as a table - log(3, 'get_stockpile_here\n') local stockpiles = {} local building = dfhack.gui.getSelectedStockpile() if not building then qerror('Please select a stockpile.') end @@ -537,7 +547,6 @@ end local function parse_types_opts(arg) -- check the types specified on the command line, or exit with error -- return the selected types as a table - log(3, 'parse_types_opts\n') local types = {} local div = '' local types_output = '' @@ -559,20 +568,19 @@ local function parse_types_opts(arg) for k3, v3 in pairs(v2) do types[k2][k3]=v3 end - types_output = types_output .. div .. df.item_type[types[k2].type_id] + types_output = types_output .. div .. types[k2].type_name div=', ' else qerror(('Expected: only one value for %s'):format(t)) end end end - log(0, types_output .. '\n') + dfhack.print(types_output .. '\n') return types end local function parse_commandline(opts, args) -- check the command line/exit on error, and set the defaults - log(3, 'parse_commandline\n') local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, diff --git a/docs/combine.rst b/docs/combine.rst index 5c851e3eaf..372abb294e 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -43,6 +43,8 @@ Options ``all``: all of the types listed here. + ``ammo``: AMMO + ``drink``: DRINK ``fat``: GLOB and CHEESE @@ -59,5 +61,8 @@ Options ``seeds``: SEEDS -``-v``, ``--verbose 1-4`` - Print verbose output, level from 1 to 4. +``-q``, ``--quiet`` + Only print changes instead of a summary of all processed stockpiles. + +``-v``, ``--verbose n`` + Print verbose output, n from 1 to 4. From f97353a715a82f12891d1b384d7599da3a42d81d Mon Sep 17 00:00:00 2001 From: silverflyone Date: Wed, 12 Apr 2023 14:11:20 +1000 Subject: [PATCH 120/732] Update combine for ammo types. --- changelog.txt | 2 +- docs/combine.rst | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4e9109b61c..f8b9fb79e3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,7 +18,7 @@ that repo. ## Fixes ## Misc Improvements -- `combine`: added seeds and powder types. +- `combine`: Now supports ammo, powders and seeds, and combines into containers. ## Removed diff --git a/docs/combine.rst b/docs/combine.rst index 372abb294e..d193145844 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -63,6 +63,21 @@ Options ``-q``, ``--quiet`` Only print changes instead of a summary of all processed stockpiles. + +``-v``, ``--verbose [0-3]`` + Print verbose output, level from 0 to 3. -``-v``, ``--verbose n`` - Print verbose output, n from 1 to 4. +Notes +----- +The following conditions prevent an item from being combined: +1. An item is not in a stockpile. +2. An item is sand or plaster. +3. An item is rotten, forbidden/hidden, marked for dumping/melting, +on fire, encased, owned by a trader/hostile/dwarf or is in a spider web. + +The following categories are used for combining: +1. Item has a race/caste: category=type + race + caste +2. Item is ammo, created by for masterwork. category=type + material + quality (+ created by) +3. Or: category= type + material + +A default stack size of 30 applies to a category, unless a larger stack exists. From e7975fe71c94e5eb00e7747672df8762e11f7862 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Fri, 14 Apr 2023 14:55:38 +1000 Subject: [PATCH 121/732] Merged changes --- docs/combine.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/combine.rst b/docs/combine.rst index d193145844..4f84c076d1 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -64,8 +64,8 @@ Options ``-q``, ``--quiet`` Only print changes instead of a summary of all processed stockpiles. -``-v``, ``--verbose [0-3]`` - Print verbose output, level from 0 to 3. +``-v``, ``--verbose n`` + Print verbose output, n from 1 to 4. Notes ----- From 24e7abd24c1454de6e0527603edd81be566e1384 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 06:24:14 +0000 Subject: [PATCH 122/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- combine.lua | 2 +- docs/combine.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/combine.lua b/combine.lua index 2c6dd1eb83..8ce4708356 100644 --- a/combine.lua +++ b/combine.lua @@ -296,7 +296,7 @@ local function isRestrictedItem(item) local flags = item.flags return flags.rotten or flags.trader or flags.hostile or flags.forbid or flags.dump or flags.on_fire or flags.garbage_collect or flags.owned - or flags.removed or flags.encased or flags.spider_web or flags.melt + or flags.removed or flags.encased or flags.spider_web or flags.melt or flags.hidden or #item.specific_refs > 0 end diff --git a/docs/combine.rst b/docs/combine.rst index 4f84c076d1..6752aa0cf1 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -63,7 +63,7 @@ Options ``-q``, ``--quiet`` Only print changes instead of a summary of all processed stockpiles. - + ``-v``, ``--verbose n`` Print verbose output, n from 1 to 4. @@ -72,7 +72,7 @@ Notes The following conditions prevent an item from being combined: 1. An item is not in a stockpile. 2. An item is sand or plaster. -3. An item is rotten, forbidden/hidden, marked for dumping/melting, +3. An item is rotten, forbidden/hidden, marked for dumping/melting, on fire, encased, owned by a trader/hostile/dwarf or is in a spider web. The following categories are used for combining: From a5e79986a3d8d006fab5d1a0432372fdd867abc9 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sat, 15 Apr 2023 15:12:34 +1000 Subject: [PATCH 123/732] Combine body parts: #3220 --- changelog.txt | 2 +- combine.lua | 271 ++++++++++++++++++++++++++++++++++++----------- docs/combine.rst | 10 +- 3 files changed, 217 insertions(+), 66 deletions(-) diff --git a/changelog.txt b/changelog.txt index f8b9fb79e3..105bafe7f3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,7 +18,7 @@ that repo. ## Fixes ## Misc Improvements -- `combine`: Now supports ammo, powders and seeds, and combines into containers. +- `combine`: Now supports ammo, parts, powders and seeds, and combines into containers. ## Removed diff --git a/combine.lua b/combine.lua index 8ce4708356..04d619d7bc 100644 --- a/combine.lua +++ b/combine.lua @@ -16,9 +16,14 @@ local opts, args = { local MAX_ITEM_STACK=30 local MAX_AMMO_STACK=25 local MAX_CONT_ITEMS=500 +local MAX_MAT_AMT=500 -- list of types that use race and caste local typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true} +local typesThatUseMaterial={CORPSEPIECE=true} + +local ITEM_QTY = 1 +local MATERIAL_AMOUNT = 2 -- list of valid item types for merging -- Notes: 1. mergeable stacks are ones with the same type_id+race+caste or type_id+mat_type+mat_index @@ -28,6 +33,7 @@ local typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PE local valid_types_map = { ['all'] = { }, ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_size=MAX_AMMO_STACK}}, + ['parts'] = {[df.item_type.CORPSEPIECE] ={type_id=df.item_type.CORPSEPIECE, max_size=1}}, ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=MAX_ITEM_STACK}}, ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=MAX_ITEM_STACK}, [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=MAX_ITEM_STACK}}, @@ -69,6 +75,7 @@ function CList:new(o) setmetatable(o, self) self.__index = self self.__len = function (t) local n = 0 for _, __ in pairs(t) do n = n + 1 end return n end + self.max = function (t) local v = 0 for _, n in pairs(t) do if n > v then v = n end end return v end return o end @@ -80,8 +87,13 @@ local function comp_item_new(comp_key, max_size) comp_item.description = '' -- description of the comp item for output comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack -- item info - comp_item.items = CList:new(nil) -- key:item.id, val:{ item, before_size, after_size, before_cont_id, after_cont_id} + comp_item.items = CList:new(nil) -- key:item.id, val:{ item, before_size, after_size, before_cont_id, after_cont_id, + -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} + -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} } comp_item.item_qty = 0 -- total quantity of items + comp_item.material_amt = 0 -- total amount of materials + comp_item.max_mat_amt = MAX_MAT_AMT -- max amount of materials in one stack + comp_item.before_stacks = 0 -- the number of stacks of the items before... comp_item.after_stacks = 0 -- ...and after the merge --container info @@ -90,7 +102,7 @@ local function comp_item_new(comp_key, max_size) return comp_item end -local function comp_item_add_item(comp_item, item, container) +local function comp_item_add_item(stack_type, comp_item, item, container) -- add an item into the comp_items table, setting the comp_item attributes. if not comp_item.items[item.id] then comp_item.item_qty = comp_item.item_qty + item.stack_size @@ -105,6 +117,25 @@ local function comp_item_add_item(comp_item, item, container) new_item.item = item new_item.before_size = item.stack_size + -- material amount used? + new_item.before_mat_amt = {} + new_item.before_mat_amt.Qty = 0 + new_item.after_mat_amt = {} + new_item.after_mat_amt.Qty = 0 + if typesThatUseMaterial[df.item_type[stack_type.type_id]] then + new_item.before_mat_amt.Leather = item.material_amount.Leather + new_item.before_mat_amt.Bone = item.material_amount.Bone + new_item.before_mat_amt.Shell = item.material_amount.Shell + new_item.before_mat_amt.Tooth = item.material_amount.Tooth + new_item.before_mat_amt.Horn = item.material_amount.Horn + new_item.before_mat_amt.HairWool = item.material_amount.HairWool + new_item.before_mat_amt.Yarn = item.material_amount.Yarn + for _, v in pairs(new_item.before_mat_amt) do if new_item.before_mat_amt.Qty < v then new_item.before_mat_amt.Qty = v end end + + comp_item.material_amt = comp_item.material_amt + new_item.before_mat_amt.Qty + if new_item.before_mat_amt.Qty > comp_item.max_mat_amt then comp_item.max_mat_amt = new_item.before_mat_amt.Qty end + end + -- item is in a container if container then new_item.before_cont_id = container.id @@ -132,6 +163,7 @@ local function stack_type_new(type_vals) -- item info stack_type.comp_items = CList:new(nil) -- key:comp_key, val:comp_item stack_type.item_qty = 0 -- total quantity of items types + stack_type.material_amt = 0 -- total amount of materials stack_type.before_stacks = 0 -- the number of stacks of the item types before ... stack_type.after_stacks = 0 -- ...and after the merge @@ -146,7 +178,11 @@ local function stacks_add_item(stacks, stack_type, item, container, contained_co local comp_key = '' if typesThatUseCreatures[df.item_type[stack_type.type_id]] then - comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) .. "+" .. tostring(item.caste) + if typesThatUseMaterial[df.item_type[stack_type.type_id]] then + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) + else + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) .. "+" .. tostring(item.caste) + end elseif item:isAmmo() then if item:getQuality() == 5 then comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.mat_type) .. "+" .. tostring(item.mat_index) .. "+" .. tostring(item:getQuality() .. "+" .. item:getMaker()) @@ -161,12 +197,15 @@ local function stacks_add_item(stacks, stack_type, item, container, contained_co stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_size) end - if comp_item_add_item(stack_type.comp_items[comp_key], item, container, contained_count) then + local new_comp_item_item = comp_item_add_item(stack_type, stack_type.comp_items[comp_key], item, container, contained_count) + if new_comp_item_item then stack_type.before_stacks = stack_type.before_stacks + 1 stack_type.item_qty = stack_type.item_qty + item.stack_size + stack_type.material_amt = stack_type.material_amt + new_comp_item_item.before_mat_amt.Qty stacks.before_stacks = stacks.before_stacks + 1 stacks.item_qty = stacks.item_qty + item.stack_size + stacks.material_amt = stacks.material_amt + new_comp_item_item.before_mat_amt.Qty if item.stack_size > stack_type.max_size then stack_type.max_size = item.stack_size @@ -184,7 +223,7 @@ local function stacks_add_item(stacks, stack_type, item, container, contained_co end end -local function sorted_items(tab) +local function sorted_items_qty(tab) -- used to sort the comp_items by contained, then size. Important for combining containers. local tmp = {} for id, val in pairs(tab) do @@ -215,6 +254,33 @@ local function sorted_items(tab) return iter end +local function sorted_items_mat(tab) + -- used to sort the comp_items by mat amt. + local tmp = {} + for id, val in pairs(tab) do + local val = {id=id, before_qty=val.before_mat_amt.Qty} + table.insert(tmp, val) + end + + table.sort(tmp, + function(a, b) + return a.before_qty > b.before_qty + end + ) + + local i = 0 + local iter = + function() + i = i + 1 + if tmp[i] == nil then + return nil + else + return tmp[i].id, tab[tmp[i].id] + end + end + return iter +end + local function sorted_desc(tab, ids) -- used to sort the lists by description local tmp = {} @@ -243,32 +309,42 @@ end local function print_stacks_details(stacks, quiet) -- print stacks details if quiet then return end - log(1, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) - for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do - log(2, (' container: %50s <%6d> before:%5d after:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size)) + if #stacks.containers > 0 then + log(1, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) + for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do + log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size)) + end end - log(1, ('Items: #qty: %6d sizes: before:%5d after:%5d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks)) - for key, stack_type in pairs(stacks.stack_types) do - log(1, (' Type: %12s <%d> #qty:%6d sizes: max:%5d before:%6d after:%6d containers: before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids)) - for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do - log(2, (' Comp item:%40s <%12s> #qty:%6d #stacks:%5d sizes: max:%5d before:%6d after:%6d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) - for _, item in sorted_items(comp_item.items) do - log(3, (' Item:%40s <%6d> before:%6d after:%6d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) - log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) - log(3, ('\n')) + if stacks.item_qty > 0 then + log(1, ('Items: #Qty: %6d sizes: bef:%5d aft:%5d Mat amt:%6d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks, stacks.material_amt)) + for key, stack_type in pairs(stacks.stack_types) do + if stack_type.item_qty > 0 then + log(1, (' Type: %12s <%d> #Qty:%6d sizes: max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) + for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do + if comp_item.item_qty > 0 then + log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) + for _, item in sorted_items_qty(comp_item.items) do + log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6d Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0)) + log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) + log(3, ('\n')) + end + end + end end end end end -local function print_stacks_summary(stacks, quiet) +local function print_stacks_summary(stacks, quiet, dry_run) -- print stacks summary to the console local printed = 0 for _, s in pairs(stacks.stack_types) do if s.before_stacks ~= s.after_stacks then printed = printed + 1 - print(('combined %d %s items from %d stacks into %d') - :format(s.item_qty, df.item_type[s.type_id], s.before_stacks, s.after_stacks)) + local str = '' + if dry_run then str = 'will combine' else str ='combined' end + print(('%s %d %s items from %d stacks into %d') + :format(str, s.item_qty, df.item_type[s.type_id], s.before_stacks, s.after_stacks)) end end if printed == 0 and not quiet then @@ -284,6 +360,7 @@ local function stacks_new() stacks.before_cont_ids = CList:new(nil) -- key=container.id, val=container.id stacks.after_cont_ids = CList:new(nil) -- key=container.id, val=container.id stacks.item_qty = 0 + stacks.material_amt = 0 -- total amount of materials - used for CORPSEPIECEs stacks.before_stacks = 0 stacks.after_stacks = 0 @@ -300,6 +377,19 @@ local function isRestrictedItem(item) or flags.hidden or #item.specific_refs > 0 end +local function isValidPart(item) + return item:getMaterial() >= 0 or + (not item.corpse_flags.unbutchered and ( + item.material_amount.Leather > 0 or + item.material_amount.Bone > 0 or + item.material_amount.Shell > 0 or + item.material_amount.Tooth > 0 or + item.material_amount.Horn > 0 or + item.material_amount.HairWool > 0 or + item.material_amount.Yarn > 0)) + +end + function stacks_add_items(stacks, items, container, contained_count, ind) -- loop through each item and add it to the matching stack[type_id].comp_items table -- recursively calls itself to add contained items @@ -311,7 +401,7 @@ function stacks_add_items(stacks, items, container, contained_count, ind) local stack_type = stacks.stack_types[type_id] -- item type in list of included types? - if stack_type and not item:isSand() and not item:isPlaster() then + if stack_type and not item:isSand() and not item:isPlaster() and isValidPart(item) then if not isRestrictedItem(item) then stacks_add_item(stacks, stack_type, item, container, contained_count) @@ -325,7 +415,7 @@ function stacks_add_items(stacks, items, container, contained_count, ind) log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, quality:%d, maker:%d\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(), item:getQuality(), item:getMaker())) else local mat_info = dfhack.matinfo.decode(item.mat_type, item.mat_index) - log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, sand:%s, plaster:%s quality:%d ovl quality:%d\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(), item:isSand(), item:isPlaster(), item:getQuality(), item:getOverallQuality())) + log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, sand:%s, plasterplaster:%s quality:%d ovl quality:%d\n'):format(ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(), item:isSand(), item:isPlaster(), item:getQuality(), item:getOverallQuality())) end else @@ -364,7 +454,7 @@ local function populate_stacks(stacks, stockpiles, types) if not stacks.stack_types[type_id] then stacks.stack_types[type_id] = stack_type_new(type_vals) local stack_type = stacks.stack_types[type_id] - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end @@ -389,41 +479,88 @@ local function preview_stacks(stacks) log(4, '\nPreview phase\n') for _, stack_type in pairs(stacks.stack_types) do - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) for comp_key, comp_item in pairs(stack_type.comp_items) do - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d before:%5d after:%5d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) - -- sort the items, according to contained first, then stack size second - if stack_type.max_size > comp_item.max_size then - comp_item.max_size = stack_type.max_size - end + -- Use item qty or material amount? + if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then - -- how many stacks are needed? - local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) + -- max size comparison + if stack_type.max_size > comp_item.max_size then + comp_item.max_size = stack_type.max_size + end - -- how many items are left over after the max stacks are allocated? - local stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + -- how many stacks are needed? + local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) - if stack_remainder > 0 then - comp_item.after_stacks = stacks_needed + 1 - else - comp_item.after_stacks = stacks_needed - end + -- how many items are left over after the max stacks are allocated? + local stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size - stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks - stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks - - -- Update the after stack sizes. - for _, item in sorted_items(comp_item.items) do - if stacks_needed > 0 then - stacks_needed = stacks_needed - 1 - item.after_size = comp_item.max_size - elseif stack_remainder > 0 then - item.after_size = stack_remainder - stack_remainder = 0 + if stack_remainder > 0 then + comp_item.after_stacks = stacks_needed + 1 else - item.after_size = 0 + comp_item.after_stacks = stacks_needed + end + + stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks + stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks + + -- Update the after stack sizes. + for _, item in sorted_items_qty(comp_item.items) do + if stacks_needed > 0 then + stacks_needed = stacks_needed - 1 + item.after_size = comp_item.max_size + elseif stack_remainder > 0 then + item.after_size = stack_remainder + stack_remainder = 0 + else + item.after_size = 0 + end + end + + else + local stacks_needed = math.floor(comp_item.material_amt / comp_item.max_mat_amt) + local stack_remainder = comp_item.material_amt - stacks_needed * comp_item.max_mat_amt + + if stack_remainder > 0 then + comp_item.after_stacks = stacks_needed + 1 + else + comp_item.after_stacks = stacks_needed + end + + stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks + stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks + + for k1, item in sorted_items_mat(comp_item.items) do + item.after_mat_amt = {} + if stacks_needed > 0 then + stacks_needed = stacks_needed - 1 + item.after_size = item.before_size + for k2, v in pairs(item.before_mat_amt) do + if v > 0 then + item.after_mat_amt[k2] = comp_item.max_mat_amt + else + item.after_mat_amt[k2] = 0 + end + end + elseif stack_remainder > 0 then + item.after_size = item.before_size + for k2, v in pairs(item.before_mat_amt) do + if v > 0 then + item.after_mat_amt[k2] = stack_remainder + else + item.after_mat_amt[k2] = 0 + end + end + stack_remainder = 0 + else + for k2, v in pairs(item.before_mat_amt) do + item.after_mat_amt[k2] = 0 + end + item.after_size = 0 + end end end @@ -431,7 +568,7 @@ local function preview_stacks(stacks) local curr_cont = nil local curr_size = 0 - for item_id, item in sorted_items(comp_item.items) do + for item_id, item in sorted_items_qty(comp_item.items) do -- non-zero quantity? if item.after_size > 0 then @@ -477,9 +614,9 @@ local function preview_stacks(stacks) before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 end end - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d before:%5d after:%5d containers: before:%5d after:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) end - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d before:%5d after:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end @@ -491,22 +628,35 @@ local function merge_stacks(stacks) for comp_key, comp_item in pairs(stack_type.comp_items) do for item_id, item in pairs(comp_item.items) do + log(4, (' item amt:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d> mat: bef:%5d aft:%5d '):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0)) -- no items left in stack? if item.after_size == 0 then - log(4, (' removing item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + log(4, ' removing\n') dfhack.items.remove(item.item) -- some items left in stack - elseif item.before_size ~= item.after_size then - log(4, (' updating item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + elseif not typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_size ~= item.after_size then + log(4, ' updating qty\n') item.item.stack_size = item.after_size + + elseif typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_mat_amt.Qty ~= item.after_mat_amt.Qty then + log(4, ' updating material\n') + item.item.material_amount.Leather = item.after_mat_amt.Leather + item.item.material_amount.Bone = item.after_mat_amt.Bone + item.item.material_amount.Shell = item.after_mat_amt.Shell + item.item.material_amount.Tooth = item.after_mat_amt.Tooth + item.item.material_amount.Horn = item.after_mat_amt.Horn + item.item.material_amount.HairWool = item.after_mat_amt.HairWool + item.item.material_amount.Yarn = item.after_mat_amt.Yarn + else + log(4, ' no change\n') end -- move to a container? if item.after_cont_id then if (item.before_cont_id or 0) ~= item.after_cont_id then - log(4, (' moving item:%40s <%6d> before:%5d after:%5d container: before:<%5d> after:<%5d>'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + log(4, (' moving item:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d>\n'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) dfhack.items.moveToContainer(item.item, stacks.containers[item.after_cont_id].container) end end @@ -555,8 +705,6 @@ local function parse_types_opts(arg) qerror('Expected: comma separated list of types') end - types_output='Types: ' - for _, t in pairs(argparse.stringList(arg)) do if not valid_types_map[t] then qerror(('Unknown type: %s'):format(t)) @@ -568,14 +716,13 @@ local function parse_types_opts(arg) for k3, v3 in pairs(v2) do types[k2][k3]=v3 end - types_output = types_output .. div .. types[k2].type_name + types_output = types_output .. div .. df.item_type[types[k2].type_id] div=', ' else qerror(('Expected: only one value for %s'):format(t)) end end end - dfhack.print(types_output .. '\n') return types end @@ -629,7 +776,7 @@ local function main() end print_stacks_details(stacks) - print_stacks_summary(stacks, opts.quiet) + print_stacks_summary(stacks, opts.quiet, opts.dry_run) end diff --git a/docs/combine.rst b/docs/combine.rst index 6752aa0cf1..904489718d 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -55,6 +55,8 @@ Options ``meat``: MEAT + ``parts``: CORPSEPIECE + ``plant``: PLANT and PLANT_GROWTH ``powders``: POWDERS_MISC @@ -74,10 +76,12 @@ The following conditions prevent an item from being combined: 2. An item is sand or plaster. 3. An item is rotten, forbidden/hidden, marked for dumping/melting, on fire, encased, owned by a trader/hostile/dwarf or is in a spider web. +4. An item is a part and not butchered. The following categories are used for combining: -1. Item has a race/caste: category=type + race + caste -2. Item is ammo, created by for masterwork. category=type + material + quality (+ created by) -3. Or: category= type + material +1. Item is a part and has a race: category=type + race +2. Item has a race/caste: category=type + race + caste +3. Item is ammo, created by for masterwork. category=type + material + quality (+ created by) +4. Or: category= type + material A default stack size of 30 applies to a category, unless a larger stack exists. From f647af27d24eb850404ec1384b2ce293bc150226 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Apr 2023 05:13:41 +0000 Subject: [PATCH 124/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- combine.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/combine.lua b/combine.lua index 04d619d7bc..df55a2579e 100644 --- a/combine.lua +++ b/combine.lua @@ -87,7 +87,7 @@ local function comp_item_new(comp_key, max_size) comp_item.description = '' -- description of the comp item for output comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack -- item info - comp_item.items = CList:new(nil) -- key:item.id, val:{ item, before_size, after_size, before_cont_id, after_cont_id, + comp_item.items = CList:new(nil) -- key:item.id, val:{ item, before_size, after_size, before_cont_id, after_cont_id, -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} } comp_item.item_qty = 0 -- total quantity of items @@ -131,7 +131,7 @@ local function comp_item_add_item(stack_type, comp_item, item, container) new_item.before_mat_amt.HairWool = item.material_amount.HairWool new_item.before_mat_amt.Yarn = item.material_amount.Yarn for _, v in pairs(new_item.before_mat_amt) do if new_item.before_mat_amt.Qty < v then new_item.before_mat_amt.Qty = v end end - + comp_item.material_amt = comp_item.material_amt + new_item.before_mat_amt.Qty if new_item.before_mat_amt.Qty > comp_item.max_mat_amt then comp_item.max_mat_amt = new_item.before_mat_amt.Qty end end @@ -387,7 +387,7 @@ local function isValidPart(item) item.material_amount.Horn > 0 or item.material_amount.HairWool > 0 or item.material_amount.Yarn > 0)) - + end function stacks_add_items(stacks, items, container, contained_count, ind) @@ -526,7 +526,7 @@ local function preview_stacks(stacks) if stack_remainder > 0 then comp_item.after_stacks = stacks_needed + 1 - else + else comp_item.after_stacks = stacks_needed end From c2786ed582cb9be2438134b7f7d2b2d64056d24f Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sat, 15 Apr 2023 22:06:42 +1000 Subject: [PATCH 125/732] Parts requires actual material type and index. --- combine.lua | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/combine.lua b/combine.lua index df55a2579e..21bb20dc43 100644 --- a/combine.lua +++ b/combine.lua @@ -87,7 +87,9 @@ local function comp_item_new(comp_key, max_size) comp_item.description = '' -- description of the comp item for output comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack -- item info - comp_item.items = CList:new(nil) -- key:item.id, val:{ item, before_size, after_size, before_cont_id, after_cont_id, + comp_item.items = CList:new(nil) -- key:item.id, val:{ item, + -- before_size, after_size, before_cont_id, after_cont_id, + -- stockpile_id, stockpile_name, -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} } comp_item.item_qty = 0 -- total quantity of items @@ -102,7 +104,7 @@ local function comp_item_new(comp_key, max_size) return comp_item end -local function comp_item_add_item(stack_type, comp_item, item, container) +local function comp_item_add_item(stockpile, stack_type, comp_item, item, container) -- add an item into the comp_items table, setting the comp_item attributes. if not comp_item.items[item.id] then comp_item.item_qty = comp_item.item_qty + item.stack_size @@ -117,6 +119,9 @@ local function comp_item_add_item(stack_type, comp_item, item, container) new_item.item = item new_item.before_size = item.stack_size + new_item.stockpile_id = stockpile.id + new_item.stockpile_name = stockpile.name + -- material amount used? new_item.before_mat_amt = {} new_item.before_mat_amt.Qty = 0 @@ -173,15 +178,15 @@ local function stack_type_new(type_vals) return stack_type end -local function stacks_add_item(stacks, stack_type, item, container, contained_count) +local function stacks_add_item(stockpile, stacks, stack_type, item, container, contained_count) -- add an item to the matching comp_items table; based on comp_key. local comp_key = '' if typesThatUseCreatures[df.item_type[stack_type.type_id]] then - if typesThatUseMaterial[df.item_type[stack_type.type_id]] then - comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) - else + if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) .. "+" .. tostring(item.caste) + else + comp_key = tostring(stack_type.type_id) .. "+" .. tostring(item.race) .. "+" .. tostring(item.caste) .. "+" .. tostring(item:getActualMaterial()) .. "+" .. tostring(item:getActualMaterialIndex()) end elseif item:isAmmo() then if item:getQuality() == 5 then @@ -197,7 +202,7 @@ local function stacks_add_item(stacks, stack_type, item, container, contained_co stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_size) end - local new_comp_item_item = comp_item_add_item(stack_type, stack_type.comp_items[comp_key], item, container, contained_count) + local new_comp_item_item = comp_item_add_item(stockpile, stack_type, stack_type.comp_items[comp_key], item, container, contained_count) if new_comp_item_item then stack_type.before_stacks = stack_type.before_stacks + 1 stack_type.item_qty = stack_type.item_qty + item.stack_size @@ -324,7 +329,7 @@ local function print_stacks_details(stacks, quiet) if comp_item.item_qty > 0 then log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) for _, item in sorted_items_qty(comp_item.items) do - log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6d Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0)) + log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6d Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) log(3, ('\n')) end @@ -390,7 +395,7 @@ local function isValidPart(item) end -function stacks_add_items(stacks, items, container, contained_count, ind) +function stacks_add_items(stockpile, stacks, items, container, contained_count, ind) -- loop through each item and add it to the matching stack[type_id].comp_items table -- recursively calls itself to add contained items if not ind then ind = '' end @@ -404,7 +409,7 @@ function stacks_add_items(stacks, items, container, contained_count, ind) if stack_type and not item:isSand() and not item:isPlaster() and isValidPart(item) then if not isRestrictedItem(item) then - stacks_add_item(stacks, stack_type, item, container, contained_count) + stacks_add_item(stockpile, stacks, stack_type, item, container, contained_count) if typesThatUseCreatures[df.item_type[type_id]] then local raceRaw = df.global.world.raws.creatures.all[item.race] @@ -431,12 +436,12 @@ function stacks_add_items(stacks, items, container, contained_count, ind) stacks.containers[item.id].container = item stacks.containers[item.id].before_size = #contained_items stacks.containers[item.id].description = utils.getItemDescription(item, 1) - log(4, (' %sContainer:%s <%6d> #items:%5d Sandbearing:%s\n'):format(ind, utils.getItemDescription(item), item.id, count, item:isSandBearing())) - stacks_add_items(stacks, contained_items, item, count, ind .. ' ') + log(4, (' %sContainer:%s <%6d> #items:%5d\n'):format(ind, utils.getItemDescription(item), item.id, count, item:isSandBearing())) + stacks_add_items(stockpile, stacks, contained_items, item, count, ind .. ' ') -- excluded item types else - log(4, (' %sitem:%40s <%6d> is excl, type %d, sand:%s plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, item:isSand(), item:isPlaster())) + log(5, (' %sitem:%40s <%6d> is excl, type %d, sand:%s plaster:%s\n'):format(ind, utils.getItemDescription(item), item.id, type_id, item:isSand(), item:isPlaster())) end end end @@ -466,7 +471,7 @@ local function populate_stacks(stacks, stockpiles, types) log(4, (' stockpile:%30s <%6d> pos:(%3d,%3d,%3d) #items:%5d\n'):format(stockpile.name, stockpile.id, stockpile.centerx, stockpile.centery, stockpile.z, #items)) if #items > 0 then - stacks_add_items(stacks, items) + stacks_add_items(stockpile, stacks, items) else log(4, ' skipping stockpile: no items\n') end From 1243ae2e10db1e19f1e7a070448c2febdbedc192 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 15 Apr 2023 16:08:54 -0700 Subject: [PATCH 126/732] add one more separator space in gui/gm-editor --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 68ce5fe37e..f3bd1d3d44 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -506,7 +506,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) self.subviews.lbl_current_item:itemById('name').text=tostring(trg.target) local t={} for k,v in pairs(trg.keys) do - table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=1,text=getStringValue(trg,v)}}}) + table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=2,text=getStringValue(trg,v)}}}) end local last_pos if preserve_pos then From 237603c656a96ad09f22b0a5f6b0d32278c6ce72 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 15 Apr 2023 17:18:45 -0700 Subject: [PATCH 127/732] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 2558ecb04a..753f3b7ec4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes ## Misc Improvements +- `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building ## Removed @@ -34,7 +35,6 @@ that repo. ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` - `gui/gm-editor`: the key column now auto-fits to the widest key -- `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building - `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) -@ `gui/control-panel`: add `faststart` to the system services From f3657fc36fec72012afc6601493fdadba49c6a2c Mon Sep 17 00:00:00 2001 From: dikbut <73856869+Tjudge1@users.noreply.github.com> Date: Sat, 15 Apr 2023 20:33:49 -0400 Subject: [PATCH 128/732] Pullin (#677) Update makeown.rst to mention that it makes units citizens --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Myk --- docs/makeown.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/makeown.rst b/docs/makeown.rst index a3e82f1787..013c5a141b 100644 --- a/docs/makeown.rst +++ b/docs/makeown.rst @@ -2,11 +2,15 @@ makeown ======= .. dfhack-tool:: - :summary: Converts the selected unit to be a member of your fortress. + :summary: Converts the selected unit to be a fortress citizen. :tags: fort armok units +Select a unit in the UI and run this tool to converts that unit to be a fortress +citizen. It also removes their foreign affiliation, if any. + Usage ----- -``makeown`` - Converts the selected unit to be a member of your fortress. +:: + + makeown From cd969339051389dc1a38ff9f1d43514ec8d00f30 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 15 Apr 2023 18:30:41 -0700 Subject: [PATCH 129/732] Update changelog.txt --- changelog.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index d11d1cfead..038dafef67 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,9 +16,11 @@ that repo. ## New Scripts ## Fixes +- `deteriorate`: ensure remains of enemy dwarves are properly deteriorated ## Misc Improvements -- `combine`: added seeds and powder types. +- `combine`: add seeds and powder types. +- `deteriorate`: add option to exclude useable parts from deterioration - `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building ## Removed @@ -32,7 +34,6 @@ that repo. -@ `gui/gm-editor`: no longer nudges last open window when opening a new one - `warn-starving`: no longer warns for dead units -@ `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode -- `deteriorate`: check for residents corpses. Added option to exclude useable parts from deterioration. ## Misc Improvements - `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` From c2e6016474eac45035b75946fcfe41425cbd994f Mon Sep 17 00:00:00 2001 From: Cubittus Date: Sun, 16 Apr 2023 09:21:18 +0100 Subject: [PATCH 130/732] gm-editor: use utils.assign to combine config --- gui/gm-editor.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 18e6b60652..8f062eb1d6 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -10,9 +10,7 @@ local utils = require 'utils' config = config or json.open('dfhack-config/gm-editor.json') function save_config(data) - for k,v in pairs(data) do - config.data[k] = v - end + utils.assign(config.data, data) config:write() end From 61d35a2015f7fc024d3134112c0a0c07d0a838c1 Mon Sep 17 00:00:00 2001 From: Cubittus Date: Sun, 16 Apr 2023 09:38:20 +0100 Subject: [PATCH 131/732] changelog: md keystroke --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 3b77f8a536..8329069a17 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,7 +20,7 @@ that repo. ## Misc Improvements - `combine`: added seeds and powder types. - `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building -- `gui/gm-editor`: press Ctrl-D to toggle read-only mode to protect from accidental changes; this state persists across sessions +- `gui/gm-editor`: press ``Ctrl-D`` to toggle read-only mode to protect from accidental changes; this state persists across sessions ## Removed From 34e9a3aab6a28dc8579405901eb4654ff45d2b47 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sun, 16 Apr 2023 20:59:18 +1000 Subject: [PATCH 132/732] Update docs/deteriorate.rst Co-authored-by: Myk --- docs/deteriorate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deteriorate.rst b/docs/deteriorate.rst index ded49560a2..2d9034c89f 100644 --- a/docs/deteriorate.rst +++ b/docs/deteriorate.rst @@ -27,7 +27,7 @@ Usage deterioration frequencies. ``deteriorate now --types [--quiet] [--useable]`` Causes all items (of the specified item types) to rot away within a few - ticks, keeping useable corpse pieces. + ticks. You can have different types of items rotting away at different rates by running ``deteriorate start`` multiple times with different options. From 9e733b5204cfacec5a478ab067e96f662d585c57 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sun, 16 Apr 2023 22:03:01 +1000 Subject: [PATCH 133/732] feedback changes --- deteriorate.lua | 21 +++++++++------------ docs/deteriorate.rst | 15 ++++++++------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/deteriorate.lua b/deteriorate.lua index b04e99adf5..a8b341e038 100644 --- a/deteriorate.lua +++ b/deteriorate.lua @@ -3,7 +3,6 @@ local argparse = require('argparse') local utils = require('utils') -local item_stockpiles = {} local function get_clothes_vectors() return {df.global.world.items.other.GLOVES, @@ -36,9 +35,8 @@ local function is_valid_clothing(item) and item.wear > 0 end -local function is_not_useable(opts, item) - local not_useable = - not(opts.useable and ( +local function keep_usable(opts, item) + return opts.keep_usable and ( not item.corpse_flags.unbutchered and ( item.corpse_flags.bone or item.corpse_flags.horn or @@ -51,26 +49,25 @@ local function is_not_useable(opts, item) item.corpse_flags.plant or item.corpse_flags.shell or item.corpse_flags.silk or - item.corpse_flags.yarn) ) ) - return not_useable + item.corpse_flags.yarn) ) end local function is_valid_corpse(opts, item) - -- check if the corpse is a resident of the fortress and is not useable + -- check if the corpse is a resident of the fortress and is not keep_usable local unit = df.unit.find(item.unit_id) if not unit then - return is_not_useable(opts, item) + return not keep_usable(opts, item) end local hf = df.historical_figure.find(unit.hist_figure_id) if not hf then - return is_not_useable(opts, item) + return not keep_usable(opts, item) end for _,link in ipairs(hf.entity_links) do if link.entity_id == df.global.plotinfo.group_id and df.histfig_entity_link_type[link:getType()] == 'MEMBER' then return false end end - return is_not_useable(opts, item) + return not keep_usable(opts, item) end local function is_valid_remains(opts, item) @@ -283,7 +280,7 @@ local opts = { mode = 'days', quiet = false, types = {}, - useable = false, + keep_usable = false, help = false, } @@ -292,7 +289,7 @@ local nonoptions = argparse.processArgsGetopt({...}, { handler=function(optarg) opts.time,opts.mode = parse_freq(optarg) end}, {'h', 'help', handler=function() opts.help = true end}, {'q', 'quiet', handler=function() opts.quiet = true end}, - {'u', 'useable', handler=function() opts.useable = true end}, + {'k', 'keep-usable', handler=function() opts.keep_usable = true end}, {'t', 'types', hasArg=true, handler=function(optarg) opts.types = parse_types(optarg) end}}) diff --git a/docs/deteriorate.rst b/docs/deteriorate.rst index 2d9034c89f..5135cd9ae3 100644 --- a/docs/deteriorate.rst +++ b/docs/deteriorate.rst @@ -18,14 +18,14 @@ dwarves feel, your FPS does not like it! Usage ----- -``deteriorate start --types [--freq ] [--quiet] [--useable]`` - Starts deteriorating the specified item types while you play, keeping useable corpse pieces. +``deteriorate start --types [--freq ] [--quiet] [--keep-usable]`` + Starts deteriorating the specified item types while you play. ``deteriorate stop --types `` Stops deteriorating the specified item types. ``deteriorate status`` Shows the item types that are currently being monitored and their deterioration frequencies. -``deteriorate now --types [--quiet] [--useable]`` +``deteriorate now --types [--quiet] [--keep-usable]`` Causes all items (of the specified item types) to rot away within a few ticks. @@ -35,9 +35,9 @@ You can have different types of items rotting away at different rates by running Examples -------- -Start deteriorating corpses and body parts:: +Start deteriorating corpses and body parts, keeping usable parts such as hair, wool:: - deteriorate start --types corpses + deteriorate start --types corpses --keep-usable Start deteriorating corpses and food and do it at twice the default rate:: @@ -57,6 +57,8 @@ Options specified. The default frequency of 1 day will result in items disappearing after several months. The number does not need to be a whole number. E.g. ``--freq=0.5,days`` is perfectly valid. +``-k``, ``--keep-usable`` + Keep usable body parts such as hair, wool, hooves, bones, and skulls. ``-q``, ``--quiet`` Silence non-error output. ``-t``, ``--types `` @@ -68,7 +70,6 @@ Types :clothes: All clothing pieces that have an armor rating of 0 and are lying on the ground. -:corpses: All resident corpses and body parts. To keep useable remains such as - hair, wool, hooves, bones, and skulls, specify --useable. +:corpses: All resident corpses and body parts. :food: All food and plants, regardless of whether they are in barrels or stockpiles. Seeds are left untouched. From 82d8724385a251eb348ed9aba15a9c6a7189f81d Mon Sep 17 00:00:00 2001 From: silverflyone Date: Mon, 17 Apr 2023 12:04:10 +1000 Subject: [PATCH 134/732] Removed skull1 - this is nervous tissue. --- deteriorate.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/deteriorate.lua b/deteriorate.lua index a8b341e038..eb94dcd143 100644 --- a/deteriorate.lua +++ b/deteriorate.lua @@ -41,7 +41,6 @@ local function keep_usable(opts, item) item.corpse_flags.bone or item.corpse_flags.horn or item.corpse_flags.leather or - item.corpse_flags.skull1 or item.corpse_flags.skull2 or item.corpse_flags.tooth) or ( item.corpse_flags.hair_wool or From 3c5b23d18c97f7b723179a1808cb86ff84dfe3bb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 16 Apr 2023 21:52:55 -0700 Subject: [PATCH 135/732] make general strike fix message more obvious --- fix/general-strike.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix/general-strike.lua b/fix/general-strike.lua index c07c738c2e..9807112b22 100644 --- a/fix/general-strike.lua +++ b/fix/general-strike.lua @@ -15,7 +15,7 @@ local function fix_seeds(quiet) end end if not quiet or count > 0 then - print(('fixed %d seed(s)'):format(count)) + print(('fix/general-strike fixed %d mislabeled seed(s)'):format(count)) end end From f53fb4145764569b2a3363606c9f958eee55219e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 16 Apr 2023 22:34:53 -0700 Subject: [PATCH 136/732] add option for hiding the terminal console at startup --- gui/control-panel.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index f9e8000e3c..2bb9a2ef65 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -46,7 +46,7 @@ end table.sort(FORT_AUTOSTART) local SYSTEM_SERVICES = { - 'automelt', -- TODO needs dynamic detection of configurability + 'automelt', 'buildingplan', 'confirm', 'overlay', @@ -60,6 +60,10 @@ end table.sort(SYSTEM_SERVICES) local PREFERENCES = { + ['dfhack']={ + HIDE_CONSOLE_ON_STARTUP={type='bool', default=true, + desc='Whether to hide the external DFHack console window by default. You can always show it again with the "show" command.'}, + }, ['gui']={ DEFAULT_INITIAL_PAUSE={type='bool', default=true, desc='Whether to pause the game when a DFHack tool is shown.'}, From 4eba884fa658a2c7543053b6f8708a6138c5c713 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 16 Apr 2023 22:42:15 -0700 Subject: [PATCH 137/732] shorten the help so it fits in the allotted space --- gui/control-panel.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 2bb9a2ef65..b098e70b66 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -62,7 +62,7 @@ table.sort(SYSTEM_SERVICES) local PREFERENCES = { ['dfhack']={ HIDE_CONSOLE_ON_STARTUP={type='bool', default=true, - desc='Whether to hide the external DFHack console window by default. You can always show it again with the "show" command.'}, + desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, }, ['gui']={ DEFAULT_INITIAL_PAUSE={type='bool', default=true, From db7e9986168418de70fb8fe8758160e937bf7d69 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Mon, 17 Apr 2023 20:46:12 +1000 Subject: [PATCH 138/732] feedback --- combine.lua | 33 ++++++++++++++++----------------- docs/combine.rst | 32 +++++++++++++++++--------------- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/combine.lua b/combine.lua index 21bb20dc43..fef0ba1c64 100644 --- a/combine.lua +++ b/combine.lua @@ -19,11 +19,8 @@ local MAX_CONT_ITEMS=500 local MAX_MAT_AMT=500 -- list of types that use race and caste -local typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true} -local typesThatUseMaterial={CORPSEPIECE=true} - -local ITEM_QTY = 1 -local MATERIAL_AMOUNT = 2 +local typesThatUseCreatures = utils.invert{'REMAINS', 'FISH', 'FISH_RAW', 'VERMIN', 'PET', 'EGG', 'CORPSE', 'CORPSEPIECE'} +local typesThatUseMaterial=utils.invert{'CORPSEPIECE'} -- list of valid item types for merging -- Notes: 1. mergeable stacks are ones with the same type_id+race+caste or type_id+mat_type+mat_index @@ -60,7 +57,7 @@ for k1,v1 in pairs(valid_types_map) do end end -function log(level, ...) +local function log(level, ...) -- if verbose is specified, then print the arguments, or don't. if not opts.quiet and opts.verbose >= level then dfhack.print(string.format(...)) end end @@ -75,7 +72,6 @@ function CList:new(o) setmetatable(o, self) self.__index = self self.__len = function (t) local n = 0 for _, __ in pairs(t) do n = n + 1 end return n end - self.max = function (t) local v = 0 for _, n in pairs(t) do if n > v then v = n end end return v end return o end @@ -87,11 +83,13 @@ local function comp_item_new(comp_key, max_size) comp_item.description = '' -- description of the comp item for output comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack -- item info - comp_item.items = CList:new(nil) -- key:item.id, val:{ item, - -- before_size, after_size, before_cont_id, after_cont_id, - -- stockpile_id, stockpile_name, - -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} - -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} } + comp_item.items = CList:new(nil) -- key:item.id, + -- val:{item, + -- before_size, after_size, before_cont_id, after_cont_id, + -- stockpile_id, stockpile_name, + -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} + -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} + -- } comp_item.item_qty = 0 -- total quantity of items comp_item.material_amt = 0 -- total amount of materials comp_item.max_mat_amt = MAX_MAT_AMT -- max amount of materials in one stack @@ -122,11 +120,13 @@ local function comp_item_add_item(stockpile, stack_type, comp_item, item, contai new_item.stockpile_id = stockpile.id new_item.stockpile_name = stockpile.name - -- material amount used? + -- material amount info new_item.before_mat_amt = {} new_item.before_mat_amt.Qty = 0 new_item.after_mat_amt = {} new_item.after_mat_amt.Qty = 0 + + -- material amount used? if typesThatUseMaterial[df.item_type[stack_type.type_id]] then new_item.before_mat_amt.Leather = item.material_amount.Leather new_item.before_mat_amt.Bone = item.material_amount.Bone @@ -370,7 +370,6 @@ local function stacks_new() stacks.after_stacks = 0 return stacks - end local function isRestrictedItem(item) @@ -392,10 +391,9 @@ local function isValidPart(item) item.material_amount.Horn > 0 or item.material_amount.HairWool > 0 or item.material_amount.Yarn > 0)) - end -function stacks_add_items(stockpile, stacks, items, container, contained_count, ind) +local function stacks_add_items(stockpile, stacks, items, container, contained_count, ind) -- loop through each item and add it to the matching stack[type_id].comp_items table -- recursively calls itself to add contained items if not ind then ind = '' end @@ -489,7 +487,7 @@ local function preview_stacks(stacks) for comp_key, comp_item in pairs(stack_type.comp_items) do log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) - -- Use item qty or material amount? + -- item qty used? if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then -- max size comparison @@ -525,6 +523,7 @@ local function preview_stacks(stacks) end end + -- material amount used. else local stacks_needed = math.floor(comp_item.material_amt / comp_item.max_mat_amt) local stack_remainder = comp_item.material_amt - stacks_needed * comp_item.max_mat_amt diff --git a/docs/combine.rst b/docs/combine.rst index 904489718d..fae44bbbfd 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -23,21 +23,23 @@ Examples ``combine all --types=meat,plant`` Merge ``meat`` and ``plant`` type stacks in all stockpiles. ``combine here`` - Merge stacks in stockpile located at game cursor. + Merge stacks in the selected stockpile. Commands -------- ``all`` Search all stockpiles. ``here`` - Search the stockpile under the game cursor. + Search the currently selected stockpile. Options ------- ``-h``, ``--help`` Prints help text. Default if no options are specified. + ``-d``, ``--dry-run`` Display the stack changes without applying them. + ``-t``, ``--types `` Filter item types. Default is ``all``. Valid types are: @@ -72,16 +74,16 @@ Options Notes ----- The following conditions prevent an item from being combined: -1. An item is not in a stockpile. -2. An item is sand or plaster. -3. An item is rotten, forbidden/hidden, marked for dumping/melting, -on fire, encased, owned by a trader/hostile/dwarf or is in a spider web. -4. An item is a part and not butchered. - -The following categories are used for combining: -1. Item is a part and has a race: category=type + race -2. Item has a race/caste: category=type + race + caste -3. Item is ammo, created by for masterwork. category=type + material + quality (+ created by) -4. Or: category= type + material - -A default stack size of 30 applies to a category, unless a larger stack exists. + 1. An item is not in a stockpile. + 2. An item is sand or plaster. + 3. An item is rotten, forbidden/hidden, marked for dumping/melting, on fire, encased, owned by a trader/hostile/dwarf or is in a spider web. + 4. An item is part of a corpse and not butchered. + +The following categories are defined: + 1. Corpse pieces, grouped by piece type and race + 2. Items that have an associated race/caste, grouped by item type, race, and caste + 3. Ammo, grouped by ammo type, material, and quality. If the ammo is a masterwork, it is also grouped by who created it. + 4. Anything else, grouped by item type and material + +Each category has a default stack size of 30 unless a larger stack already exists in your fort. +In that case the largest existing stack size is used. From c36ab2196c8b649dbb75b17630b165cf81d950b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 14:01:11 +0000 Subject: [PATCH 139/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- combine.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/combine.lua b/combine.lua index fef0ba1c64..53cc9c875f 100644 --- a/combine.lua +++ b/combine.lua @@ -88,7 +88,7 @@ local function comp_item_new(comp_key, max_size) -- before_size, after_size, before_cont_id, after_cont_id, -- stockpile_id, stockpile_name, -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} - -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} + -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} -- } comp_item.item_qty = 0 -- total quantity of items comp_item.material_amt = 0 -- total amount of materials From 48cb31d08f9c96d4bace7cf546fb463e94326998 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 17 Apr 2023 09:40:44 -0700 Subject: [PATCH 140/732] set/respect armok hiding in control panel/launcher --- gui/control-panel.lua | 20 +++++++++++++------- gui/launcher.lua | 13 ++++++++----- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index b098e70b66..daa20ef65c 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -61,19 +61,21 @@ table.sort(SYSTEM_SERVICES) local PREFERENCES = { ['dfhack']={ - HIDE_CONSOLE_ON_STARTUP={type='bool', default=true, + HIDE_CONSOLE_ON_STARTUP={label='Hide console on startup', type='bool', default=true, desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, + HIDE_ARMOK_TOOLS={label='Hide "armok" tools in command lists', type='bool', default=false, + desc='Hide tools that give you deity-level control by default.'}, }, ['gui']={ - DEFAULT_INITIAL_PAUSE={type='bool', default=true, + DEFAULT_INITIAL_PAUSE={label='DFHack tools pause game when shown', type='bool', default=true, desc='Whether to pause the game when a DFHack tool is shown.'}, }, ['gui.widgets']={ - DOUBLE_CLICK_MS={type='int', default=500, min=50, + DOUBLE_CLICK_MS={label='Mouse double click speed (ms)', type='int', default=500, min=50, desc='How long to wait for the second click of a double click, in ms.'}, - SCROLL_INITIAL_DELAY_MS={type='int', default=300, min=5, + SCROLL_INITIAL_DELAY_MS={label='Mouse initial scroll repeat delay (ms)', type='int', default=300, min=5, desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.'}, - SCROLL_DELAY_MS={type='int', default=20, min=5, + SCROLL_DELAY_MS={label='Mouse scroll repeat delay (ms)', type='int', default=20, min=5, desc='The delay between events when holding the mouse button down on a scrollbar, in ms.'}, }, } @@ -351,8 +353,11 @@ end function Services:get_choices() local enabled_map = self:get_enabled_map() local choices = {} + local hide_armok = dfhack.getHideArmokTools() for _,service in ipairs(self.services_list) do - table.insert(choices, {target=service, enabled=enabled_map[service]}) + if not hide_armok or not helpdb.is_entry(service) or not helpdb.get_entry_tags(service).armok then + table.insert(choices, {target=service, enabled=enabled_map[service]}) + end end return choices end @@ -626,7 +631,7 @@ function Preferences:refresh() {tile=CONFIGURE_PEN_CENTER}, {tile=BUTTON_PEN_RIGHT}, ' ', - id, + spec.label, ' (', tostring(ctx_env[id]), ')', @@ -636,6 +641,7 @@ function Preferences:refresh() ctx_env=ctx_env, id=id, spec=spec}) end end + table.sort(choices, function(a, b) return a.spec.label < b.spec.label end) local list = self.subviews.list local filter = list:getFilter() local selected = list:getSelected() diff --git a/gui/launcher.lua b/gui/launcher.lua index 5a5a2a6d26..45197c0c1f 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -794,8 +794,6 @@ local function sort_by_freq(entries) table.sort(entries, stable_sort_by_frequency) end -local DEV_FILTER = {tag={'dev', 'unavailable'}} - -- adds the n most closely affiliated peer entries for the given entry that -- aren't already in the entries list. affiliation is determined by how many -- tags the entries share. @@ -831,9 +829,14 @@ local function add_top_related_entries(entries, entry, n) end function LauncherUI:update_autocomplete(firstword) - local entries = helpdb.search_entries( - {str=firstword, types='command'}, - dev_mode and {} or DEV_FILTER) + local excludes + if not dev_mode then + excludes = {tag={'dev', 'unavailable'}} + if dfhack.getHideArmokTools() then + table.insert(excludes.tag, 'armok') + end + end + local entries = helpdb.search_entries({str=firstword, types='command'}, excludes) -- if firstword is in the list, extract it so we can add it to the top later -- even if it's not in the list, add it back anyway if it's a valid db entry -- (e.g. if it's a dev script that we masked out) to show that it's a valid From d4453a327797af867a00418fdcb8c184d1cee2a0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 17 Apr 2023 12:43:26 -0700 Subject: [PATCH 141/732] update wording --- gui/control-panel.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index daa20ef65c..c871ef218b 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -64,11 +64,11 @@ local PREFERENCES = { HIDE_CONSOLE_ON_STARTUP={label='Hide console on startup', type='bool', default=true, desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, HIDE_ARMOK_TOOLS={label='Hide "armok" tools in command lists', type='bool', default=false, - desc='Hide tools that give you deity-level control by default.'}, + desc='Don\'t show tools that give you deity-level control wherever tools are listed.'}, }, ['gui']={ - DEFAULT_INITIAL_PAUSE={label='DFHack tools pause game when shown', type='bool', default=true, - desc='Whether to pause the game when a DFHack tool is shown.'}, + DEFAULT_INITIAL_PAUSE={label='DFHack tools autopause game', type='bool', default=true, + desc='Whether to pause the game when a DFHack tool window is shown.'}, }, ['gui.widgets']={ DOUBLE_CLICK_MS={label='Mouse double click speed (ms)', type='int', default=500, min=50, From 36295a4bb68563092d106b2a6bf18b6545a50bd2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 17 Apr 2023 13:30:19 -0700 Subject: [PATCH 142/732] update wording (Thanks, Ozzatron!) --- gui/control-panel.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index c871ef218b..222b6923a6 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -64,7 +64,7 @@ local PREFERENCES = { HIDE_CONSOLE_ON_STARTUP={label='Hide console on startup', type='bool', default=true, desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, HIDE_ARMOK_TOOLS={label='Hide "armok" tools in command lists', type='bool', default=false, - desc='Don\'t show tools that give you deity-level control wherever tools are listed.'}, + desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.'}, }, ['gui']={ DEFAULT_INITIAL_PAUSE={label='DFHack tools autopause game', type='bool', default=true, From a6cc5359855ec1f32020af69f723e8bea4602204 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 18 Apr 2023 15:58:02 -0700 Subject: [PATCH 143/732] remember editors started with the "dialog" option --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index e17100d0a9..fed3c98844 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -656,7 +656,7 @@ local function get_editor(args) if args[1]=="dialog" then dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, "", function(entry) - view = GmScreen{target=eval(entry)}:show() + views[GmScreen{target=eval(entry)}:show()] = true end) elseif args[1]=="free" then return GmScreen{target=df.reinterpret_cast(df[args[2]],args[3])}:show() From b49db14b433bbabea61e2550f888406bd1e8282e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 18 Apr 2023 16:33:01 -0700 Subject: [PATCH 144/732] freeze option for inhibiting logic() propagation --- changelog.txt | 1 + docs/gui/gm-editor.rst | 24 +++++++++++++++++++----- gui/gm-editor.lua | 24 ++++++++++++++++++++---- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/changelog.txt b/changelog.txt index 2ae5ec8dce..4a18a41aa0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -23,6 +23,7 @@ that repo. - `deteriorate`: add option to exclude useable parts from deterioration - `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building - `gui/gm-editor`: press ``Ctrl-D`` to toggle read-only mode to protect from accidental changes; this state persists across sessions +- `gui/gm-editor`: new ``--freeze`` option for ensuring the game doesn't change while you're inspecting it ## Removed diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index 0267afe9bf..334e127f64 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -8,15 +8,19 @@ gui/gm-editor This editor allows you to inspect or modify almost anything in DF. Press :kbd:`?` for in-game help. +If you just want to browse without fear of accidentally changing anything, hit +:kbd:`Ctrl`:kbd:`D` to toggle read-only mode. + Usage ----- -``gui/gm-editor`` +``gui/gm-editor [-f]`` Open the editor on whatever is selected or viewed (e.g. unit/item description screen) -``gui/gm-editor `` - Evaluate a lua expression and opens the editor on its results. -``gui/gm-editor dialog`` +``gui/gm-editor [-f] `` + Evaluate a lua expression and opens the editor on its results. Field + prefixes of ``df.global`` can be omitted. +``gui/gm-editor [-f] dialog`` Show an in-game dialog to input the lua expression to evaluate. Works the same as the version above. @@ -25,8 +29,18 @@ Examples ``gui/gm-editor`` Opens the editor on the selected unit/item/job/workorder/stockpile etc. -``gui/gm-editor df.global.world.items.all`` +``gui/gm-editor world.items.all`` Opens the editor on the items list. +``gui/gm-editor --freeze scr`` + Opens the editor on the current viewscreen data and prevents it from getting updates while you have the editor open. + +Options +------- + +``-f``, ``--freeze`` + Freeze the underlying viewscreen so that it does not receive logic updates. + Note that this will prevent scrolling the map by draggint with the middle + mouse button. Screenshot ---------- diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index fed3c98844..b54fe6c54e 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -637,6 +637,7 @@ end GmScreen = defclass(GmScreen, gui.ZScreen) GmScreen.ATTRS { focus_path='gm-editor', + freeze=false, } function GmScreen:init(args) @@ -644,27 +645,42 @@ function GmScreen:init(args) if not target then qerror('Target not found') end + self.force_pause = self.freeze self:addviews{GmEditorUi{target=target}} end +function GmScreen:onIdle() + if not self.freeze then + GmScreen.super.onIdle(self) + elseif self.force_pause and dfhack.isMapLoaded() then + df.global.pause_state = true + end +end + function GmScreen:onDismiss() views[self] = nil end local function get_editor(args) + local freeze = false + if args[1] == '-f' or args[1] == '--freeze' then + freeze = true + table.remove(args, 1) + end if #args~=0 then if args[1]=="dialog" then dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, "", function(entry) - views[GmScreen{target=eval(entry)}:show()] = true + local view = GmScreen{freeze=freeze, target=eval(entry)}:show() + views[view] = true end) elseif args[1]=="free" then - return GmScreen{target=df.reinterpret_cast(df[args[2]],args[3])}:show() + return GmScreen{freeze=freeze, target=df.reinterpret_cast(df[args[2]],args[3])}:show() else - return GmScreen{target=eval(args[1])}:show() + return GmScreen{freeze=freeze, target=eval(args[1])}:show() end else - return GmScreen{target=getTargetFromScreens()}:show() + return GmScreen{freeze=freeze, target=getTargetFromScreens()}:show() end end From a313a5506927c36806d4a7eedbab1e65dbe9614e Mon Sep 17 00:00:00 2001 From: humblegar Date: Wed, 19 Apr 2023 20:38:36 +0200 Subject: [PATCH 145/732] Now also empties in buildings --- fix/dry-buckets.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/fix/dry-buckets.lua b/fix/dry-buckets.lua index 650c5b905b..d1b63fab23 100644 --- a/fix/dry-buckets.lua +++ b/fix/dry-buckets.lua @@ -1,28 +1,35 @@ -- Removes water from buckets (for lye-making). +-- Can also be used to remove water (muddy or not) that is blocking well operations. --[====[ fix/dry-buckets =============== Removes water from all buckets in your fortress, allowing them -to be used for making lye. Skips buckets in buildings (eg a well), -being carried, or currently used by a job. +to be used for making lye. Skips buckets being carried, or currently used by a job. ]====] local emptied = 0 +local inBuilding = 0 local water_type = dfhack.matinfo.find('WATER').type for _,item in ipairs(df.global.world.items.all) do local container = dfhack.items.getContainer(item) if container ~= nil and container:getType() == df.item_type.BUCKET - and not (container.flags.in_job or container.flags.in_building) + and not (container.flags.in_job) and item:getMaterial() == water_type and item:getType() == df.item_type.LIQUID_MISC - and not (item.flags.in_job or item.flags.in_building) then + and not (item.flags.in_job) then + if container.flags.in_building or item.flags.in_building then + inBuilding = inBuilding + 1 + end dfhack.items.remove(item) emptied = emptied + 1 end end print('Emptied '..emptied..' buckets.') +if emptied > 0 then + print(''..inBuilding..' of those were in a building.') +end \ No newline at end of file From 23ec4ad012b920880957b27bacd9f8dd4d1e58c6 Mon Sep 17 00:00:00 2001 From: humblegar Date: Wed, 19 Apr 2023 20:58:56 +0200 Subject: [PATCH 146/732] Moved doc. Better print. --- docs/fix/dry-buckets.rst | 5 +++-- fix/dry-buckets.lua | 17 ++++------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/docs/fix/dry-buckets.rst b/docs/fix/dry-buckets.rst index e72fa25a3f..1520a1588e 100644 --- a/docs/fix/dry-buckets.rst +++ b/docs/fix/dry-buckets.rst @@ -8,8 +8,9 @@ fix/dry-buckets Sometimes, dwarves drop buckets of water on the ground if their water hauling job is interrupted. These buckets then become unavailable for any other kind of use, such as making lye. This tool finds those discarded buckets and removes the -water from them. Buckets in wells or being actively carried or used by a job are -not affected. +water from them. + +This tool can also be used to remote water (muddy or not) that is blocking well operations. Usage ----- diff --git a/fix/dry-buckets.lua b/fix/dry-buckets.lua index d1b63fab23..0db4c85f5a 100644 --- a/fix/dry-buckets.lua +++ b/fix/dry-buckets.lua @@ -1,16 +1,7 @@ --- Removes water from buckets (for lye-making). --- Can also be used to remove water (muddy or not) that is blocking well operations. ---[====[ -fix/dry-buckets -=============== -Removes water from all buckets in your fortress, allowing them -to be used for making lye. Skips buckets being carried, or currently used by a job. - -]====] local emptied = 0 -local inBuilding = 0 +local in_building = 0 local water_type = dfhack.matinfo.find('WATER').type for _,item in ipairs(df.global.world.items.all) do @@ -22,7 +13,7 @@ for _,item in ipairs(df.global.world.items.all) do and item:getType() == df.item_type.LIQUID_MISC and not (item.flags.in_job) then if container.flags.in_building or item.flags.in_building then - inBuilding = inBuilding + 1 + in_building = in_building + 1 end dfhack.items.remove(item) emptied = emptied + 1 @@ -31,5 +22,5 @@ end print('Emptied '..emptied..' buckets.') if emptied > 0 then - print(''..inBuilding..' of those were in a building.') -end \ No newline at end of file + print(('Unclogged %d wells.'):format(in_building)) +end From d7d7c13ee0f6b802166457c20fedfd37fec2fb3b Mon Sep 17 00:00:00 2001 From: humblegar Date: Wed, 19 Apr 2023 21:13:15 +0200 Subject: [PATCH 147/732] Trim whitespace --- fix/dry-buckets.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fix/dry-buckets.lua b/fix/dry-buckets.lua index 0db4c85f5a..1b463b6499 100644 --- a/fix/dry-buckets.lua +++ b/fix/dry-buckets.lua @@ -14,7 +14,7 @@ for _,item in ipairs(df.global.world.items.all) do and not (item.flags.in_job) then if container.flags.in_building or item.flags.in_building then in_building = in_building + 1 - end + end dfhack.items.remove(item) emptied = emptied + 1 end @@ -22,5 +22,5 @@ end print('Emptied '..emptied..' buckets.') if emptied > 0 then - print(('Unclogged %d wells.'):format(in_building)) + print(('Unclogged %d wells.'):format(in_building)) end From e23c01c37cebc9293881a3ab4b5554c84aae1f96 Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 19 Apr 2023 12:37:15 -0700 Subject: [PATCH 148/732] Update docs/fix/dry-buckets.rst --- docs/fix/dry-buckets.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fix/dry-buckets.rst b/docs/fix/dry-buckets.rst index 1520a1588e..311e3e6c2e 100644 --- a/docs/fix/dry-buckets.rst +++ b/docs/fix/dry-buckets.rst @@ -10,7 +10,7 @@ job is interrupted. These buckets then become unavailable for any other kind of use, such as making lye. This tool finds those discarded buckets and removes the water from them. -This tool can also be used to remote water (muddy or not) that is blocking well operations. +This tool also fixes over-full buckets that are blocking well operations. Usage ----- From b648fbc021dda0b22f2ea8a11e69cc34636bc194 Mon Sep 17 00:00:00 2001 From: Kira Date: Fri, 21 Apr 2023 06:00:27 +0100 Subject: [PATCH 149/732] list-waves: fix a typo --- list-waves.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/list-waves.lua b/list-waves.lua index 5d2c39b6e3..8a6b6114bb 100644 --- a/list-waves.lua +++ b/list-waves.lua @@ -51,7 +51,7 @@ These options are used to specify what wave information to display The script must loop through all active units in df.global.world.units.active and build each wave one dwarf at a time. This requires calculating arrival information for each dwarf and combining this information into a sort of unique wave ID number. After this -is finished these wave UIDs are loops through and normalized so they start at zero and +is finished these wave UIDs are looped through and normalized so they start at zero and incremented by one for each new wave UID. As you might surmise, this is a relatively dumb script and every execution has the From f7fd2939902d4dac6d39ce8bcaa9bdcc3a8305de Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sun, 23 Apr 2023 15:53:33 +0200 Subject: [PATCH 150/732] Fix suspendmanager preentively suspending jobs --- changelog.txt | 1 + suspendmanager.lua | 33 ++++++++++++++++++++++----------- unsuspend.lua | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4a18a41aa0..56843cec51 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ that repo. ## Fixes - `deteriorate`: ensure remains of enemy dwarves are properly deteriorated +- `suspendmanager`: Fix actively suspending jobs that could still be tentatively done ## Misc Improvements - `combine`: Now supports ammo, parts, powders, and seeds, and combines into containers diff --git a/suspendmanager.lua b/suspendmanager.lua index b4a56e4957..605f18543a 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -187,11 +187,21 @@ function isBlocking(job) end --- Return true with a reason if a job should be suspended. ---- It takes in account water flow, buildingplan plugin, and optionally ---- the risk of creating stuck construction buildings +--- It optionally takes in account the risk of creating stuck +--- construction buildings --- @param job job --- @param accountblocking boolean function shouldBeSuspended(job, accountblocking) + if accountblocking and isBlocking(job) then + return true, 'blocking' + end + return false, nil +end + +--- Return true with a reason if a job should not be unsuspended. +function shouldStaySuspended(job, accountblocking) + -- External reasons to be suspended + if dfhack.maps.getTileFlags(job.pos).flow_size > 1 then return true, 'underwater' end @@ -201,19 +211,20 @@ function shouldBeSuspended(job, accountblocking) return true, 'buildingplan' end - if accountblocking and isBlocking(job) then - return true, 'blocking' - end - return false, nil + -- Internal reasons to be suspended, determined by suspendmanager + return shouldBeSuspended(job, accountblocking) end local function run_now() foreach_construction_job(function(job) - local shouldBeSuspended, _ = shouldBeSuspended(job, preventblocking) - if shouldBeSuspended and not job.flags.suspend then - suspend(job) - elseif not shouldBeSuspended and job.flags.suspend then - unsuspend(job) + if job.flags.suspend then + if not shouldStaySuspended(job, preventblocking) then + unsuspend(job) + end + else + if shouldBeSuspended(job, preventblocking) then + suspend(job) + end end end) end diff --git a/unsuspend.lua b/unsuspend.lua index 13b2d5d492..9351564d5d 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -189,7 +189,7 @@ local unsuspended_count = 0 suspendmanager.foreach_construction_job(function(job) if not job.flags.suspend then return end - local skip,reason=suspendmanager.shouldBeSuspended(job, skipblocking) + local skip,reason=suspendmanager.shouldStaySuspended(job, skipblocking) if skip then skipped_counts[reason] = (skipped_counts[reason] or 0) + 1 return From 4925dd2abcd1cec5e6907ea47ca711411cd55417 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 23 Apr 2023 16:29:59 -0700 Subject: [PATCH 151/732] remove old help text --- twaterlvl.lua | 8 -------- 1 file changed, 8 deletions(-) diff --git a/twaterlvl.lua b/twaterlvl.lua index 29fe8deea2..46b0c598bf 100644 --- a/twaterlvl.lua +++ b/twaterlvl.lua @@ -1,11 +1,3 @@ --- Toggle display of water depth ---[====[ - -twaterlvl -========= -Toggle between displaying/not displaying liquid depth as numbers. - -]====] df.global.d_init.flags1.SHOW_FLOW_AMOUNTS = not df.global.d_init.flags1.SHOW_FLOW_AMOUNTS print('Water level display toggled.') From 43296e78d46d4e1e6d83a25829632e55691f0869 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 23 Apr 2023 16:30:26 -0700 Subject: [PATCH 152/732] add toggle-kbd-cursor --- docs/toggle-kbd-cursor.rst | 15 +++++++++++++++ toggle-kbd-cursor.lua | 12 ++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 docs/toggle-kbd-cursor.rst create mode 100644 toggle-kbd-cursor.lua diff --git a/docs/toggle-kbd-cursor.rst b/docs/toggle-kbd-cursor.rst new file mode 100644 index 0000000000..80b0e08e62 --- /dev/null +++ b/docs/toggle-kbd-cursor.rst @@ -0,0 +1,15 @@ +toggle-kbd-cursor +================= + +.. dfhack-tool:: + :summary: Toggles the keyboard cursor. + :tags: interface + +This tool simply toggles the keyboard cursor so you can quickly switch it on when you need it. Many other tools, like `autodump`, need a keyboard cursor for selecting a target tile. Note that you'll still need to enter an interface mode where the keyboard cursor is visible, like mining mode or dumping mode, in order to use the cursor. + +Usage +----- + +:: + + toggle-kbd-cursor diff --git a/toggle-kbd-cursor.lua b/toggle-kbd-cursor.lua new file mode 100644 index 0000000000..b8bfd1ec37 --- /dev/null +++ b/toggle-kbd-cursor.lua @@ -0,0 +1,12 @@ +local guidm = require('gui.dwarfmode') + +local flags4 = df.global.d_init.flags4 + +if flags4.KEYBOARD_CURSOR then + flags4.KEYBOARD_CURSOR = false + print('Keyboard cursor disabled.') +else + guidm.setCursorPos(guidm.Viewport.get():getCenter()) + flags4.KEYBOARD_CURSOR = true + print('Keyboard cursor enabled.') +end From 23a2a974895a82dad75f9cf84a5121f9b6f57d1e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 23 Apr 2023 18:28:19 -0700 Subject: [PATCH 153/732] add DFHack version to the launcher help text --- gui/launcher.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index 45197c0c1f..6784196746 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -469,7 +469,10 @@ Type a command to see its help text here. Hit ENTER to run the command, or tap b Not sure what to do? First, try running "quickstart-guide" to get oriented with DFHack and its capabilities. Then maybe try the "tags" command to see the different categories of tools DFHack has to offer! Run "tags " (e.g. "tags design") to see the tools in that category. -To see help for this command launcher (including info on mouse controls), type "launcher" and click on "gui/launcher" to autocomplete.]] +To see help for this command launcher (including info on mouse controls), type "launcher" and click on "gui/launcher" to autocomplete. + +You're running DFHack ]] .. dfhack.getDFHackVersion() .. + (dfhack.isPrerelease() and (' (git: %s)'):format(dfhack.getGitCommit(true)) or '') function HelpPanel:init() self.cur_entry = '' From c2373d4e2b66c59c2bf9b32acddcce71f466cde4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 23 Apr 2023 18:29:06 -0700 Subject: [PATCH 154/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 4a18a41aa0..531034e9dd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -24,6 +24,7 @@ that repo. - `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building - `gui/gm-editor`: press ``Ctrl-D`` to toggle read-only mode to protect from accidental changes; this state persists across sessions - `gui/gm-editor`: new ``--freeze`` option for ensuring the game doesn't change while you're inspecting it +- `gui/launcher`: DFHack version now shown in the default help text ## Removed From ebbd75745b7af2181567d5099d115a4dac9ff25a Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 23 Apr 2023 18:44:39 -0700 Subject: [PATCH 155/732] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 56843cec51..bd3f53a636 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,7 +17,7 @@ that repo. ## Fixes - `deteriorate`: ensure remains of enemy dwarves are properly deteriorated -- `suspendmanager`: Fix actively suspending jobs that could still be tentatively done +- `suspendmanager`: Fix over-aggressive suspension of jobs that could still possibly be done (e.g. jobs that are partially submerged in water) ## Misc Improvements - `combine`: Now supports ammo, parts, powders, and seeds, and combines into containers From 68f6d354b0d815ad0985dbe9b5faa140c980af14 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 23 Apr 2023 19:02:08 -0700 Subject: [PATCH 156/732] reinstate tags for gui/seedwatch --- docs/gui/seedwatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/seedwatch.rst b/docs/gui/seedwatch.rst index 61412a1c51..95ae306c20 100644 --- a/docs/gui/seedwatch.rst +++ b/docs/gui/seedwatch.rst @@ -3,7 +3,7 @@ gui/seedwatch .. dfhack-tool:: :summary: Manages seed and plant cooking based on seed stock levels. - + :tags: fort auto plants This is the configuration interface for the `seedwatch` plugin. You can configure a target stock amount for each seed type. If the number of seeds of that type falls From 01942093ff5c8bd0fff4d84f8551f211300e49fe Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 23 Apr 2023 22:53:47 -0700 Subject: [PATCH 157/732] modernize gui/prerelease-warning --- changelog.txt | 1 + gui/prerelease-warning.lua | 94 +++++++++++++++++++++----------------- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/changelog.txt b/changelog.txt index bd3f53a636..06aff09647 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ that repo. - `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building - `gui/gm-editor`: press ``Ctrl-D`` to toggle read-only mode to protect from accidental changes; this state persists across sessions - `gui/gm-editor`: new ``--freeze`` option for ensuring the game doesn't change while you're inspecting it +- `gui/prerelease-warning`: widgets are now clickable ## Removed diff --git a/gui/prerelease-warning.lua b/gui/prerelease-warning.lua index c5bae7dfa6..480b734110 100644 --- a/gui/prerelease-warning.lua +++ b/gui/prerelease-warning.lua @@ -1,6 +1,7 @@ -local dlg = require 'gui.dialogs' -local json = require 'json' -local utils = require 'utils' +local gui = require('gui') +local json = require('json') +local widgets = require('gui.widgets') +local utils = require('utils') local force = ({...})[1] == 'force' local config = json.open('dfhack-config/prerelease-warning.json') @@ -22,7 +23,7 @@ if not utils.invert{'alpha', 'beta', 'rc', 'r'}[state] then state = 'unknown' end -message = ({ +local message = ({ alpha = { 'Warning', COLOR_YELLOW, @@ -61,17 +62,18 @@ message = ({ } })[state] -title = table.remove(message, 1) -color = table.remove(message, 1) +local title = table.remove(message, 1) +local color = table.remove(message, 1) -pack_message = [[ +local pack_message = [[ This should not be enabled by default in a pack. If you are seeing this message and did not enable/install DFHack yourself, please report this to your pack's maintainer.]] -path = dfhack.getHackPath():lower() -if #pack_message > 0 and (path:find('lnp') or path:find('starter') or path:find('newb') or path:find('lazy') or path:find('pack')) then +local path = dfhack.getHackPath():lower() +if path:find('lnp') or path:find('starter') or path:find('newb') or path:find('lazy') or + path:find('pack') then for _, v in pairs(pack_message:split('\n')) do table.insert(message, NEWLINE) table.insert(message, {text=v, pen=COLOR_LIGHTMAGENTA}) @@ -81,13 +83,13 @@ end for _, v in pairs(([[ REMINDER: Please report any issues you encounter while -using this DFHack build on GitHub (github.com/dfhack/dfhack/issues) +using this DFHack build on GitHub (github.com/DFHack/dfhack/issues) or the Bay12 forums (dfhack.org/bay12).]]):split('\n')) do table.insert(message, NEWLINE) table.insert(message, {text=v, pen=COLOR_LIGHTCYAN}) end -nightly_message = [[ +local nightly_message = [[ You appear to be using a nightly build of DFHack. If you experience problems, check dfhack.org/builds for updates.]] @@ -100,7 +102,7 @@ end dfhack.print('\n') -for k,v in ipairs(message) do +for _, v in ipairs(message) do if type(v) == 'table' then dfhack.color(v.pen) dfhack.print(v.text) @@ -113,34 +115,44 @@ end dfhack.color(COLOR_RESET) dfhack.print('\n\n') -WarningBox = defclass(nil, dlg.MessageBox) - -function WarningBox:getWantedFrameSize() - local w, h = WarningBox.super.getWantedFrameSize(self) - return w, h + 2 -end - -function WarningBox:onRenderFrame(dc,rect) - WarningBox.super.onRenderFrame(self,dc,rect) - dc:pen(COLOR_WHITE):key_pen(COLOR_LIGHTRED) - :seek(rect.x1 + 2, rect.y2 - 2) - :key('CUSTOM_D'):string(': Do not show again') - :advance(10) - :key('LEAVESCREEN'):string('/') - :key('SELECT'):string(': Dismiss') -end - -function WarningBox:onInput(keys) - if keys.CUSTOM_D then - config.data.hide = true - config:write() - keys.LEAVESCREEN = true - end - WarningBox.super.onInput(self, keys) +WarningBoxScreen = defclass(WarningBoxScreen, gui.ZScreenModal) +WarningBoxScreen.ATTRS{focus_path='prerelease-warning'} + +function WarningBoxScreen:init() + self:addviews{ + widgets.Window{ + frame_title=title, + frame={w=74, h=14}, + resizable=true, + resize_min={h=11}, + subviews={ + widgets.Label{ + frame={t=0, b=2}, + text=message, + text_pen=color, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='CUSTOM_D', + label='Do not show again', + auto_width=true, + on_activate=function() + config.data.hide = true + config:write() + self:dismiss() + end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=30}, + key='SELECT', + label='Dismiss', + auto_width=true, + on_activate=function() self:dismiss() end, + }, + }, + }, + } end -view = view or WarningBox{ - frame_title = title, - text = message, - text_pen = color -}:show() +-- never reset view; we only want to show this dialog once +view = view or WarningBoxScreen{}:show() From ad1998a0032ce50e90d05429a4178b668c0840ba Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 24 Apr 2023 16:09:00 -0700 Subject: [PATCH 158/732] alter the logic for the version string --- gui/launcher.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index 6784196746..38332de95d 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -472,7 +472,7 @@ Not sure what to do? First, try running "quickstart-guide" to get oriented with To see help for this command launcher (including info on mouse controls), type "launcher" and click on "gui/launcher" to autocomplete. You're running DFHack ]] .. dfhack.getDFHackVersion() .. - (dfhack.isPrerelease() and (' (git: %s)'):format(dfhack.getGitCommit(true)) or '') + (dfhack.isRelease() and '' or (' (git: %s)'):format(dfhack.getGitCommit(true))) function HelpPanel:init() self.cur_entry = '' From 8e856f3b11fb22613cfdb9b8a3f52b074e1593f0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 27 Apr 2023 14:23:19 -0700 Subject: [PATCH 159/732] add work-now as a user-controlled System service --- gui/control-panel.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 222b6923a6..c0051c49ac 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -53,6 +53,7 @@ local SYSTEM_SERVICES = { } local SYSTEM_USER_SERVICES = { 'faststart', + 'work-now', } for _,v in ipairs(SYSTEM_USER_SERVICES) do table.insert(SYSTEM_SERVICES, v) From 710157938cbbed17492572d0b05ea86fc7665e35 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Thu, 27 Apr 2023 22:37:49 -0400 Subject: [PATCH 160/732] Working C++ and refactors --- gui/design.lua | 444 +++++++++++-------------------------- internal/design/shapes.lua | 1 + internal/design/util.lua | 86 +++++++ 3 files changed, 213 insertions(+), 318 deletions(-) create mode 100644 internal/design/util.lua diff --git a/gui/design.lua b/gui/design.lua index ef2e0dcdd0..298ae55d1b 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -39,6 +39,11 @@ local guidm = require("gui.dwarfmode") local widgets = require("gui.widgets") local quickfort = reqscript("quickfort") local shapes = reqscript("internal/design/shapes") +local util = reqscript("internal/design/util") +local plugin = require("plugins.design") + +local Point = util.Point +local getMousePoint = util.getMousePoint local tile_attrs = df.tiletype.attrs @@ -109,15 +114,6 @@ end -- Utilities -local function same_xy(pos1, pos2) - if not pos1 or not pos2 then return false end - return pos1.x == pos2.x and pos1.y == pos2.y -end - -local function same_xyz(pos1, pos2) - return same_xy(pos1, pos2) and pos1.z == pos2.z -end - local function get_icon_pens() local start = dfhack.textures.getControlPanelTexposStart() local valid = start > 0 @@ -156,36 +152,30 @@ DISABLED_PEN_LEFT, DISABLED_PEN_CENTER, DISABLED_PEN_RIGHT, BUTTON_PEN_LEFT, BUTTON_PEN_RIGHT, HELP_PEN_CENTER, CONFIGURE_PEN_CENTER = get_icon_pens() -local uibs = df.global.buildreq - -local function get_cur_filters() - return dfhack.buildings.getFiltersByType({}, uibs.building_type, - uibs.building_subtype, uibs.custom_type) -end - -- Debug window SHOW_DEBUG_WINDOW = false +DEBUG_PROFILING = true local function table_to_string(tbl, indent) indent = indent or "" local result = {} for k, v in pairs(tbl) do - local key = type(k) == "number" and "["..tostring(k).."]" or tostring(k) + local key = type(k) == "number" and "[" .. tostring(k) .. "]" or tostring(k) if type(v) == "table" then - table.insert(result, indent..key.." = {") - local subTable = table_to_string(v, indent.." ") + table.insert(result, indent .. key .. " = {") + local subTable = table_to_string(v, indent .. " ") for _, line in ipairs(subTable) do table.insert(result, line) end - table.insert(result, indent.."},") + table.insert(result, indent .. "},") elseif type(v) == "function" then local res = v() - local value = type(res) == "number" and tostring(res) or "\""..tostring(res).."\"" - table.insert(result, indent..key.." = "..value..",") + local value = type(res) == "number" and tostring(res) or "\"" .. tostring(res) .. "\"" + table.insert(result, indent .. key .. " = " .. value .. ",") else - local value = type(v) == "number" and tostring(v) or "\""..tostring(v).."\"" - table.insert(result, indent..key.." = "..value..",") + local value = type(v) == "number" and tostring(v) or "\"" .. tostring(v) .. "\"" + table.insert(result, indent .. key .. " = " .. value .. ",") end end return result @@ -240,16 +230,16 @@ function DesignDebugWindow:init() end self:addviews { widgets.WrappedLabel { - view_id = "debug_label_"..attr, + view_id = "debug_label_" .. attr, text_to_wrap = function() if type(self.design_window[attr]) ~= "table" then - return tostring(attr)..": "..tostring(self.design_window[attr]) + return tostring(attr) .. ": " .. tostring(self.design_window[attr]) end if sizeOnly then - return '#'..tostring(attr)..": "..tostring(#self.design_window[attr]) + return '#' .. tostring(attr) .. ": " .. tostring(#self.design_window[attr]) else - return { tostring(attr)..": ", table.unpack(table_to_string(self.design_window[attr], " ")) } + return { tostring(attr) .. ": ", table.unpack(table_to_string(self.design_window[attr], " ")) } end end, } } @@ -278,14 +268,16 @@ function MarksPanel:update_mark_labels() end if #self.design_panel.marks > 1 then - local last_mark = self.design_panel.marks[#self.design_panel.marks] + local last_index = #self.design_panel.marks - (self.design_panel.placing_mark.active and 1 or 0) + local last_mark = self.design_panel.marks[last_index] if last_mark then table.insert(label_text, - string.format("Last Mark (%d): %d, %d, %d ", #self.design_panel.marks, last_mark.x, last_mark.y, last_mark.z)) + string.format("Last Mark (%d): %d, %d, %d ", last_index, last_mark.x, last_mark.y, + last_mark.z)) end end - local mouse_pos = dfhack.gui.getMousePos() + local mouse_pos = getMousePoint() if mouse_pos then table.insert(label_text, string.format("Mouse: %d, %d, %d", mouse_pos.x, mouse_pos.y, mouse_pos.z)) end @@ -344,20 +336,20 @@ function ActionPanel:get_action_text() else text = "Select any draggable points" end - return text.." with the mouse. Use right-click to dismiss points in order." + return text .. " with the mouse. Use right-click to dismiss points in order." end function ActionPanel:get_area_text() local label = "Area: " local bounds = self.design_panel:get_view_bounds() - if not bounds then return label.."N/A" end + if not bounds then return label .. "N/A" end local width = math.abs(bounds.x2 - bounds.x1) + 1 local height = math.abs(bounds.y2 - bounds.y1) + 1 local depth = math.abs(bounds.z2 - bounds.z1) + 1 local tiles = self.design_panel.shape.num_tiles * depth local plural = tiles > 1 and "s" or "" - return label..("%dx%dx%d (%d tile%s)"):format( + return label .. ("%dx%dx%d (%d tile%s)"):format( width, height, depth, @@ -372,14 +364,10 @@ function ActionPanel:get_mark_text(num) local label = string.format("Mark %d: ", num) if not mark then - return label.."Not set" + return label .. "Not set" end - return label..("%d, %d, %d"):format( - mark.x, - mark.y, - mark.z - ) + return label .. tostring(mark) end -- Generic options not specific to shapes @@ -529,6 +517,8 @@ function GenericOptionsPanel:init() self.design_panel.placing_mirror = false self.design_panel.mirror_point = nil end + self.design_panel.needs_update = true + self.design_panel:updateLayout() end }, widgets.ResizingPanel { @@ -615,10 +605,10 @@ function GenericOptionsPanel:init() label = function() local msg = "Place extra point: " if #self.design_panel.extra_points < #self.design_panel.shape.extra_points then - return msg..self.design_panel.shape.extra_points[#self.design_panel.extra_points + 1].label + return msg .. self.design_panel.shape.extra_points[#self.design_panel.extra_points + 1].label end - return msg.."N/A" + return msg .. "N/A" end, active = true, visible = function() return self.design_panel.shape and #self.design_panel.shape.extra_points > 0 end, @@ -635,8 +625,9 @@ function GenericOptionsPanel:init() self.design_panel.placing_extra.active = true self.design_panel.placing_extra.index = #self.design_panel.extra_points + 1 elseif #self.design_panel.marks then - local mouse_pos = dfhack.gui.getMousePos() - if mouse_pos then table.insert(self.design_panel.extra_points, { x = mouse_pos.x, y = mouse_pos.y }) end + local mouse_pos = getMousePoint() + if mouse_pos then table.insert(self.design_panel.extra_points, + Point { x = mouse_pos.x, y = mouse_pos.y }) end end self.design_panel.needs_update = true end, @@ -662,7 +653,8 @@ function GenericOptionsPanel:init() show_tooltip = true, on_activate = function() self.design_panel.placing_mark.active = not self.design_panel.placing_mark.active - self.design_panel.placing_mark.index = (self.design_panel.placing_mark.active) and #self.design_panel.marks + 1 or + self.design_panel.placing_mark.index = (self.design_panel.placing_mark.active) and + #self.design_panel.marks + 1 or nil if not self.design_panel.placing_mark.active then table.remove(self.design_panel.marks, #self.design_panel.marks) @@ -869,7 +861,7 @@ function GenericOptionsPanel:init() widgets.WrappedLabel { view_id = "shape_prio_label", text_to_wrap = function() - return "Priority: "..tostring(self.design_panel.prio) + return "Priority: " .. tostring(self.design_panel.prio) end, }, widgets.HotkeyLabel { @@ -953,45 +945,6 @@ function GenericOptionsPanel:change_shape(new, old) self.design_panel:updateLayout() end --- --- For tile graphics --- - -local CURSORS = { - INSIDE = { 1, 2 }, - NORTH = { 1, 1 }, - N_NUB = { 3, 2 }, - S_NUB = { 4, 2 }, - W_NUB = { 3, 1 }, - E_NUB = { 5, 1 }, - NE = { 2, 1 }, - NW = { 0, 1 }, - WEST = { 0, 2 }, - EAST = { 2, 2 }, - SW = { 0, 3 }, - SOUTH = { 1, 3 }, - SE = { 2, 3 }, - VERT_NS = { 3, 3 }, - VERT_EW = { 4, 1 }, - POINT = { 4, 3 }, -} - --- Bit positions to use for keys in PENS table -local PEN_MASK = { - NORTH = 1, - SOUTH = 2, - EAST = 3, - WEST = 4, - DRAG_POINT = 5, - MOUSEOVER = 6, - INSHAPE = 7, - EXTRA_POINT = 8, -} - --- Populated dynamically as needed --- The pens will be stored with keys corresponding to the directions passed to gen_pen_key() -local PENS = {} - -- -- Design -- @@ -1025,22 +978,20 @@ Design.ATTRS { placing_mirror = false, mirror_point = DEFAULT_NIL, mirror = { horizontal = false, vertical = false }, - show_guides = true + show_guides = true, } -- Check to see if we're moving a point, or some change was made that implise we need to update the shape -- This stop us needing to update the shape geometery every frame which can tank FPS function Design:shape_needs_update() - -- if #self.marks < self.shape.min_points then return false end if self.needs_update then return true end - local mouse_pos = dfhack.gui.getMousePos() + local mouse_pos = getMousePoint() if mouse_pos then local mouse_moved = not self.last_mouse_point and mouse_pos or ( - self.last_mouse_point.x ~= mouse_pos.x or self.last_mouse_point.y ~= mouse_pos.y or - self.last_mouse_point.z ~= mouse_pos.z) + self.last_mouse_point ~= mouse_pos) if self.placing_mark.active and mouse_moved then return true @@ -1054,106 +1005,6 @@ function Design:shape_needs_update() return false end --- Get the pen to use when drawing a type of tile based on it's position in the shape and --- neighboring tiles. The first time a certain tile type needs to be drawn, it's pen --- is generated and stored in PENS. On subsequent calls, the cached pen will be used for --- other tiles with the same position/direction -function Design:get_pen(x, y, mousePos) - - local get_point = self.shape:get_point(x, y) - local mouse_over = (mousePos) and (x == mousePos.x and y == mousePos.y) or false - - local drag_point = false - - -- Basic shapes are bounded by rectangles and therefore can have corner drag points - -- even if they're not real points in the shape - if #self.marks >= self.shape.min_points and self.shape.basic_shape then - local shape_top_left, shape_bot_right = self.shape:get_point_dims() - if x == shape_top_left.x and y == shape_top_left.y and self.shape.drag_corners.nw then - drag_point = true - elseif x == shape_bot_right.x and y == shape_top_left.y and self.shape.drag_corners.ne then - drag_point = true - elseif x == shape_top_left.x and y == shape_bot_right.y and self.shape.drag_corners.sw then - drag_point = true - elseif x == shape_bot_right.x and y == shape_bot_right.y and self.shape.drag_corners.se then - drag_point = true - end - end - - for i, mark in ipairs(self.marks) do - if same_xy(mark, xy2pos(x, y)) then - drag_point = true - end - end - - if self.mirror_point and same_xy(self.mirror_point, xy2pos(x, y)) then - drag_point = true - end - - -- Is there an extra point - local extra_point = false - for i, point in ipairs(self.extra_points) do - if x == point.x and y == point.y then - extra_point = true - break - end - end - - -- Show center point if both marks are set - if (self.shape.basic_shape and #self.marks == self.shape.max_points) or - (not self.shape.basic_shape and not self.placing_mark.active and #self.marks > 0) then - local center_x, center_y = self.shape:get_center() - - if x == center_x and y == center_y then - extra_point = true - end - end - - - local n, w, e, s = false, false, false, false - if self.shape:get_point(x, y) then - if y == 0 or not self.shape:get_point(x, y - 1) then n = true end - if x == 0 or not self.shape:get_point(x - 1, y) then w = true end - if not self.shape:get_point(x + 1, y) then e = true end - if not self.shape:get_point(x, y + 1) then s = true end - end - - -- Get the bit field to use as a key for the PENS map - local pen_key = self:gen_pen_key(n, s, e, w, drag_point, mouse_over, get_point, extra_point) - - - -- Determine the cursor to use based on the input parameters - local cursor = nil - if pen_key and not PENS[pen_key] then - if get_point and not n and not w and not e and not s then cursor = CURSORS.INSIDE - elseif get_point and n and w and not e and not s then cursor = CURSORS.NW - elseif get_point and n and not w and not e and not s then cursor = CURSORS.NORTH - elseif get_point and n and e and not w and not s then cursor = CURSORS.NE - elseif get_point and not n and w and not e and not s then cursor = CURSORS.WEST - elseif get_point and not n and not w and e and not s then cursor = CURSORS.EAST - elseif get_point and not n and w and not e and s then cursor = CURSORS.SW - elseif get_point and not n and not w and not e and s then cursor = CURSORS.SOUTH - elseif get_point and not n and not w and e and s then cursor = CURSORS.SE - elseif get_point and n and w and e and not s then cursor = CURSORS.N_NUB - elseif get_point and n and not w and e and s then cursor = CURSORS.E_NUB - elseif get_point and n and w and not e and s then cursor = CURSORS.W_NUB - elseif get_point and not n and w and e and s then cursor = CURSORS.S_NUB - elseif get_point and not n and w and e and not s then cursor = CURSORS.VERT_NS - elseif get_point and n and not w and not e and s then cursor = CURSORS.VERT_EW - elseif get_point and n and w and e and s then cursor = CURSORS.POINT - elseif drag_point and not get_point then cursor = CURSORS.INSIDE - elseif extra_point then cursor = CURSORS.INSIDE - else cursor = nil - end - end - - -- Create the pen if the cursor is set - if cursor then PENS[pen_key] = self:make_pen(cursor, drag_point, mouse_over, get_point, extra_point) end - - -- Return the pen for the caller - return PENS[pen_key] -end - function Design:init() self:addviews { ActionPanel { @@ -1206,7 +1057,7 @@ function Design:add_shape_options() if option.type == "bool" then self:addviews { widgets.ToggleHotkeyLabel { - view_id = "shape_option_"..option.name, + view_id = "shape_option_" .. option.name, key = option.key, label = option.name, active = true, @@ -1242,9 +1093,9 @@ function Design:add_shape_options() self:addviews { widgets.HotkeyLabel { - view_id = "shape_option_"..option.name.."_minus", + view_id = "shape_option_" .. option.name .. "_minus", key = option.keys[1], - label = "Decrease "..option.name, + label = "Decrease " .. option.name, active = true, enabled = function() if option.enabled then @@ -1264,9 +1115,9 @@ function Design:add_shape_options() end, }, widgets.HotkeyLabel { - view_id = "shape_option_"..option.name.."_plus", + view_id = "shape_option_" .. option.name .. "_plus", key = option.keys[2], - label = "Increase "..option.name, + label = "Increase " .. option.name, active = true, enabled = function() if option.enabled then @@ -1312,7 +1163,7 @@ function Design:on_transform(val) elseif val == 'flipv' then y = center_y - (y - center_y) end - self.marks[i] = { x = math.floor(x + 0.5), y = math.floor(y + 0.5), z = self.marks[i].z } + self.marks[i] = Point { x = math.floor(x + 0.5), y = math.floor(y + 0.5), z = self.marks[i].z } end -- Transform extra points @@ -1327,26 +1178,25 @@ function Design:on_transform(val) elseif val == 'flipv' then y = center_y - (y - center_y) end - self.extra_points[i] = { x = math.floor(x + 0.5), y = math.floor(y + 0.5), z = self.extra_points[i].z } + self.extra_points[i] = Point { x = math.floor(x + 0.5), y = math.floor(y + 0.5), z = self.extra_points[i].z } end -- Calculate center point after transformation self.shape:update(self.marks, self.extra_points) local new_center_x, new_center_y = self.shape:get_center() + local center = Point{x = center_x, y = center_y} + local new_center = Point{x = new_center_x, y = new_center_y} -- Calculate delta between old and new center points - local delta_x = center_x - new_center_x - local delta_y = center_y - new_center_y + local delta = center - new_center -- Adjust marks and extra points based on delta for i, mark in ipairs(self.marks) do - self.marks[i].x = self.marks[i].x + delta_x - self.marks[i].y = self.marks[i].y + delta_y + self.marks[i] = mark + Point{x = delta.x, y = delta.y, z = 0} end for i, point in ipairs(self.extra_points) do - self.extra_points[i].x = self.extra_points[i].x + delta_x - self.extra_points[i].y = self.extra_points[i].y + delta_y + self.extra_points[i] = point + Point{x = delta.x, y = delta.y, z = 0} end self:updateLayout() @@ -1364,7 +1214,7 @@ function Design:get_view_bounds() local max_z = self.marks[1].z local marks_plus_next = copyall(self.marks) - local mouse_pos = dfhack.gui.getMousePos() + local mouse_pos = getMousePoint() if mouse_pos then table.insert(marks_plus_next, mouse_pos) end @@ -1381,59 +1231,8 @@ function Design:get_view_bounds() return { x1 = min_x, y1 = min_y, z1 = min_z, x2 = max_x, y2 = max_y, z2 = max_z } end --- return the pen, alter based on if we want to display a corner and a mouse over corner -function Design:make_pen(direction, is_corner, is_mouse_over, inshape, extra_point) - - local color = COLOR_GREEN - local ycursor_mod = 0 - if not extra_point then - if is_corner then - color = COLOR_CYAN - ycursor_mod = ycursor_mod + 6 - if is_mouse_over then - color = COLOR_MAGENTA - ycursor_mod = ycursor_mod + 3 - end - end - elseif extra_point then - ycursor_mod = ycursor_mod + 15 - color = COLOR_LIGHTRED - - if is_mouse_over then - color = COLOR_RED - ycursor_mod = ycursor_mod + 3 - end - - end - return to_pen { - ch = inshape and "X" or "o", - fg = color, - tile = dfhack.screen.findGraphicsTile( - "CURSORS", - direction[1], - direction[2] + ycursor_mod - ), - } -end - --- Generate a bit field to store as keys in PENS -function Design:gen_pen_key(n, s, e, w, is_corner, is_mouse_over, inshape, extra_point) - local ret = 0 - if n then ret = ret + (1 << PEN_MASK.NORTH) end - if s then ret = ret + (1 << PEN_MASK.SOUTH) end - if e then ret = ret + (1 << PEN_MASK.EAST) end - if w then ret = ret + (1 << PEN_MASK.WEST) end - if is_corner then ret = ret + (1 << PEN_MASK.DRAG_POINT) end - if is_mouse_over then ret = ret + (1 << PEN_MASK.MOUSEOVER) end - if inshape then ret = ret + (1 << PEN_MASK.INSHAPE) end - if extra_point then ret = ret + (1 << PEN_MASK.EXTRA_POINT) end - - return ret -end - -- TODO Function is too long function Design:onRenderFrame(dc, rect) - if (SHOW_DEBUG_WINDOW) then self.parent_view.debug_window:updateLayout() end @@ -1444,15 +1243,11 @@ function Design:onRenderFrame(dc, rect) self.shape = shapes.all_shapes[self.subviews.shape_name:getOptionValue()] end - local mouse_pos = dfhack.gui.getMousePos() + local mouse_pos = getMousePoint() self.subviews.marks_panel:update_mark_labels() - local function get_overlay_pen(pos) - return self:get_pen(pos.x, pos.y, mouse_pos) - end - - if self.placing_mark.active and self.placing_mark.index then + if self.placing_mark.active and self.placing_mark.index and mouse_pos then self.marks[self.placing_mark.index] = mouse_pos end @@ -1461,11 +1256,11 @@ function Design:onRenderFrame(dc, rect) -- Set the pos of the currently moving extra point if self.placing_extra.active then - self.extra_points[self.placing_extra.index] = { x = mouse_pos.x, y = mouse_pos.y } + self.extra_points[self.placing_extra.index] = mouse_pos end if self.placing_mirror and mouse_pos then - if not self.mirror_point or (mouse_pos.x ~= self.mirror_point.x or mouse_pos.y ~= self.mirror_point.y) then + if not self.mirror_point or (mouse_pos ~= self.mirror_point) then self.needs_update = true end self.mirror_point = mouse_pos @@ -1474,33 +1269,25 @@ function Design:onRenderFrame(dc, rect) -- Check if moving center, if so shift the shape by the delta between the previous and current points -- TODO clean this up if self.prev_center and - ( - (self.shape.basic_shape and #self.marks == self.shape.max_points) - or (not self.shape.basic_shape and not self.placing_mark.active) - ) - and mouse_pos and ( - (self.prev_center.x ~= mouse_pos.x) - or (self.prev_center.y ~= mouse_pos.y) - or (self.prev_center.z ~= mouse_pos.z) - ) then + ((self.shape.basic_shape and #self.marks == self.shape.max_points) + or (not self.shape.basic_shape and not self.placing_mark.active)) + and mouse_pos and ( self.prev_center ~= mouse_pos) then self.needs_update = true - local transform = { x = mouse_pos.x - self.prev_center.x, y = mouse_pos.y - self.prev_center.y, - z = mouse_pos.z - self.prev_center.z } + local transform = mouse_pos - self.prev_center - for i, _ in ipairs(self.marks) do - self.marks[i].x = self.marks[i].x + transform.x - self.marks[i].y = self.marks[i].y + transform.y - self.marks[i].z = self.marks[i].z + transform.z + transform.z = transform.z or mouse_pos.z + + for i, mark in ipairs(self.marks) do + mark.z = mark.z or transform.z + self.marks[i] = mark + transform end for i, point in ipairs(self.extra_points) do - self.extra_points[i].x = self.extra_points[i].x + transform.x - self.extra_points[i].y = self.extra_points[i].y + transform.y + self.extra_points[i] = point + transform end if self.mirror_point then - self.mirror_point.x = self.mirror_point.x + transform.x - self.mirror_point.y = self.mirror_point.y + transform.y + self.mirror_point = self.mirror_point + transform end self.prev_center = mouse_pos @@ -1516,6 +1303,7 @@ function Design:onRenderFrame(dc, rect) self.needs_update = false self:add_shape_options() self:updateLayout() + plugin.clear_shape(self.shape.arr) end -- Generate bounds based on the shape's dimensions @@ -1565,7 +1353,30 @@ function Design:onRenderFrame(dc, rect) end end - guidm.renderMapOverlay(get_overlay_pen, bounds) + plugin.draw_shape(self.shape.arr) + + if #self.marks >= self.shape.min_points and self.shape.basic_shape then + local shape_top_left, shape_bot_right = self.shape:get_point_dims() + local drag_points = { + { x = shape_top_left.x, y = shape_top_left.y }, + { x = shape_bot_right.x, y = shape_bot_right.y }, + { x = shape_top_left.x, y = shape_bot_right.y }, + { x = shape_bot_right.x, y = shape_top_left.y } + } + plugin.draw_points({ drag_points, "drag_point" }) + else + plugin.draw_points({ self.marks, "drag_point" }) + end + + plugin.draw_points({ self.extra_points, "extra_point" }) + + if (self.shape.basic_shape and #self.marks == self.shape.max_points) or + (not self.shape.basic_shape and not self.placing_mark.active and #self.marks > 0) then + local center_x, center_y = self.shape:get_center() + plugin.draw_points({ {{x = center_x, y = center_y}}, "extra_point" }) + end + plugin.draw_points({ {self.mirror_point}, "extra_point" }) + end -- TODO function too long @@ -1576,8 +1387,8 @@ function Design:onInput(keys) -- Secret shortcut to kill the panel if it becomes -- unresponsive during development, should not release - -- if keys.CUSTOM_M then - -- self.parent_view:dismiss() + -- if keys.CUSTOM_SHIFT_Q then + -- plugin.getPen(self.shape.arr) -- return -- end @@ -1587,19 +1398,14 @@ function Design:onInput(keys) -- If center draggin, put the shape back to the original center if self.prev_center then - local transform = { x = self.start_center.x - self.prev_center.x, - y = self.start_center.y - self.prev_center.y, - z = self.start_center.z - self.prev_center.z } - - for i, _ in ipairs(self.marks) do - self.marks[i].x = self.marks[i].x + transform.x - self.marks[i].y = self.marks[i].y + transform.y - self.marks[i].z = self.marks[i].z + transform.z + local transform = self.start_center - self.prev_center + + for i, mark in ipairs(self.marks) do + self.marks[i] = mark + transform end for i, point in ipairs(self.extra_points) do - self.extra_points[i].x = self.extra_points[i].x + transform.x - self.extra_points[i].y = self.extra_points[i].y + transform.y + self.extra_points[i] = point + transform end self.prev_center = nil @@ -1639,13 +1445,13 @@ function Design:onInput(keys) local pos = nil if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then - pos = dfhack.gui.getMousePos() - if pos then - guidm.setCursorPos(pos) - end + pos = getMousePoint() + if not pos then return true end + guidm.setCursorPos(dfhack.gui.getMousePos()) elseif keys.SELECT then pos = guidm.getCursorPos() end + pos = Point(pos) if keys._MOUSE_L_DOWN and pos then -- TODO Refactor this a bit @@ -1655,7 +1461,7 @@ function Design:onInput(keys) self.placing_mark.active = false -- The statement after the or is to allow the 1x1 special case for easy doorways self.needs_update = true - if self.autocommit or (same_xy(self.marks[1], self.marks[2])) then + if self.autocommit or (self.marks[1] == self.marks[2]) then self:commit() end elseif not self.placing_extra.active and self.placing_mark.active then @@ -1671,7 +1477,7 @@ function Design:onInput(keys) self.needs_update = true self.placing_extra.active = false elseif self.placing_mirror then - self.mirror_point = pos + self.mirror_point = Point(pos) self.placing_mirror = false self.needs_update = true else @@ -1679,17 +1485,19 @@ function Design:onInput(keys) -- Clicking a corner of a basic shape local shape_top_left, shape_bot_right = self.shape:get_point_dims() local corner_drag_info = { - { pos = shape_top_left, opposite_x = shape_bot_right.x, opposite_y = shape_bot_right.y, corner = "nw" }, - { pos = xy2pos(shape_bot_right.x, shape_top_left.y), opposite_x = shape_top_left.x, + { pos = Point(shape_top_left), opposite_x = shape_bot_right.x, opposite_y = shape_bot_right.y, + corner = "nw" }, + { pos = Point { x = shape_bot_right.x, y = shape_top_left.y }, opposite_x = shape_top_left.x, opposite_y = shape_bot_right.y, corner = "ne" }, - { pos = xy2pos(shape_top_left.x, shape_bot_right.y), opposite_x = shape_bot_right.x, + { pos = Point { x = shape_top_left.x, y = shape_bot_right.y }, opposite_x = shape_bot_right.x, opposite_y = shape_top_left.y, corner = "sw" }, - { pos = shape_bot_right, opposite_x = shape_top_left.x, opposite_y = shape_top_left.y, corner = "se" } + { pos = Point(shape_bot_right), opposite_x = shape_top_left.x, opposite_y = shape_top_left.y, + corner = "se" } } for _, info in ipairs(corner_drag_info) do - if same_xy(pos, info.pos) and self.shape.drag_corners[info.corner] then - self.marks[1] = xyz2pos(info.opposite_x, info.opposite_y, self.marks[1].z) + if pos == info.pos and self.shape.drag_corners[info.corner] then + self.marks[1] = Point { x = info.opposite_x, y = info.opposite_y, z = self.marks[1].z } table.remove(self.marks, 2) self.placing_mark = { active = true, index = 2 } break @@ -1697,7 +1505,7 @@ function Design:onInput(keys) end else for i, point in ipairs(self.marks) do - if same_xy(pos, point) then + if pos == point then self.placing_mark = { active = true, index = i, continue = false } end end @@ -1705,7 +1513,7 @@ function Design:onInput(keys) -- Clicking an extra point for i = 1, #self.extra_points do - if same_xy(pos, self.extra_points[i]) then + if pos == self.extra_points[i] then self.placing_extra = { active = true, index = i } self.needs_update = true return true @@ -1715,7 +1523,7 @@ function Design:onInput(keys) -- Clicking center point if #self.marks > 0 then local center_x, center_y = self.shape:get_center() - if same_xy(pos, xy2pos(center_x, center_y)) and not self.prev_center then + if pos == Point { x = center_x, y = center_y } and not self.prev_center then self.start_center = pos self.prev_center = pos return true @@ -1726,7 +1534,7 @@ function Design:onInput(keys) end end - if same_xy(self.mirror_point, pos) then + if self.mirror_point == pos then self.placing_mirror = true end end @@ -1756,7 +1564,7 @@ function Design:get_designation(x, y, z) if z == 0 then return stairs_bottom_type == "auto" and "u" or stairs_bottom_type elseif view_bounds and z == math.abs(view_bounds.z1 - view_bounds.z2) then - local pos = xyz2pos(view_bounds.x1 + x, view_bounds.y1 + y, view_bounds.z1 + z) + local pos = Point { x = view_bounds.x1 + x, y = view_bounds.y1 + y, z = view_bounds.z1 + z } local tile_type = dfhack.maps.getTileType(pos) local tile_shape = tile_type and tile_attrs[tile_type].shape or nil local designation = dfhack.maps.getTileFlags(pos) @@ -1816,7 +1624,7 @@ function Design:commit() local desig = self:get_designation(col, row, zlevel) if desig ~= "`" then data[zlevel][row][col] = - desig..(mode ~= "build" and tostring(self.prio) or "") + desig .. (mode ~= "build" and tostring(self.prio) or "") end end end @@ -1839,7 +1647,7 @@ function Design:commit() local grid = self.shape:transform(0, 0) -- Special case for 1x1 to ease doorway marking - if same_xy(top_left, bot_right) then + if top_left == bot_right then grid = {} grid[0] = {} grid[0][0] = true @@ -1884,7 +1692,7 @@ function Design:get_mirrored_points(points) end end - table.insert(mirrored_points, { z = point.z, x = point.x, y = mirrored_y }) + table.insert(mirrored_points, Point { z = point.z, x = point.x, y = mirrored_y }) end end @@ -1904,7 +1712,7 @@ function Design:get_mirrored_points(points) end end - table.insert(mirrored_points, { z = point.z, x = mirrored_x, y = mirrored_y }) + table.insert(mirrored_points, Point { z = point.z, x = mirrored_x, y = mirrored_y }) end end @@ -1922,12 +1730,12 @@ function Design:get_mirrored_points(points) end end - table.insert(mirrored_points, { z = point.z, x = mirrored_x, y = point.y }) + table.insert(mirrored_points, Point { z = point.z, x = mirrored_x, y = point.y }) end end for i, point in ipairs(mirrored_points) do - table.insert(points, mirrored_points[i]) + table.insert(points, Point(mirrored_points[i])) end return points diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index a03c5e8697..24597887a9 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -516,6 +516,7 @@ function Line:update(points, extra_points) self.num_tiles = 0 self.points = copyall(points) local top_left, bot_right = self:get_point_dims() + if not top_left or not bot_right then return end self.arr = {} self.height = bot_right.x - top_left.x self.width = bot_right.y - top_left.y diff --git a/internal/design/util.lua b/internal/design/util.lua new file mode 100644 index 0000000000..ae3886dce8 --- /dev/null +++ b/internal/design/util.lua @@ -0,0 +1,86 @@ +-- Utilities for design.lua +--@ module = true + +-- Point class used by gui/design +Point = defclass(Point) +Point.ATTRS { + __is_point = true, + x = DEFAULT_NIL, + y = DEFAULT_NIL, + z = DEFAULT_NIL +} + +function Point:init(init_table) + self.x = init_table.x + self.y = init_table.y + if init_table.z then self.z = init_table.z end +end + +function Point:check_valid(point) + point = point or self + if not point.x or not point.y then error("Invalid Point: x and y values are required") end + if not type(point.x) == "number" then error("Invalid value for x, must be a number") end + if not type(point.y) == "number" then error("Invalid value for y, must be a number") end + if point.z and not type(point.y) == "number" then error("Invalid value for z, must be a number") end +end + +function Point:is_mouse_over() + local pos = dfhack.gui.getMousePos() + if not pos then return false end + + return Point(pos) == self +end + +function Point:__tostring() + return "("..tostring(self.x)..", "..tostring(self.y)..", "..tostring(self.z)..")" +end + +function Point:get_add_sub_table(arg) + local t_other = { x = 0, y = 0, z = 0 } + + if type(arg) == "number" then + t_other = Point { x = arg, y = arg } -- As of now we don't want to add anything to z unless explicit + elseif type(arg) == "table" then + if not (arg.x ~= nil or arg.y ~= nil or arg.z ~= nil) then + error("Adding table that doesn't have x, y or z values.") + end + if arg.x then + if type(arg.x) == "number" then + t_other.x = arg.x + end + end + if arg.y then + if type(arg.y) == "number" then + t_other.y = arg.y + end + end + if arg.z then + if type(arg.z) == "number" then + t_other.z = arg.z + end + end + end + + return t_other +end + +function Point:__add(arg) + local t_other = self:get_add_sub_table(arg) + return Point { x = self.x + t_other.x, y = self.y + t_other.y, z = (self.z and t_other.z) and self.z + t_other.z or nil } +end + +function Point:__sub(arg) + local t_other = self:get_add_sub_table(arg) + return Point { x = self.x - t_other.x, y = self.y - t_other.y, z = (self.z and t_other.z) and self.z - t_other.z or nil} +end + +-- For the purposes of gui/design, we only care about x and y being equal, z is only used for determining boundaries and x levels to apply the shape +function Point:__eq(other) + self:check_valid(other) + return self.x == other.x and self.y == other.y +end + +function getMousePoint() + local pos = dfhack.gui.getMousePos() + return pos and Point{x = pos.x, y = pos.y, z = pos.z} or nil +end From 7e0c98f95a1d1c74b1d0c1abbc607f824846d140 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Fri, 28 Apr 2023 11:09:54 -0400 Subject: [PATCH 161/732] Cleanup --- gui/design.lua | 69 ++++++++++++++++++-------------------- internal/design/shapes.lua | 8 +++-- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 298ae55d1b..b390836677 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -627,7 +627,8 @@ function GenericOptionsPanel:init() elseif #self.design_panel.marks then local mouse_pos = getMousePoint() if mouse_pos then table.insert(self.design_panel.extra_points, - Point { x = mouse_pos.x, y = mouse_pos.y }) end + mouse_pos) + end end self.design_panel.needs_update = true end, @@ -1142,7 +1143,7 @@ function Design:add_shape_options() end function Design:on_transform(val) - local center_x, center_y = self.shape:get_center() + local center = self.shape:get_center() -- Save mirrored points first if self.mirror_point then @@ -1155,13 +1156,13 @@ function Design:on_transform(val) for i, mark in ipairs(self.marks) do local x, y = mark.x, mark.y if val == 'cw' then - x, y = center_x - (y - center_y), center_y + (x - center_x) + x, y = center.x - (y - center.y), center.y + (x - center.x) elseif val == 'ccw' then - x, y = center_x + (y - center_y), center_y - (x - center_x) + x, y = center.x + (y - center.y), center.y - (x - center.x) elseif val == 'fliph' then - x = center_x - (x - center_x) + x = center.x - (x - center.x) elseif val == 'flipv' then - y = center_y - (y - center_y) + y = center.y - (y - center.y) end self.marks[i] = Point { x = math.floor(x + 0.5), y = math.floor(y + 0.5), z = self.marks[i].z } end @@ -1170,33 +1171,31 @@ function Design:on_transform(val) for i, point in ipairs(self.extra_points) do local x, y = point.x, point.y if val == 'cw' then - x, y = center_x - (y - center_y), center_y + (x - center_x) + x, y = center.x - (y - center.y), center.y + (x - center.x) elseif val == 'ccw' then - x, y = center_x + (y - center_y), center_y - (x - center_x) + x, y = center.x + (y - center.y), center.y - (x - center.x) elseif val == 'fliph' then - x = center_x - (x - center_x) + x = center.x - (x - center.x) elseif val == 'flipv' then - y = center_y - (y - center_y) + y = center.y - (y - center.y) end self.extra_points[i] = Point { x = math.floor(x + 0.5), y = math.floor(y + 0.5), z = self.extra_points[i].z } end -- Calculate center point after transformation self.shape:update(self.marks, self.extra_points) - local new_center_x, new_center_y = self.shape:get_center() - local center = Point{x = center_x, y = center_y} - local new_center = Point{x = new_center_x, y = new_center_y} + local new_center = self.shape:get_center() -- Calculate delta between old and new center points local delta = center - new_center -- Adjust marks and extra points based on delta for i, mark in ipairs(self.marks) do - self.marks[i] = mark + Point{x = delta.x, y = delta.y, z = 0} + self.marks[i] = mark + Point { x = delta.x, y = delta.y, z = 0 } end for i, point in ipairs(self.extra_points) do - self.extra_points[i] = point + Point{x = delta.x, y = delta.y, z = 0} + self.extra_points[i] = point + Point { x = delta.x, y = delta.y, z = 0 } end self:updateLayout() @@ -1270,8 +1269,8 @@ function Design:onRenderFrame(dc, rect) -- TODO clean this up if self.prev_center and ((self.shape.basic_shape and #self.marks == self.shape.max_points) - or (not self.shape.basic_shape and not self.placing_mark.active)) - and mouse_pos and ( self.prev_center ~= mouse_pos) then + or (not self.shape.basic_shape and not self.placing_mark.active)) + and mouse_pos and (self.prev_center ~= mouse_pos) then self.needs_update = true local transform = mouse_pos - self.prev_center @@ -1354,14 +1353,14 @@ function Design:onRenderFrame(dc, rect) end plugin.draw_shape(self.shape.arr) - + if #self.marks >= self.shape.min_points and self.shape.basic_shape then local shape_top_left, shape_bot_right = self.shape:get_point_dims() local drag_points = { - { x = shape_top_left.x, y = shape_top_left.y }, - { x = shape_bot_right.x, y = shape_bot_right.y }, - { x = shape_top_left.x, y = shape_bot_right.y }, - { x = shape_bot_right.x, y = shape_top_left.y } + Point { x = shape_top_left.x, y = shape_top_left.y }, + Point { x = shape_bot_right.x, y = shape_bot_right.y }, + Point { x = shape_top_left.x, y = shape_bot_right.y }, + Point { x = shape_bot_right.x, y = shape_top_left.y } } plugin.draw_points({ drag_points, "drag_point" }) else @@ -1372,10 +1371,9 @@ function Design:onRenderFrame(dc, rect) if (self.shape.basic_shape and #self.marks == self.shape.max_points) or (not self.shape.basic_shape and not self.placing_mark.active and #self.marks > 0) then - local center_x, center_y = self.shape:get_center() - plugin.draw_points({ {{x = center_x, y = center_y}}, "extra_point" }) + plugin.draw_points({ { self.shape:get_center() }, "extra_point" }) end - plugin.draw_points({ {self.mirror_point}, "extra_point" }) + plugin.draw_points({ { self.mirror_point }, "extra_point" }) end @@ -1449,9 +1447,8 @@ function Design:onInput(keys) if not pos then return true end guidm.setCursorPos(dfhack.gui.getMousePos()) elseif keys.SELECT then - pos = guidm.getCursorPos() + pos = Point(guidm.getCursorPos()) end - pos = Point(pos) if keys._MOUSE_L_DOWN and pos then -- TODO Refactor this a bit @@ -1477,7 +1474,7 @@ function Design:onInput(keys) self.needs_update = true self.placing_extra.active = false elseif self.placing_mirror then - self.mirror_point = Point(pos) + self.mirror_point = pos self.placing_mirror = false self.needs_update = true else @@ -1485,13 +1482,13 @@ function Design:onInput(keys) -- Clicking a corner of a basic shape local shape_top_left, shape_bot_right = self.shape:get_point_dims() local corner_drag_info = { - { pos = Point(shape_top_left), opposite_x = shape_bot_right.x, opposite_y = shape_bot_right.y, + { pos = shape_top_left, opposite_x = shape_bot_right.x, opposite_y = shape_bot_right.y, corner = "nw" }, { pos = Point { x = shape_bot_right.x, y = shape_top_left.y }, opposite_x = shape_top_left.x, opposite_y = shape_bot_right.y, corner = "ne" }, { pos = Point { x = shape_top_left.x, y = shape_bot_right.y }, opposite_x = shape_bot_right.x, opposite_y = shape_top_left.y, corner = "sw" }, - { pos = Point(shape_bot_right), opposite_x = shape_top_left.x, opposite_y = shape_top_left.y, + { pos = shape_bot_right, opposite_x = shape_top_left.x, opposite_y = shape_top_left.y, corner = "se" } } @@ -1522,8 +1519,8 @@ function Design:onInput(keys) -- Clicking center point if #self.marks > 0 then - local center_x, center_y = self.shape:get_center() - if pos == Point { x = center_x, y = center_y } and not self.prev_center then + local center = self.shape:get_center() + if pos == center and not self.prev_center then self.start_center = pos self.prev_center = pos return true @@ -1550,7 +1547,7 @@ end -- Put any special logic for designation type here -- Right now it's setting the stair type based on the z-level -- Fell through, pass through the option directly from the options value -function Design:get_designation(x, y, z) +function Design:get_designation(point) local mode = self.subviews.mode_name:getOptionValue() local view_bounds = self:get_view_bounds() @@ -1564,7 +1561,7 @@ function Design:get_designation(x, y, z) if z == 0 then return stairs_bottom_type == "auto" and "u" or stairs_bottom_type elseif view_bounds and z == math.abs(view_bounds.z1 - view_bounds.z2) then - local pos = Point { x = view_bounds.x1 + x, y = view_bounds.y1 + y, z = view_bounds.z1 + z } + local pos = Point { x = view_bounds.x1, y = view_bounds.y1, z = view_bounds.z1} + point local tile_type = dfhack.maps.getTileType(pos) local tile_shape = tile_type and tile_attrs[tile_type].shape or nil local designation = dfhack.maps.getTileFlags(pos) @@ -1590,7 +1587,7 @@ function Design:get_designation(x, y, z) -- If not completed surrounded, then use outer tile for i, d in ipairs(darr) do - if not (self.shape:get_point(top_left.x + x + d[1], top_left.y + y + d[2])) then + if not (self.shape:get_point(top_left.x + point.x + d[1], top_left.y + point.y + d[2])) then return building_outer_tiles end end @@ -1621,7 +1618,7 @@ function Design:commit() data[zlevel][row] = {} for col = 0, math.abs(bot_right.x - top_left.x) do if grid[col] and grid[col][row] then - local desig = self:get_designation(col, row, zlevel) + local desig = self:get_designation(Point{col, row, zlevel}) if desig ~= "`" then data[zlevel][row][col] = desig .. (mode ~= "build" and tostring(self.prio) or "") diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 24597887a9..d092048c94 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -1,6 +1,8 @@ -- shape definitions for gui/dig --@ module = true +local Point = reqscript("internal/design/util").Point + if not dfhack_flags.module then qerror("this script cannot be called directly") end @@ -60,7 +62,7 @@ function Shape:get_point_dims() max_y = math.max(max_y, point.y) end - return { x = min_x, y = min_y }, { x = max_x, y = max_y } + return Point{ x = min_x, y = min_y }, Point{ x = max_x, y = max_y } end -- Get dimensions as defined by the array of the shape @@ -161,8 +163,8 @@ function Shape:get_center() -- Simple way to get the center defined by the point dims if #self.points == 0 then return nil, nil end local top_left, bot_right = self:get_point_dims() - return math.floor((bot_right.x - top_left.x) / 2) + top_left.x, - math.floor((bot_right.y - top_left.y) / 2) + top_left.y + return Point{x = math.floor((bot_right.x - top_left.x) / 2) + top_left.x, + y = math.floor((bot_right.y - top_left.y) / 2) + top_left.y} end From 70788484b4cf797aa71e5c6737cb77838407a677 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 29 Apr 2023 18:58:08 -0700 Subject: [PATCH 162/732] bump to 50.08-r1 --- changelog.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index f47eb24a25..af023b1bc2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,14 @@ that repo. ## New Scripts +## Fixes + +## Misc Improvements + +## Removed + +# 50.08-r1 + ## Fixes - `deteriorate`: ensure remains of enemy dwarves are properly deteriorated - `suspendmanager`: Fix over-aggressive suspension of jobs that could still possibly be done (e.g. jobs that are partially submerged in water) @@ -28,8 +36,6 @@ that repo. - `gui/launcher`: DFHack version now shown in the default help text - `gui/prerelease-warning`: widgets are now clickable -## Removed - # 50.07-r1 ## New Scripts From 81d22ef1bad47e0b4ee2e25bffaaff4ab3cb90c8 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Mon, 1 May 2023 14:20:32 -0400 Subject: [PATCH 163/732] Rename calls --- gui/design.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index b390836677..02b9453653 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1302,7 +1302,7 @@ function Design:onRenderFrame(dc, rect) self.needs_update = false self:add_shape_options() self:updateLayout() - plugin.clear_shape(self.shape.arr) + plugin.design_clear_shape(self.shape.arr) end -- Generate bounds based on the shape's dimensions @@ -1352,7 +1352,7 @@ function Design:onRenderFrame(dc, rect) end end - plugin.draw_shape(self.shape.arr) + plugin.design_draw_shape(self.shape.arr) if #self.marks >= self.shape.min_points and self.shape.basic_shape then local shape_top_left, shape_bot_right = self.shape:get_point_dims() @@ -1362,18 +1362,18 @@ function Design:onRenderFrame(dc, rect) Point { x = shape_top_left.x, y = shape_bot_right.y }, Point { x = shape_bot_right.x, y = shape_top_left.y } } - plugin.draw_points({ drag_points, "drag_point" }) + plugin.design_draw_points({ drag_points, "drag_point" }) else - plugin.draw_points({ self.marks, "drag_point" }) + plugin.design_draw_points({ self.marks, "drag_point" }) end - plugin.draw_points({ self.extra_points, "extra_point" }) + plugin.design_draw_points({ self.extra_points, "extra_point" }) if (self.shape.basic_shape and #self.marks == self.shape.max_points) or (not self.shape.basic_shape and not self.placing_mark.active and #self.marks > 0) then - plugin.draw_points({ { self.shape:get_center() }, "extra_point" }) + plugin.design_draw_points({ { self.shape:get_center() }, "extra_point" }) end - plugin.draw_points({ { self.mirror_point }, "extra_point" }) + plugin.design_draw_points({ { self.mirror_point }, "extra_point" }) end From 5c47f1ebdd4d531a8538df34143379cfddc99fe3 Mon Sep 17 00:00:00 2001 From: Atte Haarni Date: Tue, 2 May 2023 20:02:55 +0300 Subject: [PATCH 164/732] Add necronomicon script to find books of life and death --- docs/necronomicon.rst | 15 ++++++ necronomicon.lua | 110 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 docs/necronomicon.rst create mode 100644 necronomicon.lua diff --git a/docs/necronomicon.rst b/docs/necronomicon.rst new file mode 100644 index 0000000000..cded397863 --- /dev/null +++ b/docs/necronomicon.rst @@ -0,0 +1,15 @@ +necronomicon +============ + +.. dfhack-tool:: + :summary: Find books that contain the secrets of life and death + :tags: fort items + +Lists all books in the fortress that contain the secrets to life and death. + +Usage +----- + +:: + + necronomicon diff --git a/necronomicon.lua b/necronomicon.lua new file mode 100644 index 0000000000..334d21253b --- /dev/null +++ b/necronomicon.lua @@ -0,0 +1,110 @@ +-- Author: Ajhaa + +-- lists books that contain secrets to life and death + +function get_book_interactions(item) + local book_interactions = {} + for _, improvement in ipairs(item.improvements) do + if improvement._type == df.itemimprovement_pagesst or + improvement._type == df.itemimprovement_writingst then + for _, content_id in ipairs(improvement.contents) do + written_content = df.written_content.find(content_id) + + for _, ref in ipairs (written_content.refs) do + if ref._type == df.general_ref_interactionst then + local interaction = df.global.world.raws.interactions[ref.interaction_id] + table.insert(book_interactions, interaction) + end + end + end + end + end + + return book_interactions +end + +-- should we check that the interaction is actually a SECRET +function is_secrets_book(item) + local interactions = get_book_interactions(item) + + return next(interactions) ~= nil +end + +function check_slab_secrets(item) + local type_id = item.engraving_type + local type = df.slab_engraving_type[type_id] + return type == "Secrets" +end + +function get_item_artifact(item) + for _, ref in ipairs(item.general_refs) do + if ref._type == df.general_ref_is_artifactst then + return df.global.world.artifacts.all[ref.artifact_id] + end + end +end + +function print_interactions(interactions) + for _, interaction in ipairs(interactions) do + -- Search interaction.str for the tag [CDI:ADV_NAME:] + -- for example: [CDI:ADV_NAME:Raise fetid corpse] + for _, str in ipairs(interaction.str) do + local _, e = string.find(str.value, "ADV_NAME") + if e then + print("\t", string.sub(str.value, e + 2, #str.value - 1)) + end + end + end +end + +function necronomicon(scope) + if scope == "fort" then + print("SLABS:") + for _, item in ipairs(df.global.world.items.other.SLAB) do + if check_slab_secrets(item) then + artifact = get_item_artifact(item) + name = dfhack.TranslateName(artifact.name) + print(dfhack.df2console(name)) + end + end + print("\nBOOKS:") + for _, item in ipairs(df.global.world.items.other.BOOK) do + if is_secrets_book(item) then + print(item.title) + print_interactions(interactions) + end + end + elseif scope == "world" then + -- currently not in use by the script, because the information might be invalid and useless + -- use written contents instead of artifacts? + for _, artifact in ipairs(df.global.world.artifacts.all) do + local item = artifact.item + + if item._type == df.item_bookst then + local interactions = get_book_interactions(item) + if next(interactions) ~= nil then + print(item.title, artifact.id) + print_interactions(interactions) + end + end + + if item._type == df.item_slabst then + if check_slab_secrets(item) then + local name = dfhack.TranslateName(artifact.name) + print(dfhack.df2console(name)) + end + end + end + end +end + + +local args = {...} +local cmd = args[1] + + +if cmd == "" or cmd == nil then + necronomicon("fort") +else + print("invalid argument") +end From 9b959589fdff725a7028b5458aa09007e93fbba4 Mon Sep 17 00:00:00 2001 From: Atte Haarni Date: Wed, 3 May 2023 15:25:45 +0300 Subject: [PATCH 165/732] Fixes and improvements from code review - remove unused global search - move slabs behind a flag - add help - improve docs --- docs/necronomicon.rst | 13 +++++++-- necronomicon.lua | 65 +++++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/docs/necronomicon.rst b/docs/necronomicon.rst index cded397863..39192dafa8 100644 --- a/docs/necronomicon.rst +++ b/docs/necronomicon.rst @@ -2,14 +2,23 @@ necronomicon ============ .. dfhack-tool:: - :summary: Find books that contain the secrets of life and death + :summary: Find books that contain the secrets of life and death. :tags: fort items Lists all books in the fortress that contain the secrets to life and death. +To find the books in fortress mode, go to the Written content submenu in Objects (O). +Tablets are not shown by default, because dwarves cannot read the secrets from a slab in fort mode. Usage ----- :: - necronomicon + necronomicon [] + +Options +------- + +``-s``, ``--include-slabs`` + Also list slabs that contain the secrets of life and death. Note that dwarves cannot read the secrets from a slab in fort mode. + diff --git a/necronomicon.lua b/necronomicon.lua index 334d21253b..7f8672cad4 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -1,6 +1,9 @@ -- Author: Ajhaa -- lists books that contain secrets to life and death +local utils = require("utils") +local argparse = require("argparse") + function get_book_interactions(item) local book_interactions = {} @@ -23,13 +26,6 @@ function get_book_interactions(item) return book_interactions end --- should we check that the interaction is actually a SECRET -function is_secrets_book(item) - local interactions = get_book_interactions(item) - - return next(interactions) ~= nil -end - function check_slab_secrets(item) local type_id = item.engraving_type local type = df.slab_engraving_type[type_id] @@ -57,8 +53,8 @@ function print_interactions(interactions) end end -function necronomicon(scope) - if scope == "fort" then +function necronomicon(include_slabs) + if include_slabs then print("SLABS:") for _, item in ipairs(df.global.world.items.other.SLAB) do if check_slab_secrets(item) then @@ -67,44 +63,33 @@ function necronomicon(scope) print(dfhack.df2console(name)) end end - print("\nBOOKS:") - for _, item in ipairs(df.global.world.items.other.BOOK) do - if is_secrets_book(item) then - print(item.title) - print_interactions(interactions) - end - end - elseif scope == "world" then - -- currently not in use by the script, because the information might be invalid and useless - -- use written contents instead of artifacts? - for _, artifact in ipairs(df.global.world.artifacts.all) do - local item = artifact.item - - if item._type == df.item_bookst then - local interactions = get_book_interactions(item) - if next(interactions) ~= nil then - print(item.title, artifact.id) - print_interactions(interactions) - end - end + print() + end + print("BOOKS:") + for _, item in ipairs(df.global.world.items.other.BOOK) do + local interactions = get_book_interactions(item) - if item._type == df.item_slabst then - if check_slab_secrets(item) then - local name = dfhack.TranslateName(artifact.name) - print(dfhack.df2console(name)) - end - end + if next(interactions) ~= nil then + print(item.title) + print_interactions(interactions) end end end -local args = {...} -local cmd = args[1] +local help = false +local include_slabs = false +local args = argparse.processArgsGetopt({...}, { + {"s", "include-slabs", handler=function() include_slabs = true end}, + {"h", "help", handler=function() help = true end} +}) +local cmd = args[1] -if cmd == "" or cmd == nil then - necronomicon("fort") +if help or cmd == "help" then + print(dfhack.script_help()) +elseif cmd == nil or cmd == "" then + necronomicon(include_slabs) else - print("invalid argument") + print("necronomicon: Invalid argument \"" .. cmd .. "\"") end From a5e9a7b624f4eac8552c3f93917604fb074300ff Mon Sep 17 00:00:00 2001 From: Atte Haarni Date: Wed, 3 May 2023 15:29:40 +0300 Subject: [PATCH 166/732] Remove extra newline --- docs/necronomicon.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/necronomicon.rst b/docs/necronomicon.rst index 39192dafa8..c08ea20c71 100644 --- a/docs/necronomicon.rst +++ b/docs/necronomicon.rst @@ -21,4 +21,3 @@ Options ``-s``, ``--include-slabs`` Also list slabs that contain the secrets of life and death. Note that dwarves cannot read the secrets from a slab in fort mode. - From dfdc921fdfe2682482d4ced25e00b4e59a0d004e Mon Sep 17 00:00:00 2001 From: Daniel Porter Date: Wed, 8 Mar 2023 18:52:54 +0000 Subject: [PATCH 167/732] Create fix/stuck-instruments fix/stuck-instruments is a fixer script for instruments that are permanently stuck in an activity job, usually caused by DF bug 9485 which results in instruments being picked up for a performance, but are instead simulated and never get disassociated with the performance activity or have their in_job flag removed. --- fix/stuck-instruments.lua | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 fix/stuck-instruments.lua diff --git a/fix/stuck-instruments.lua b/fix/stuck-instruments.lua new file mode 100644 index 0000000000..ff14597799 --- /dev/null +++ b/fix/stuck-instruments.lua @@ -0,0 +1,65 @@ +-- Fixes instruments that never got played during a performance + +local help = [====[ + +fix/stuck-instruments +===================== + +Fixes instruments that were picked up for a performance, but were instead +simulated and are now stuck permanently in a job that no longer exists. + +This works around the issue encountered with :bug:`9485`, and should be run +if you notice any instruments lying on the ground that seem to be stuck in a +job. + +Run ``fix/stuck-instruments -n`` or ``fix/stuck-instruments --dry-run`` to +list how many instruments would be fixed without performing the action. + +]====] + + +function fixInstruments(args) + local dry_run = false + local fixed = 0 + for _, arg in pairs(args) do + if args[1]:match('-h') or args[1]:match('help') then + print(help) + return + elseif args[1]:match('-n') or args[1]:match('dry') then + dry_run = true + end + end + for _, item in ipairs(df.global.world.items.all) do + if item:getType() == df.item_type.INSTRUMENT then + for i, ref in pairs(item.general_refs) do + if ref:getType() == df.general_ref_type.ACTIVITY_EVENT then + local activity = df.activity_entry.find(ref.activity_id) + if not activity then + if not dry_run then + --remove dead activity reference + item.general_refs:erase(i) + if item.flags.in_job then + --remove stuck in_job flag if true + item.flags.in_job = false + end + end + fixed = fixed + 1 + break + end + end + end + end + end + + if fixed > 0 or dry_run then + print(("%s %d stuck instruments."):format( + dry_run and "Found" or "Fixed", + fixed + )) + end +end + + +if not dfhack_flags.module then + fixInstruments{...} +end From 7bd1648686c983544a03c5255bdfc644a02e9d4e Mon Sep 17 00:00:00 2001 From: Daniel Porter Date: Wed, 3 May 2023 14:42:12 +0100 Subject: [PATCH 168/732] Add documentation for fix/stuck-instruments --- docs/fix/stuck-instruments.rst | 22 ++++++++++++++++++++++ fix/stuck-instruments.lua | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 docs/fix/stuck-instruments.rst diff --git a/docs/fix/stuck-instruments.rst b/docs/fix/stuck-instruments.rst new file mode 100644 index 0000000000..e16c38fcc5 --- /dev/null +++ b/docs/fix/stuck-instruments.rst @@ -0,0 +1,22 @@ +fix/stuck-instruments +===================== + +.. dfhack-tool:: + :summary: Allow bugged instruments to be interacted with again. + :tags: fort bugfix items + +Fixes instruments that were picked up for a performance, but were instead +simulated and are now stuck permanently in a job that no longer exists. + +This works around the issue encountered with :bug:`9485`, and should be run +if you notice any instruments lying on the ground that seem to be stuck in a +job. + + +Usage +----- + +``fix/stuck-instruments`` + Fixes item data for all stuck instruments on the map. +``fix/stuck-instruments -n``, ``fix/stuck-instruments --dry-run`` + List how many instruments would be fixed without performing the action. diff --git a/fix/stuck-instruments.lua b/fix/stuck-instruments.lua index ff14597799..1b8c58ce92 100644 --- a/fix/stuck-instruments.lua +++ b/fix/stuck-instruments.lua @@ -23,7 +23,7 @@ function fixInstruments(args) local fixed = 0 for _, arg in pairs(args) do if args[1]:match('-h') or args[1]:match('help') then - print(help) + print(dfhack.script_help()) return elseif args[1]:match('-n') or args[1]:match('dry') then dry_run = true From ea37765d81499dbd198c9ba5896ae627b7a99d6b Mon Sep 17 00:00:00 2001 From: Daniel Porter Date: Wed, 3 May 2023 16:44:17 +0100 Subject: [PATCH 169/732] Simplify fix/stuck-instruments to only iterate through instruments --- fix/stuck-instruments.lua | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/fix/stuck-instruments.lua b/fix/stuck-instruments.lua index 1b8c58ce92..8b4080fb4e 100644 --- a/fix/stuck-instruments.lua +++ b/fix/stuck-instruments.lua @@ -29,23 +29,22 @@ function fixInstruments(args) dry_run = true end end - for _, item in ipairs(df.global.world.items.all) do - if item:getType() == df.item_type.INSTRUMENT then - for i, ref in pairs(item.general_refs) do - if ref:getType() == df.general_ref_type.ACTIVITY_EVENT then - local activity = df.activity_entry.find(ref.activity_id) - if not activity then - if not dry_run then - --remove dead activity reference - item.general_refs:erase(i) - if item.flags.in_job then - --remove stuck in_job flag if true - item.flags.in_job = false - end + + for _, item in ipairs(df.global.world.items.other.INSTRUMENT) do + for i, ref in pairs(item.general_refs) do + if ref:getType() == df.general_ref_type.ACTIVITY_EVENT then + local activity = df.activity_entry.find(ref.activity_id) + if not activity then + if not dry_run then + --remove dead activity reference + item.general_refs:erase(i) + if item.flags.in_job then + --remove stuck in_job flag if true + item.flags.in_job = false end - fixed = fixed + 1 - break end + fixed = fixed + 1 + break end end end From c2e9a5c4dcc642227f196ff7abf5c336fa45734f Mon Sep 17 00:00:00 2001 From: Daniel Porter Date: Wed, 3 May 2023 16:50:44 +0100 Subject: [PATCH 170/732] Add fix/stuck-instruments to REPEATS table As the activity ref fix for instruments should be run periodically to stop broken instruments form littering the fort, the tool has been added to REPEATS with a daily delay to facilitate this function. --- gui/control-panel.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index c0051c49ac..e183ddd67e 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -94,6 +94,9 @@ local REPEATS = { ['combine']={ desc='Combine partial stacks in stockpiles into full stacks.', command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, + ['fixInstruments']={ + desc='Fix activity references on stuck instruments to make them usable again.', + command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, ['general-strike']={ desc='Prevent dwarves from getting stuck and refusing to work.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, From 0b3a866679840ce659523417bed8cb1a28f76a1f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 3 May 2023 13:57:47 -0700 Subject: [PATCH 171/732] get exportlegends basically working --- changelog.txt | 1 + docs/exportlegends.rst | 48 ++------ docs/open-legends.rst | 2 +- exportlegends.lua | 272 +++-------------------------------------- open-legends.lua | 103 +++++----------- 5 files changed, 64 insertions(+), 362 deletions(-) diff --git a/changelog.txt b/changelog.txt index af023b1bc2..ccecf30457 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,6 +14,7 @@ that repo. # Future ## New Scripts +- `exportlegends`: export extended legends information for external browsing ## Fixes diff --git a/docs/exportlegends.rst b/docs/exportlegends.rst index 33bf20afd7..6f0088178b 100644 --- a/docs/exportlegends.rst +++ b/docs/exportlegends.rst @@ -2,46 +2,24 @@ exportlegends ============= .. dfhack-tool:: - :summary: Exports legends data for external viewing. - :tags: unavailable legends inspection + :summary: Exports extended legends data for external viewing. + :tags: legends inspection -When run from legends mode, you can export detailed data about your world so -that it can be browsed with external programs like -:forums:`World Viewer <128932>` and other similar utilities. The data exported -with this tool is more detailed than what you can get with vanilla export -functionality, and some external tools depend on this extra information. +When run from the legends mode screen, you can export detailed data about your +world so that it can be browsed with external programs like +:forums:`Legends Browser <179848>` and other similar utilities. The data +exported with this tool is more detailed than what you can get with vanilla +export functionality, and some external tools depend on this extra information. -``exportlegends`` can be especially useful when you are generating a lot of -worlds that you later want to inspect or when you want a map of every site when -there are several hundred. +To use: + +- enter legends mode +- click the vanilla "Export XML" button to get the standard export +- run this command (``exportlegends``) to get the extended export Usage ----- :: - exportlegends [] - -Valid commands are: - -:info: Exports the world/gen info, the legends XML, and an extended info file. -:custom: Exports just the extended info file. -:sites: Exports all available site maps. -:maps: Exports all seventeen detailed maps. -:all: Equivalent to calling all of the above, in that order. - -The default folder name is generated from the region number of the world and the -current in-world date: ``legends-regionX-YYYYY-MM-DD``. You can use a different -folder by naming it on the ``exportlegends`` command line. Nested paths are -accepted, but all but the last folder has to already exist. To export to the -top-level DF folder, specify ``.`` as the folder name. - -Examples --------- - -``exportlegends all`` - Export all information to the ``legends-regionX-YYYYY-MM-DD`` folder. -``exportlegends all legends/myregion`` - Export all information to the ``legends/myregion`` folder. -``exportlegends custom .`` - Export just the extended info file to the DF folder (no subfolder). + exportlegends diff --git a/docs/open-legends.rst b/docs/open-legends.rst index 85f258f8f2..b0c0395d6e 100644 --- a/docs/open-legends.rst +++ b/docs/open-legends.rst @@ -3,7 +3,7 @@ open-legends .. dfhack-tool:: :summary: Open a legends screen from fort or adventure mode. - :tags: unavailable adventure fort legends + :tags: unavailable legends inspection You can use this tool to open legends mode from a world loaded in fortress or adventure mode. You can browse around, or even run `exportlegends` while you're diff --git a/exportlegends.lua b/exportlegends.lua index f392dda385..97e3281de9 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -1,107 +1,23 @@ -- Export everything from legends mode ---[====[ - -exportlegends -============= -Controls legends mode to export data - especially useful to set-and-forget large -worlds, or when you want a map of every site when there are several hundred. - -The 'info' option exports more data than is possible in vanilla, to a -:file:`region-date-legends_plus.xml` file developed to extend -:forums:`World Viewer <128932>` and other legends utilities. - -Usage:: - - exportlegends OPTION [FOLDER_NAME] - -Valid values for ``OPTION`` are: - -:info: Exports the world/gen info, the legends XML, and a custom XML with more information -:custom: Exports a custom XML with more information -:sites: Exports all available site maps -:maps: Exports all seventeen detailed maps -:all: Equivalent to calling all of the above, in that order - -``FOLDER_NAME``, if specified, is the name of the folder where all the files -will be saved. This defaults to the ``legends-regionX-YYYYY-MM-DD`` format. A path is -also allowed, although everything but the last folder has to exist. To export -to the top-level DF folder, pass ``.`` for this argument. - -Examples: - -* Export all information to the ``legends-regionX-YYYYY-MM-DD`` folder:: - - exportlegends all - -* Export all information to the ``region6`` folder:: - - exportlegends all region6 - -* Export just the files included in ``info`` (above) to the ``legends-regionX-YYYYY-MM-DD`` folder:: - - exportlegends info - -* Export just the custom XML file to the DF folder (no subfolder):: - - exportlegends custom . - -]====] - --luacheck-flags: strictsubtype --- General note: If you are looking for main function look at the buttom of this script file. - -local gui = require 'gui' -local script = require 'gui.script' +local gui = require('gui') local args = {...} -local vs = dfhack.gui.getCurViewscreen() - --- List of all the detailed maps -local MAPS = { - "Standard biome+site map", - "Elevations including lake and ocean floors", - "Elevations respecting water level", - "Biome", - "Hydrosphere", - "Temperature", - "Rainfall", - "Drainage", - "Savagery", - "Volcanism", - "Current vegetation", - "Evil", - "Salinity", - "Structures/fields/roads/etc.", - "Trade", - "Nobility and Holdings", - "Diplomacy", -} --- Get that date of the world as a string +-- Get the date of the world as a string -- Format: "YYYYY-MM-DD" -function get_world_date_str() +local function get_world_date_str() local month = dfhack.world.ReadCurrentMonth() + 1 --days and months are 1-indexed local day = dfhack.world.ReadCurrentDay() local date_str = string.format('%05d-%02d-%02d', df.global.cur_year, month, day) return date_str end --- Go back to root folder so dfhack does not break, returns true if successfully -function move_back_to_main_folder() - return dfhack.filesystem.restore_cwd() -end - --- Set default folder name -local folder_name = "legends-" .. df.global.world.cur_savegame.save_dir .. "-" .. get_world_date_str() --- Go to save folder, returns true if successfully -function move_to_save_folder() - if move_back_to_main_folder() then - return dfhack.filesystem.chdir(folder_name) - end - return false +local function escape_xml(str) + return str:gsub('&', '&'):gsub('<', '<'):gsub('>', '>') end -function getItemSubTypeName(itemType, subType) +local function getItemSubTypeName(itemType, subType) if (dfhack.items.getSubtypeCount(itemType)) <= 0 then return tostring(-1) end @@ -113,16 +29,8 @@ function getItemSubTypeName(itemType, subType) end end -function table_contains(self, element) - for _, value in pairs(self) do - if value == element then - return true - end - end - return false -end - -function table_containskey(self, key) +-- is this faster than pcall? +local function table_containskey(self, key) for value, _ in pairs(self) do if value == key then return true @@ -131,12 +39,8 @@ function table_containskey(self, key) return false end -function escape_xml(str) - return str:gsub('&', '&'):gsub('<', '<'):gsub('>', '>') -end - --luacheck: skip -function progress_ipairs(vector, desc, interval) +local function progress_ipairs(vector, desc, interval) desc = desc or 'item' interval = interval or 10000 local cb = ipairs(vector) @@ -159,7 +63,7 @@ setmetatable(df_enums, { local t = {} setmetatable(t, { __index = function(self, k) - return df[enum][k] or 'unknown ' .. k + return df[enum][k] or ('unknown ' .. k) end }) return t @@ -170,25 +74,21 @@ setmetatable(df_enums, { -- prints a line with the value inside the tags if the value isn't -1. Intended to be used -- for fields where -1 is a known "no info" value. Relies on 'indentation' being set to indicate -- the current indentation level -function printifvalue (file, indentation, tag, value) +local function printifvalue (file, indentation, tag, value) if value ~= -1 then file:write(string.rep("\t", indentation).."<"..tag..">"..tostring(value).."\n") end end -- Export additional legends data, legends_plus.xml -function export_more_legends_xml() +local function export_more_legends_xml() local problem_elements = {} - -- Move into the save folder - if not move_to_save_folder() then - qerror('Could not move into the save folder.') - end local filename = df.global.world.cur_savegame.save_dir.."-"..get_world_date_str().."-legends_plus.xml" local file = io.open(filename, 'w') - move_back_to_main_folder() if not file then qerror("could not open file: " .. filename) + return -- so lint understands what's going on end file:write("\n") @@ -556,7 +456,6 @@ function export_more_legends_xml() or df.history_event_remove_hf_entity_linkst:is_instance(event) or df.history_event_remove_hf_site_linkst:is_instance(event) or df.history_event_replaced_buildingst:is_instance(event) - or df.history_event_masterpiece_created_arch_designst:is_instance(event) or df.history_event_masterpiece_created_dye_itemst:is_instance(event) or df.history_event_masterpiece_created_arch_constructst:is_instance(event) or df.history_event_masterpiece_created_itemst:is_instance(event) @@ -1061,150 +960,11 @@ function export_more_legends_xml() end end --- Export world information and legends.xml (keys: 'p and x') -function export_legends_info() - -- Move into the save folder - if not move_to_save_folder() then - qerror('Could not move into the save folder.') - end - print(' Exporting: World map/gen info') - gui.simulateInput(vs, 'LEGENDS_EXPORT_MAP') - print(' Exporting: Legends xml') - gui.simulateInput(vs, 'LEGENDS_EXPORT_XML') - move_back_to_main_folder() -- Move back out of the save folder - print(" Exporting: Extra legends_plus xml") +-- Check if on legends screen and trigger the export if so +if dfhack.gui.matchFocusString('legends') then export_more_legends_xml() -end - --- Export all the detailed maps like biome and elevation maps. (key: 'd') -function export_detailed_maps() - script.start( - function() - -- When script is finished run `move_back_to_main_folder()` - dfhack.with_finalize( - -- Function when script is finished - function() - -- This makes sure it will always go back to the main folder. - -- Even if an error occurs - move_back_to_main_folder() - -- Make sure this is always printed even when error occurs. - print(" Done exporting.") - end, - -- Run script - function() - -- Loop over all the detailed maps and export them. - for i = 1, #MAPS do - -- Select the detailed map section - local vs = dfhack.gui.getViewscreenByType(df.viewscreen_export_graphical_mapst, 0) - if not vs then - local legends_vs = dfhack.gui.getViewscreenByType(df.viewscreen_legendsst, 0) - if not legends_vs then - qerror("Could not find legends screen") - end - - gui.simulateInput(legends_vs, 'LEGENDS_EXPORT_DETAILED_MAP') - end - - vs = dfhack.gui.getViewscreenByType(df.viewscreen_export_graphical_mapst, 0) - if not vs then - qerror("Could not find map export screen") - end - - vs.sel_type = i - 1 - -- Move into the save folder - if not move_to_save_folder() then - qerror('Could not move into the save folder.') - end - print(' Exporting map ' ..i.. '/' ..#MAPS..': '.. MAPS[i]) - -- Select the map and start exporting - gui.simulateInput(vs, 'SELECT') - -- Wait for the map to finish exporting - while dfhack.gui.getCurViewscreen() == vs do - script.sleep(10, 'frames') - end - -- Move back out of the save folder - move_back_to_main_folder() - end - end - ) - end - ) -end - --- Export the maps of all the sites (cities, towns,...) (key: 'sites', 'p') -function export_site_maps() - local vs = dfhack.gui.getCurViewscreen() - if ((dfhack.gui.getCurFocus() ~= "legends" ) and (not table_contains(vs, "main_cursor"))) then -- Using open-legends - vs = vs.parent --luacheck: retype - end - if df.viewscreen_legendsst:is_instance(vs) then - -- Move into the save folder - if not move_to_save_folder() then - qerror('Could not move into the save folder.') - end - print(' Exporting: All possible site maps') - vs.main_cursor = 1 - gui.simulateInput(vs, 'SELECT') - for i=1, #vs.sites do - gui.simulateInput(vs, 'LEGENDS_EXPORT_MAP') - gui.simulateInput(vs, 'STANDARDSCROLL_DOWN') - end - gui.simulateInput(vs, 'LEAVESCREEN') - move_back_to_main_folder() -- Move back out of the save folder - else - qerror('this command can only be used in Legends mode') - end -end - --- Check if a folder with this name could be created or already exists -function create_folder(folder_name) - if folder_name == "-00000-01-01" then - qerror('"'..folder_name..'" is the default foldername, this folder will not be created as you are probably not in the legends screen.') - end - -- check if it is a file, not a folder - if dfhack.filesystem.isfile(folder_name) then - qerror(folder_name..' is a file, not a folder') - end - if dfhack.filesystem.exists(folder_name) then - return true - else - return dfhack.filesystem.mkdir(folder_name) - end -end - --- If folder_name is given as a argument use that -if #args >= 2 then - folder_name = args[2] -end --- Create folder to export all files into, if possible. -if not create_folder(folder_name) then - -- no valid folder name or could not create folder - qerror('The foldername '..folder_name..' could not be created') -end -print("Writing all files in: "..folder_name) - --- Main: Check if on legends screen and trigger the correct export. -if dfhack.gui.getCurFocus() == "legends" or dfhack.gui.getCurFocus() == "dfhack/lua/legends" then - -- either native legends mode, or using the open-legends.lua script - if args[1] == "all" then - export_legends_info() - export_site_maps() - export_detailed_maps() - elseif args[1] == "info" then - export_legends_info() - elseif args[1] == "custom" then - export_more_legends_xml() - elseif args[1] == "maps" then - export_detailed_maps() - elseif args[1] == "sites" then - export_site_maps() - else - qerror('Valid arguments are "all", "info", "custom", "maps" or "sites"') - end -elseif args[1] == "maps" and dfhack.gui.getCurFocus() == "export_graphical_map" then - export_detailed_maps() else qerror('exportlegends must be run from the main legends view') end -print("Exported files can be found in the \""..folder_name.."\" folder.") +print("Exported files can be found in the top-level DF game folder.") diff --git a/open-legends.lua b/open-legends.lua index 5cfd93f536..c584f14f46 100644 --- a/open-legends.lua +++ b/open-legends.lua @@ -1,82 +1,44 @@ -- open legends screen when in fortress mode --@ module = true ---[====[ -open-legends -============ -Open a legends screen when in fortress mode. Requires a world loaded in fortress -or adventure mode. Compatible with `exportlegends`. - -Note that this script carries a significant risk of save corruption if the game -is saved after exiting legends mode. To avoid this: - -1. Pause DF -2. Run `quicksave` to save the game -3. Run `open-legends` (this script) and browse legends mode as usual -4. Immediately after exiting legends mode, run `die` to quit DF without saving - (saving at this point instead may corrupt your save) - -Note that it should be safe to run "open-legends" itself multiple times in the -same DF session, as long as DF is killed immediately after the last run. -Unpausing DF or running other commands risks accidentally autosaving the game, -which can lead to save corruption. - -The optional ``force`` argument will bypass all safety checks, as well as the -save corruption warning. - -]====] - -local dialogs = require 'gui.dialogs' -local gui = require 'gui' -local utils = require 'utils' - -Wrapper = defclass(Wrapper, gui.Screen) -Wrapper.focus_path = 'legends' - -local region_details_backup = {} --as:df.world_region_details[] - -function Wrapper:onRender() - self._native.parent:render() -end - -function Wrapper:onIdle() - self._native.parent:logic() +local dialogs = require('gui.dialogs') +local gui = require('gui') +local utils = require('utils') + +Restorer = defclass(Restorer, gui.Screen) +Restorer.ATTRS{ + focus_path='open-legends' +} + +function Restorer:init() + print('initializing restorer') + self.region_details_backup = {} --as:df.world_region_details[] + local v = df.global.world.world_data.region_details + while (#v > 0) do + table.insert(self.region_details_backup, 1, v[0]) + v:erase(0) + end end -function Wrapper:onHelp() - self._native.parent:help() +function Restorer:onIdle() + self:dismiss() end -function Wrapper:onInput(keys) - if self._native.parent.cur_page == 0 and keys.LEAVESCREEN then --hint:df.viewscreen_legendsst - local v = df.global.world.world_data.region_details - while (#v > 0) do v:erase(0) end - for _,item in pairs(region_details_backup) do - v:insert(0, item) - end - self:dismiss() - dfhack.screen.dismiss(self._native.parent) - return +function Restorer:onDismiss() + print('dismissing restorer') + local v = df.global.world.world_data.region_details + while (#v > 0) do v:erase(0) end + for _,item in pairs(self.region_details_backup) do + v:insert(0, item) end - gui.simulateInput(self._native.parent, keys) end function show_screen() - local old_view = dfhack.gui.getCurViewscreen(true) local ok, err = pcall(function() + Restorer{}:show() dfhack.screen.show(df.viewscreen_legendsst:new()) - Wrapper():show() end) - if ok then - local v = df.global.world.world_data.region_details - while (#v > 0) do - table.insert(region_details_backup, 1, v[0]) - v:erase(0) - end - else - while dfhack.gui.getCurViewscreen(true) ~= old_view do - dfhack.screen.dismiss(dfhack.gui.getCurViewscreen(true)) - end + if not ok then qerror('Failed to set up legends screen: ' .. tostring(err)) end end @@ -93,7 +55,6 @@ function main(force) end view = view.child end - local old_view = dfhack.gui.getCurViewscreen() if not dfhack.world.isFortressMode(df.global.gametype) and not dfhack.world.isAdventureMode(df.global.gametype) and not force then qerror('mode not tested: ' .. df.game_type[df.global.gametype] .. ' (use "force" to force)') @@ -110,7 +71,7 @@ function main(force) '1. Press "esc" to exit this prompt\n' .. '2. Pause DF\n' .. '3. Run "quicksave" to save this world\n' .. - '4. Run this script again and press "y" to enter legends mode\n' .. + '4. Run this script again and press ENTER to enter legends mode\n' .. '5. IMMEDIATELY AFTER EXITING LEGENDS, run "die" to quit DF\n\n' .. 'Press "esc" below to go back, or "y" to enter legends mode.\n' .. 'By pressing "y", you acknowledge that your save could be\n' .. @@ -121,7 +82,9 @@ function main(force) end end -if not moduleMode then - local iargs = utils.invert{...} - main(iargs.force) +if dfhack_flags.module then + return end + +local iargs = utils.invert{...} +main(iargs.force) From a37a47e6602dd5c8f378bedefad77bdc2457ff71 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Apr 2023 21:56:38 -0700 Subject: [PATCH 172/732] reformat, fix typos --- gui/create-item.lua | 950 +++++++++++++++++++++++--------------------- 1 file changed, 488 insertions(+), 462 deletions(-) diff --git a/gui/create-item.lua b/gui/create-item.lua index 8cd330c558..9a3fda7a4c 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -3,523 +3,549 @@ -- edited by expwnent --@module = true -local utils = require 'utils' +local eventful = require('plugins.eventful') local guidm = require('gui.dwarfmode') +local script = require('gui.script') +local utils = require('utils') + +local args function getGenderString(gender) - local sym = df.pronoun_type.attrs[gender].symbol - if not sym then - return "" - end - return "("..sym..")" + local sym = df.pronoun_type.attrs[gender].symbol + if not sym then return '' end + return '(' .. sym .. ')' end function getCreatureList() - local crList={} - for k,cr in ipairs(df.global.world.raws.creatures.alphabetic) do - for kk,ca in ipairs(cr.caste) do - local str=ca.caste_name[0] - str=str..' '..getGenderString(ca.sex) - table.insert(crList,{str,nil,ca}) - end - end - return crList + local crList = {} + for k,cr in ipairs(df.global.world.raws.creatures.alphabetic) do + for kk,ca in ipairs(cr.caste) do + local str = ca.caste_name[0] + str = str .. ' ' .. getGenderString(ca.sex) + table.insert(crList, {str, nil, ca}) + end + end + return crList end function getCreaturePartList(creatureID, casteID) - local crpList={{"generic"}} - for k,crp in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts) do - local str = crp.name_singular[0][0] - table.insert(crpList,{str}) - end - return crpList + local crpList = {{'generic'}} + for k,crp in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts) do + local str = crp.name_singular[0][0] + table.insert(crpList, {str}) + end + return crpList end function getCreaturePartLayerList(creatureID, casteID, partID) - local crplList={{"whole"}} - for k,crpl in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts[partID].layers) do - local str = crpl.layer_name - table.insert(crplList,{str}) - end - return crplList + local crplList = {{'whole'}} + for k,crpl in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts[partID].layers) do + local str = crpl.layer_name + table.insert(crplList, {str}) + end + return crplList end function getCreatureMaterialList(creatureID, casteID) - local crmList={} - for k,crm in ipairs(df.global.world.raws.creatures.all[creatureID].material) do - local str = crm.id - table.insert(crmList,{str}) - end - return crmList + local crmList = {} + for k,crm in ipairs(df.global.world.raws.creatures.all[creatureID].material) do + local str = crm.id + table.insert(crmList, {str}) + end + return crmList end function getRestrictiveMatFilter(itemType) - if args.unrestricted then return nil end - local itemTypes={ - WEAPON=function(mat,parent,typ,idx) - return (mat.flags.ITEMS_WEAPON or mat.flags.ITEMS_WEAPON_RANGED) - end, - AMMO=function(mat,parent,typ,idx) - return (mat.flags.ITEMS_AMMO) - end, - ARMOR=function(mat,parent,typ,idx) - return (mat.flags.ITEMS_ARMOR) - end, - INSTRUMENT=function(mat,parent,typ,idx) - return (mat.flags.ITEMS_HARD) - end, - AMULET=function(mat,parent,typ,idx) - return (mat.flags.ITEMS_SOFT or mat.flags.ITEMS_HARD) - end, - ROCK=function(mat,parent,typ,idx) - return (mat.flags.IS_STONE) - end, - BOULDER=ROCK, - BAR=function(mat,parent,typ,idx) - return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id==COAL) - end - - } - for k,v in ipairs({'GOBLET','FLASK','TOY','RING','CROWN','SCEPTER','FIGURINE','TOOL'}) do - itemTypes[v]=itemTypes.INSTRUMENT - end - for k,v in ipairs({'SHOES','SHIELD','HELM','GLOVES'}) do - itemTypes[v]=itemTypes.ARMOR - end - for k,v in ipairs({'EARRING','BRACELET'}) do - itemTypes[v]=itemTypes.AMULET - end - itemTypes.BOULDER=itemTypes.ROCK - return itemTypes[df.item_type[itemType]] + if args.unrestricted then return nil end + local rock = function(mat, parent, typ, idx) + return (mat.flags.IS_STONE) + end + local itemTypes = { + WEAPON = function(mat, parent, typ, idx) + return (mat.flags.ITEMS_WEAPON or mat.flags.ITEMS_WEAPON_RANGED) + end, + AMMO = function(mat, parent, typ, idx) + return (mat.flags.ITEMS_AMMO) + end, + ARMOR = function(mat, parent, typ, idx) + return (mat.flags.ITEMS_ARMOR) + end, + INSTRUMENT = function(mat, parent, typ, idx) + return (mat.flags.ITEMS_HARD) + end, + AMULET = function(mat, parent, typ, idx) + return (mat.flags.ITEMS_SOFT or mat.flags.ITEMS_HARD) + end, + ROCK = rock, + BOULDER = rock, + BAR = function(mat, parent, typ, idx) + return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id == 'COAL') + end, + } + for k,v in ipairs{'GOBLET', 'FLASK', 'TOY', 'RING', 'CROWN', 'SCEPTER', 'FIGURINE', 'TOOL'} do + itemTypes[v] = itemTypes.INSTRUMENT + end + for k,v in ipairs{'SHOES', 'SHIELD', 'HELM', 'GLOVES'} do + itemTypes[v] = itemTypes.ARMOR + end + for k,v in ipairs{'EARRING', 'BRACELET'} do + itemTypes[v] = itemTypes.AMULET + end + itemTypes.BOULDER = itemTypes.ROCK + return itemTypes[df.item_type[itemType]] end function getMatFilter(itemtype) - local itemTypes={ - SEEDS=function(mat,parent,typ,idx) - return mat.flags.SEED_MAT - end, - PLANT=function(mat,parent,typ,idx) - return mat.flags.STRUCTURAL_PLANT_MAT - end, - LEAVES=function(mat,parent,typ,idx) - return mat.flags.LEAF_MAT - end, - MEAT=function(mat,parent,typ,idx) - return mat.flags.MEAT - end, - CHEESE=function(mat,parent,typ,idx) - return (mat.flags.CHEESE_PLANT or mat.flags.CHEESE_CREATURE) - end, - LIQUID_MISC=function(mat,parent,typ,idx) - return (mat.flags.LIQUID_MISC_PLANT or mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER) - end, - POWDER_MISC=function(mat,parent,typ,idx) - return (mat.flags.POWDER_MISC_PLANT or mat.flags.POWDER_MISC_CREATURE) - end, - DRINK=function(mat,parent,typ,idx) - return (mat.flags.ALCOHOL_PLANT or mat.flags.ALCOHOL_CREATURE) - end, - GLOB=function(mat,parent,typ,idx) - return (mat.flags.STOCKPILE_GLOB) - end, - WOOD=function(mat,parent,typ,idx) - return (mat.flags.WOOD) - end, - THREAD=function(mat,parent,typ,idx) - return (mat.flags.THREAD_PLANT) - end, - LEATHER=function(mat,parent,typ,idx) - return (mat.flags.LEATHER) - end - } - return itemTypes[df.item_type[itemtype]] or getRestrictiveMatFilter(itemtype) + local itemTypes = { + SEEDS = function(mat, parent, typ, idx) + return mat.flags.SEED_MAT + end, + PLANT = function(mat, parent, typ, idx) + return mat.flags.STRUCTURAL_PLANT_MAT + end, + LEAVES = function(mat, parent, typ, idx) + return mat.flags.LEAF_MAT + end, + MEAT = function(mat, parent, typ, idx) + return mat.flags.MEAT + end, + CHEESE = function(mat, parent, typ, idx) + return (mat.flags.CHEESE_PLANT or mat.flags.CHEESE_CREATURE) + end, + LIQUID_MISC = function(mat, parent, typ, idx) + return (mat.flags.LIQUID_MISC_PLANT or mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER) + end, + POWDER_MISC = function(mat, parent, typ, idx) + return (mat.flags.POWDER_MISC_PLANT or mat.flags.POWDER_MISC_CREATURE) + end, + DRINK = function(mat, parent, typ, idx) + return (mat.flags.ALCOHOL_PLANT or mat.flags.ALCOHOL_CREATURE) + end, + GLOB = function(mat, parent, typ, idx) + return (mat.flags.STOCKPILE_GLOB) + end, + WOOD = function(mat, parent, typ, idx) + return (mat.flags.WOOD) + end, + THREAD = function(mat, parent, typ, idx) + return (mat.flags.THREAD_PLANT) + end, + LEATHER = function(mat, parent, typ, idx) + return (mat.flags.LEATHER) + end, + } + return itemTypes[df.item_type[itemtype]] or getRestrictiveMatFilter(itemtype) end -function createItem(mat,itemType,quality,creator,description,amount) - local item=df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) - local item2=nil - assert(item, 'failed to create item') - quality = math.max(0, math.min(5, quality - 1)) - item:setQuality(quality) - if df.item_type[itemType[1]]=='SLAB' then - item.description=description - end - if df.item_type[itemType[1]]=='GLOVES' then - --create matching gloves - item:setGloveHandedness(1) - item2=df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) - assert(item2, 'failed to create item') - item2:setQuality(quality) - item2:setGloveHandedness(2) - end - if df.item_type[itemType[1]]=='SHOES' then - --create matching shoes - item2=df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) - assert(item2, 'failed to create item') - item2:setQuality(quality) - end - if tonumber(amount) > 1 then - item:setStackSize(amount) - if item2 then item2:setStackSize(amount) end - end +function createItem(mat, itemType, quality, creator, description, amount) + local item = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) + local item2 = nil + assert(item, 'failed to create item') + quality = math.max(0, math.min(5, quality - 1)) + item:setQuality(quality) + if df.item_type[itemType[1]] == 'SLAB' then + item.description = description + end + if df.item_type[itemType[1]] == 'GLOVES' then + --create matching gloves + item:setGloveHandedness(1) + item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) + assert(item2, 'failed to create item') + item2:setQuality(quality) + item2:setGloveHandedness(2) + end + if df.item_type[itemType[1]] == 'SHOES' then + --create matching shoes + item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) + assert(item2, 'failed to create item') + item2:setQuality(quality) + end + if tonumber(amount) > 1 then + item:setStackSize(amount) + if item2 then item2:setStackSize(amount) end + end end function qualityTable() - return {{'None'}, - {'-Well-crafted-'}, - {'+Finely-crafted+'}, - {'*Superior*'}, - {string.char(240)..'Exceptional'..string.char(240)}, - {string.char(15)..'Masterwork'..string.char(15)} - } + return {{'None'}, + {'-Well-crafted-'}, + {'+Finely-crafted+'}, + {'*Superior*'}, + {string.char(240) .. 'Exceptional' .. string.char(240)}, + {string.char(15) .. 'Masterwork' .. string.char(15)}, + } end -local script=require('gui.script') +function showItemPrompt(text, item_filter, hide_none) + require('gui.materials').ItemTypeDialog{ + prompt = text, + item_filter = item_filter, + hide_none = hide_none, + on_select = script.mkresume(true), + on_cancel = script.mkresume(false), + on_close = script.qresume(nil), + }:show() -function showItemPrompt(text,item_filter,hide_none) - require('gui.materials').ItemTypeDialog{ - prompt=text, - item_filter=item_filter, - hide_none=hide_none, - on_select=script.mkresume(true), - on_cancel=script.mkresume(false), - on_close=script.qresume(nil) - }:show() - - return script.wait() + return script.wait() end function showMaterialPrompt(title, prompt, filter, inorganic, creature, plant) --the one included with DFHack doesn't have a filter or the inorganic, creature, plant things available - require('gui.materials').MaterialDialog{ - frame_title = title, - prompt = prompt, - mat_filter = filter, - use_inorganic = inorganic, - use_creature = creature, - use_plant = plant, - on_select = script.mkresume(true), - on_cancel = script.mkresume(false), - on_close = script.qresume(nil) - }:show() + require('gui.materials').MaterialDialog{ + frame_title = title, + prompt = prompt, + mat_filter = filter, + use_inorganic = inorganic, + use_creature = creature, + use_plant = plant, + on_select = script.mkresume(true), + on_cancel = script.mkresume(false), + on_close = script.qresume(nil), + }:show() - return script.wait() + return script.wait() end function usesCreature(itemtype) - typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true} - return typesThatUseCreatures[df.item_type[itemtype]] + typesThatUseCreatures = { + REMAINS = true, + FISH = true, + FISH_RAW = true, + VERMIN = true, + PET = true, + EGG = true, + CORPSE = true, + CORPSEPIECE = true, + } + return typesThatUseCreatures[df.item_type[itemtype]] end local function getCreatureRaceAndCaste(caste) - return df.global.world.raws.creatures.list_creature[caste.index],df.global.world.raws.creatures.list_caste[caste.index] + return df.global.world.raws.creatures.list_creature[caste.index], + df.global.world.raws.creatures.list_caste[caste.index] end -local CORPSE_PIECES = utils.invert{'BONE', 'SKIN', 'CARTILAGE', 'TOOTH', 'NERVE', 'NAIL', 'HORN', 'HOOF', 'CHITIN', 'SHELL', 'IVORY', 'SCALE' } -local HAIR_PIECES = utils.invert{'HAIR', 'EYEBROW', 'EYELASH', 'MOUSTACHE', 'CHIN_WHISKERS', 'SIDEBURNS' } -local LIQUID_PIECES = utils.invert{'BLOOD', 'PUS', 'VENOM', 'SWEAT', 'TEARS', 'SPIT', 'MILK' } +local CORPSE_PIECES = utils.invert{'BONE', 'SKIN', 'CARTILAGE', 'TOOTH', 'NERVE', 'NAIL', 'HORN', 'HOOF', 'CHITIN', + 'SHELL', 'IVORY', 'SCALE'} +local HAIR_PIECES = utils.invert{'HAIR', 'EYEBROW', 'EYELASH', 'MOUSTACHE', 'CHIN_WHISKERS', 'SIDEBURNS'} +local LIQUID_PIECES = utils.invert{'BLOOD', 'PUS', 'VENOM', 'SWEAT', 'TEARS', 'SPIT', 'MILK'} -function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic, quality) -- this part was written by four rabbits in a trenchcoat (ppaawwll) - -- (partlayer is also used to determine the material if we're spawning a "generic" body part (i'm just lazy lol)) - quality = math.max(0, math.min(5, quality - 1)) - creatureID = tonumber(creatureID) - -- get the actual raws of the target creature - local creatorRaceRaw = df.creature_raw.find(creatureID) - local wholePart = false - casteID = tonumber(casteID) - bodypart = tonumber(bodypart) - partlayer = tonumber(partlayer) - if partlayer == -1 and not generic then -- somewhat similar to the bodypart variable below, a value of -1 here means that the user wants to spawn a whole body part. we set the partlayer to 0 (outermost) because the specific layer isn't important, and we're spawning them all anyway. if it's a generic corpsepiece we ignore it, as it gets added to anyway below (we can't do it below because between here and there there's lines that reference the part layer - partlayer = 0 - wholePart = true - end - -- get body info for easy reference - local creatorBody = creatorRaceRaw.caste[casteID].body_info - local layerName - local layerMat = "BONE" - local tissueID - local liquid = false - local isCorpse = bodypart == -1 and not generic -- in the hackWish function, the bodypart variable is initialized to -1, which isn't changed if the spawned item is a corpse - if not generic and not isCorpse then -- if we have a specified body part and layer, figure all the stuff out about that - -- store the tissue id of the specific layer we selected - tissueID = tonumber(creatorBody.body_parts[bodypart].layers[partlayer].tissue_id) - layerMat = {} - -- get the material name from the material itself - for i in string.gmatch(dfhack.matinfo.getToken(creatorRaceRaw.tissue[tissueID].mat_type,creatureID), "([^:]+)") do - table.insert(layerMat,i) - end - layerMat = layerMat[3] - layerName = creatorBody.body_parts[bodypart].layers[partlayer].layer_name - elseif not isCorpse then -- otherwise, figure out the mat name from the dual-use partlayer argument - partlayer = partlayer + 1 -- no "whole" option at the start of the generic creature material selection prompt means that the value we get is actually further along than intended - layerMat = creatorRaceRaw.material[partlayer].id - layerName = layerMat - end - -- default is MEAT, so if anything else fails to change it to something else, we know that the body layer is a meat item - local item_type = "MEAT" - -- get race name and layer name, both for finding the item material, and the latter for determining the corpsepiece flags to set - local raceName = string.upper(creatorRaceRaw.creature_id) - -- every key is a valid non-hair corpsepiece, so if we try to index a key that's not on the table, we don't have a non-hair corpsepiece - -- we do the same as above but with hair - -- if the layer is fat, spawn a glob of fat and DON'T check for other layer types - if layerName == "FAT" then - item_type = "GLOB" - elseif CORPSE_PIECES[layerName] or HAIR_PIECES[layerName] then -- check if hair - item_type = "CORPSEPIECE" - elseif LIQUID_PIECES[layerName] then - item_type = "LIQUID_MISC" - liquid = true - end - if isCorpse then - item_type = "CORPSE" - generic = true - end - local itemType = dfhack.items.findType(item_type..":NONE") - local itemSubtype = dfhack.items.findSubtype(item_type..":NONE") - local material = "CREATURE_MAT:"..raceName..":"..layerMat - local materialInfo = dfhack.matinfo.find(material) - local item_id = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) - local item = df.item.find(item_id) - if liquid then - local bucketMat = dfhack.matinfo.find("PLANT_MAT:NETHER_CAP:WOOD") - if not bucketMat then - for i,n in ipairs(df.global.world.raws.plants.all) do - if n.flags.TREE then - bucketMat = dfhack.matinfo.find("PLANT_MAT:"..n.id..":WOOD") +-- this part was written by four rabbits in a trenchcoat (ppaawwll) +function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic, quality) + -- (partlayer is also used to determine the material if we're spawning a "generic" body part (i'm just lazy lol)) + quality = math.max(0, math.min(5, quality - 1)) + creatureID = tonumber(creatureID) + -- get the actual raws of the target creature + local creatorRaceRaw = df.creature_raw.find(creatureID) + local wholePart = false + casteID = tonumber(casteID) + bodypart = tonumber(bodypart) + partlayer = tonumber(partlayer) + -- somewhat similar to the bodypart variable below, a value of -1 here means that the user wants to spawn a whole body part. we set the partlayer to 0 (outermost) because the specific layer isn't important, and we're spawning them all anyway. if it's a generic corpsepiece we ignore it, as it gets added to anyway below (we can't do it below because between here and there there's lines that reference the part layer + if partlayer == -1 and not generic then + partlayer = 0 + wholePart = true + end + -- get body info for easy reference + local creatorBody = creatorRaceRaw.caste[casteID].body_info + local layerName + local layerMat = 'BONE' + local tissueID + local liquid = false + -- in the hackWish function, the bodypart variable is initialized to -1, which isn't changed if the spawned item is a corpse + local isCorpse = bodypart == -1 and not generic + if not generic and not isCorpse then -- if we have a specified body part and layer, figure all the stuff out about that + -- store the tissue id of the specific layer we selected + tissueID = tonumber(creatorBody.body_parts[bodypart].layers[partlayer].tissue_id) + layerMat = {} + -- get the material name from the material itself + for i in string.gmatch(dfhack.matinfo.getToken(creatorRaceRaw.tissue[tissueID].mat_type, creatureID), '([^:]+)') do + table.insert(layerMat, i) + end + layerMat = layerMat[3] + layerName = creatorBody.body_parts[bodypart].layers[partlayer].layer_name + elseif not isCorpse then -- otherwise, figure out the mat name from the dual-use partlayer argument + -- no "whole" option at the start of the generic creature material selection prompt means that the value we get is actually further along than intended + partlayer = partlayer + 1 + layerMat = creatorRaceRaw.material[partlayer].id + layerName = layerMat + end + -- default is MEAT, so if anything else fails to change it to something else, we know that the body layer is a meat item + local item_type = 'MEAT' + -- get race name and layer name, both for finding the item material, and the latter for determining the corpsepiece flags to set + local raceName = string.upper(creatorRaceRaw.creature_id) + -- every key is a valid non-hair corpsepiece, so if we try to index a key that's not on the table, we don't have a non-hair corpsepiece + -- we do the same as above but with hair + -- if the layer is fat, spawn a glob of fat and DON'T check for other layer types + if layerName == 'FAT' then + item_type = 'GLOB' + elseif CORPSE_PIECES[layerName] or HAIR_PIECES[layerName] then -- check if hair + item_type = 'CORPSEPIECE' + elseif LIQUID_PIECES[layerName] then + item_type = 'LIQUID_MISC' + liquid = true + end + if isCorpse then + item_type = 'CORPSE' + generic = true + end + local itemType = dfhack.items.findType(item_type .. ':NONE') + local itemSubtype = dfhack.items.findSubtype(item_type .. ':NONE') + local material = 'CREATURE_MAT:' .. raceName .. ':' .. layerMat + local materialInfo = dfhack.matinfo.find(material) + local item_id = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) + local item = df.item.find(item_id) + if liquid then + local bucketMat = dfhack.matinfo.find('PLANT_MAT:NETHER_CAP:WOOD') + if not bucketMat then + for i,n in ipairs(df.global.world.raws.plants.all) do + if n.flags.TREE then + bucketMat = dfhack.matinfo.find('PLANT_MAT:' .. n.id .. ':WOOD') + end + if bucketMat then break end + end + end + local prevCursorPos = guidm.getCursorPos() + local bucketType = dfhack.items.findType('BUCKET:NONE') + local bucket = df.item.find(dfhack.items.createItem(bucketType, -1, bucketMat.type, bucketMat.index, creator)) + dfhack.items.moveToContainer(item, bucket) + guidm.setCursorPos(creator.pos) + dfhack.run_command('spotclean') + guidm.setCursorPos(prevCursorPos) end - if bucketMat then break end - end - end - local prevCursorPos = guidm.getCursorPos() - local bucketType = dfhack.items.findType("BUCKET:NONE") - local bucket = df.item.find(dfhack.items.createItem(bucketType, -1, bucketMat.type, bucketMat.index, creator)) - dfhack.items.moveToContainer(item, bucket) - guidm.setCursorPos(creator.pos) - dfhack.run_command("spotclean") - guidm.setCursorPos(prevCursorPos) - end - -- if the item type is a corpsepiece, we know we have one, and then go on to set the appropriate flags - if item_type == "CORPSEPIECE" then - if layerName == "BONE" then -- check if bones - item.corpse_flags.bone = true - item.material_amount.Bone = 1 - elseif layerName == "SKIN" then -- check if skin/leather - item.corpse_flags.leather = true - item.material_amount.Leather = 1 - -- elseif layerName == "CARTILAGE" then -- check if cartilage (NO SPECIAL FLAGS) - elseif layerName == "HAIR" then -- check if hair (simplified from before) - item.corpse_flags.hair_wool = true - item.material_amount.HairWool = 1 - if materialInfo.material.flags.YARN then - item.corpse_flags.yarn = true - item.material_amount.Yarn = 1 - end - elseif layerName == "TOOTH" or layerName == "IVORY" then -- check if tooth - item.corpse_flags.tooth = true - item.material_amount.Tooth = 1 - elseif layerName == "NERVE" then -- check if nervous tissue - item.corpse_flags.skull1 = true -- apparently "skull1" is supposed to be named "rots/can_rot" - item.corpse_flags.separated_part = true - -- elseif layerName == "NAIL" then -- check if nail (NO SPECIAL FLAGS) - elseif layerName == "HORN" or layerName == "HOOF" then -- check if nail - item.corpse_flags.horn = true - item.material_amount.Horn = 1 - elseif layerName == "SHELL" then - item.corpse_flags.shell = true - item.material_amount.Shell = 1 - end - -- checking for skull - if not generic and not isCorpse and creatorBody.body_parts[bodypart].token == "SKULL" then - item.corpse_flags.skull2 = true - end - end - local matType - -- figure out which material type the material is (probably a better way of doing this but whatever) - for i in pairs(creatorRaceRaw.tissue) do - if creatorRaceRaw.tissue[i].tissue_material_str[1] == layerMat then - matType = creatorRaceRaw.tissue[i].mat_type - end - end - if item_type == "CORPSEPIECE" or item_type == "CORPSE" then - --referencing the source unit for, material, relation purposes??? - item.race = creatureID - item.normal_race = creatureID - item.normal_caste = casteID - if casteID < 2 and #(creatorRaceRaw.caste) > 1 then -- usually the first two castes are for the creature's sex, so we set the item's sex to the caste if both the creature has one and it's a valid sex id (0 or 1) - item.sex = casteID - else - item.sex = -1 -- it - end - -- on a dwarf tissue index 3 (bone) is 22, but this is not always the case for all creatures, so we get the mat_type of index 3 instead - -- here we also set the actual referenced creature material of the corpsepiece - item.bone1.mat_type = matType - item.bone1.mat_index = creatureID - item.bone2.mat_type = matType - item.bone2.mat_index = creatureID - -- skin (and presumably other parts) use body part modifiers for size or amount - for i=0,200 do -- fuck it this works - -- inserts - item.body.bp_modifiers:insert('#',1) --jus,t, set a lot of it to one who cares - end - -- copy target creature's relsizes to the item's's body relsizes thing - for i,n in pairs(creatorBody.body_parts) do - -- inserts - item.body.body_part_relsize:insert('#',n.relsize) - item.body.components.body_part_status:insert(i,creator.body.components.body_part_status[0]) --copy the status of the creator's first part to every body_part_status of the desired creature - item.body.components.body_part_status[i].missing = true - end - for i in pairs(creatorBody.layer_part) do - -- inserts - item.body.components.layer_status:insert(i,creator.body.components.layer_status[0]) --copy the layer status of the creator's first layer to every layer_status of the desired creature - item.body.components.layer_status[i].gone = true - end - if item_type == "CORPSE" then - item.corpse_flags.unbutchered = true - end - if not generic then - -- keeps the body part that the user selected to spawn the item from - item.body.components.body_part_status[bodypart].missing = false - -- restores the selected layer of the selected body part - item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[partlayer].layer_id].gone = false - elseif generic then - for i in pairs(creatorBody.body_parts) do - for n in pairs(creatorBody.body_parts[i].layers) do - if item_type == "CORPSE" then - item.body.components.body_part_status[i].missing = false - item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false - else - -- search through the target creature's body parts and bring back every one which has the desired material - if creatorRaceRaw.tissue[creatorBody.body_parts[i].layers[n].tissue_id].tissue_material_str[1] == layerMat and creatorBody.body_parts[i].token ~= "SKULL" and not creatorBody.body_parts[i].flags.SMALL then - item.body.components.body_part_status[i].missing = false - item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false - -- save the index of the bone layer to a variable - end - end - end + -- if the item type is a corpsepiece, we know we have one, and then go on to set the appropriate flags + if item_type == 'CORPSEPIECE' then + if layerName == 'BONE' then -- check if bones + item.corpse_flags.bone = true + item.material_amount.Bone = 1 + elseif layerName == 'SKIN' then -- check if skin/leather + item.corpse_flags.leather = true + item.material_amount.Leather = 1 + -- elseif layerName == "CARTILAGE" then -- check if cartilage (NO SPECIAL FLAGS) + elseif layerName == 'HAIR' then -- check if hair (simplified from before) + item.corpse_flags.hair_wool = true + item.material_amount.HairWool = 1 + if materialInfo.material.flags.YARN then + item.corpse_flags.yarn = true + item.material_amount.Yarn = 1 + end + elseif layerName == 'TOOTH' or layerName == 'IVORY' then -- check if tooth + item.corpse_flags.tooth = true + item.material_amount.Tooth = 1 + elseif layerName == 'NERVE' then -- check if nervous tissue + item.corpse_flags.skull1 = true -- apparently "skull1" is supposed to be named "rots/can_rot" + item.corpse_flags.separated_part = true + -- elseif layerName == "NAIL" then -- check if nail (NO SPECIAL FLAGS) + elseif layerName == 'HORN' or layerName == 'HOOF' then -- check if nail + item.corpse_flags.horn = true + item.material_amount.Horn = 1 + elseif layerName == 'SHELL' then + item.corpse_flags.shell = true + item.material_amount.Shell = 1 + end + -- checking for skull + if not generic and not isCorpse and creatorBody.body_parts[bodypart].token == 'SKULL' then + item.corpse_flags.skull2 = true + end + end + local matType + -- figure out which material type the material is (probably a better way of doing this but whatever) + for i in pairs(creatorRaceRaw.tissue) do + if creatorRaceRaw.tissue[i].tissue_material_str[1] == layerMat then + matType = creatorRaceRaw.tissue[i].mat_type + end end - end - if wholePart then -- brings back every tissue layer associated with a body part if we're spawning the entire thing - for i in pairs(creatorBody.body_parts[bodypart].layers) do - item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[i].layer_id].gone = false - item.corpse_flags.separated_part = true - item.corpse_flags.unbutchered = true + if item_type == 'CORPSEPIECE' or item_type == 'CORPSE' then + --referencing the source unit for, material, relation purposes??? + item.race = creatureID + item.normal_race = creatureID + item.normal_caste = casteID + -- usually the first two castes are for the creature's sex, so we set the item's sex to the caste if both the creature has one and it's a valid sex id (0 or 1) + if casteID < 2 and #(creatorRaceRaw.caste) > 1 then + item.sex = casteID + else + item.sex = -1 -- it + end + -- on a dwarf tissue index 3 (bone) is 22, but this is not always the case for all creatures, so we get the mat_type of index 3 instead + -- here we also set the actual referenced creature material of the corpsepiece + item.bone1.mat_type = matType + item.bone1.mat_index = creatureID + item.bone2.mat_type = matType + item.bone2.mat_index = creatureID + -- skin (and presumably other parts) use body part modifiers for size or amount + for i = 0,200 do -- fuck it this works + -- inserts + item.body.bp_modifiers:insert('#', 1) --jus,t, set a lot of it to one who cares + end + -- copy target creature's relsizes to the item's's body relsizes thing + for i,n in pairs(creatorBody.body_parts) do + -- inserts + item.body.body_part_relsize:insert('#', n.relsize) + item.body.components.body_part_status:insert(i, creator.body.components.body_part_status[0]) --copy the status of the creator's first part to every body_part_status of the desired creature + item.body.components.body_part_status[i].missing = true + end + for i in pairs(creatorBody.layer_part) do + -- inserts + item.body.components.layer_status:insert(i, creator.body.components.layer_status[0]) --copy the layer status of the creator's first layer to every layer_status of the desired creature + item.body.components.layer_status[i].gone = true + end + if item_type == 'CORPSE' then + item.corpse_flags.unbutchered = true + end + if not generic then + -- keeps the body part that the user selected to spawn the item from + item.body.components.body_part_status[bodypart].missing = false + -- restores the selected layer of the selected body part + item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[partlayer].layer_id].gone = false + elseif generic then + for i in pairs(creatorBody.body_parts) do + for n in pairs(creatorBody.body_parts[i].layers) do + if item_type == 'CORPSE' then + item.body.components.body_part_status[i].missing = false + item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false + else + -- search through the target creature's body parts and bring back every one which has the desired material + if creatorRaceRaw.tissue[creatorBody.body_parts[i].layers[n].tissue_id].tissue_material_str[1] == layerMat and creatorBody.body_parts[i].token ~= 'SKULL' and not creatorBody.body_parts[i].flags.SMALL then + item.body.components.body_part_status[i].missing = false + item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false + -- save the index of the bone layer to a variable + end + end + end + end + end + -- brings back every tissue layer associated with a body part if we're spawning the entire thing + if wholePart then + for i in pairs(creatorBody.body_parts[bodypart].layers) do + item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[i].layer_id].gone = false + item.corpse_flags.separated_part = true + item.corpse_flags.unbutchered = true + end + end + -- DO THIS LAST or else the game crashes for some reason + item.caste = casteID end - end - -- DO THIS LAST or else the game crashes for some reason - item.caste = casteID - end end function hackWish(unit) - script.start(function() - local amountok, amount - local matok,mattype,matindex,matFilter - local partlayerok, partlayerID = false, 0 - local qualityok, quality = false, 0 - local itemok,itemtype,itemsubtype=showItemPrompt('What item do you want?',function(itype) return df.item_type[itype]~='FOOD' end ,true) - local corpsepieceGeneric - local bodypart = -1 - if not itemok then return end - if not args.notRestrictive then - matFilter=getMatFilter(itemtype) - end - if not usesCreature(itemtype) then - matok,mattype,matindex=showMaterialPrompt('Wish','And what material should it be made of?',matFilter) - if not matok then return end - else - local creatureok,useless,creatureTable=script.showListPrompt('Wish','What creature should it be?',COLOR_LIGHTGREEN,getCreatureList(),1,true) - if not creatureok then return end - mattype,matindex=getCreatureRaceAndCaste(creatureTable[3]) - end - if df.item_type[itemtype]=='CORPSEPIECE' then - local bodpartok,bodypartLocal=script.showListPrompt('Wish','What body part should it be?',COLOR_LIGHTGREEN,getCreaturePartList(mattype,matindex),1,true) - -- createCorpsePiece() references the bodypart variable so it can't be local to here - bodypart = bodypartLocal - if bodypart == 1 then - corpsepieceGeneric = true - end - if not bodpartok then return end - if not corpsepieceGeneric then -- probably a better way of doing this tbh - partlayerok,partlayerID=script.showListPrompt('Wish','What tissue layer should it be?',COLOR_LIGHTGREEN,getCreaturePartLayerList(mattype,matindex,bodypart-2),1,true) - else - partlayerok,partlayerID=script.showListPrompt('Wish','What creature material should it be?',COLOR_LIGHTGREEN,getCreatureMaterialList(mattype,matindex),1,true) - end - if not partlayerok then return end - elseif df.item_type[itemtype]~='CORPSE' then - qualityok,quality=script.showListPrompt('Wish','What quality should it be?',COLOR_LIGHTGREEN,qualityTable()) - if not qualityok then return end - end - local description - if df.item_type[itemtype]=='SLAB' then - local descriptionok - descriptionok,description=script.showInputPrompt('Slab','What should the slab say?',COLOR_WHITE) - if not descriptionok then return end - end - if bodypart ~= -1 then - bodypart = bodypart - 2 --the offsets here are cause indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt - end - if args.multi then - repeat amountok,amount=script.showInputPrompt('Wish','How many do you want? (numbers only!)',COLOR_LIGHTGREEN) until tonumber(amount) or not amountok - if not amountok then return end - if mattype and itemtype then - if df.item_type.attrs[itemtype].is_stackable then - createItem({mattype,matindex},{itemtype,itemsubtype},quality,unit,description,amount) - else - local isCorpsePiece = itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE - for i=1,amount do - if not isCorpsePiece then - createItem({mattype,matindex},{itemtype,itemsubtype},quality,unit,description,1) - else - createCorpsePiece(unit,bodypart,partlayerID-2,mattype,matindex,corpsepieceGeneric,quality) - end - end - end - return true - end - return false - else - if mattype and itemtype then - if itemtype ~= df.item_type.CORPSEPIECE and itemtype ~= df.item_type.CORPSE then - createItem({mattype,matindex},{itemtype,itemsubtype},quality,unit,description,1) - else - createCorpsePiece(unit,bodypart,partlayerID-2,mattype,matindex,corpsepieceGeneric,quality) - end - return true - end - return false - end - end) + script.start(function() + local amountok, amount + local matok, mattype, matindex, matFilter + local partlayerok, partlayerID = false, 0 + local qualityok, quality = false, 0 + local itemok, itemtype, itemsubtype = showItemPrompt('What item do you want?', + function(itype) return df.item_type[itype] ~= 'FOOD' end, true) + local corpsepieceGeneric + local bodypart = -1 + if not itemok then return end + if not args.notRestrictive then + matFilter = getMatFilter(itemtype) + end + if not usesCreature(itemtype) then + matok, mattype, matindex = showMaterialPrompt('Wish', 'And what material should it be made of?', matFilter) + if not matok then return end + else + local creatureok, useless, creatureTable = script.showListPrompt('Wish', 'What creature should it be?', + COLOR_LIGHTGREEN, getCreatureList(), 1, true) + if not creatureok then return end + mattype, matindex = getCreatureRaceAndCaste(creatureTable[3]) + end + if df.item_type[itemtype] == 'CORPSEPIECE' then + local bodpartok, bodypartLocal = script.showListPrompt('Wish', 'What body part should it be?', + COLOR_LIGHTGREEN, + getCreaturePartList(mattype, matindex), 1, true) + -- createCorpsePiece() references the bodypart variable so it can't be local to here + bodypart = bodypartLocal + if bodypart == 1 then + corpsepieceGeneric = true + end + if not bodpartok then return end + if not corpsepieceGeneric then -- probably a better way of doing this tbh + partlayerok, partlayerID = script.showListPrompt('Wish', 'What tissue layer should it be?', + COLOR_LIGHTGREEN, + getCreaturePartLayerList(mattype, matindex, bodypart - 2), 1, true) + else + partlayerok, partlayerID = script.showListPrompt('Wish', 'What creature material should it be?', + COLOR_LIGHTGREEN, + getCreatureMaterialList(mattype, matindex), 1, true) + end + if not partlayerok then return end + elseif df.item_type[itemtype] ~= 'CORPSE' then + qualityok, quality = script.showListPrompt('Wish', 'What quality should it be?', COLOR_LIGHTGREEN, + qualityTable()) + if not qualityok then return end + end + local description + if df.item_type[itemtype] == 'SLAB' then + local descriptionok + descriptionok, description = script.showInputPrompt('Slab', 'What should the slab say?', COLOR_WHITE) + if not descriptionok then return end + end + if bodypart ~= -1 then + --the offsets here are cause indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt + bodypart = bodypart - 2 + end + if args.multi then + repeat + amountok, amount = script.showInputPrompt('Wish', 'How many do you want? (numbers only!)', + COLOR_LIGHTGREEN) + until tonumber(amount) or not amountok + if not amountok then return end + if mattype and itemtype then + if df.item_type.attrs[itemtype].is_stackable then + createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, amount) + else + local isCorpsePiece = itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE + for i = 1,amount do + if not isCorpsePiece then + createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) + else + createCorpsePiece(unit, bodypart, partlayerID - 2, mattype, matindex, corpsepieceGeneric, + quality) + end + end + end + return true + end + return false + else + if mattype and itemtype then + if itemtype ~= df.item_type.CORPSEPIECE and itemtype ~= df.item_type.CORPSE then + createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) + else + createCorpsePiece(unit, bodypart, partlayerID - 2, mattype, matindex, corpsepieceGeneric, quality) + end + return true + end + return false + end + end) end -scriptArgs={...} - -utils=require('utils') - -validArgs = utils.invert({ - 'startup', - 'unrestricted', - 'unit', - 'multi' -}) - -if moduleMode then - return +if dfhack_flags.module then + return end +validArgs = utils.invert{ + 'startup', + 'unrestricted', + 'unit', + 'multi', +} + args = utils.processArgs({...}, validArgs) -eventful=require('plugins.eventful') +if args.startup then + eventful.onReactionComplete.hackWishP = function(reaction, unit) + if not reaction.code:find('DFHACK_WISH') then return end + hackWish(unit) + end + return +end -if not args.startup then - local unit=tonumber(args.unit) and df.unit.find(tonumber(args.unit)) or dfhack.gui.getSelectedUnit(true) - if unit then - hackWish(unit) - else - qerror('A unit needs to be selected to use gui/create-item.') - end +local unit = tonumber(args.unit) and df.unit.find(tonumber(args.unit)) or dfhack.gui.getSelectedUnit(true) +if unit then + hackWish(unit) else - eventful.onReactionComplete.hackWishP=function(reaction,unit,input_items,input_reagents,output_items,call_native) - if not reaction.code:find('DFHACK_WISH') then return nil end - hackWish(unit) - end + qerror('A unit needs to be selected to use gui/create-item.') end From f5d41dd1895941203cf4b73b313e8093594eda09 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Apr 2023 23:34:26 -0700 Subject: [PATCH 173/732] finish cleanup --- docs/gui/create-item.rst | 37 ++++----- docs/modtools/create-item.rst | 13 ++- gui/create-item.lua | 147 ++++++++++++++++++++-------------- modtools/create-item.lua | 37 +-------- 4 files changed, 114 insertions(+), 120 deletions(-) diff --git a/docs/gui/create-item.rst b/docs/gui/create-item.rst index c0a95f3bfc..3a832a1313 100644 --- a/docs/gui/create-item.rst +++ b/docs/gui/create-item.rst @@ -2,19 +2,16 @@ gui/create-item =============== .. dfhack-tool:: - :summary: Magically summon any item. + :summary: Summon items from the aether. :tags: fort armok items This tool provides a graphical interface for creating items of your choice. It walks you through the creation process with a series of prompts, asking you -for the type of item, the material, the quality, and (if ``--multi`` is passed -on the commandline) the quantity. +for the type of item, the material, the quality, and the quantity. -Be sure to select a unit before running this tool so the created item can have -a valid "creator" assigned. - -See also `createitem` or `modtools/create-item` for different interfaces for -creating items. +If a unit is selected, that unit will be designated the creator of the summoned +items. The items will appear at that unit's feet. If no unit is selected, the +first citizen unit will be used as the creator. Usage ----- @@ -26,27 +23,27 @@ Usage Examples -------- -``gui/create-item --multi`` - Only provide options for creating items that normally exist in the game. - Also include the prompt for quantity so you can create more than just one - item at a time. -``gui/create-item --unrestricted`` +``gui/create-item`` + Walk player through the creation of an item that can normally exist in the + game. +``gui/create-item --unrestricted --count 1`` Create one item made of anything in the game. For example, you can create a bar of vomit, if you please. Options ------- -``--multi`` - Also prompt for the quantity of items to create. +``--count `` + Set the quantity of items to create instead of prompting for it. ``--unit `` Use the specified unit as the "creator" of the generated item instead of the - selected unit. + selected unit or the first citizen. ``--unrestricted`` Don't restrict the material options to only those that are normally appropriate for the selected item type. ``--startup`` - Instead of showing the item creation interface, start monitoring reactions - for a modded reaction with a code of ``DFHACK_WISH``. When a reaction with - that code is completed, show the item creation gui. This allows you to mod - in "wands of wishing" that can let your adventurer make wishes for items. + Instead of showing the item creation interface, start monitoring for a + modded reaction with a code of ``DFHACK_WISH``. When a reaction with that + code is completed, show the item creation gui (with ``--count 1``). This + allows you to mod in "wands of wishing" that can let your adventurer make + wishes for an item. diff --git a/docs/modtools/create-item.rst b/docs/modtools/create-item.rst index 4704502c08..8fe4aaedde 100644 --- a/docs/modtools/create-item.rst +++ b/docs/modtools/create-item.rst @@ -5,10 +5,17 @@ modtools/create-item :summary: Create arbitrary items. :tags: unavailable dev -Replaces the `createitem` plugin, with standard -arguments. The other versions will be phased out in a later version. +This tool provides a commandline interface for creating items of your choice. -Arguments:: +Usage +----- + +:: + + modtools/create-item + +Options +------- -creator id specify the id of the unit who will create the item, diff --git a/gui/create-item.lua b/gui/create-item.lua index 9a3fda7a4c..a3e943e3fa 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -8,7 +8,39 @@ local guidm = require('gui.dwarfmode') local script = require('gui.script') local utils = require('utils') -local args +local no_quality_item_types = utils.invert{ + 'BAR', + 'SMALLGEM', + 'BLOCKS', + 'ROUGH', + 'BOULDER', + 'WOOD', + 'CORPSE', + 'CORPSEPIECE', + 'REMAINS', + 'MEAT', + 'FISH', + 'FISH_RAW', + 'VERMIN', + 'PET', + 'SEEDS', + 'PLANT', + 'SKIN_TANNED', + 'PLANT_GROWTH', + 'THREAD', + 'DRINK', + 'POWDER_MISC', + 'CHEESE', + 'FOOD', + 'LIQUID_MISC', + 'COIN', + 'GLOB', + 'ROCK', + 'EGG', + 'BRANCH', +} + +------------------------------ function getGenderString(gender) local sym = df.pronoun_type.attrs[gender].symbol @@ -55,11 +87,8 @@ function getCreatureMaterialList(creatureID, casteID) return crmList end -function getRestrictiveMatFilter(itemType) - if args.unrestricted then return nil end - local rock = function(mat, parent, typ, idx) - return (mat.flags.IS_STONE) - end +function getRestrictiveMatFilter(itemType, opts) + if opts.unrestricted then return nil end local itemTypes = { WEAPON = function(mat, parent, typ, idx) return (mat.flags.ITEMS_WEAPON or mat.flags.ITEMS_WEAPON_RANGED) @@ -76,8 +105,9 @@ function getRestrictiveMatFilter(itemType) AMULET = function(mat, parent, typ, idx) return (mat.flags.ITEMS_SOFT or mat.flags.ITEMS_HARD) end, - ROCK = rock, - BOULDER = rock, + ROCK = function(mat, parent, typ, idx) + return (mat.flags.IS_STONE) + end, BAR = function(mat, parent, typ, idx) return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id == 'COAL') end, @@ -95,7 +125,7 @@ function getRestrictiveMatFilter(itemType) return itemTypes[df.item_type[itemType]] end -function getMatFilter(itemtype) +function getMatFilter(itemtype, opts) local itemTypes = { SEEDS = function(mat, parent, typ, idx) return mat.flags.SEED_MAT @@ -134,7 +164,7 @@ function getMatFilter(itemtype) return (mat.flags.LEATHER) end, } - return itemTypes[df.item_type[itemtype]] or getRestrictiveMatFilter(itemtype) + return itemTypes[df.item_type[itemtype]] or getRestrictiveMatFilter(itemtype, opts) end function createItem(mat, itemType, quality, creator, description, amount) @@ -230,9 +260,8 @@ local HAIR_PIECES = utils.invert{'HAIR', 'EYEBROW', 'EYELASH', 'MOUSTACHE', 'CHI local LIQUID_PIECES = utils.invert{'BLOOD', 'PUS', 'VENOM', 'SWEAT', 'TEARS', 'SPIT', 'MILK'} -- this part was written by four rabbits in a trenchcoat (ppaawwll) -function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic, quality) +function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic) -- (partlayer is also used to determine the material if we're spawning a "generic" body part (i'm just lazy lol)) - quality = math.max(0, math.min(5, quality - 1)) creatureID = tonumber(creatureID) -- get the actual raws of the target creature local creatorRaceRaw = df.creature_raw.find(creatureID) @@ -256,12 +285,12 @@ function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, ge if not generic and not isCorpse then -- if we have a specified body part and layer, figure all the stuff out about that -- store the tissue id of the specific layer we selected tissueID = tonumber(creatorBody.body_parts[bodypart].layers[partlayer].tissue_id) - layerMat = {} + local mats = {} -- get the material name from the material itself for i in string.gmatch(dfhack.matinfo.getToken(creatorRaceRaw.tissue[tissueID].mat_type, creatureID), '([^:]+)') do - table.insert(layerMat, i) + table.insert(mats, i) end - layerMat = layerMat[3] + layerMat = mats[3] layerName = creatorBody.body_parts[bodypart].layers[partlayer].layer_name elseif not isCorpse then -- otherwise, figure out the mat name from the dual-use partlayer argument -- no "whole" option at the start of the generic creature material selection prompt means that the value we get is actually further along than intended @@ -427,25 +456,22 @@ function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, ge end end -function hackWish(unit) +function hackWish(unit, opts) script.start(function() - local amountok, amount - local matok, mattype, matindex, matFilter + local matok, mattype, matindex local partlayerok, partlayerID = false, 0 - local qualityok, quality = false, 0 + local qualityok, quality = false, df.item_quality.Ordinary local itemok, itemtype, itemsubtype = showItemPrompt('What item do you want?', function(itype) return df.item_type[itype] ~= 'FOOD' end, true) local corpsepieceGeneric local bodypart = -1 if not itemok then return end - if not args.notRestrictive then - matFilter = getMatFilter(itemtype) - end if not usesCreature(itemtype) then - matok, mattype, matindex = showMaterialPrompt('Wish', 'And what material should it be made of?', matFilter) + matok, mattype, matindex = showMaterialPrompt('Wish', 'And what material should it be made of?', + not opts.unrestricted and getMatFilter(itemtype, opts) or nil) if not matok then return end else - local creatureok, useless, creatureTable = script.showListPrompt('Wish', 'What creature should it be?', + local creatureok, _, creatureTable = script.showListPrompt('Wish', 'What creature should it be?', COLOR_LIGHTGREEN, getCreatureList(), 1, true) if not creatureok then return end mattype, matindex = getCreatureRaceAndCaste(creatureTable[3]) @@ -470,7 +496,7 @@ function hackWish(unit) getCreatureMaterialList(mattype, matindex), 1, true) end if not partlayerok then return end - elseif df.item_type[itemtype] ~= 'CORPSE' then + elseif not no_quality_item_types[df.item_type[itemtype]] then qualityok, quality = script.showListPrompt('Wish', 'What quality should it be?', COLOR_LIGHTGREEN, qualityTable()) if not qualityok then return end @@ -485,43 +511,47 @@ function hackWish(unit) --the offsets here are cause indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt bodypart = bodypart - 2 end - if args.multi then + local count = opts.count + if not count then repeat - amountok, amount = script.showInputPrompt('Wish', 'How many do you want? (numbers only!)', + local amountok, amount = script.showInputPrompt('Wish', 'How many do you want? (numbers only!)', COLOR_LIGHTGREEN) - until tonumber(amount) or not amountok - if not amountok then return end - if mattype and itemtype then - if df.item_type.attrs[itemtype].is_stackable then - createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, amount) - else - local isCorpsePiece = itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE - for i = 1,amount do - if not isCorpsePiece then - createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) - else - createCorpsePiece(unit, bodypart, partlayerID - 2, mattype, matindex, corpsepieceGeneric, - quality) - end - end - end - return true - end + if not amountok then return end + count = tonumber(amount) + until count + end + if not mattype or not itemtype then return false + end + if df.item_type.attrs[itemtype].is_stackable then + createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, count) else - if mattype and itemtype then - if itemtype ~= df.item_type.CORPSEPIECE and itemtype ~= df.item_type.CORPSE then - createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) + for _ = 1,count do + if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then + createCorpsePiece(unit, bodypart, partlayerID - 2, mattype, matindex, corpsepieceGeneric) else - createCorpsePiece(unit, bodypart, partlayerID - 2, mattype, matindex, corpsepieceGeneric, quality) + createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) end - return true end - return false end + return true end) end +local function get_first_citizen() + local citizens = dfhack.units.getCitizens() + if not citizens or not citizens[1] then + qerror('Could not choose a creator unit. Please select one in the UI') + end + return citizens[1] +end + +local function get_creator(opts) + return tonumber(opts.unit) and df.unit.find(tonumber(opts.unit)) or + dfhack.gui.getSelectedUnit(true) or + get_first_citizen() +end + if dfhack_flags.module then return end @@ -530,22 +560,17 @@ validArgs = utils.invert{ 'startup', 'unrestricted', 'unit', - 'multi', + 'count', } -args = utils.processArgs({...}, validArgs) +local opts = utils.processArgs({...}, validArgs) -if args.startup then +if opts.startup then eventful.onReactionComplete.hackWishP = function(reaction, unit) if not reaction.code:find('DFHACK_WISH') then return end - hackWish(unit) + hackWish(unit, {count = 1}) end return end -local unit = tonumber(args.unit) and df.unit.find(tonumber(args.unit)) or dfhack.gui.getSelectedUnit(true) -if unit then - hackWish(unit) -else - qerror('A unit needs to be selected to use gui/create-item.') -end +hackWish(get_creator(opts), opts) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index 0139e3962f..b790ac17e5 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -1,40 +1,5 @@ -- creates an item of a given type and material --author expwnent -local usage = [====[ - -modtools/create-item -==================== -Replaces the `createitem` plugin, with standard -arguments. The other versions will be phased out in a later version. - -Arguments:: - - -creator id - specify the id of the unit who will create the item, - or \\LAST to indicate the unit with id df.global.unit_next_id-1 - examples: - 0 - 2 - \\LAST - -material matstring - specify the material of the item to be created - examples: - INORGANIC:IRON - CREATURE_MAT:DWARF:BRAIN - PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK - -item itemstr - specify the itemdef of the item to be created - examples: - WEAPON:ITEM_WEAPON_PICK - -quality qualitystr - specify the quality level of the item to be created (df.item_quality) - examples: Ordinary, WellCrafted, FinelyCrafted, Masterful, or 0-5 - -matchingShoes - create two of this item - -matchingGloves - create two of this item, and set handedness appropriately - -]====] local utils = require 'utils' local validArgs = utils.invert({ @@ -134,7 +99,7 @@ end local args = utils.processArgs({...}, validArgs) if args.help then - print(usage) + print(dfhack.script_help()) return end From 2405d12c4625356fc621ec55e3e24e83e4eed48e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Apr 2023 02:26:09 -0700 Subject: [PATCH 174/732] first draft of revised create-item ecosystem --- docs/gui/create-item.rst | 6 +- docs/modtools/create-item.rst | 48 ++-- gui/create-item.lua | 487 ++++++------------------------- modtools/create-item.lua | 523 +++++++++++++++++++++++++++------- 4 files changed, 533 insertions(+), 531 deletions(-) diff --git a/docs/gui/create-item.rst b/docs/gui/create-item.rst index 3a832a1313..3358d3ab91 100644 --- a/docs/gui/create-item.rst +++ b/docs/gui/create-item.rst @@ -33,12 +33,12 @@ Examples Options ------- -``--count `` +``-c``, ``--count `` Set the quantity of items to create instead of prompting for it. -``--unit `` +``-u``, ``--unit `` Use the specified unit as the "creator" of the generated item instead of the selected unit or the first citizen. -``--unrestricted`` +``-f``, ``--unrestricted`` Don't restrict the material options to only those that are normally appropriate for the selected item type. ``--startup`` diff --git a/docs/modtools/create-item.rst b/docs/modtools/create-item.rst index 8fe4aaedde..f33ec59a62 100644 --- a/docs/modtools/create-item.rst +++ b/docs/modtools/create-item.rst @@ -14,30 +14,30 @@ Usage modtools/create-item +Examples +-------- + +``modtools/create-item -u 23145 -i WEAPON:ITEM_WEAPON_PICK -m INORGANIC:IRON -q4`` + Have unit 23145 create an exceptionally crafted iron pick. +``modtools/create-item -u 323 -i CORPSEPIECE:NONE -m CREATURE_MAT:DWARF:BRAIN`` + Have unit 323 produce a lump of brain. +``modtools/create-item -i BOULDER:NONE -m INORGANIC:ADAMANTINE -c 5`` +``modtools/create-item -i BOULDER:NONE -m PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK`` + Options ------- - -creator id - specify the id of the unit who will create the item, - or \\LAST to indicate the unit with id df.global.unit_next_id-1 - examples: - 0 - 2 - \\LAST - -material matstring - specify the material of the item to be created - examples: - INORGANIC:IRON - CREATURE_MAT:DWARF:BRAIN - PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK - -item itemstr - specify the itemdef of the item to be created - examples: - WEAPON:ITEM_WEAPON_PICK - -quality qualitystr - specify the quality level of the item to be created (df.item_quality) - examples: Ordinary, WellCrafted, FinelyCrafted, Masterful, or 0-5 - -matchingShoes - create two of this item - -matchingGloves - create two of this item, and set handedness appropriately +``-u``, ``--unit `` (default: first citizen) + The ID of the unit to use as the item's creator. You can also pass the + string "\\LAST" to use the most recently created unit. +``-i``, ``--item `` (required) + The def string of the item you want to create. +``-m``, ``--material`` (required) + That def string of the material you want the item to be made out of. +``-q``, ``--quality`` (default: ``0``, which is ``df.item_quality.Ordinary``) + The quality of the created item. +``-d``, ``--description`` (required if you are creating a slab) + The text that will be engraved on the created slab. +``-c``, ``--count`` (default: ``1``) + The number of items to create. If the item is stackable, this will be the + stack size. diff --git a/gui/create-item.lua b/gui/create-item.lua index a3e943e3fa..e5d378845c 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -1,54 +1,20 @@ -- A gui-based item creation script. -- author Putnam -- edited by expwnent ---@module = true +local argparse = require('argparse') +local createitem = reqscript('modtools/create-item') local eventful = require('plugins.eventful') -local guidm = require('gui.dwarfmode') local script = require('gui.script') local utils = require('utils') -local no_quality_item_types = utils.invert{ - 'BAR', - 'SMALLGEM', - 'BLOCKS', - 'ROUGH', - 'BOULDER', - 'WOOD', - 'CORPSE', - 'CORPSEPIECE', - 'REMAINS', - 'MEAT', - 'FISH', - 'FISH_RAW', - 'VERMIN', - 'PET', - 'SEEDS', - 'PLANT', - 'SKIN_TANNED', - 'PLANT_GROWTH', - 'THREAD', - 'DRINK', - 'POWDER_MISC', - 'CHEESE', - 'FOOD', - 'LIQUID_MISC', - 'COIN', - 'GLOB', - 'ROCK', - 'EGG', - 'BRANCH', -} - ------------------------------- - -function getGenderString(gender) +local function getGenderString(gender) local sym = df.pronoun_type.attrs[gender].symbol if not sym then return '' end return '(' .. sym .. ')' end -function getCreatureList() +local function getCreatureList() local crList = {} for k,cr in ipairs(df.global.world.raws.creatures.alphabetic) do for kk,ca in ipairs(cr.caste) do @@ -60,7 +26,7 @@ function getCreatureList() return crList end -function getCreaturePartList(creatureID, casteID) +local function getCreaturePartList(creatureID, casteID) local crpList = {{'generic'}} for k,crp in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts) do local str = crp.name_singular[0][0] @@ -69,7 +35,7 @@ function getCreaturePartList(creatureID, casteID) return crpList end -function getCreaturePartLayerList(creatureID, casteID, partID) +local function getCreaturePartLayerList(creatureID, casteID, partID) local crplList = {{'whole'}} for k,crpl in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts[partID].layers) do local str = crpl.layer_name @@ -78,7 +44,7 @@ function getCreaturePartLayerList(creatureID, casteID, partID) return crplList end -function getCreatureMaterialList(creatureID, casteID) +local function getCreatureMaterialList(creatureID, casteID) local crmList = {} for k,crm in ipairs(df.global.world.raws.creatures.all[creatureID].material) do local str = crm.id @@ -87,7 +53,12 @@ function getCreatureMaterialList(creatureID, casteID) return crmList end -function getRestrictiveMatFilter(itemType, opts) +local function getCreatureRaceAndCaste(caste) + return df.global.world.raws.creatures.list_creature[caste.index], + df.global.world.raws.creatures.list_caste[caste.index] +end + +local function getRestrictiveMatFilter(itemType, opts) if opts.unrestricted then return nil end local itemTypes = { WEAPON = function(mat, parent, typ, idx) @@ -125,7 +96,7 @@ function getRestrictiveMatFilter(itemType, opts) return itemTypes[df.item_type[itemType]] end -function getMatFilter(itemtype, opts) +local function getMatFilter(itemtype, opts) local itemTypes = { SEEDS = function(mat, parent, typ, idx) return mat.flags.SEED_MAT @@ -167,36 +138,7 @@ function getMatFilter(itemtype, opts) return itemTypes[df.item_type[itemtype]] or getRestrictiveMatFilter(itemtype, opts) end -function createItem(mat, itemType, quality, creator, description, amount) - local item = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) - local item2 = nil - assert(item, 'failed to create item') - quality = math.max(0, math.min(5, quality - 1)) - item:setQuality(quality) - if df.item_type[itemType[1]] == 'SLAB' then - item.description = description - end - if df.item_type[itemType[1]] == 'GLOVES' then - --create matching gloves - item:setGloveHandedness(1) - item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) - assert(item2, 'failed to create item') - item2:setQuality(quality) - item2:setGloveHandedness(2) - end - if df.item_type[itemType[1]] == 'SHOES' then - --create matching shoes - item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) - assert(item2, 'failed to create item') - item2:setQuality(quality) - end - if tonumber(amount) > 1 then - item:setStackSize(amount) - if item2 then item2:setStackSize(amount) end - end -end - -function qualityTable() +local function qualityTable() return {{'None'}, {'-Well-crafted-'}, {'+Finely-crafted+'}, @@ -206,7 +148,7 @@ function qualityTable() } end -function showItemPrompt(text, item_filter, hide_none) +local function showItemPrompt(text, item_filter, hide_none) require('gui.materials').ItemTypeDialog{ prompt = text, item_filter = item_filter, @@ -215,11 +157,10 @@ function showItemPrompt(text, item_filter, hide_none) on_cancel = script.mkresume(false), on_close = script.qresume(nil), }:show() - return script.wait() end -function showMaterialPrompt(title, prompt, filter, inorganic, creature, plant) --the one included with DFHack doesn't have a filter or the inorganic, creature, plant things available +local function showMaterialPrompt(title, prompt, filter, inorganic, creature, plant) require('gui.materials').MaterialDialog{ frame_title = title, prompt = prompt, @@ -231,346 +172,90 @@ function showMaterialPrompt(title, prompt, filter, inorganic, creature, plant) - on_cancel = script.mkresume(false), on_close = script.qresume(nil), }:show() - return script.wait() end -function usesCreature(itemtype) - typesThatUseCreatures = { - REMAINS = true, - FISH = true, - FISH_RAW = true, - VERMIN = true, - PET = true, - EGG = true, - CORPSE = true, - CORPSEPIECE = true, - } - return typesThatUseCreatures[df.item_type[itemtype]] -end - -local function getCreatureRaceAndCaste(caste) - return df.global.world.raws.creatures.list_creature[caste.index], - df.global.world.raws.creatures.list_caste[caste.index] -end - -local CORPSE_PIECES = utils.invert{'BONE', 'SKIN', 'CARTILAGE', 'TOOTH', 'NERVE', 'NAIL', 'HORN', 'HOOF', 'CHITIN', - 'SHELL', 'IVORY', 'SCALE'} -local HAIR_PIECES = utils.invert{'HAIR', 'EYEBROW', 'EYELASH', 'MOUSTACHE', 'CHIN_WHISKERS', 'SIDEBURNS'} -local LIQUID_PIECES = utils.invert{'BLOOD', 'PUS', 'VENOM', 'SWEAT', 'TEARS', 'SPIT', 'MILK'} - --- this part was written by four rabbits in a trenchcoat (ppaawwll) -function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic) - -- (partlayer is also used to determine the material if we're spawning a "generic" body part (i'm just lazy lol)) - creatureID = tonumber(creatureID) - -- get the actual raws of the target creature - local creatorRaceRaw = df.creature_raw.find(creatureID) - local wholePart = false - casteID = tonumber(casteID) - bodypart = tonumber(bodypart) - partlayer = tonumber(partlayer) - -- somewhat similar to the bodypart variable below, a value of -1 here means that the user wants to spawn a whole body part. we set the partlayer to 0 (outermost) because the specific layer isn't important, and we're spawning them all anyway. if it's a generic corpsepiece we ignore it, as it gets added to anyway below (we can't do it below because between here and there there's lines that reference the part layer - if partlayer == -1 and not generic then - partlayer = 0 - wholePart = true - end - -- get body info for easy reference - local creatorBody = creatorRaceRaw.caste[casteID].body_info - local layerName - local layerMat = 'BONE' - local tissueID - local liquid = false - -- in the hackWish function, the bodypart variable is initialized to -1, which isn't changed if the spawned item is a corpse - local isCorpse = bodypart == -1 and not generic - if not generic and not isCorpse then -- if we have a specified body part and layer, figure all the stuff out about that - -- store the tissue id of the specific layer we selected - tissueID = tonumber(creatorBody.body_parts[bodypart].layers[partlayer].tissue_id) - local mats = {} - -- get the material name from the material itself - for i in string.gmatch(dfhack.matinfo.getToken(creatorRaceRaw.tissue[tissueID].mat_type, creatureID), '([^:]+)') do - table.insert(mats, i) - end - layerMat = mats[3] - layerName = creatorBody.body_parts[bodypart].layers[partlayer].layer_name - elseif not isCorpse then -- otherwise, figure out the mat name from the dual-use partlayer argument - -- no "whole" option at the start of the generic creature material selection prompt means that the value we get is actually further along than intended - partlayer = partlayer + 1 - layerMat = creatorRaceRaw.material[partlayer].id - layerName = layerMat - end - -- default is MEAT, so if anything else fails to change it to something else, we know that the body layer is a meat item - local item_type = 'MEAT' - -- get race name and layer name, both for finding the item material, and the latter for determining the corpsepiece flags to set - local raceName = string.upper(creatorRaceRaw.creature_id) - -- every key is a valid non-hair corpsepiece, so if we try to index a key that's not on the table, we don't have a non-hair corpsepiece - -- we do the same as above but with hair - -- if the layer is fat, spawn a glob of fat and DON'T check for other layer types - if layerName == 'FAT' then - item_type = 'GLOB' - elseif CORPSE_PIECES[layerName] or HAIR_PIECES[layerName] then -- check if hair - item_type = 'CORPSEPIECE' - elseif LIQUID_PIECES[layerName] then - item_type = 'LIQUID_MISC' - liquid = true - end - if isCorpse then - item_type = 'CORPSE' - generic = true - end - local itemType = dfhack.items.findType(item_type .. ':NONE') - local itemSubtype = dfhack.items.findSubtype(item_type .. ':NONE') - local material = 'CREATURE_MAT:' .. raceName .. ':' .. layerMat - local materialInfo = dfhack.matinfo.find(material) - local item_id = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) - local item = df.item.find(item_id) - if liquid then - local bucketMat = dfhack.matinfo.find('PLANT_MAT:NETHER_CAP:WOOD') - if not bucketMat then - for i,n in ipairs(df.global.world.raws.plants.all) do - if n.flags.TREE then - bucketMat = dfhack.matinfo.find('PLANT_MAT:' .. n.id .. ':WOOD') - end - if bucketMat then break end - end - end - local prevCursorPos = guidm.getCursorPos() - local bucketType = dfhack.items.findType('BUCKET:NONE') - local bucket = df.item.find(dfhack.items.createItem(bucketType, -1, bucketMat.type, bucketMat.index, creator)) - dfhack.items.moveToContainer(item, bucket) - guidm.setCursorPos(creator.pos) - dfhack.run_command('spotclean') - guidm.setCursorPos(prevCursorPos) - end - - -- if the item type is a corpsepiece, we know we have one, and then go on to set the appropriate flags - if item_type == 'CORPSEPIECE' then - if layerName == 'BONE' then -- check if bones - item.corpse_flags.bone = true - item.material_amount.Bone = 1 - elseif layerName == 'SKIN' then -- check if skin/leather - item.corpse_flags.leather = true - item.material_amount.Leather = 1 - -- elseif layerName == "CARTILAGE" then -- check if cartilage (NO SPECIAL FLAGS) - elseif layerName == 'HAIR' then -- check if hair (simplified from before) - item.corpse_flags.hair_wool = true - item.material_amount.HairWool = 1 - if materialInfo.material.flags.YARN then - item.corpse_flags.yarn = true - item.material_amount.Yarn = 1 - end - elseif layerName == 'TOOTH' or layerName == 'IVORY' then -- check if tooth - item.corpse_flags.tooth = true - item.material_amount.Tooth = 1 - elseif layerName == 'NERVE' then -- check if nervous tissue - item.corpse_flags.skull1 = true -- apparently "skull1" is supposed to be named "rots/can_rot" - item.corpse_flags.separated_part = true - -- elseif layerName == "NAIL" then -- check if nail (NO SPECIAL FLAGS) - elseif layerName == 'HORN' or layerName == 'HOOF' then -- check if nail - item.corpse_flags.horn = true - item.material_amount.Horn = 1 - elseif layerName == 'SHELL' then - item.corpse_flags.shell = true - item.material_amount.Shell = 1 - end - -- checking for skull - if not generic and not isCorpse and creatorBody.body_parts[bodypart].token == 'SKULL' then - item.corpse_flags.skull2 = true - end - end - local matType - -- figure out which material type the material is (probably a better way of doing this but whatever) - for i in pairs(creatorRaceRaw.tissue) do - if creatorRaceRaw.tissue[i].tissue_material_str[1] == layerMat then - matType = creatorRaceRaw.tissue[i].mat_type - end - end - if item_type == 'CORPSEPIECE' or item_type == 'CORPSE' then - --referencing the source unit for, material, relation purposes??? - item.race = creatureID - item.normal_race = creatureID - item.normal_caste = casteID - -- usually the first two castes are for the creature's sex, so we set the item's sex to the caste if both the creature has one and it's a valid sex id (0 or 1) - if casteID < 2 and #(creatorRaceRaw.caste) > 1 then - item.sex = casteID - else - item.sex = -1 -- it - end - -- on a dwarf tissue index 3 (bone) is 22, but this is not always the case for all creatures, so we get the mat_type of index 3 instead - -- here we also set the actual referenced creature material of the corpsepiece - item.bone1.mat_type = matType - item.bone1.mat_index = creatureID - item.bone2.mat_type = matType - item.bone2.mat_index = creatureID - -- skin (and presumably other parts) use body part modifiers for size or amount - for i = 0,200 do -- fuck it this works - -- inserts - item.body.bp_modifiers:insert('#', 1) --jus,t, set a lot of it to one who cares - end - -- copy target creature's relsizes to the item's's body relsizes thing - for i,n in pairs(creatorBody.body_parts) do - -- inserts - item.body.body_part_relsize:insert('#', n.relsize) - item.body.components.body_part_status:insert(i, creator.body.components.body_part_status[0]) --copy the status of the creator's first part to every body_part_status of the desired creature - item.body.components.body_part_status[i].missing = true - end - for i in pairs(creatorBody.layer_part) do - -- inserts - item.body.components.layer_status:insert(i, creator.body.components.layer_status[0]) --copy the layer status of the creator's first layer to every layer_status of the desired creature - item.body.components.layer_status[i].gone = true - end - if item_type == 'CORPSE' then - item.corpse_flags.unbutchered = true - end - if not generic then - -- keeps the body part that the user selected to spawn the item from - item.body.components.body_part_status[bodypart].missing = false - -- restores the selected layer of the selected body part - item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[partlayer].layer_id].gone = false - elseif generic then - for i in pairs(creatorBody.body_parts) do - for n in pairs(creatorBody.body_parts[i].layers) do - if item_type == 'CORPSE' then - item.body.components.body_part_status[i].missing = false - item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false - else - -- search through the target creature's body parts and bring back every one which has the desired material - if creatorRaceRaw.tissue[creatorBody.body_parts[i].layers[n].tissue_id].tissue_material_str[1] == layerMat and creatorBody.body_parts[i].token ~= 'SKULL' and not creatorBody.body_parts[i].flags.SMALL then - item.body.components.body_part_status[i].missing = false - item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false - -- save the index of the bone layer to a variable - end - end - end - end - end - -- brings back every tissue layer associated with a body part if we're spawning the entire thing - if wholePart then - for i in pairs(creatorBody.body_parts[bodypart].layers) do - item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[i].layer_id].gone = false - item.corpse_flags.separated_part = true - item.corpse_flags.unbutchered = true - end - end - -- DO THIS LAST or else the game crashes for some reason - item.caste = casteID - end -end - -function hackWish(unit, opts) - script.start(function() - local matok, mattype, matindex - local partlayerok, partlayerID = false, 0 - local qualityok, quality = false, df.item_quality.Ordinary - local itemok, itemtype, itemsubtype = showItemPrompt('What item do you want?', +local default_accessors = { + get_unit = function(opts) + return tonumber(opts.unit) and df.unit.find(tonumber(opts.unit)) or + dfhack.gui.getSelectedUnit(true) + end, + get_item_type = function() + return showItemPrompt('What item do you want?', function(itype) return df.item_type[itype] ~= 'FOOD' end, true) - local corpsepieceGeneric - local bodypart = -1 - if not itemok then return end - if not usesCreature(itemtype) then - matok, mattype, matindex = showMaterialPrompt('Wish', 'And what material should it be made of?', - not opts.unrestricted and getMatFilter(itemtype, opts) or nil) - if not matok then return end - else - local creatureok, _, creatureTable = script.showListPrompt('Wish', 'What creature should it be?', - COLOR_LIGHTGREEN, getCreatureList(), 1, true) - if not creatureok then return end - mattype, matindex = getCreatureRaceAndCaste(creatureTable[3]) - end - if df.item_type[itemtype] == 'CORPSEPIECE' then - local bodpartok, bodypartLocal = script.showListPrompt('Wish', 'What body part should it be?', - COLOR_LIGHTGREEN, - getCreaturePartList(mattype, matindex), 1, true) - -- createCorpsePiece() references the bodypart variable so it can't be local to here - bodypart = bodypartLocal - if bodypart == 1 then - corpsepieceGeneric = true - end - if not bodpartok then return end - if not corpsepieceGeneric then -- probably a better way of doing this tbh - partlayerok, partlayerID = script.showListPrompt('Wish', 'What tissue layer should it be?', - COLOR_LIGHTGREEN, - getCreaturePartLayerList(mattype, matindex, bodypart - 2), 1, true) - else - partlayerok, partlayerID = script.showListPrompt('Wish', 'What creature material should it be?', - COLOR_LIGHTGREEN, - getCreatureMaterialList(mattype, matindex), 1, true) - end - if not partlayerok then return end - elseif not no_quality_item_types[df.item_type[itemtype]] then - qualityok, quality = script.showListPrompt('Wish', 'What quality should it be?', COLOR_LIGHTGREEN, - qualityTable()) - if not qualityok then return end - end - local description - if df.item_type[itemtype] == 'SLAB' then - local descriptionok - descriptionok, description = script.showInputPrompt('Slab', 'What should the slab say?', COLOR_WHITE) - if not descriptionok then return end - end - if bodypart ~= -1 then - --the offsets here are cause indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt - bodypart = bodypart - 2 - end - local count = opts.count - if not count then - repeat - local amountok, amount = script.showInputPrompt('Wish', 'How many do you want? (numbers only!)', - COLOR_LIGHTGREEN) - if not amountok then return end - count = tonumber(amount) - until count - end - if not mattype or not itemtype then - return false - end - if df.item_type.attrs[itemtype].is_stackable then - createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, count) - else - for _ = 1,count do - if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then - createCorpsePiece(unit, bodypart, partlayerID - 2, mattype, matindex, corpsepieceGeneric) - else - createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) - end - end - end - return true - end) -end + end, + get_mat = function(itype, opts) + return showMaterialPrompt('Wish', 'And what material should it be made of?', + not opts.unrestricted and getMatFilter(itype, opts) or nil) + end, + get_creature_mat = function() + local creatureok, _, creatureTable = script.showListPrompt('Wish', 'What creature should it be?', + COLOR_LIGHTGREEN, getCreatureList(), 1, true) + if not creatureok then return false end + return true, getCreatureRaceAndCaste(creatureTable[3]) + end, + get_corpse_part = function(mattype, matindex) + return script.showListPrompt('Wish', 'What body part should it be?', + COLOR_LIGHTGREEN, getCreaturePartList(mattype, matindex), 1, true) + end, + get_tissue_layer = function(mattype, matindex, bodypart) + return script.showListPrompt('Wish', 'What tissue layer should it be?', + COLOR_LIGHTGREEN, getCreaturePartLayerList(mattype, matindex, bodypart), 1, true) + end, + get_creature_part_mat = function(mattype, matindex) + return script.showListPrompt('Wish', 'What creature material should it be?', + COLOR_LIGHTGREEN, getCreatureMaterialList(mattype, matindex), 1, true) + end, + get_quality = function() + return script.showListPrompt('Wish', 'What quality should it be?', + COLOR_LIGHTGREEN, qualityTable()) + end, + get_description = function() + return script.showInputPrompt('Slab', 'What should the slab say?', COLOR_WHITE) + end, + get_count = function() + return script.showInputPrompt('Wish', 'How many do you want? (numbers only!)', + COLOR_LIGHTGREEN) + end, +} -local function get_first_citizen() - local citizens = dfhack.units.getCitizens() - if not citizens or not citizens[1] then - qerror('Could not choose a creator unit. Please select one in the UI') - end - return citizens[1] +if not dfhack.isMapLoaded() then + qerror('create-item needs a loaded map to work') end -local function get_creator(opts) - return tonumber(opts.unit) and df.unit.find(tonumber(opts.unit)) or - dfhack.gui.getSelectedUnit(true) or - get_first_citizen() -end +local opts = {} +local positionals = argparse.processArgsGetopt({...}, { + {nil, 'startup', handler = function() opts.startup = true end}, + {'f', 'unrestricted', handler = function() opts.unrestricted = true end}, + {'h', 'help', handler = function() opts.help = true end}, + { + 'u', + 'unit', + hasArg = true, + handler = function(arg) opts.unit = argparse.nonnegativeInt(arg, 'unit') end, + }, + { + 'c', + 'count', + hasArg = true, + handler = function(arg) opts.count = argparse.nonnegativeInt(arg, 'count') end, + }, +}) -if dfhack_flags.module then +if positionals[1] == 'help' then opts.help = true end +if opts.help then + print(dfhack.script_help()) return end -validArgs = utils.invert{ - 'startup', - 'unrestricted', - 'unit', - 'count', -} - -local opts = utils.processArgs({...}, validArgs) - if opts.startup then eventful.onReactionComplete.hackWishP = function(reaction, unit) if not reaction.code:find('DFHACK_WISH') then return end - hackWish(unit, {count = 1}) + local accessors = copyall(default_accessors) + accessors.get_unit = function() return unit end + createitem.hackWish(accessors, {count = 1}) end return end -hackWish(get_creator(opts), opts) +createitem.hackWish(default_accessors, opts) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index b790ac17e5..df1f2f8fcb 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -1,119 +1,436 @@ -- creates an item of a given type and material --author expwnent -local utils = require 'utils' - -local validArgs = utils.invert({ - 'help', - 'creator', - 'material', - 'item', --- 'creature', --- 'caste', - 'leftHand', - 'rightHand', - 'matchingGloves', - 'matchingShoes', - 'quality' -}) -local organicTypes = utils.invert({ - df.item_type.REMAINS, - df.item_type.FISH, - df.item_type.FISH_RAW, - df.item_type.VERMIN, - df.item_type.PET, - df.item_type.EGG, -}) +local guidm = require('gui.dwarfmode') +local script = require('gui.script') +local utils = require('utils') -local badTypes = utils.invert({ - df.item_type.CORPSE, - df.item_type.CORPSEPIECE, - df.item_type.FOOD, -}) +local no_quality_item_types = utils.invert{ + 'BAR', + 'SMALLGEM', + 'BLOCKS', + 'ROUGH', + 'BOULDER', + 'WOOD', + 'CORPSE', + 'CORPSEPIECE', + 'REMAINS', + 'MEAT', + 'FISH', + 'FISH_RAW', + 'VERMIN', + 'PET', + 'SEEDS', + 'PLANT', + 'SKIN_TANNED', + 'PLANT_GROWTH', + 'THREAD', + 'DRINK', + 'POWDER_MISC', + 'CHEESE', + 'FOOD', + 'LIQUID_MISC', + 'COIN', + 'GLOB', + 'ROCK', + 'EGG', + 'BRANCH', +} -function createItem(creatorID, item, material, leftHand, rightHand, quality, matchingGloves, matchingShoes) - local itemQuality = df.item_quality[quality] or tonumber(quality) or df.item_quality.Ordinary - - local creator = df.unit.find(creatorID) - if not creator then - error 'Invalid creator.' - end - - if not item then - error 'Invalid item.' - end - local itemType = dfhack.items.findType(item) - if itemType == -1 then - error 'Invalid item.' - end - local itemSubtype = dfhack.items.findSubtype(item) - - if organicTypes[itemType] then - --TODO: look up creature and caste - error 'Not yet supported.' - end - - if badTypes[itemType] then - error 'Not supported.' - end - - if not material then - error 'Invalid material.' - end - local materialInfo = dfhack.matinfo.find(material) - if not materialInfo then - error 'Invalid material.' - end - - local item1 = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) - local item = df.item.find(item1) - - item:setQuality(itemQuality) - - if matchingGloves then - if not isGloves(item) then - error "Passed -matchingGloves with non-glove item" - end - - local item2 = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) - local item_alt = df.item.find(item2) - item.handedness[0] = 1 - item_alt.handedness[1] = 1 - item_alt:setQuality(itemQuality) - end - if matchingShoes then - if not string.find(item.subtype.id, "ITEM_SHOES") then - error "Passed -matchingShoes with non-shoe item" - end - - local item3 = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) - local item2_alt = df.item.find(item3) - item2_alt:setQuality(itemQuality) - end +local CORPSE_PIECES = utils.invert{'BONE', 'SKIN', 'CARTILAGE', 'TOOTH', 'NERVE', 'NAIL', 'HORN', 'HOOF', 'CHITIN', + 'SHELL', 'IVORY', 'SCALE'} +local HAIR_PIECES = utils.invert{'HAIR', 'EYEBROW', 'EYELASH', 'MOUSTACHE', 'CHIN_WHISKERS', 'SIDEBURNS'} +local LIQUID_PIECES = utils.invert{'BLOOD', 'PUS', 'VENOM', 'SWEAT', 'TEARS', 'SPIT', 'MILK'} + +local function usesCreature(itemtype) + local typesThatUseCreatures = { + REMAINS = true, + FISH = true, + FISH_RAW = true, + VERMIN = true, + PET = true, + EGG = true, + CORPSE = true, + CORPSEPIECE = true, + } + return typesThatUseCreatures[df.item_type[itemtype]] end -if moduleMode then - return +-- this part was written by four rabbits in a trenchcoat (ppaawwll) +local function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic) + -- (partlayer is also used to determine the material if we're spawning a "generic" body part (i'm just lazy lol)) + creatureID = tonumber(creatureID) + -- get the actual raws of the target creature + local creatorRaceRaw = df.creature_raw.find(creatureID) + local wholePart = false + casteID = tonumber(casteID) + bodypart = tonumber(bodypart) + partlayer = tonumber(partlayer) + -- somewhat similar to the bodypart variable below, a value of -1 here means that the user wants to spawn a whole body part. we set the partlayer to 0 (outermost) because the specific layer isn't important, and we're spawning them all anyway. if it's a generic corpsepiece we ignore it, as it gets added to anyway below (we can't do it below because between here and there there's lines that reference the part layer + if partlayer == -1 and not generic then + partlayer = 0 + wholePart = true + end + -- get body info for easy reference + local creatorBody = creatorRaceRaw.caste[casteID].body_info + local layerName + local layerMat = 'BONE' + local tissueID + local liquid = false + -- in the hackWish function, the bodypart variable is initialized to -1, which isn't changed if the spawned item is a corpse + local isCorpse = bodypart == -1 and not generic + if not generic and not isCorpse then -- if we have a specified body part and layer, figure all the stuff out about that + -- store the tissue id of the specific layer we selected + tissueID = tonumber(creatorBody.body_parts[bodypart].layers[partlayer].tissue_id) + local mats = {} + -- get the material name from the material itself + for i in string.gmatch(dfhack.matinfo.getToken(creatorRaceRaw.tissue[tissueID].mat_type, creatureID), '([^:]+)') do + table.insert(mats, i) + end + layerMat = mats[3] + layerName = creatorBody.body_parts[bodypart].layers[partlayer].layer_name + elseif not isCorpse then -- otherwise, figure out the mat name from the dual-use partlayer argument + layerMat = creatorRaceRaw.material[partlayer].id + layerName = layerMat + end + -- default is MEAT, so if anything else fails to change it to something else, we know that the body layer is a meat item + local item_type = 'MEAT' + -- get race name and layer name, both for finding the item material, and the latter for determining the corpsepiece flags to set + local raceName = string.upper(creatorRaceRaw.creature_id) + -- every key is a valid non-hair corpsepiece, so if we try to index a key that's not on the table, we don't have a non-hair corpsepiece + -- we do the same as above but with hair + -- if the layer is fat, spawn a glob of fat and DON'T check for other layer types + if layerName == 'FAT' then + item_type = 'GLOB' + elseif CORPSE_PIECES[layerName] or HAIR_PIECES[layerName] then -- check if hair + item_type = 'CORPSEPIECE' + elseif LIQUID_PIECES[layerName] then + item_type = 'LIQUID_MISC' + liquid = true + end + if isCorpse then + item_type = 'CORPSE' + generic = true + end + local itemType = dfhack.items.findType(item_type .. ':NONE') + local itemSubtype = dfhack.items.findSubtype(item_type .. ':NONE') + local material = 'CREATURE_MAT:' .. raceName .. ':' .. layerMat + local materialInfo = dfhack.matinfo.find(material) + local item_id = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) + local item = df.item.find(item_id) + if liquid then + local bucketMat = dfhack.matinfo.find('PLANT_MAT:NETHER_CAP:WOOD') + if not bucketMat then + for i,n in ipairs(df.global.world.raws.plants.all) do + if n.flags.TREE then + bucketMat = dfhack.matinfo.find('PLANT_MAT:' .. n.id .. ':WOOD') + end + if bucketMat then break end + end + end + local prevCursorPos = guidm.getCursorPos() + local bucketType = dfhack.items.findType('BUCKET:NONE') + local bucket = df.item.find(dfhack.items.createItem(bucketType, -1, bucketMat.type, bucketMat.index, creator)) + dfhack.items.moveToContainer(item, bucket) + guidm.setCursorPos(creator.pos) + dfhack.run_command('spotclean') + guidm.setCursorPos(prevCursorPos) + end + + -- if the item type is a corpsepiece, we know we have one, and then go on to set the appropriate flags + if item_type == 'CORPSEPIECE' then + if layerName == 'BONE' then -- check if bones + item.corpse_flags.bone = true + item.material_amount.Bone = 1 + elseif layerName == 'SKIN' then -- check if skin/leather + item.corpse_flags.leather = true + item.material_amount.Leather = 1 + -- elseif layerName == "CARTILAGE" then -- check if cartilage (NO SPECIAL FLAGS) + elseif layerName == 'HAIR' then -- check if hair (simplified from before) + item.corpse_flags.hair_wool = true + item.material_amount.HairWool = 1 + if materialInfo.material.flags.YARN then + item.corpse_flags.yarn = true + item.material_amount.Yarn = 1 + end + elseif layerName == 'TOOTH' or layerName == 'IVORY' then -- check if tooth + item.corpse_flags.tooth = true + item.material_amount.Tooth = 1 + elseif layerName == 'NERVE' then -- check if nervous tissue + item.corpse_flags.skull1 = true -- apparently "skull1" is supposed to be named "rots/can_rot" + item.corpse_flags.separated_part = true + -- elseif layerName == "NAIL" then -- check if nail (NO SPECIAL FLAGS) + elseif layerName == 'HORN' or layerName == 'HOOF' then -- check if nail + item.corpse_flags.horn = true + item.material_amount.Horn = 1 + elseif layerName == 'SHELL' then + item.corpse_flags.shell = true + item.material_amount.Shell = 1 + end + -- checking for skull + if not generic and not isCorpse and creatorBody.body_parts[bodypart].token == 'SKULL' then + item.corpse_flags.skull2 = true + end + end + local matType + -- figure out which material type the material is (probably a better way of doing this but whatever) + for i in pairs(creatorRaceRaw.tissue) do + if creatorRaceRaw.tissue[i].tissue_material_str[1] == layerMat then + matType = creatorRaceRaw.tissue[i].mat_type + end + end + if item_type == 'CORPSEPIECE' or item_type == 'CORPSE' then + --referencing the source unit for, material, relation purposes??? + item.race = creatureID + item.normal_race = creatureID + item.normal_caste = casteID + -- usually the first two castes are for the creature's sex, so we set the item's sex to the caste if both the creature has one and it's a valid sex id (0 or 1) + if casteID < 2 and #(creatorRaceRaw.caste) > 1 then + item.sex = casteID + else + item.sex = -1 -- it + end + -- on a dwarf tissue index 3 (bone) is 22, but this is not always the case for all creatures, so we get the mat_type of index 3 instead + -- here we also set the actual referenced creature material of the corpsepiece + item.bone1.mat_type = matType + item.bone1.mat_index = creatureID + item.bone2.mat_type = matType + item.bone2.mat_index = creatureID + -- skin (and presumably other parts) use body part modifiers for size or amount + for i = 0,200 do -- fuck it this works + -- inserts + item.body.bp_modifiers:insert('#', 1) --jus,t, set a lot of it to one who cares + end + -- copy target creature's relsizes to the item's's body relsizes thing + for i,n in pairs(creatorBody.body_parts) do + -- inserts + item.body.body_part_relsize:insert('#', n.relsize) + item.body.components.body_part_status:insert(i, creator.body.components.body_part_status[0]) --copy the status of the creator's first part to every body_part_status of the desired creature + item.body.components.body_part_status[i].missing = true + end + for i in pairs(creatorBody.layer_part) do + -- inserts + item.body.components.layer_status:insert(i, creator.body.components.layer_status[0]) --copy the layer status of the creator's first layer to every layer_status of the desired creature + item.body.components.layer_status[i].gone = true + end + if item_type == 'CORPSE' then + item.corpse_flags.unbutchered = true + end + if not generic then + -- keeps the body part that the user selected to spawn the item from + item.body.components.body_part_status[bodypart].missing = false + -- restores the selected layer of the selected body part + item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[partlayer].layer_id].gone = false + elseif generic then + for i in pairs(creatorBody.body_parts) do + for n in pairs(creatorBody.body_parts[i].layers) do + if item_type == 'CORPSE' then + item.body.components.body_part_status[i].missing = false + item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false + else + -- search through the target creature's body parts and bring back every one which has the desired material + if creatorRaceRaw.tissue[creatorBody.body_parts[i].layers[n].tissue_id].tissue_material_str[1] == layerMat and creatorBody.body_parts[i].token ~= 'SKULL' and not creatorBody.body_parts[i].flags.SMALL then + item.body.components.body_part_status[i].missing = false + item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false + -- save the index of the bone layer to a variable + end + end + end + end + end + -- brings back every tissue layer associated with a body part if we're spawning the entire thing + if wholePart then + for i in pairs(creatorBody.body_parts[bodypart].layers) do + item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[i].layer_id].gone = false + item.corpse_flags.separated_part = true + item.corpse_flags.unbutchered = true + end + end + -- DO THIS LAST or else the game crashes for some reason + item.caste = casteID + end end -local args = utils.processArgs({...}, validArgs) +local function createItem(mat, itemType, quality, creator, description, amount) + local item = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) + local item2 = nil + assert(item, 'failed to create item') + quality = math.max(0, math.min(5, quality - 1)) + item:setQuality(quality) + if df.item_type[itemType[1]] == 'SLAB' then + item.description = description + end + if df.item_type[itemType[1]] == 'GLOVES' then + --create matching gloves + item:setGloveHandedness(1) + item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) + assert(item2, 'failed to create item') + item2:setQuality(quality) + item2:setGloveHandedness(2) + end + if df.item_type[itemType[1]] == 'SHOES' then + --create matching shoes + item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) + assert(item2, 'failed to create item') + item2:setQuality(quality) + end + if tonumber(amount) > 1 then + item:setStackSize(amount) + if item2 then item2:setStackSize(amount) end + end +end -if args.help then - print(dfhack.script_help()) - return +local function get_first_citizen() + local citizens = dfhack.units.getCitizens() + if not citizens or not citizens[1] then + qerror('Could not choose a creator unit. Please select one in the UI') + end + return citizens[1] +end + +function hackWish(accessors, opts) + local unit = accessors.get_unit(opts) or get_first_citizen() + script.start(function() + local matok, mattype, matindex + local partlayerok, partlayerID = false, 0 + local qualityok, quality = false, df.item_quality.Ordinary + local itemok, itemtype, itemsubtype = accessors.get_item_type() + local corpsepieceGeneric + local bodypart = -1 + if not itemok then return end + if not usesCreature(itemtype) then + matok, mattype, matindex = accessors.get_mat(itemtype, opts) + if not matok then return end + else + local creatureok + creatureok, mattype, matindex = accessors.get_creature_mat() + if not creatureok then return end + end + if df.item_type[itemtype] == 'CORPSEPIECE' then + local bodpartok, bodypartLocal = accessors.get_corpse_part(mattype, matindex) + -- createCorpsePiece() references the bodypart variable so it can't be local to here + bodypart = bodypartLocal + if bodypart == 1 then + corpsepieceGeneric = true + else + --the offsets here are cause indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt + bodypart = bodypart - 2 + end + if not bodpartok then return end + if not corpsepieceGeneric then -- probably a better way of doing this tbh + partlayerok, partlayerID = accessors.get_tissue_layer(mattype, matindex, bodypart) + partlayerID = partlayerID - 1 + else + partlayerok, partlayerID = accessors.get_creature_part_mat(mattype, matindex) + end + if not partlayerok then return end + partlayerID = partlayerID - 1 + elseif not no_quality_item_types[df.item_type[itemtype]] then + qualityok, quality = accessors.get_quality() + if not qualityok then return end + end + local description + if df.item_type[itemtype] == 'SLAB' then + local descriptionok + descriptionok, description = accessors.get_description() + if not descriptionok then return end + end + local count = opts.count + if not count then + repeat + local amountok, amount = accessors.get_count() + if not amountok then return end + count = tonumber(amount) + until count + end + if not mattype or not itemtype then + return false + end + if df.item_type.attrs[itemtype].is_stackable then + createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, count) + else + for _ = 1,count do + if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then + createCorpsePiece(unit, bodypart, partlayerID, mattype, matindex, corpsepieceGeneric) + else + createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) + end + end + end + return true + end) end -if args.creator == '\\LAST' then - args.creator = tostring(df.global.unit_next_id-1) +if dfhack_flags.module then + return end -function isGloves(i) - for key,value in pairs(i) do - if key == 'handedness' then - return true - end - end - return false +if not dfhack.isMapLoaded() then + qerror('modtools/create-item needs a loaded map to work') end -createItem(tonumber(args.creator), args.item, args.material, args.leftHand, args.rightHand, args.quality, args.matchingGloves, args.matchingShoes) +local opts = {} +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler = function() opts.help = true end}, + {'u', 'unit', hasArg = true, handler = function(arg) opts.unit = arg end}, + {'i', 'item', hasArg = true, handler = function(arg) opts.item = arg end}, + {'m', 'material', hasArg = true, handler = function(arg) opts.mat = arg end}, + {'q', 'quality', hasArg = true, handler = function(arg) opts.quality = arg end}, + {'d', 'description', hasArg = true, handler = function(arg) opts.description = arg end}, + { + 'c', + 'count', + hasArg = true, + handler = function(arg) opts.count = argparse.nonnegativeInt(arg, 'count') end, + }, +}) + +if positionals[1] == 'help' then opts.help = true end +if opts.help then + print(dfhack.script_help()) + return +end + +if opts.unit == '\\LAST' then + opts.unit = tostring(df.global.unit_next_id - 1) +end + +local accessors = { + get_unit = function() + return tonumber(opts.unit) and df.unit.find(tonumber(opts.unit)) or nil + end, + get_item_type = function() + local item_type = dfhack.items.findType(opts.item) + if item_type == -1 then + error('invalid item: ' .. tostring(opts.item)) + end + return true, item_type, dfhack.items.findSubtype(opts.item) + end, + get_mat = function() + local mat_info = dfhack.matinfo.find(opts.mat) + if not mat_info then + error('invalid material: ' .. tostring(opts.mat)) + end + return true, mat_info['type'], mat_info.index + end, + get_corpse_part = function() + -- TODO: return 2 if it's a specific corpse part + return true, 1 + end, + get_tissue_layer = function() + error('not implemented') + end, + get_quality = function() + return true, opts.quality or df.item_quality.Ordinary + end, + get_description = function() + return true, opts.description or error('description not specified') + end, + get_count = function() + return true, opts.count or 1 + end, +} +accessors.get_creature_mat = accessors.get_mat +accessors.get_creature_part_mat = accessors.get_mat + +hackWish(accessors, {}) From c28574f2002f260ec4c2c0fdbfe03c46a24c636a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Apr 2023 02:53:26 -0700 Subject: [PATCH 175/732] fix filters for blocks and bars --- gui/create-item.lua | 6 +++++- modtools/create-item.lua | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gui/create-item.lua b/gui/create-item.lua index e5d378845c..4c803a0d0c 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -80,7 +80,11 @@ local function getRestrictiveMatFilter(itemType, opts) return (mat.flags.IS_STONE) end, BAR = function(mat, parent, typ, idx) - return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id == 'COAL') + return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id == 'COAL' or + mat.id == 'POTASH' or mat.id == 'ASH' or mat.id == 'PEARLASH') + end, + BLOCKS = function(mat, parent, typ, idx) + return mat.flags.IS_STONE or mat.flags.IS_METAL or mat.flags.IS_GLASS end, } for k,v in ipairs{'GOBLET', 'FLASK', 'TOY', 'RING', 'CROWN', 'SCEPTER', 'FIGURINE', 'TOOL'} do diff --git a/modtools/create-item.lua b/modtools/create-item.lua index df1f2f8fcb..3d4c8b78c6 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -1,5 +1,6 @@ -- creates an item of a given type and material --author expwnent +--@module=true local guidm = require('gui.dwarfmode') local script = require('gui.script') From 77cfc751fb2dd8f6d3633cd7561d80ccca7373d6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Apr 2023 03:44:07 -0700 Subject: [PATCH 176/732] handle water and lye and drinks --- docs/modtools/create-item.rst | 8 +++--- gui/create-item.lua | 3 ++- modtools/create-item.lua | 49 +++++++++++++++++++---------------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/docs/modtools/create-item.rst b/docs/modtools/create-item.rst index f33ec59a62..586dc9b397 100644 --- a/docs/modtools/create-item.rst +++ b/docs/modtools/create-item.rst @@ -3,7 +3,7 @@ modtools/create-item .. dfhack-tool:: :summary: Create arbitrary items. - :tags: unavailable dev + :tags: dev This tool provides a commandline interface for creating items of your choice. @@ -21,8 +21,10 @@ Examples Have unit 23145 create an exceptionally crafted iron pick. ``modtools/create-item -u 323 -i CORPSEPIECE:NONE -m CREATURE_MAT:DWARF:BRAIN`` Have unit 323 produce a lump of brain. -``modtools/create-item -i BOULDER:NONE -m INORGANIC:ADAMANTINE -c 5`` -``modtools/create-item -i BOULDER:NONE -m PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK`` +``modtools/create-item -i BOULDER:NONE -m INORGANIC:RAW_ADAMANTINE -c 5`` + Spawn 5 raw adamantine boulders. +``modtools/create-item -i DRINK:NONE -m PLANT:MUSHROOM_HELMET_PLUMP:DRINK`` + Spawn a barrel of dwarven ale. Options ------- diff --git a/gui/create-item.lua b/gui/create-item.lua index 4c803a0d0c..ce69e817ad 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -118,7 +118,8 @@ local function getMatFilter(itemtype, opts) return (mat.flags.CHEESE_PLANT or mat.flags.CHEESE_CREATURE) end, LIQUID_MISC = function(mat, parent, typ, idx) - return (mat.flags.LIQUID_MISC_PLANT or mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER) + return mat.id == 'WATER' or mat.id == 'LYE' or mat.flags.LIQUID_MISC_PLANT or + mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER end, POWDER_MISC = function(mat, parent, typ, idx) return (mat.flags.POWDER_MISC_PLANT or mat.flags.POWDER_MISC_CREATURE) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index 3d4c8b78c6..46dcffeb85 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -2,7 +2,7 @@ --author expwnent --@module=true -local guidm = require('gui.dwarfmode') +local argparse = require('argparse') local script = require('gui.script') local utils = require('utils') @@ -57,6 +57,20 @@ local function usesCreature(itemtype) return typesThatUseCreatures[df.item_type[itemtype]] end +local function moveToContainer(item, creator, container_type) + local containerMat = dfhack.matinfo.find('PLANT_MAT:NETHER_CAP:WOOD') + if not containerMat then + for i,n in ipairs(df.global.world.raws.plants.all) do + if n.flags.TREE then + containerMat = dfhack.matinfo.find('PLANT_MAT:' .. n.id .. ':WOOD') + end + if containerMat then break end + end + end + local bucketType = dfhack.items.findType(container_type .. ':NONE') + local bucket = df.item.find(dfhack.items.createItem(bucketType, -1, containerMat.type, containerMat.index, creator)) + dfhack.items.moveToContainer(item, bucket) +end -- this part was written by four rabbits in a trenchcoat (ppaawwll) local function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic) -- (partlayer is also used to determine the material if we're spawning a "generic" body part (i'm just lazy lol)) @@ -120,22 +134,7 @@ local function createCorpsePiece(creator, bodypart, partlayer, creatureID, caste local item_id = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) local item = df.item.find(item_id) if liquid then - local bucketMat = dfhack.matinfo.find('PLANT_MAT:NETHER_CAP:WOOD') - if not bucketMat then - for i,n in ipairs(df.global.world.raws.plants.all) do - if n.flags.TREE then - bucketMat = dfhack.matinfo.find('PLANT_MAT:' .. n.id .. ':WOOD') - end - if bucketMat then break end - end - end - local prevCursorPos = guidm.getCursorPos() - local bucketType = dfhack.items.findType('BUCKET:NONE') - local bucket = df.item.find(dfhack.items.createItem(bucketType, -1, bucketMat.type, bucketMat.index, creator)) - dfhack.items.moveToContainer(item, bucket) - guidm.setCursorPos(creator.pos) - dfhack.run_command('spotclean') - guidm.setCursorPos(prevCursorPos) + moveToContainer(item, creator, 'BUCKET') end -- if the item type is a corpsepiece, we know we have one, and then go on to set the appropriate flags @@ -256,24 +255,28 @@ local function createItem(mat, itemType, quality, creator, description, amount) local item = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) local item2 = nil assert(item, 'failed to create item') + local mat_token = dfhack.matinfo.decode(item):getToken() quality = math.max(0, math.min(5, quality - 1)) item:setQuality(quality) - if df.item_type[itemType[1]] == 'SLAB' then + local item_type = df.item_type[itemType[1]] + if item_type == 'SLAB' then item.description = description - end - if df.item_type[itemType[1]] == 'GLOVES' then + elseif item_type == 'GLOVES' then --create matching gloves item:setGloveHandedness(1) item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) assert(item2, 'failed to create item') item2:setQuality(quality) item2:setGloveHandedness(2) - end - if df.item_type[itemType[1]] == 'SHOES' then + elseif item_type == 'SHOES' then --create matching shoes item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) assert(item2, 'failed to create item') item2:setQuality(quality) + elseif item_type == 'DRINK' then + moveToContainer(item, creator, 'BARREL') + elseif mat_token == 'WATER' or mat_token == 'LYE' then + moveToContainer(item, creator, 'BUCKET') end if tonumber(amount) > 1 then item:setStackSize(amount) @@ -422,7 +425,7 @@ local accessors = { error('not implemented') end, get_quality = function() - return true, opts.quality or df.item_quality.Ordinary + return true, (tonumber(opts.quality) or df.item_quality.Ordinary) + 1 end, get_description = function() return true, opts.description or error('description not specified') From 88b88b961d827e0bb2451631187749cf242c012e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Apr 2023 03:51:23 -0700 Subject: [PATCH 177/732] fix example token --- docs/modtools/create-item.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modtools/create-item.rst b/docs/modtools/create-item.rst index 586dc9b397..edcc698d8c 100644 --- a/docs/modtools/create-item.rst +++ b/docs/modtools/create-item.rst @@ -19,8 +19,8 @@ Examples ``modtools/create-item -u 23145 -i WEAPON:ITEM_WEAPON_PICK -m INORGANIC:IRON -q4`` Have unit 23145 create an exceptionally crafted iron pick. -``modtools/create-item -u 323 -i CORPSEPIECE:NONE -m CREATURE_MAT:DWARF:BRAIN`` - Have unit 323 produce a lump of brain. +``modtools/create-item -u 323 -i MEAT:NONE -m CREATURE_MAT:DWARF:BRAIN`` + Have unit 323 produce a lump of (prepared) brain. ``modtools/create-item -i BOULDER:NONE -m INORGANIC:RAW_ADAMANTINE -c 5`` Spawn 5 raw adamantine boulders. ``modtools/create-item -i DRINK:NONE -m PLANT:MUSHROOM_HELMET_PLUMP:DRINK`` From c44d3ba70b0161375170db8405d2a22138b8d126 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 24 Apr 2023 16:08:45 -0700 Subject: [PATCH 178/732] wip --- gui/create-item.lua | 118 +++++++++++++++++++++++---------------- modtools/create-item.lua | 66 +++------------------- 2 files changed, 79 insertions(+), 105 deletions(-) diff --git a/gui/create-item.lua b/gui/create-item.lua index ce69e817ad..0a29a2c58f 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -6,7 +6,6 @@ local argparse = require('argparse') local createitem = reqscript('modtools/create-item') local eventful = require('plugins.eventful') local script = require('gui.script') -local utils = require('utils') local function getGenderString(gender) local sym = df.pronoun_type.attrs[gender].symbol @@ -14,48 +13,62 @@ local function getGenderString(gender) return '(' .. sym .. ')' end +local function usesCreature(itemtype) + local typesThatUseCreatures = { + REMAINS = true, + FISH = true, + FISH_RAW = true, + VERMIN = true, + PET = true, + EGG = true, + CORPSE = true, + CORPSEPIECE = true, + } + return typesThatUseCreatures[df.item_type[itemtype]] +end + local function getCreatureList() local crList = {} - for k,cr in ipairs(df.global.world.raws.creatures.alphabetic) do - for kk,ca in ipairs(cr.caste) do + for k, cr in ipairs(df.global.world.raws.creatures.alphabetic) do + for kk, ca in ipairs(cr.caste) do local str = ca.caste_name[0] str = str .. ' ' .. getGenderString(ca.sex) - table.insert(crList, {str, nil, ca}) + table.insert(crList, { str, nil, ca }) end end return crList end local function getCreaturePartList(creatureID, casteID) - local crpList = {{'generic'}} - for k,crp in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts) do + local crpList = { { 'generic' } } + for k, crp in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts) do local str = crp.name_singular[0][0] - table.insert(crpList, {str}) + table.insert(crpList, { str }) end return crpList end local function getCreaturePartLayerList(creatureID, casteID, partID) - local crplList = {{'whole'}} - for k,crpl in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts[partID].layers) do + local crplList = { { 'whole' } } + for k, crpl in ipairs(df.global.world.raws.creatures.all[creatureID].caste[casteID].body_info.body_parts[partID].layers) do local str = crpl.layer_name - table.insert(crplList, {str}) + table.insert(crplList, { str }) end return crplList end local function getCreatureMaterialList(creatureID, casteID) local crmList = {} - for k,crm in ipairs(df.global.world.raws.creatures.all[creatureID].material) do + for k, crm in ipairs(df.global.world.raws.creatures.all[creatureID].material) do local str = crm.id - table.insert(crmList, {str}) + table.insert(crmList, { str }) end return crmList end local function getCreatureRaceAndCaste(caste) return df.global.world.raws.creatures.list_creature[caste.index], - df.global.world.raws.creatures.list_caste[caste.index] + df.global.world.raws.creatures.list_caste[caste.index] end local function getRestrictiveMatFilter(itemType, opts) @@ -81,19 +94,19 @@ local function getRestrictiveMatFilter(itemType, opts) end, BAR = function(mat, parent, typ, idx) return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id == 'COAL' or - mat.id == 'POTASH' or mat.id == 'ASH' or mat.id == 'PEARLASH') + mat.id == 'POTASH' or mat.id == 'ASH' or mat.id == 'PEARLASH') end, BLOCKS = function(mat, parent, typ, idx) return mat.flags.IS_STONE or mat.flags.IS_METAL or mat.flags.IS_GLASS end, } - for k,v in ipairs{'GOBLET', 'FLASK', 'TOY', 'RING', 'CROWN', 'SCEPTER', 'FIGURINE', 'TOOL'} do + for k, v in ipairs { 'GOBLET', 'FLASK', 'TOY', 'RING', 'CROWN', 'SCEPTER', 'FIGURINE', 'TOOL' } do itemTypes[v] = itemTypes.INSTRUMENT end - for k,v in ipairs{'SHOES', 'SHIELD', 'HELM', 'GLOVES'} do + for k, v in ipairs { 'SHOES', 'SHIELD', 'HELM', 'GLOVES' } do itemTypes[v] = itemTypes.ARMOR end - for k,v in ipairs{'EARRING', 'BRACELET'} do + for k, v in ipairs { 'EARRING', 'BRACELET' } do itemTypes[v] = itemTypes.AMULET end itemTypes.BOULDER = itemTypes.ROCK @@ -119,7 +132,7 @@ local function getMatFilter(itemtype, opts) end, LIQUID_MISC = function(mat, parent, typ, idx) return mat.id == 'WATER' or mat.id == 'LYE' or mat.flags.LIQUID_MISC_PLANT or - mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER + mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER end, POWDER_MISC = function(mat, parent, typ, idx) return (mat.flags.POWDER_MISC_PLANT or mat.flags.POWDER_MISC_CREATURE) @@ -144,17 +157,17 @@ local function getMatFilter(itemtype, opts) end local function qualityTable() - return {{'None'}, - {'-Well-crafted-'}, - {'+Finely-crafted+'}, - {'*Superior*'}, - {string.char(240) .. 'Exceptional' .. string.char(240)}, - {string.char(15) .. 'Masterwork' .. string.char(15)}, + return { { 'None' }, + { '-Well-crafted-' }, + { '+Finely-crafted+' }, + { '*Superior*' }, + { string.char(240) .. 'Exceptional' .. string.char(240) }, + { string.char(15) .. 'Masterwork' .. string.char(15) }, } end local function showItemPrompt(text, item_filter, hide_none) - require('gui.materials').ItemTypeDialog{ + require('gui.materials').ItemTypeDialog { prompt = text, item_filter = item_filter, hide_none = hide_none, @@ -166,7 +179,7 @@ local function showItemPrompt(text, item_filter, hide_none) end local function showMaterialPrompt(title, prompt, filter, inorganic, creature, plant) - require('gui.materials').MaterialDialog{ + require('gui.materials').MaterialDialog { frame_title = title, prompt = prompt, mat_filter = filter, @@ -183,33 +196,42 @@ end local default_accessors = { get_unit = function(opts) return tonumber(opts.unit) and df.unit.find(tonumber(opts.unit)) or - dfhack.gui.getSelectedUnit(true) + dfhack.gui.getSelectedUnit(true) end, get_item_type = function() return showItemPrompt('What item do you want?', function(itype) return df.item_type[itype] ~= 'FOOD' end, true) end, get_mat = function(itype, opts) - return showMaterialPrompt('Wish', 'And what material should it be made of?', - not opts.unrestricted and getMatFilter(itype, opts) or nil) - end, - get_creature_mat = function() + if not usesCreature(itype) then + return showMaterialPrompt('Wish', 'And what material should it be made of?', + not opts.unrestricted and getMatFilter(itype, opts) or nil) + end local creatureok, _, creatureTable = script.showListPrompt('Wish', 'What creature should it be?', COLOR_LIGHTGREEN, getCreatureList(), 1, true) if not creatureok then return false end - return true, getCreatureRaceAndCaste(creatureTable[3]) - end, - get_corpse_part = function(mattype, matindex) - return script.showListPrompt('Wish', 'What body part should it be?', + local mattype, matindex = getCreatureRaceAndCaste(creatureTable[3]) + if df.item_type[itype] ~= 'CORPSEPIECE' then + return true, mattype, matindex, -1 + end + local bodpartok, bodypart = script.showListPrompt('Wish', 'What body part should it be?', COLOR_LIGHTGREEN, getCreaturePartList(mattype, matindex), 1, true) - end, - get_tissue_layer = function(mattype, matindex, bodypart) - return script.showListPrompt('Wish', 'What tissue layer should it be?', - COLOR_LIGHTGREEN, getCreaturePartLayerList(mattype, matindex, bodypart), 1, true) - end, - get_creature_part_mat = function(mattype, matindex) - return script.showListPrompt('Wish', 'What creature material should it be?', - COLOR_LIGHTGREEN, getCreatureMaterialList(mattype, matindex), 1, true) + if not bodpartok then return false end + local corpsepieceGeneric = false + local partlayerok, partlayerID + if bodypart == 1 then + corpsepieceGeneric = true + partlayerok, partlayerID = script.showListPrompt('Wish', 'What creature material should it be?', + COLOR_LIGHTGREEN, getCreatureMaterialList(mattype, matindex), 1, true) + else + --the offsets here are because indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt + bodypart = bodypart - 2 + partlayerok, partlayerID = script.showListPrompt('Wish', 'What tissue layer should it be?', + COLOR_LIGHTGREEN, getCreaturePartLayerList(mattype, matindex, bodypart), 1, true) + partlayerID = partlayerID - 1 + end + if not partlayerok then return end + return true, mattype, matindex, bodypart, partlayerID - 1, corpsepieceGeneric end, get_quality = function() return script.showListPrompt('Wish', 'What quality should it be?', @@ -229,10 +251,10 @@ if not dfhack.isMapLoaded() then end local opts = {} -local positionals = argparse.processArgsGetopt({...}, { - {nil, 'startup', handler = function() opts.startup = true end}, - {'f', 'unrestricted', handler = function() opts.unrestricted = true end}, - {'h', 'help', handler = function() opts.help = true end}, +local positionals = argparse.processArgsGetopt({ ... }, { + { nil, 'startup', handler = function() opts.startup = true end }, + { 'f', 'unrestricted', handler = function() opts.unrestricted = true end }, + { 'h', 'help', handler = function() opts.help = true end }, { 'u', 'unit', @@ -258,7 +280,7 @@ if opts.startup then if not reaction.code:find('DFHACK_WISH') then return end local accessors = copyall(default_accessors) accessors.get_unit = function() return unit end - createitem.hackWish(accessors, {count = 1}) + createitem.hackWish(accessors, { count = 1 }) end return end diff --git a/modtools/create-item.lua b/modtools/create-item.lua index 46dcffeb85..68be28d9f0 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -43,20 +43,6 @@ local CORPSE_PIECES = utils.invert{'BONE', 'SKIN', 'CARTILAGE', 'TOOTH', 'NERVE' local HAIR_PIECES = utils.invert{'HAIR', 'EYEBROW', 'EYELASH', 'MOUSTACHE', 'CHIN_WHISKERS', 'SIDEBURNS'} local LIQUID_PIECES = utils.invert{'BLOOD', 'PUS', 'VENOM', 'SWEAT', 'TEARS', 'SPIT', 'MILK'} -local function usesCreature(itemtype) - local typesThatUseCreatures = { - REMAINS = true, - FISH = true, - FISH_RAW = true, - VERMIN = true, - PET = true, - EGG = true, - CORPSE = true, - CORPSEPIECE = true, - } - return typesThatUseCreatures[df.item_type[itemtype]] -end - local function moveToContainer(item, creator, container_type) local containerMat = dfhack.matinfo.find('PLANT_MAT:NETHER_CAP:WOOD') if not containerMat then @@ -295,41 +281,12 @@ end function hackWish(accessors, opts) local unit = accessors.get_unit(opts) or get_first_citizen() script.start(function() - local matok, mattype, matindex - local partlayerok, partlayerID = false, 0 local qualityok, quality = false, df.item_quality.Ordinary local itemok, itemtype, itemsubtype = accessors.get_item_type() - local corpsepieceGeneric - local bodypart = -1 if not itemok then return end - if not usesCreature(itemtype) then - matok, mattype, matindex = accessors.get_mat(itemtype, opts) - if not matok then return end - else - local creatureok - creatureok, mattype, matindex = accessors.get_creature_mat() - if not creatureok then return end - end - if df.item_type[itemtype] == 'CORPSEPIECE' then - local bodpartok, bodypartLocal = accessors.get_corpse_part(mattype, matindex) - -- createCorpsePiece() references the bodypart variable so it can't be local to here - bodypart = bodypartLocal - if bodypart == 1 then - corpsepieceGeneric = true - else - --the offsets here are cause indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt - bodypart = bodypart - 2 - end - if not bodpartok then return end - if not corpsepieceGeneric then -- probably a better way of doing this tbh - partlayerok, partlayerID = accessors.get_tissue_layer(mattype, matindex, bodypart) - partlayerID = partlayerID - 1 - else - partlayerok, partlayerID = accessors.get_creature_part_mat(mattype, matindex) - end - if not partlayerok then return end - partlayerID = partlayerID - 1 - elseif not no_quality_item_types[df.item_type[itemtype]] then + local matok, mattype, matindex, bodypart, partlayerID, corpsepieceGeneric = accessors.get_mat(itemtype, opts) + if not matok then return end + if not no_quality_item_types[df.item_type[itemtype]] then qualityok, quality = accessors.get_quality() if not qualityok then return end end @@ -410,19 +367,16 @@ local accessors = { end return true, item_type, dfhack.items.findSubtype(opts.item) end, - get_mat = function() + get_mat = function(itype) local mat_info = dfhack.matinfo.find(opts.mat) if not mat_info then error('invalid material: ' .. tostring(opts.mat)) end - return true, mat_info['type'], mat_info.index - end, - get_corpse_part = function() - -- TODO: return 2 if it's a specific corpse part - return true, 1 - end, - get_tissue_layer = function() - error('not implemented') + -- TODO: also return bodypart, partlayerID, corpsepieceGeneric + if df.item_type[itype] ~= 'CORPSEPIECE' then + return true, mat_info['type'], mat_info.index, -1 + end + return false end, get_quality = function() return true, (tonumber(opts.quality) or df.item_quality.Ordinary) + 1 @@ -434,7 +388,5 @@ local accessors = { return true, opts.count or 1 end, } -accessors.get_creature_mat = accessors.get_mat -accessors.get_creature_part_mat = accessors.get_mat hackWish(accessors, {}) From 14f08baee3ebf721b3ec5673f5cc025f8a065a18 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 24 Apr 2023 17:28:39 -0700 Subject: [PATCH 179/732] one step closer --- gui/create-item.lua | 12 ++++++------ modtools/create-item.lua | 21 +++++++++++++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/gui/create-item.lua b/gui/create-item.lua index 0a29a2c58f..fbc0a71596 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -210,28 +210,28 @@ local default_accessors = { local creatureok, _, creatureTable = script.showListPrompt('Wish', 'What creature should it be?', COLOR_LIGHTGREEN, getCreatureList(), 1, true) if not creatureok then return false end - local mattype, matindex = getCreatureRaceAndCaste(creatureTable[3]) + local raceId, casteId = getCreatureRaceAndCaste(creatureTable[3]) if df.item_type[itype] ~= 'CORPSEPIECE' then - return true, mattype, matindex, -1 + return true, -1, raceId, casteId, -1 end local bodpartok, bodypart = script.showListPrompt('Wish', 'What body part should it be?', - COLOR_LIGHTGREEN, getCreaturePartList(mattype, matindex), 1, true) + COLOR_LIGHTGREEN, getCreaturePartList(raceId, casteId), 1, true) if not bodpartok then return false end local corpsepieceGeneric = false local partlayerok, partlayerID if bodypart == 1 then corpsepieceGeneric = true partlayerok, partlayerID = script.showListPrompt('Wish', 'What creature material should it be?', - COLOR_LIGHTGREEN, getCreatureMaterialList(mattype, matindex), 1, true) + COLOR_LIGHTGREEN, getCreatureMaterialList(raceId, casteId), 1, true) else --the offsets here are because indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt bodypart = bodypart - 2 partlayerok, partlayerID = script.showListPrompt('Wish', 'What tissue layer should it be?', - COLOR_LIGHTGREEN, getCreaturePartLayerList(mattype, matindex, bodypart), 1, true) + COLOR_LIGHTGREEN, getCreaturePartLayerList(raceId, casteId, bodypart), 1, true) partlayerID = partlayerID - 1 end if not partlayerok then return end - return true, mattype, matindex, bodypart, partlayerID - 1, corpsepieceGeneric + return true, -1, raceId, bodypart, partlayerID - 1, corpsepieceGeneric end, get_quality = function() return script.showListPrompt('Wish', 'What quality should it be?', diff --git a/modtools/create-item.lua b/modtools/create-item.lua index 68be28d9f0..c0a7798584 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -284,8 +284,10 @@ function hackWish(accessors, opts) local qualityok, quality = false, df.item_quality.Ordinary local itemok, itemtype, itemsubtype = accessors.get_item_type() if not itemok then return end - local matok, mattype, matindex, bodypart, partlayerID, corpsepieceGeneric = accessors.get_mat(itemtype, opts) + local matok, mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric = accessors.get_mat(itemtype, opts) if not matok then return end + print(mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric) + print(dfhack.matinfo.getToken(mattype, matindex)) if not no_quality_item_types[df.item_type[itemtype]] then qualityok, quality = accessors.get_quality() if not qualityok then return end @@ -312,7 +314,7 @@ function hackWish(accessors, opts) else for _ = 1,count do if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then - createCorpsePiece(unit, bodypart, partlayerID, mattype, matindex, corpsepieceGeneric) + createCorpsePiece(unit, bodypart, partlayerID, matindex, casteId, corpsepieceGeneric) else createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) end @@ -336,6 +338,7 @@ local positionals = argparse.processArgsGetopt({...}, { {'u', 'unit', hasArg = true, handler = function(arg) opts.unit = arg end}, {'i', 'item', hasArg = true, handler = function(arg) opts.item = arg end}, {'m', 'material', hasArg = true, handler = function(arg) opts.mat = arg end}, + {'t', 'caste', hasArg = true, handler = function(arg) opts.caste = arg end}, {'q', 'quality', hasArg = true, handler = function(arg) opts.quality = arg end}, {'d', 'description', hasArg = true, handler = function(arg) opts.description = arg end}, { @@ -356,6 +359,16 @@ if opts.unit == '\\LAST' then opts.unit = tostring(df.global.unit_next_id - 1) end +local function get_caste(race_id, caste) + if not caste then return 0 end + if tonumber(caste) then return tonumber(caste) end + caste = caste:lower() + for i, c in ipairs(df.creature_raw.find(race_id).caste) do + if caste == tostring(c.caste_id):lower() then return i end + end + return 0 +end + local accessors = { get_unit = function() return tonumber(opts.unit) and df.unit.find(tonumber(opts.unit)) or nil @@ -372,10 +385,10 @@ local accessors = { if not mat_info then error('invalid material: ' .. tostring(opts.mat)) end - -- TODO: also return bodypart, partlayerID, corpsepieceGeneric if df.item_type[itype] ~= 'CORPSEPIECE' then - return true, mat_info['type'], mat_info.index, -1 + return true, mat_info['type'], mat_info.index, get_caste(mat_info.index, opts.caste), -1 end + -- TODO: also return bodypart, partlayerID, corpsepieceGeneric return false end, get_quality = function() From 5fb980b5272fcda2a9f411f813998c891ce3ecf3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 25 Apr 2023 12:26:36 -0700 Subject: [PATCH 180/732] wip --- gui/create-item.lua | 2 +- modtools/create-item.lua | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gui/create-item.lua b/gui/create-item.lua index fbc0a71596..60114007fd 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -231,7 +231,7 @@ local default_accessors = { partlayerID = partlayerID - 1 end if not partlayerok then return end - return true, -1, raceId, bodypart, partlayerID - 1, corpsepieceGeneric + return true, -1, raceId, casteId, bodypart, partlayerID - 1, corpsepieceGeneric end, get_quality = function() return script.showListPrompt('Wish', 'What quality should it be?', diff --git a/modtools/create-item.lua b/modtools/create-item.lua index c0a7798584..ece8d5ef63 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -385,11 +385,12 @@ local accessors = { if not mat_info then error('invalid material: ' .. tostring(opts.mat)) end + local caste = get_caste(mat_info.index, opts.caste) if df.item_type[itype] ~= 'CORPSEPIECE' then - return true, mat_info['type'], mat_info.index, get_caste(mat_info.index, opts.caste), -1 + return true, mat_info['type'], mat_info.index, caste, -1 end -- TODO: also return bodypart, partlayerID, corpsepieceGeneric - return false + return true, -1, mat_info.index, caste, get_body_part(), get_part_layer() end, get_quality = function() return true, (tonumber(opts.quality) or df.item_quality.Ordinary) + 1 From 05b555ba3ed0a6d290312a3e75e0caa37442ab65 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 25 Apr 2023 17:24:51 -0700 Subject: [PATCH 181/732] implement spawning items at a specified location --- gui/create-item.lua | 4 +- modtools/create-item.lua | 108 +++++++++++++++++++++------------------ 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/gui/create-item.lua b/gui/create-item.lua index 60114007fd..a155b135cf 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -280,9 +280,9 @@ if opts.startup then if not reaction.code:find('DFHACK_WISH') then return end local accessors = copyall(default_accessors) accessors.get_unit = function() return unit end - createitem.hackWish(accessors, { count = 1 }) + script.start(createitem.hackWish, accessors, { count = 1 }) end return end -createitem.hackWish(default_accessors, opts) +script.start(createitem.hackWish, default_accessors, opts) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index ece8d5ef63..4845738482 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -3,7 +3,6 @@ --@module=true local argparse = require('argparse') -local script = require('gui.script') local utils = require('utils') local no_quality_item_types = utils.invert{ @@ -56,7 +55,9 @@ local function moveToContainer(item, creator, container_type) local bucketType = dfhack.items.findType(container_type .. ':NONE') local bucket = df.item.find(dfhack.items.createItem(bucketType, -1, containerMat.type, containerMat.index, creator)) dfhack.items.moveToContainer(item, bucket) + return bucket end + -- this part was written by four rabbits in a trenchcoat (ppaawwll) local function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic) -- (partlayer is also used to determine the material if we're spawning a "generic" body part (i'm just lazy lol)) @@ -119,10 +120,6 @@ local function createCorpsePiece(creator, bodypart, partlayer, creatureID, caste local materialInfo = dfhack.matinfo.find(material) local item_id = dfhack.items.createItem(itemType, itemSubtype, materialInfo['type'], materialInfo.index, creator) local item = df.item.find(item_id) - if liquid then - moveToContainer(item, creator, 'BUCKET') - end - -- if the item type is a corpsepiece, we know we have one, and then go on to set the appropriate flags if item_type == 'CORPSEPIECE' then if layerName == 'BONE' then -- check if bones @@ -235,6 +232,10 @@ local function createCorpsePiece(creator, bodypart, partlayer, creatureID, caste -- DO THIS LAST or else the game crashes for some reason item.caste = casteID end + if liquid then + return moveToContainer(item, creator, 'BUCKET') + end + return item end local function createItem(mat, itemType, quality, creator, description, amount) @@ -259,15 +260,17 @@ local function createItem(mat, itemType, quality, creator, description, amount) item2 = df.item.find(dfhack.items.createItem(itemType[1], itemType[2], mat[1], mat[2], creator)) assert(item2, 'failed to create item') item2:setQuality(quality) - elseif item_type == 'DRINK' then - moveToContainer(item, creator, 'BARREL') - elseif mat_token == 'WATER' or mat_token == 'LYE' then - moveToContainer(item, creator, 'BUCKET') end if tonumber(amount) > 1 then item:setStackSize(amount) if item2 then item2:setStackSize(amount) end end + if item_type == 'DRINK' then + return moveToContainer(item, creator, 'BARREL') + elseif mat_token == 'WATER' or mat_token == 'LYE' then + return moveToContainer(item, creator, 'BUCKET') + end + return {item, item2} end local function get_first_citizen() @@ -278,50 +281,49 @@ local function get_first_citizen() return citizens[1] end +-- returns the list of created items, or nil on error function hackWish(accessors, opts) local unit = accessors.get_unit(opts) or get_first_citizen() - script.start(function() - local qualityok, quality = false, df.item_quality.Ordinary - local itemok, itemtype, itemsubtype = accessors.get_item_type() - if not itemok then return end - local matok, mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric = accessors.get_mat(itemtype, opts) - if not matok then return end - print(mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric) - print(dfhack.matinfo.getToken(mattype, matindex)) - if not no_quality_item_types[df.item_type[itemtype]] then - qualityok, quality = accessors.get_quality() - if not qualityok then return end - end - local description - if df.item_type[itemtype] == 'SLAB' then - local descriptionok - descriptionok, description = accessors.get_description() - if not descriptionok then return end - end - local count = opts.count - if not count then - repeat - local amountok, amount = accessors.get_count() - if not amountok then return end - count = tonumber(amount) - until count - end - if not mattype or not itemtype then - return false - end - if df.item_type.attrs[itemtype].is_stackable then - createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, count) + local qualityok, quality = false, df.item_quality.Ordinary + local itemok, itemtype, itemsubtype = accessors.get_item_type() + if not itemok then return end + local matok, mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric = accessors.get_mat(itemtype, opts) + if not matok then return end + print(mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric) + print(dfhack.matinfo.getToken(mattype, matindex)) + if not no_quality_item_types[df.item_type[itemtype]] then + qualityok, quality = accessors.get_quality() + if not qualityok then return end + end + local description + if df.item_type[itemtype] == 'SLAB' then + local descriptionok + descriptionok, description = accessors.get_description() + if not descriptionok then return end + end + local count = opts.count + if not count then + repeat + local amountok, amount = accessors.get_count() + if not amountok then return end + count = tonumber(amount) + until count + end + if not mattype or not itemtype then return end + if df.item_type.attrs[itemtype].is_stackable then + return createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, count) + end + local items = {} + for _ = 1,count do + if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then + table.insert(items, createCorpsePiece(unit, bodypart, partlayerID, matindex, casteId, corpsepieceGeneric)) else - for _ = 1,count do - if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then - createCorpsePiece(unit, bodypart, partlayerID, matindex, casteId, corpsepieceGeneric) - else - createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1) - end + for _,item in ipairs(createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1)) do + table.insert(items, item) end end - return true - end) + end + return items end if dfhack_flags.module then @@ -347,6 +349,7 @@ local positionals = argparse.processArgsGetopt({...}, { hasArg = true, handler = function(arg) opts.count = argparse.nonnegativeInt(arg, 'count') end, }, + {'p', 'pos', hasArg = true, handler = function(arg) opts.pos = argparse.coords(arg) end}, }) if positionals[1] == 'help' then opts.help = true end @@ -390,7 +393,7 @@ local accessors = { return true, mat_info['type'], mat_info.index, caste, -1 end -- TODO: also return bodypart, partlayerID, corpsepieceGeneric - return true, -1, mat_info.index, caste, get_body_part(), get_part_layer() + return true, -1, mat_info.index, caste, 1, 0, true end, get_quality = function() return true, (tonumber(opts.quality) or df.item_quality.Ordinary) + 1 @@ -403,4 +406,9 @@ local accessors = { end, } -hackWish(accessors, {}) +local items = hackWish(accessors, {}) +if items and opts.pos then + for _,item in ipairs(items) do + dfhack.items.moveToGround(item, opts.pos) + end +end From f4d9866190e352a7a8e1e51639aebcabb3ffcb65 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 27 Apr 2023 14:16:16 -0700 Subject: [PATCH 182/732] update wording --- gui/create-item.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/create-item.lua b/gui/create-item.lua index a155b135cf..decf3143db 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -241,7 +241,7 @@ local default_accessors = { return script.showInputPrompt('Slab', 'What should the slab say?', COLOR_WHITE) end, get_count = function() - return script.showInputPrompt('Wish', 'How many do you want? (numbers only!)', + return script.showInputPrompt('Wish', 'How many do you want?', COLOR_LIGHTGREEN) end, } From 39e299d70d66f270981a70ad7ed1f344ac0d7a45 Mon Sep 17 00:00:00 2001 From: Lizreu Date: Thu, 4 May 2023 01:15:32 +0300 Subject: [PATCH 183/732] add modlist manager overlay script --- gui/modlistman.lua | 446 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 gui/modlistman.lua diff --git a/gui/modlistman.lua b/gui/modlistman.lua new file mode 100644 index 0000000000..70e52a8c35 --- /dev/null +++ b/gui/modlistman.lua @@ -0,0 +1,446 @@ +-- Simple modlist manager +--@ module = true + +local argparse = require('argparse') +local overlay = require('plugins.overlay') +local gui = require('gui') +local widgets = require('gui.widgets') +local repeatutil = require('repeat-util') +local dialogs = require('gui.dialogs') +local json = require('json') +local utils = require('utils') + +local presets_file = json.open("dfhack-config/modpresets.json") +local GLOBAL_KEY = 'modlistloader' + +local function get_newregion_viewscreen() + local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) + return vs +end + +local function get_modlist_fields(kind, viewscreen) + if kind == "available" then + return { + id = viewscreen.available_id, + numeric_version = viewscreen.available_numeric_version, + earliest_compat_numeric_version = viewscreen.available_earliest_compat_numeric_version, + src_dir = viewscreen.available_src_dir, + name = viewscreen.available_name, + displayed_version = viewscreen.available_displayed_version, + mod_header = viewscreen.available_mod_header, + } + elseif kind == "base_available" then + return { + id = viewscreen.base_available_id, + numeric_version = viewscreen.base_available_numeric_version, + earliest_compat_numeric_version = viewscreen.base_available_earliest_compat_numeric_version, + src_dir = viewscreen.base_available_src_dir, + name = viewscreen.base_available_name, + displayed_version = viewscreen.base_available_displayed_version, + mod_header = viewscreen.base_available_mod_header, + } + elseif kind == "object_load_order" then + return { + id = viewscreen.object_load_order_id, + numeric_version = viewscreen.object_load_order_numeric_version, + earliest_compat_numeric_version = viewscreen.object_load_order_earliest_compat_numeric_version, + src_dir = viewscreen.object_load_order_src_dir, + name = viewscreen.object_load_order_name, + displayed_version = viewscreen.object_load_order_displayed_version, + mod_header = viewscreen.object_load_order_mod_header, + } + else + error("Invalid kind: " .. kind) + end +end + +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 + 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 + mod_index = i + break + end + end + + if mod_index == nil then + return false + end + + for k, v in pairs(to_fields) do + if type(from_fields[k][mod_index]) == "userdata" then + v:insert('#', from_fields[k][mod_index]:new()) + else + v:insert('#', from_fields[k][mod_index]) + end + end + + for k, v in pairs(from_fields) do + v:erase(mod_index) + end + + return true +end + +local function enable_mod(viewscreen, mod_id, mod_version) + return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) +end + +local function disable_mod(viewscreen, mod_id, mod_version) + return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) +end + +local function get_active_modlist(viewscreen) + local t = {} + local fields = get_modlist_fields("object_load_order", viewscreen) + for i, v in ipairs(fields.id) do + local version = fields.numeric_version[i] + table.insert(t, { version = version, id = v.value }) + end + return t +end + +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 + + local failures = {} + for _, v in ipairs(modlist) do + if not enable_mod(viewscreen, v.id, v.version) then + table.insert(failures, v.id) + end + end + return failures +end + +local function is_modlist_visible() + -- local viewscreen = dfhack.gui.getCurViewscreen() + -- if viewscreen._type == df.viewscreen_new_regionst then + -- if viewscreen.doing_mods then + -- return true + -- end + -- end + -- return false + + local vs = get_newregion_viewscreen() + if vs and vs.doing_mods then + return true + end +end + +ModmanageMenu = defclass(ModmanageMenu, widgets.Window) +ModmanageMenu.ATTRS { + view_id = "modman_menu", + frame_title = "Modlist Manager", + frame_style = gui.WINDOW_FRAME, + frame_inset=0, + + frame = { w = 40, t = 10, b = 15 }, + + resizable = true, + autoarrange_subviews=false, +} + +local function save_new_preset(preset_name) + local viewscreen = get_newregion_viewscreen() + local modlist = get_active_modlist(viewscreen) + table.insert(presets_file.data, { name = preset_name, modlist = modlist }) + presets_file:write() +end + +local function remove_preset(idx) + if idx > #presets_file.data then + return + end + + table.remove(presets_file.data, idx) + presets_file:write() +end + +local function overwrite_preset(idx) + if idx > #presets_file.data then + return + end + + local viewscreen = get_newregion_viewscreen() + local modlist = get_active_modlist(viewscreen) + presets_file.data[idx].modlist = modlist + presets_file:write() +end + +local function load_preset(idx) + if idx > #presets_file.data then + return + end + + local viewscreen = get_newregion_viewscreen() + local modlist = presets_file.data[idx].modlist + local failures = swap_modlist(viewscreen, modlist) + + if #failures > 0 then + local failures_str = "" + for _, v in ipairs(failures) do + failures_str = failures_str .. v .. "\n" + end + dialogs.showMessage("Warning", "Failed to load some mods", COLOR_LIGHTRED) + end +end + +local function find_preset_by_name(name) + for i, v in ipairs(presets_file.data) do + if v.name == name then + return i + end + end +end + +local function rename_preset(idx, new_name) + if idx > #presets_file.data then + return + end + + presets_file.data[idx].name = new_name + presets_file:write() +end + +local function toggle_default(idx) + if idx > #presets_file.data then + return + end + + if presets_file.data[idx].default then + presets_file.data[idx].default = false + presets_file:write() + else + for i, v in ipairs(presets_file.data) do + v.default = false + end + presets_file.data[idx].default = true + presets_file:write() + end +end + +function ModmanageMenu:init() + local presetList + + local function refresh_list() + presets_file:read() + local presets = utils.clone(presets_file.data, true) + local default_set = false + for _, v in ipairs(presets) do + v.text = v.name + + if v.default and not default_set then + v.text = v.text .. " (default)" + default_set = true + end + end + + presetList:setChoices(presets) + end + + presetList = widgets.List { + frame = { l =1, r = 1, t = 1, b = 5 }, + on_double_click = function(idx, current) + load_preset(idx) + end, + } + + refresh_list() + + self:addviews{ + presetList, + + widgets.HotkeyLabel{ + frame = { l = 1, b = 1, w = 15 }, + key = "CUSTOM_S", + label = "Save current", + on_activate = function() + dialogs.showInputPrompt("Enter preset name", nil, nil, "", function(t) + local existing_idx = find_preset_by_name(t) + if existing_idx then + dialogs.showYesNoPrompt( + "Confirmation", + "Overwrite " .. t .. "?", + nil, + function() + overwrite_preset(existing_idx) + refresh_list() + end + ) + + return + else + save_new_preset(t) + refresh_list() + end + end) + end, + }, + + widgets.HotkeyLabel{ + frame = { l = 17, b = 1, w = 15 }, + key = "CUSTOM_R", + label = "Rename", + on_activate = function() + local idx, current = presetList:getSelected() + + if not idx then + return + end + + dialogs.showInputPrompt("Enter new name", nil, nil, current.name, function(t) + local existing_idx = find_preset_by_name(t) + + if existing_idx then + if existing_idx == idx then + return + end + + dialogs.showYesNoPrompt( + "Confirmation", + "Overwrite " .. t .. "?", + nil, + function() + remove_preset(existing_idx) + rename_preset(idx, t) + refresh_list() + end + ) + else + rename_preset(idx, t) + refresh_list() + end + end) + end, + }, + + widgets.HotkeyLabel{ + frame = { l = 1, b = 2, w = 15 }, + key = "SELECT", + label = "Load", + on_activate = function() + local idx, current = presetList:getSelected() + + if not idx then + return + end + + load_preset(idx) + end, + }, + + widgets.HotkeyLabel{ + frame = { l = 17, b = 2, w = 15 }, + key = "CUSTOM_D", + label = "Delete", + on_activate = function() + local idx, current = presetList:getSelected() + + if not idx then + return + end + + dialogs.showYesNoPrompt( + "Confirmation", + "Delete " .. current.text .. "?", + nil, + function() + remove_preset(idx) + refresh_list() + end + ) + end, + }, + + widgets.HotkeyLabel{ + frame = { l = 1, b = 0, w = 20 }, + key = "CUSTOM_Q", + label = "Set as default", + on_activate = function() + local idx, current = presetList:getSelected() + + if not idx then + return + end + + toggle_default(idx) + refresh_list() + end + }, + } +end + +ModmanageScreen = defclass(ModmanageScreen, gui.ZScreen) +ModmanageScreen.ATTRS { + focus_path = "modman_screen", + defocusable = false, +} + +function ModmanageScreen:init() + self:addviews{ + ModmanageMenu{} + } +end + +ModmanageOverlay = defclass(ModmanageOverlay, overlay.OverlayWidget) +ModmanageOverlay.ATTRS { + frame = { w=30, h=1 }, + default_pos = {x=5, y=-5}, + viewscreens = {"new_region"}, + default_enabled=true, + overlay_only=true, + hotspot=true, + overlay_onupdate_max_freq_seconds=0, +} + +function ModmanageOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame = {l=0, t=0, w = 20, h = 1}, + key = "CUSTOM_M", + view_id = "modman_open", + label = "Mod Manager", + on_activate = function() + ModmanageScreen{}:show() + end, + }, + } +end + +function ModmanageOverlay:overlay_onupdate(viewscreen) + self.visible = is_modlist_visible() + + for k, v in pairs(self.subviews) do + v.visible = self.visible + end +end + +OVERLAY_WIDGETS = { + button = ModmanageOverlay, +} + +local default_applied = false + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_VIEWSCREEN_CHANGED then + local vs = get_newregion_viewscreen() + if vs and not default_applied then + default_applied = true + for i, v in ipairs(presets_file.data) do + if v.default then + load_preset(i) + break + end + end + elseif not vs then + default_applied = false + end + end +end \ No newline at end of file From 9f88267e8808f69d4e27ca62ed3b277ee06ee403 Mon Sep 17 00:00:00 2001 From: Lizreu Date: Thu, 4 May 2023 01:20:23 +0300 Subject: [PATCH 184/732] adjusted changelog --- changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog.txt b/changelog.txt index ccecf30457..4e636d6760 100644 --- a/changelog.txt +++ b/changelog.txt @@ -24,6 +24,9 @@ that repo. # 50.08-r1 +## New Scripts +- `gui/modlistman`: a simple gui mod list manager + ## Fixes - `deteriorate`: ensure remains of enemy dwarves are properly deteriorated - `suspendmanager`: Fix over-aggressive suspension of jobs that could still possibly be done (e.g. jobs that are partially submerged in water) From 4e6ffb0be23475be7e61351041fba2a62ef9b1a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 22:23:05 +0000 Subject: [PATCH 185/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- gui/modlistman.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/gui/modlistman.lua b/gui/modlistman.lua index 70e52a8c35..7a13141268 100644 --- a/gui/modlistman.lua +++ b/gui/modlistman.lua @@ -241,7 +241,7 @@ function ModmanageMenu:init() default_set = true end end - + presetList:setChoices(presets) end @@ -251,7 +251,7 @@ function ModmanageMenu:init() load_preset(idx) end, } - + refresh_list() self:addviews{ @@ -266,10 +266,10 @@ function ModmanageMenu:init() local existing_idx = find_preset_by_name(t) if existing_idx then dialogs.showYesNoPrompt( - "Confirmation", + "Confirmation", "Overwrite " .. t .. "?", - nil, - function() + nil, + function() overwrite_preset(existing_idx) refresh_list() end @@ -297,17 +297,17 @@ function ModmanageMenu:init() dialogs.showInputPrompt("Enter new name", nil, nil, current.name, function(t) local existing_idx = find_preset_by_name(t) - + if existing_idx then if existing_idx == idx then return end dialogs.showYesNoPrompt( - "Confirmation", + "Confirmation", "Overwrite " .. t .. "?", - nil, - function() + nil, + function() remove_preset(existing_idx) rename_preset(idx, t) refresh_list() @@ -348,9 +348,9 @@ function ModmanageMenu:init() end dialogs.showYesNoPrompt( - "Confirmation", - "Delete " .. current.text .. "?", - nil, + "Confirmation", + "Delete " .. current.text .. "?", + nil, function() remove_preset(idx) refresh_list() @@ -443,4 +443,4 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) default_applied = false end end -end \ No newline at end of file +end From 1dd46f3c2ccfde638c2acd9d5722a8c8462aa949 Mon Sep 17 00:00:00 2001 From: Lizreu Date: Thu, 4 May 2023 01:30:31 +0300 Subject: [PATCH 186/732] add docs --- docs/gui/modlistman.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/gui/modlistman.rst diff --git a/docs/gui/modlistman.rst b/docs/gui/modlistman.rst new file mode 100644 index 0000000000..9a0e787320 --- /dev/null +++ b/docs/gui/modlistman.rst @@ -0,0 +1,17 @@ +gui/modlistman +=========== + +.. dfhack-tool:: + :summary: Simple modlist manager. + :tags: dfhack interface + +Adds an optional overlay to the mod list screen that +allows you to save and load mod list presets, as well +as set a default mod list preset for new worlds. + +Usage +----- + +:: + + gui/modlistman From 48b411713f8250d30b229328c4bd4b5df474e5eb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 3 May 2023 15:31:47 -0700 Subject: [PATCH 187/732] clean up --- docs/modtools/create-item.rst | 16 +++++++++++----- modtools/create-item.lua | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/modtools/create-item.rst b/docs/modtools/create-item.rst index edcc698d8c..fe96abdd47 100644 --- a/docs/modtools/create-item.rst +++ b/docs/modtools/create-item.rst @@ -19,7 +19,7 @@ Examples ``modtools/create-item -u 23145 -i WEAPON:ITEM_WEAPON_PICK -m INORGANIC:IRON -q4`` Have unit 23145 create an exceptionally crafted iron pick. -``modtools/create-item -u 323 -i MEAT:NONE -m CREATURE_MAT:DWARF:BRAIN`` +``modtools/create-item -u 323 -i MEAT:NONE -m CREATURE:DWARF:BRAIN`` Have unit 323 produce a lump of (prepared) brain. ``modtools/create-item -i BOULDER:NONE -m INORGANIC:RAW_ADAMANTINE -c 5`` Spawn 5 raw adamantine boulders. @@ -34,12 +34,18 @@ Options string "\\LAST" to use the most recently created unit. ``-i``, ``--item `` (required) The def string of the item you want to create. -``-m``, ``--material`` (required) +``-m``, ``--material `` (required) That def string of the material you want the item to be made out of. -``-q``, ``--quality`` (default: ``0``, which is ``df.item_quality.Ordinary``) +``-q``, ``--quality `` (default: ``0``, equal to ``df.item_quality.Ordinary``) The quality of the created item. -``-d``, ``--description`` (required if you are creating a slab) +``-d``, ``--description `` (required if you are creating a slab) The text that will be engraved on the created slab. -``-c``, ``--count`` (default: ``1``) +``-c``, ``--count `` (default: ``1``) The number of items to create. If the item is stackable, this will be the stack size. +``-t``, ``--caste `` (default: ``0``) + Used if producing a corpse or other creature-based item that could have a + caste associated with it. +``-p``, ``--pos ,,`` + If specified, items will be spawned at the given coordinates instead of at + the creator unit's feet. diff --git a/modtools/create-item.lua b/modtools/create-item.lua index 4845738482..b6325657c4 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -289,8 +289,6 @@ function hackWish(accessors, opts) if not itemok then return end local matok, mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric = accessors.get_mat(itemtype, opts) if not matok then return end - print(mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric) - print(dfhack.matinfo.getToken(mattype, matindex)) if not no_quality_item_types[df.item_type[itemtype]] then qualityok, quality = accessors.get_quality() if not qualityok then return end @@ -392,7 +390,9 @@ local accessors = { if df.item_type[itype] ~= 'CORPSEPIECE' then return true, mat_info['type'], mat_info.index, caste, -1 end - -- TODO: also return bodypart, partlayerID, corpsepieceGeneric + -- support only generic corpse pieces for now. this can be extended to support + -- everything that gui/create-item can produce, but we need more commandline + -- arguments to provide the info return true, -1, mat_info.index, caste, 1, 0, true end, get_quality = function() From 127046d548901a30de2a9d9b4e3903a73d5c1e4a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 3 May 2023 15:33:02 -0700 Subject: [PATCH 188/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index ccecf30457..e502a0f59f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,7 @@ that repo. ## New Scripts - `exportlegends`: export extended legends information for external browsing +- `modtools/create-item`: commandline and API interface for creating items ## Fixes From 37437f2db6f689d8b7aa66fb743534f002a18fee Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 3 May 2023 15:34:19 -0700 Subject: [PATCH 189/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e502a0f59f..f09a082f57 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ that repo. ## Fixes ## Misc Improvements +- `gui/create-item`: ask for number of items to spawn by default ## Removed From 4e7ebbdfe33e7ebc41dc3e766819b719c9ca8b8b Mon Sep 17 00:00:00 2001 From: John Cosker Date: Wed, 3 May 2023 19:54:45 -0400 Subject: [PATCH 190/732] Remove unused variable --- gui/design.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/design.lua b/gui/design.lua index 02b9453653..137a2cc8da 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -155,7 +155,6 @@ HELP_PEN_CENTER, CONFIGURE_PEN_CENTER = get_icon_pens() -- Debug window SHOW_DEBUG_WINDOW = false -DEBUG_PROFILING = true local function table_to_string(tbl, indent) indent = indent or "" From 43a74b6c8679807a24028d56c1b157b28af59e73 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 3 May 2023 17:19:17 -0700 Subject: [PATCH 191/732] fix typo --- internal/quickfort/dig.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index 2fd95dff39..e6e3b6643e 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -263,7 +263,7 @@ local function do_remove_ramps(digctx) if digctx.on_map_edge or digctx.flags.hidden then return nil end if is_construction(digctx.tileattrs) or not is_removable_shape(digctx.tileattrs) then - return mo; + return nil; end return function() digctx.flags.dig = values.dig_default end end From bb5d1a6a01734b7ab3c84819141b0054b87b249c Mon Sep 17 00:00:00 2001 From: Atte Haarni Date: Thu, 4 May 2023 10:12:53 +0300 Subject: [PATCH 192/732] necronomicon: Make variables local, add to changelog --- changelog.txt | 1 + necronomicon.lua | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index af023b1bc2..bac46668f7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,6 +14,7 @@ that repo. # Future ## New Scripts +- `necronomicon`: search fort for items containing the secrets of life and death ## Fixes diff --git a/necronomicon.lua b/necronomicon.lua index 7f8672cad4..3b6d37d489 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -1,6 +1,6 @@ -- Author: Ajhaa --- lists books that contain secrets to life and death +-- lists books that contain secrets of life and death local utils = require("utils") local argparse = require("argparse") @@ -11,7 +11,7 @@ function get_book_interactions(item) if improvement._type == df.itemimprovement_pagesst or improvement._type == df.itemimprovement_writingst then for _, content_id in ipairs(improvement.contents) do - written_content = df.written_content.find(content_id) + local written_content = df.written_content.find(content_id) for _, ref in ipairs (written_content.refs) do if ref._type == df.general_ref_interactionst then @@ -58,8 +58,8 @@ function necronomicon(include_slabs) print("SLABS:") for _, item in ipairs(df.global.world.items.other.SLAB) do if check_slab_secrets(item) then - artifact = get_item_artifact(item) - name = dfhack.TranslateName(artifact.name) + local artifact = get_item_artifact(item) + local name = dfhack.TranslateName(artifact.name) print(dfhack.df2console(name)) end end From e9702520cd914aa488fc86601268bab16ddd9530 Mon Sep 17 00:00:00 2001 From: Atte Haarni Date: Thu, 4 May 2023 11:15:35 +0300 Subject: [PATCH 193/732] gui/gm-editor: Add zones to available targets --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index b54fe6c54e..cabdb807a5 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -52,7 +52,7 @@ end function getTargetFromScreens() local my_trg = dfhack.gui.getSelectedUnit(true) or dfhack.gui.getSelectedItem(true) or dfhack.gui.getSelectedJob(true) or dfhack.gui.getSelectedBuilding(true) - or dfhack.gui.getSelectedStockpile(true) + or dfhack.gui.getSelectedStockpile(true) or dfhack.gui.getSelectedCivZone(true) if not my_trg then qerror("No valid target found") end From 7aa301ec16a2ff980833037f8904729884ccf9ce Mon Sep 17 00:00:00 2001 From: Lizreu Date: Thu, 4 May 2023 16:49:17 +0300 Subject: [PATCH 194/732] review comments --- changelog.txt | 2 +- docs/gui/mod-manager.rst | 56 +-- docs/gui/modlistman.rst | 17 - gui/mod-manager.lua | 738 ++++++++++++++++++++++----------------- gui/modlistman.lua | 446 ----------------------- 5 files changed, 427 insertions(+), 832 deletions(-) delete mode 100644 docs/gui/modlistman.rst delete mode 100644 gui/modlistman.lua diff --git a/changelog.txt b/changelog.txt index 4e636d6760..407d94a6cd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,7 +25,7 @@ that repo. # 50.08-r1 ## New Scripts -- `gui/modlistman`: a simple gui mod list manager +- `gui/mod-manager`: a simple gui mod list manager, replaces the old `gui/mod-manager` script ## Fixes - `deteriorate`: ensure remains of enemy dwarves are properly deteriorated diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 6eb56b2937..7e70a3404d 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -1,19 +1,13 @@ gui/mod-manager -=============== +============== .. dfhack-tool:: - :summary: Easily install and uninstall mods. - :tags: unavailable dfhack + :summary: Simple modlist manager. + :tags: dfhack interface -This tool provides a simple way to install and remove small mods that you have -downloaded from the internet -- or have created yourself! Several mods are -available `here `_. Mods that you want -to manage with this tool should go in the :file:`mods` subfolder under your main -DF folder. - -The mod manager must be invoked on the Dwarf Fortress title screen, *before* a -world is generated. Any mods that you install will only affect worlds generated -after you install them. +Adds an optional overlay to the mod list screen that +allows you to save and load mod list presets, as well +as set a default mod list preset for new worlds. Usage ----- @@ -21,41 +15,3 @@ Usage :: gui/mod-manager - -Mod format ----------- - -Each mod must include a lua script that defines the following variables: - -:name: The name that should be displayed in the mod manager list. -:author: The mod author. -:description: A description of the mod - -Of course, this doesn't actually make a mod - so one or more of the following -variables should also be defined: - -:raws_list: A list (table) of file names that need to be copied over to DF - raws. -:patch_entity: A chunk of text to use to patch the :file:`entity_default.txt` - file. -:patch_init: A chunk of text to add to the :file:`init.lua` file in the raws. -:patch_dofile: A list (table) of files to run from :file:`init.lua`. -:patch_files: A table of files to patch, each element containing the following - subfields: - - :filename: A filename (relative to the raws folder) to patch. - :patch: The text to add. - :after: A string after which to insert the text. - -:guard: A token that is used in raw files to find additions from this - mod and remove them on uninstall. -:guard_init: A token that is used in the :file:`init.lua` file to find - additions from this mod and remove them on uninstall. -:[pre|post]_(un)install: - Callback functions for each installation/uninstallation stage - that can be used to trigger more complex install behavior. - -Screenshot ----------- - -.. image:: /docs/images/mod-manager.png diff --git a/docs/gui/modlistman.rst b/docs/gui/modlistman.rst deleted file mode 100644 index e2ea253835..0000000000 --- a/docs/gui/modlistman.rst +++ /dev/null @@ -1,17 +0,0 @@ -gui/modlistman -=========== - -.. dfhack-tool:: - :summary: Simple modlist manager. - :tags: dfhack interface - -Adds an optional overlay to the mod list screen that -allows you to save and load mod list presets, as well -as set a default mod list preset for new worlds. - -Usage ------ - -:: - - gui/modlistman diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 80ed63f40d..a1aec374ee 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -1,365 +1,467 @@ --- a graphical mod manager for df -local gui=require 'gui' -local widgets=require 'gui.widgets' ---[====[ - -gui/mod-manager -=============== -A simple way to install and remove small mods, which are not included -in DFHack. Examples are `available here `_. - -.. image:: /docs/images/mod-manager.png - -Each mod is a lua script located in :file:`{}/mods/`, which MUST define -the following variables: - -:name: a name that is displayed in list -:author: mod author, also displayed -:description: a description of the mod - -Of course, this doesn't actually make a mod - so one or more of the -following should also be defined: - -:raws_list: a list (table) of file names that need to be copied over to df raws -:patch_entity: a chunk of text to patch entity - *TODO: add settings to which entities to add* -:patch_init: a chunk of lua to add to lua init -:patch_dofile: a list (table) of files to add to lua init as "dofile" -:patch_files: a table of files to patch - - :filename: a filename (in raws folder) to patch - :patch: what to add - :after: a string after which to insert - -:guard: a token that is used in raw files to find additions and remove them on uninstall -:guard_init: a token for lua file -:[pre|post]_(un)install: - Callback functions, which can trigger more complicated behavior - -]====] -local entity_file=dfhack.getDFPath().."/raw/objects/entity_default.txt" -local init_file=dfhack.getDFPath().."/raw/init.lua" -local mod_dir=dfhack.getDFPath().."/hack/mods" - -function fileExists(filename) - local file=io.open(filename,"rb") - if file==nil then - return - else - file:close() - return true - end -end -if not fileExists(init_file) then - local initFile=io.open(init_file,"a") - initFile:close() -end -function copyFile(from,to) --oh so primitive - local filefrom=io.open(from,"rb") - local fileto=io.open(to,"w+b") - local buf=filefrom:read("*a") - printall(buf) - fileto:write(buf) - filefrom:close() - fileto:close() -end -function patchInit(initFileName,patch_guard,code) - local initFile=io.open(initFileName,"a") - initFile:write(string.format("\n%s\n%s\n%s",patch_guard[1], - code,patch_guard[2])) - initFile:close() -end -function patchDofile( luaFileName,patch_guard,dofile_list,mod_path ) - local luaFile=io.open(luaFileName,"a") - luaFile:write(patch_guard[1].."\n") - for _,v in ipairs(dofile_list) do - local fixed_path=mod_path:gsub("\\","/") - luaFile:write(string.format("dofile('%s/%s')\n",fixed_path,v)) - end - luaFile:write(patch_guard[2].."\n") - luaFile:close() -end -function patchFile(file_name,patch_guard,after_string,code) - local input_lines=patch_guard[1].."\n"..code.."\n"..patch_guard[2] - - local badchars="[%:%[%]]" - local find_string=after_string:gsub(badchars,"%%%1") --escape some bad chars - - local entityFile=io.open(file_name,"r") - local buf=entityFile:read("*all") - entityFile:close() - local entityFile=io.open(file_name,"w+") - buf=string.gsub(buf,find_string,after_string.."\n"..input_lines) - entityFile:write(buf) - entityFile:close() -end -function findGuards(str,start,patch_guard) - local pStart=string.find(str,patch_guard[1],start) - if pStart==nil then return nil end - local pEnd=string.find(str,patch_guard[2],pStart) - if pEnd==nil then error("Start guard token found, but end was not found") end - return pStart-1,pEnd+#patch_guard[2]+1 -end -function findGuardsFile(filename,patch_guard) - local file=io.open(filename,"r") - local buf=file:read("*all") - return findGuards(buf,1,patch_guard) -end -function unPatchFile(filename,patch_guard) - local file=io.open(filename,"r") - local buf=file:read("*all") - file:close() - - local newBuf="" - local pos=1 - local lastPos=1 - repeat - local endPos - pos,endPos=findGuards(buf,lastPos,patch_guard) - newBuf=newBuf..string.sub(buf,lastPos,pos) - if endPos~=nil then - lastPos=endPos - end - until pos==nil +-- Simple modlist manager +--@ module = true + +local argparse = require('argparse') +local overlay = require('plugins.overlay') +local gui = require('gui') +local widgets = require('gui.widgets') +local repeatutil = require('repeat-util') +local dialogs = require('gui.dialogs') +local json = require('json') +local utils = require('utils') - local file=io.open(filename,"w+") - file:write(newBuf) - file:close() +local presets_file = json.open("dfhack-config/modpresets.json") +local GLOBAL_KEY = 'modlistloader' + +local function get_newregion_viewscreen() + local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) + return vs end -function checkInstalled(dfMod) --try to figure out if installed - if dfMod.checkInstalled then - return dfMod.checkInstalled() + +local function get_modlist_fields(kind, viewscreen) + if kind == "available" then + return { + id = viewscreen.available_id, + numeric_version = viewscreen.available_numeric_version, + earliest_compat_numeric_version = viewscreen.available_earliest_compat_numeric_version, + src_dir = viewscreen.available_src_dir, + name = viewscreen.available_name, + displayed_version = viewscreen.available_displayed_version, + mod_header = viewscreen.available_mod_header, + } + elseif kind == "base_available" then + return { + id = viewscreen.base_available_id, + numeric_version = viewscreen.base_available_numeric_version, + earliest_compat_numeric_version = viewscreen.base_available_earliest_compat_numeric_version, + src_dir = viewscreen.base_available_src_dir, + name = viewscreen.base_available_name, + displayed_version = viewscreen.base_available_displayed_version, + mod_header = viewscreen.base_available_mod_header, + } + elseif kind == "object_load_order" then + return { + id = viewscreen.object_load_order_id, + numeric_version = viewscreen.object_load_order_numeric_version, + earliest_compat_numeric_version = viewscreen.object_load_order_earliest_compat_numeric_version, + src_dir = viewscreen.object_load_order_src_dir, + name = viewscreen.object_load_order_name, + displayed_version = viewscreen.object_load_order_displayed_version, + mod_header = viewscreen.object_load_order_mod_header, + } else - if dfMod.raws_list then - for k,v in pairs(dfMod.raws_list) do - if fileExists(dfhack.getDFPath().."/raw/objects/"..v) then - return true,v - end - end - end - if dfMod.patch_entity then - if findGuardsFile(entity_file,dfMod.guard)~=nil then - return true,"entity_default.txt" - end - end - if dfMod.patch_files then - for k,v in pairs(dfMod.patch_files) do - if findGuardsFile(dfhack.getDFPath().."/raw/objects/"..v.filename,dfMod.guard)~=nil then - return true,"v.filename" - end - end - end - if dfMod.patch_init then - if findGuardsFile(init_file,dfMod.guard_init)~=nil then - return true,"init.lua" - end - end + error("Invalid kind: " .. kind) end end -manager=defclass(manager,gui.FramedScreen) -function manager:init(args) - self.mods={} - local mods=self.mods - local mlist=dfhack.internal.getDir(mod_dir) +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) - if mlist==nil or #mlist==0 then - qerror("Mod directory not found! Are you sure it is in:"..mod_dir) - end - for k,v in ipairs(mlist) do - if v~="." and v~=".." then - local f,modData=pcall(dofile,mod_dir.."/".. v .. "/init.lua") - if f then - mods[modData.name]=modData - modData.guard=modData.guard or {">>"..modData.name.." patch","<" end + return failures end -function manager:formAuthor() - return self.selected.author or "" + +ModmanageMenu = defclass(ModmanageMenu, widgets.Window) +ModmanageMenu.ATTRS { + view_id = "modman_menu", + frame_title = "Modlist Manager", + frame_style = gui.WINDOW_FRAME, + + resize_min = { w = 30, h = 15 }, + frame = { w = 40, t = 10, b = 15 }, + + resizable = true, + autoarrange_subviews=false, +} + +local function save_new_preset(preset_name) + local viewscreen = get_newregion_viewscreen() + local modlist = get_active_modlist(viewscreen) + table.insert(presets_file.data, { name = preset_name, modlist = modlist }) + presets_file:write() end -function manager:selectMod(idx,choice) - self.selected=choice.data - if self.subviews.info then - self.subviews.info:setText(self:formDescription()) - self:updateLayout() + +local function remove_preset(idx) + if idx > #presets_file.data then + return end + + table.remove(presets_file.data, idx) + presets_file:write() end -function manager:updateState() - for k,v in pairs(self.mods) do - v.installed=checkInstalled(v) + +local function overwrite_preset(idx) + if idx > #presets_file.data then + return end + + local viewscreen = get_newregion_viewscreen() + local modlist = get_active_modlist(viewscreen) + presets_file.data[idx].modlist = modlist + presets_file:write() end -function manager:installCurrent() - self:install(self.selected) -end -function manager:uninstallCurrent() - self:uninstall(self.selected) -end -function manager:install(trgMod,force) - if trgMod==nil then - qerror 'Mod does not exist' - end - if not force then - local isInstalled,file=checkInstalled(trgMod) -- maybe load from .installed? - if isInstalled then - qerror("Mod already installed. File:"..file) - end - end - print("installing:"..trgMod.name) - if trgMod.pre_install then - trgMod.pre_install(args) +local function load_preset(idx) + if idx > #presets_file.data then + return end - if trgMod.raws_list then - for k,v in pairs(trgMod.raws_list) do - copyFile(trgMod.path..v,dfhack.getDFPath().."/raw/objects/"..v) + + local viewscreen = get_newregion_viewscreen() + local modlist = presets_file.data[idx].modlist + local failures = swap_modlist(viewscreen, modlist) + + if #failures > 0 then + local failures_str = "" + for _, v in ipairs(failures) do + failures_str = failures_str .. v .. "\n" end + dialogs.showMessage("Warning", "Failed to load some mods", COLOR_LIGHTRED) end - if trgMod.patch_entity then - local entity_target="[ENTITY:MOUNTAIN]" --TODO configure - patchFile(entity_file,trgMod.guard,entity_target,trgMod.patch_entity) - end - if trgMod.patch_files then - for k,v in pairs(trgMod.patch_files) do - patchFile(dfhack.getDFPath().."/raw/objects/"..v.filename,trgMod.guard,v.after,v.patch) +end + +local function find_preset_by_name(name) + for i, v in ipairs(presets_file.data) do + if v.name == name then + return i end end - if trgMod.patch_init then - patchInit(init_file,trgMod.guard_init,trgMod.patch_init) - end - if trgMod.patch_dofile then - patchDofile(init_file,trgMod.guard_init,trgMod.patch_dofile,trgMod.path) - end - trgMod.installed=true +end - if trgMod.post_install then - trgMod.post_install(self) +local function rename_preset(idx, new_name) + if idx > #presets_file.data then + return end - print("done") + + presets_file.data[idx].name = new_name + presets_file:write() end -function manager:uninstall(trgMod) - print("Uninstalling:"..trgMod.name) - if trgMod.pre_uninstall then - trgMod.pre_uninstall(args) + +local function toggle_default(idx) + if idx > #presets_file.data then + return end - if trgMod.raws_list then - for k,v in pairs(trgMod.raws_list) do - os.remove(dfhack.getDFPath().."/raw/objects/"..v) + if presets_file.data[idx].default then + presets_file.data[idx].default = false + presets_file:write() + else + for i, v in ipairs(presets_file.data) do + v.default = false end + presets_file.data[idx].default = true + presets_file:write() end - if trgMod.patch_entity then - unPatchFile(entity_file,trgMod.guard) - end - if trgMod.patch_files then - for k,v in pairs(trgMod.patch_files) do - unPatchFile(dfhack.getDFPath().."/raw/objects/"..v.filename,trgMod.guard) +end + +function ModmanageMenu:init() + local presetList + + local function refresh_list() + presets_file:read() + local presets = utils.clone(presets_file.data, true) + local default_set = false + for _, v in ipairs(presets) do + v.text = v.name + + if v.default and not default_set then + v.text = v.text .. " (default)" + default_set = true + end end - end - if trgMod.patch_init or trgMod.patch_dofile then - unPatchFile(init_file,trgMod.guard_init) + + presetList:setChoices(presets) end - trgMod.installed=false - if trgMod.post_uninstall then - trgMod.post_uninstall(args) - end - print("done") + presetList = widgets.List { + frame = { b = 5 }, + on_double_click = function(idx, current) + load_preset(idx) + end, + } + + refresh_list() + + self:addviews{ + presetList, + + widgets.HotkeyLabel{ + frame = { l = 0, b = 1, w = 15 }, + key = "CUSTOM_S", + label = "Save current", + on_activate = function() + dialogs.showInputPrompt("Enter preset name", nil, nil, "", function(t) + local existing_idx = find_preset_by_name(t) + if existing_idx then + dialogs.showYesNoPrompt( + "Confirmation", + "Overwrite " .. t .. "?", + nil, + function() + overwrite_preset(existing_idx) + refresh_list() + end + ) + + return + else + save_new_preset(t) + refresh_list() + end + end) + end, + }, + + widgets.HotkeyLabel{ + frame = { l = 16, b = 1, w = 15 }, + key = "CUSTOM_R", + label = "Rename", + on_activate = function() + local idx, current = presetList:getSelected() + + if not idx then + return + end + + dialogs.showInputPrompt("Enter new name", nil, nil, current.name, function(t) + local existing_idx = find_preset_by_name(t) + + if existing_idx then + if existing_idx == idx then + return + end + + dialogs.showYesNoPrompt( + "Confirmation", + "Overwrite " .. t .. "?", + nil, + function() + remove_preset(existing_idx) + rename_preset(idx, t) + refresh_list() + end + ) + else + rename_preset(idx, t) + refresh_list() + end + end) + end, + }, + + widgets.HotkeyLabel{ + frame = { l = 0, b = 2, w = 15 }, + key = "SELECT", + label = "Load", + on_activate = function() + local idx, current = presetList:getSelected() + + if not idx then + return + end + + load_preset(idx) + end, + }, + + widgets.HotkeyLabel{ + frame = { l = 16, b = 2, w = 15 }, + key = "CUSTOM_D", + label = "Delete", + on_activate = function() + local idx, current = presetList:getSelected() + + if not idx then + return + end + + dialogs.showYesNoPrompt( + "Confirmation", + "Delete " .. current.text .. "?", + nil, + function() + remove_preset(idx) + refresh_list() + end + ) + end, + }, + + widgets.HotkeyLabel{ + frame = { l = 0, b = 0, w = 20 }, + key = "CUSTOM_Q", + label = "Set as default", + on_activate = function() + local idx, current = presetList:getSelected() + + if not idx then + return + end + + toggle_default(idx) + refresh_list() + end + }, + } end -function manager:onInput(keys) - if keys.LEAVESCREEN then - self:dismiss() - else - self:inputToSubviews(keys) +ModmanageScreen = defclass(ModmanageScreen, gui.ZScreen) +ModmanageScreen.ATTRS { + focus_path = "modman_screen", + defocusable = false, +} + +function ModmanageScreen:init() + self:addviews{ + ModmanageMenu{} + } +end + +ModmanageOverlay = defclass(ModmanageOverlay, overlay.OverlayWidget) +ModmanageOverlay.ATTRS { + frame = { w=30, h=1 }, + default_pos = { x=5, y=-5 }, + viewscreens = { "new_region/Mods" }, + default_enabled=true, +} + +function ModmanageOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame = {l=0, t=0, w = 20, h = 1}, + key = "CUSTOM_M", + view_id = "modman_open", + label = "Mod Manager", + on_activate = function() + ModmanageScreen{}:show() + end, + }, + } +end + +NotificationOverlay = defclass(NotificationOverlay, overlay.OverlayWidget) +NotificationOverlay.ATTRS { + frame = { w=60, h=1 }, + default_pos = { x=3, y=-2 }, + viewscreens = { "new_region" }, + default_enabled=true, + focus_path = "modman_notification", +} + +function NotificationOverlay:init() + notification_overlay_instance = self + + self:addviews{ + widgets.Label{ + frame = { l=0, t=0, w = 60, h = 1 }, + view_id = "lbl", + text = "", + text_pen = { fg = COLOR_GREEN, bold = true, bg = nil }, + + }, + } +end + +OVERLAY_WIDGETS = { + button = ModmanageOverlay, + notification = NotificationOverlay, +} + +next_notification_timer_call = 0 +notification_overlay_end = 0 +function notification_timer_fn() + if notification_overlay_instance and #notification_overlay_instance.subviews.lbl.text > 0 then + if notification_overlay_end < dfhack.getTickCount() then + notification_overlay_instance.subviews.lbl:setText("") + end end + dfhack.timeout(50, 'frames', notification_timer_fn) end -if dfhack.gui.getCurFocus()~='title' then - qerror("Can only be used in title screen") + +notification_timer_fn() + +local default_applied = false +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_VIEWSCREEN_CHANGED then + local vs = get_newregion_viewscreen() + if vs and not default_applied then + default_applied = true + for i, v in ipairs(presets_file.data) do + if v.default then + load_preset(i) + + if notification_overlay_instance then + notification_overlay_instance.subviews.lbl:setText("*** Loaded mod list '" .. v.name .. "'!") + notification_overlay_end = dfhack.getTickCount() + 5000 + end + + break + end + end + elseif not vs then + default_applied = false + end + end +end + +if dfhack_flags.module then + return end -local m=manager{} -m:show() diff --git a/gui/modlistman.lua b/gui/modlistman.lua deleted file mode 100644 index 7a13141268..0000000000 --- a/gui/modlistman.lua +++ /dev/null @@ -1,446 +0,0 @@ --- Simple modlist manager ---@ module = true - -local argparse = require('argparse') -local overlay = require('plugins.overlay') -local gui = require('gui') -local widgets = require('gui.widgets') -local repeatutil = require('repeat-util') -local dialogs = require('gui.dialogs') -local json = require('json') -local utils = require('utils') - -local presets_file = json.open("dfhack-config/modpresets.json") -local GLOBAL_KEY = 'modlistloader' - -local function get_newregion_viewscreen() - local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) - return vs -end - -local function get_modlist_fields(kind, viewscreen) - if kind == "available" then - return { - id = viewscreen.available_id, - numeric_version = viewscreen.available_numeric_version, - earliest_compat_numeric_version = viewscreen.available_earliest_compat_numeric_version, - src_dir = viewscreen.available_src_dir, - name = viewscreen.available_name, - displayed_version = viewscreen.available_displayed_version, - mod_header = viewscreen.available_mod_header, - } - elseif kind == "base_available" then - return { - id = viewscreen.base_available_id, - numeric_version = viewscreen.base_available_numeric_version, - earliest_compat_numeric_version = viewscreen.base_available_earliest_compat_numeric_version, - src_dir = viewscreen.base_available_src_dir, - name = viewscreen.base_available_name, - displayed_version = viewscreen.base_available_displayed_version, - mod_header = viewscreen.base_available_mod_header, - } - elseif kind == "object_load_order" then - return { - id = viewscreen.object_load_order_id, - numeric_version = viewscreen.object_load_order_numeric_version, - earliest_compat_numeric_version = viewscreen.object_load_order_earliest_compat_numeric_version, - src_dir = viewscreen.object_load_order_src_dir, - name = viewscreen.object_load_order_name, - displayed_version = viewscreen.object_load_order_displayed_version, - mod_header = viewscreen.object_load_order_mod_header, - } - else - error("Invalid kind: " .. kind) - end -end - -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 - 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 - mod_index = i - break - end - end - - if mod_index == nil then - return false - end - - for k, v in pairs(to_fields) do - if type(from_fields[k][mod_index]) == "userdata" then - v:insert('#', from_fields[k][mod_index]:new()) - else - v:insert('#', from_fields[k][mod_index]) - end - end - - for k, v in pairs(from_fields) do - v:erase(mod_index) - end - - return true -end - -local function enable_mod(viewscreen, mod_id, mod_version) - return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) -end - -local function disable_mod(viewscreen, mod_id, mod_version) - return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) -end - -local function get_active_modlist(viewscreen) - local t = {} - local fields = get_modlist_fields("object_load_order", viewscreen) - for i, v in ipairs(fields.id) do - local version = fields.numeric_version[i] - table.insert(t, { version = version, id = v.value }) - end - return t -end - -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 - - local failures = {} - for _, v in ipairs(modlist) do - if not enable_mod(viewscreen, v.id, v.version) then - table.insert(failures, v.id) - end - end - return failures -end - -local function is_modlist_visible() - -- local viewscreen = dfhack.gui.getCurViewscreen() - -- if viewscreen._type == df.viewscreen_new_regionst then - -- if viewscreen.doing_mods then - -- return true - -- end - -- end - -- return false - - local vs = get_newregion_viewscreen() - if vs and vs.doing_mods then - return true - end -end - -ModmanageMenu = defclass(ModmanageMenu, widgets.Window) -ModmanageMenu.ATTRS { - view_id = "modman_menu", - frame_title = "Modlist Manager", - frame_style = gui.WINDOW_FRAME, - frame_inset=0, - - frame = { w = 40, t = 10, b = 15 }, - - resizable = true, - autoarrange_subviews=false, -} - -local function save_new_preset(preset_name) - local viewscreen = get_newregion_viewscreen() - local modlist = get_active_modlist(viewscreen) - table.insert(presets_file.data, { name = preset_name, modlist = modlist }) - presets_file:write() -end - -local function remove_preset(idx) - if idx > #presets_file.data then - return - end - - table.remove(presets_file.data, idx) - presets_file:write() -end - -local function overwrite_preset(idx) - if idx > #presets_file.data then - return - end - - local viewscreen = get_newregion_viewscreen() - local modlist = get_active_modlist(viewscreen) - presets_file.data[idx].modlist = modlist - presets_file:write() -end - -local function load_preset(idx) - if idx > #presets_file.data then - return - end - - local viewscreen = get_newregion_viewscreen() - local modlist = presets_file.data[idx].modlist - local failures = swap_modlist(viewscreen, modlist) - - if #failures > 0 then - local failures_str = "" - for _, v in ipairs(failures) do - failures_str = failures_str .. v .. "\n" - end - dialogs.showMessage("Warning", "Failed to load some mods", COLOR_LIGHTRED) - end -end - -local function find_preset_by_name(name) - for i, v in ipairs(presets_file.data) do - if v.name == name then - return i - end - end -end - -local function rename_preset(idx, new_name) - if idx > #presets_file.data then - return - end - - presets_file.data[idx].name = new_name - presets_file:write() -end - -local function toggle_default(idx) - if idx > #presets_file.data then - return - end - - if presets_file.data[idx].default then - presets_file.data[idx].default = false - presets_file:write() - else - for i, v in ipairs(presets_file.data) do - v.default = false - end - presets_file.data[idx].default = true - presets_file:write() - end -end - -function ModmanageMenu:init() - local presetList - - local function refresh_list() - presets_file:read() - local presets = utils.clone(presets_file.data, true) - local default_set = false - for _, v in ipairs(presets) do - v.text = v.name - - if v.default and not default_set then - v.text = v.text .. " (default)" - default_set = true - end - end - - presetList:setChoices(presets) - end - - presetList = widgets.List { - frame = { l =1, r = 1, t = 1, b = 5 }, - on_double_click = function(idx, current) - load_preset(idx) - end, - } - - refresh_list() - - self:addviews{ - presetList, - - widgets.HotkeyLabel{ - frame = { l = 1, b = 1, w = 15 }, - key = "CUSTOM_S", - label = "Save current", - on_activate = function() - dialogs.showInputPrompt("Enter preset name", nil, nil, "", function(t) - local existing_idx = find_preset_by_name(t) - if existing_idx then - dialogs.showYesNoPrompt( - "Confirmation", - "Overwrite " .. t .. "?", - nil, - function() - overwrite_preset(existing_idx) - refresh_list() - end - ) - - return - else - save_new_preset(t) - refresh_list() - end - end) - end, - }, - - widgets.HotkeyLabel{ - frame = { l = 17, b = 1, w = 15 }, - key = "CUSTOM_R", - label = "Rename", - on_activate = function() - local idx, current = presetList:getSelected() - - if not idx then - return - end - - dialogs.showInputPrompt("Enter new name", nil, nil, current.name, function(t) - local existing_idx = find_preset_by_name(t) - - if existing_idx then - if existing_idx == idx then - return - end - - dialogs.showYesNoPrompt( - "Confirmation", - "Overwrite " .. t .. "?", - nil, - function() - remove_preset(existing_idx) - rename_preset(idx, t) - refresh_list() - end - ) - else - rename_preset(idx, t) - refresh_list() - end - end) - end, - }, - - widgets.HotkeyLabel{ - frame = { l = 1, b = 2, w = 15 }, - key = "SELECT", - label = "Load", - on_activate = function() - local idx, current = presetList:getSelected() - - if not idx then - return - end - - load_preset(idx) - end, - }, - - widgets.HotkeyLabel{ - frame = { l = 17, b = 2, w = 15 }, - key = "CUSTOM_D", - label = "Delete", - on_activate = function() - local idx, current = presetList:getSelected() - - if not idx then - return - end - - dialogs.showYesNoPrompt( - "Confirmation", - "Delete " .. current.text .. "?", - nil, - function() - remove_preset(idx) - refresh_list() - end - ) - end, - }, - - widgets.HotkeyLabel{ - frame = { l = 1, b = 0, w = 20 }, - key = "CUSTOM_Q", - label = "Set as default", - on_activate = function() - local idx, current = presetList:getSelected() - - if not idx then - return - end - - toggle_default(idx) - refresh_list() - end - }, - } -end - -ModmanageScreen = defclass(ModmanageScreen, gui.ZScreen) -ModmanageScreen.ATTRS { - focus_path = "modman_screen", - defocusable = false, -} - -function ModmanageScreen:init() - self:addviews{ - ModmanageMenu{} - } -end - -ModmanageOverlay = defclass(ModmanageOverlay, overlay.OverlayWidget) -ModmanageOverlay.ATTRS { - frame = { w=30, h=1 }, - default_pos = {x=5, y=-5}, - viewscreens = {"new_region"}, - default_enabled=true, - overlay_only=true, - hotspot=true, - overlay_onupdate_max_freq_seconds=0, -} - -function ModmanageOverlay:init() - self:addviews{ - widgets.HotkeyLabel{ - frame = {l=0, t=0, w = 20, h = 1}, - key = "CUSTOM_M", - view_id = "modman_open", - label = "Mod Manager", - on_activate = function() - ModmanageScreen{}:show() - end, - }, - } -end - -function ModmanageOverlay:overlay_onupdate(viewscreen) - self.visible = is_modlist_visible() - - for k, v in pairs(self.subviews) do - v.visible = self.visible - end -end - -OVERLAY_WIDGETS = { - button = ModmanageOverlay, -} - -local default_applied = false - -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_VIEWSCREEN_CHANGED then - local vs = get_newregion_viewscreen() - if vs and not default_applied then - default_applied = true - for i, v in ipairs(presets_file.data) do - if v.default then - load_preset(i) - break - end - end - elseif not vs then - default_applied = false - end - end -end From a7aa06b3653e218e2c2b0e39ec98f22df8c02dfa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 13:51:40 +0000 Subject: [PATCH 195/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- gui/mod-manager.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index a1aec374ee..54b97653b4 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -226,7 +226,7 @@ function ModmanageMenu:init() default_set = true end end - + presetList:setChoices(presets) end @@ -236,7 +236,7 @@ function ModmanageMenu:init() load_preset(idx) end, } - + refresh_list() self:addviews{ @@ -251,10 +251,10 @@ function ModmanageMenu:init() local existing_idx = find_preset_by_name(t) if existing_idx then dialogs.showYesNoPrompt( - "Confirmation", + "Confirmation", "Overwrite " .. t .. "?", - nil, - function() + nil, + function() overwrite_preset(existing_idx) refresh_list() end @@ -282,17 +282,17 @@ function ModmanageMenu:init() dialogs.showInputPrompt("Enter new name", nil, nil, current.name, function(t) local existing_idx = find_preset_by_name(t) - + if existing_idx then if existing_idx == idx then return end dialogs.showYesNoPrompt( - "Confirmation", + "Confirmation", "Overwrite " .. t .. "?", - nil, - function() + nil, + function() remove_preset(existing_idx) rename_preset(idx, t) refresh_list() @@ -333,9 +333,9 @@ function ModmanageMenu:init() end dialogs.showYesNoPrompt( - "Confirmation", - "Delete " .. current.text .. "?", - nil, + "Confirmation", + "Delete " .. current.text .. "?", + nil, function() remove_preset(idx) refresh_list() @@ -452,7 +452,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) notification_overlay_instance.subviews.lbl:setText("*** Loaded mod list '" .. v.name .. "'!") notification_overlay_end = dfhack.getTickCount() + 5000 end - + break end end From c46cb38370eb87696f06cd3c5163ff9cb6920762 Mon Sep 17 00:00:00 2001 From: Lizreu Date: Thu, 4 May 2023 17:14:41 +0300 Subject: [PATCH 196/732] fix doc lint --- docs/gui/mod-manager.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 7e70a3404d..18f4c83139 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -1,5 +1,5 @@ gui/mod-manager -============== +=============== .. dfhack-tool:: :summary: Simple modlist manager. From c203369c629e26e7d02efd54644f7d1863c80241 Mon Sep 17 00:00:00 2001 From: John Cosker Date: Mon, 8 May 2023 14:33:00 -0400 Subject: [PATCH 197/732] Fix errors when trying to designate buildings, fix auto stair designations --- changelog.txt | 1 + gui/design.lua | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index bfc97292f7..1c67eb8a1d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ that repo. - `necronomicon`: search fort for items containing the secrets of life and death ## Fixes +- `gui/design`: Fix building and stairs designation ## Misc Improvements - `gui/create-item`: ask for number of items to spawn by default diff --git a/gui/design.lua b/gui/design.lua index 137a2cc8da..696dbb46b6 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1557,13 +1557,13 @@ function Design:get_designation(point) local stairs_top_type = self.subviews.stairs_top_subtype:getOptionValue() local stairs_middle_type = self.subviews.stairs_middle_subtype:getOptionValue() local stairs_bottom_type = self.subviews.stairs_bottom_subtype:getOptionValue() - if z == 0 then + if point.z == 0 then return stairs_bottom_type == "auto" and "u" or stairs_bottom_type - elseif view_bounds and z == math.abs(view_bounds.z1 - view_bounds.z2) then + elseif view_bounds and point.z == math.abs(view_bounds.z1 - view_bounds.z2) then local pos = Point { x = view_bounds.x1, y = view_bounds.y1, z = view_bounds.z1} + point - local tile_type = dfhack.maps.getTileType(pos) + local tile_type = dfhack.maps.getTileType({x = pos.x, y = pos.y, z = pos.z}) local tile_shape = tile_type and tile_attrs[tile_type].shape or nil - local designation = dfhack.maps.getTileFlags(pos) + local designation = dfhack.maps.getTileFlags({x = pos.x, y = pos.y, z = pos.z}) -- If top of the view_bounds is down stair, 'auto' should change it to up/down to match vanilla stair logic local up_or_updown_dug = ( @@ -1617,7 +1617,7 @@ function Design:commit() data[zlevel][row] = {} for col = 0, math.abs(bot_right.x - top_left.x) do if grid[col] and grid[col][row] then - local desig = self:get_designation(Point{col, row, zlevel}) + local desig = self:get_designation(Point{x = col, y = row, z = zlevel}) if desig ~= "`" then data[zlevel][row][col] = desig .. (mode ~= "build" and tostring(self.prio) or "") From 69a9f58cc8776b69ebc3ab69e188390f808fb11a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 15 May 2023 16:14:25 -0700 Subject: [PATCH 198/732] update light-aquifers-only --- docs/light-aquifers-only.rst | 14 ++++---- gui/control-panel.lua | 12 ++++++- light-aquifers-only.lua | 68 ++++++++++++------------------------ 3 files changed, 41 insertions(+), 53 deletions(-) diff --git a/docs/light-aquifers-only.rst b/docs/light-aquifers-only.rst index f3a77206d2..d5bd7716f8 100644 --- a/docs/light-aquifers-only.rst +++ b/docs/light-aquifers-only.rst @@ -3,12 +3,12 @@ light-aquifers-only .. dfhack-tool:: :summary: Change heavy and varied aquifers to light aquifers. - :tags: unavailable embark fort armok map + :tags: embark fort armok map This script behaves differently depending on whether it's called pre-embark or post-embark. Pre-embark, it changes all aquifers in the world to light ones, -while post-embark it only modifies the map tiles, leaving the rest of the world -unchanged. +while post-embark it only modifies the active map tiles, leaving the rest of +the world unchanged. Usage ----- @@ -17,15 +17,15 @@ Usage light-aquifers-only -If you don't ever want to have to deal with heavy aquifers, you can add the -``light-aquifers-only`` command to your :file:`dfhack-config/init/onMapLoad.init` -file. +If you don't ever want to have to deal with heavy aquifers, you can enable the +``light-aquifers-only`` command in the "Autostart" tab of `gui/control-panel` +so it will be run automatically whenever you start a new fort. Technical details ----------------- When run pre-embark, this script changes the drainage of all world tiles that -would generate Heavy aquifers into a value that results in Light aquifers +would generate heavy aquifers into a value that results in Light aquifers instead, based on logic revealed by ToadyOne in a FotF answer: http://www.bay12forums.com/smf/index.php?topic=169696.msg8099138#msg8099138 diff --git a/gui/control-panel.lua b/gui/control-panel.lua index c0051c49ac..d9af6eb8d3 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -39,6 +39,7 @@ local FORT_AUTOSTART = { 'ban-cooking all', 'buildingplan set boulders false', 'buildingplan set logs false', + 'light-aquifers-only fort', } for _,v in ipairs(FORT_SERVICES) do table.insert(FORT_AUTOSTART, v) @@ -351,12 +352,21 @@ function Services:get_enabled_map() return enabled_map end +local function get_first_word(text) + local word = text:trim():split(' +')[1] + if word:startswith(':') then word = word:sub(2) end + print(text, word) + return word +end + function Services:get_choices() local enabled_map = self:get_enabled_map() local choices = {} local hide_armok = dfhack.getHideArmokTools() for _,service in ipairs(self.services_list) do - if not hide_armok or not helpdb.is_entry(service) or not helpdb.get_entry_tags(service).armok then + local entry_name = get_first_word(service) + if not hide_armok or not helpdb.is_entry(entry_name) + or not helpdb.get_entry_tags(entry_name).armok then table.insert(choices, {target=service, enabled=enabled_map[service]}) end end diff --git a/light-aquifers-only.lua b/light-aquifers-only.lua index 28e50474f9..27c437bcfb 100644 --- a/light-aquifers-only.lua +++ b/light-aquifers-only.lua @@ -1,57 +1,35 @@ -- Changes heavy aquifers to light globally pre embark or locally post embark -local help = [====[ -light-aquifers-only -=================== -This script behaves differently depending on whether it's called pre embark or post -embark. Pre embark it changes all aquifers in the world to light ones, while post -embark it only changes the ones at the embark to light ones, leaving the rest of the -world unchanged. +local args = {...} -Pre embark: -Changes the Drainage of all world tiles that would generate Heavy aquifers into -a value that results in Light aquifers instead. - -This script is based on logic revealed by ToadyOne in a FotF answer: -http://www.bay12forums.com/smf/index.php?topic=169696.msg8099138#msg8099138 -Basically the Drainage is used as an "RNG" to cause an aquifer to be heavy -about 5% of the time. The script shifts the matching numbers to a neighboring -one, which does not result in any change of the biome. - -Post embark: -Clears the flags that mark aquifer tiles as heavy, converting them to light. -]====] -function lightaqonly (arg) - if arg and arg:match('help') then - print(help) +if args[1] == 'help' then + print(dfhack.script_help()) return - end - if not dfhack.isWorldLoaded () then - qerror ("Error: This script requires a world to be loaded.") - end +end + +if not dfhack.isWorldLoaded() then + qerror("Error: This script requires a world to be loaded.") +end - if dfhack.isMapLoaded () then - for i, block in ipairs (df.global.world.map.map_blocks) do - if block.flags.has_aquifer then - for k = 0, 15 do - for l = 0, 15 do - block.occupancy [k] [l].heavy_aquifer = false - end +if dfhack.isMapLoaded() then + for _, block in ipairs(df.global.world.map.map_blocks) do + if block.flags.has_aquifer then + for k = 0, 15 do + for l = 0, 15 do + block.occupancy[k][l].heavy_aquifer = false + end + end end - end end + return +end - else - for i = 0, df.global.world.world_data.world_width - 1 do - for k = 0, df.global.world.world_data.world_height - 1 do - local tile = df.global.world.world_data.region_map [i]:_displace (k) - +-- pre-embark +for i = 0, df.global.world.world_data.world_width - 1 do + for k = 0, df.global.world.world_data.world_height - 1 do + local tile = df.global.world.world_data.region_map[i]:_displace(k) if tile.drainage % 20 == 7 then - tile.drainage = tile.drainage + 1 + tile.drainage = tile.drainage + 1 end - end end - end end - -lightaqonly (...) From 9bb9e12aa1d88c6aed5d525916b6591a9544b18c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 15 May 2023 16:18:25 -0700 Subject: [PATCH 199/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index f09a082f57..c45f054053 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,7 @@ that repo. ## New Scripts - `exportlegends`: export extended legends information for external browsing - `modtools/create-item`: commandline and API interface for creating items +- `light-aquifers-only`: convert heavy aquifers to light (available as an fort autostart option in `gui/control-panel`) ## Fixes From 708533573b82683892607c52f10e55b2348c9a23 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 15 May 2023 18:49:46 -0700 Subject: [PATCH 200/732] Update changelog.txt --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 064e7f7225..2d23f540bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,7 +16,7 @@ that repo. ## New Scripts - `exportlegends`: reinstated: export extended legends information for external browsing - `modtools/create-item`: reinstated: commandline and API interface for creating items -- `light-aquifers-only`: reinstated: convert heavy aquifers to light (available as an fort autostart option in `gui/control-panel`) +- `light-aquifers-only`: reinstated: convert heavy aquifers to light - `necronomicon`: search fort for items containing the secrets of life and death ## Fixes @@ -24,6 +24,7 @@ that repo. ## Misc Improvements - `gui/create-item`: ask for number of items to spawn by default +- `light-aquifers-only`: now available as an fort autostart option in `gui/control-panel` ## Removed From 670a3d01184ffec0d322bd66bb9d6624034c756f Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 15 May 2023 18:52:07 -0700 Subject: [PATCH 201/732] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 2d23f540bb..6b840967fd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -24,7 +24,7 @@ that repo. ## Misc Improvements - `gui/create-item`: ask for number of items to spawn by default -- `light-aquifers-only`: now available as an fort autostart option in `gui/control-panel` +- `light-aquifers-only`: now available as an fort autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. ## Removed From 1aaad9a73796e242d27a3e84928e61a6a096fb9d Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 15 May 2023 22:09:07 -0700 Subject: [PATCH 202/732] Update docs/light-aquifers-only.rst Co-authored-by: Alan --- docs/light-aquifers-only.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/light-aquifers-only.rst b/docs/light-aquifers-only.rst index d5bd7716f8..f8780cd63d 100644 --- a/docs/light-aquifers-only.rst +++ b/docs/light-aquifers-only.rst @@ -25,7 +25,7 @@ Technical details ----------------- When run pre-embark, this script changes the drainage of all world tiles that -would generate heavy aquifers into a value that results in Light aquifers +would generate heavy aquifers into a value that results in light aquifers instead, based on logic revealed by ToadyOne in a FotF answer: http://www.bay12forums.com/smf/index.php?topic=169696.msg8099138#msg8099138 From cae43e076be3474ce6b0bc6eb3b7a456e39144dd Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 15 May 2023 22:09:45 -0700 Subject: [PATCH 203/732] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 6b840967fd..b91fbbaabe 100644 --- a/changelog.txt +++ b/changelog.txt @@ -24,7 +24,7 @@ that repo. ## Misc Improvements - `gui/create-item`: ask for number of items to spawn by default -- `light-aquifers-only`: now available as an fort autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. +- `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. ## Removed From f2edea15b62b229584c58ec5823b70bddc8ae86e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 15 May 2023 22:43:59 -0700 Subject: [PATCH 204/732] also halt rendering when --freeze is passed --- changelog.txt | 1 + docs/gui/gm-editor.rst | 11 ++++++++--- gui/gm-editor.lua | 8 ++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index b91fbbaabe..0d72391dc6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ that repo. ## Misc Improvements - `gui/create-item`: ask for number of items to spawn by default - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. +- `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for the gm-editor window itself) ## Removed diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index 334e127f64..54229e6d22 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -38,9 +38,14 @@ Options ------- ``-f``, ``--freeze`` - Freeze the underlying viewscreen so that it does not receive logic updates. - Note that this will prevent scrolling the map by draggint with the middle - mouse button. + Freeze the underlying viewscreen so that it does not receive any updates. + This allows you to be sure that whatever you are inspecting or modifying + will not be changed by the game until you are done with it. Note that this + will also prevent any rendering refreshes, so the background may strobe, + and if you drag the `gui/gm-editor` window around then you will see visual + artifacts. You might want to maximize the gm-editor window by + double-clicking in the title bar, otherwise the stobe effect of the + background might get tiring on the eyes. Screenshot ---------- diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index cabdb807a5..5eed34057d 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -657,6 +657,14 @@ function GmScreen:onIdle() end end +function GmScreen:render(dc) + if self.freeze then + GmScreen.super.super.render(self, dc) + else + GmScreen.super.render(self, dc) + end +end + function GmScreen:onDismiss() views[self] = nil end From 6a7de38cd597024943da79495dd3bcbae2333578 Mon Sep 17 00:00:00 2001 From: Lizreu Date: Tue, 16 May 2023 19:17:52 +0200 Subject: [PATCH 205/732] review comments --- changelog.txt | 4 +--- gui/mod-manager.lua | 27 +++++++++++---------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/changelog.txt b/changelog.txt index 407d94a6cd..7902954269 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,7 @@ that repo. ## New Scripts - `exportlegends`: export extended legends information for external browsing +- `gui/mod-manager`: automatically restore your list of active mods when generating new worlds ## Fixes @@ -24,9 +25,6 @@ that repo. # 50.08-r1 -## New Scripts -- `gui/mod-manager`: a simple gui mod list manager, replaces the old `gui/mod-manager` script - ## Fixes - `deteriorate`: ensure remains of enemy dwarves are properly deteriorated - `suspendmanager`: Fix over-aggressive suspension of jobs that could still possibly be done (e.g. jobs that are partially submerged in water) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 54b97653b4..be16009829 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -1,17 +1,16 @@ --- Simple modlist manager +-- Save and restore lists of active mods. --@ module = true local argparse = require('argparse') local overlay = require('plugins.overlay') local gui = require('gui') local widgets = require('gui.widgets') -local repeatutil = require('repeat-util') local dialogs = require('gui.dialogs') local json = require('json') local utils = require('utils') -local presets_file = json.open("dfhack-config/modpresets.json") -local GLOBAL_KEY = 'modlistloader' +local presets_file = json.open("dfhack-config/mod-manager.json") +local GLOBAL_KEY = 'mod-manager' local function get_newregion_viewscreen() local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) @@ -402,17 +401,17 @@ NotificationOverlay.ATTRS { default_pos = { x=3, y=-2 }, viewscreens = { "new_region" }, default_enabled=true, - focus_path = "modman_notification", } +notification_message = "" +next_notification_timer_call = 0 +notification_overlay_end = 0 function NotificationOverlay:init() - notification_overlay_instance = self - self:addviews{ widgets.Label{ frame = { l=0, t=0, w = 60, h = 1 }, view_id = "lbl", - text = "", + text = {{ text = function() return notification_message end }}, text_pen = { fg = COLOR_GREEN, bold = true, bg = nil }, }, @@ -424,12 +423,10 @@ OVERLAY_WIDGETS = { notification = NotificationOverlay, } -next_notification_timer_call = 0 -notification_overlay_end = 0 function notification_timer_fn() - if notification_overlay_instance and #notification_overlay_instance.subviews.lbl.text > 0 then + if #notification_message > 0 then if notification_overlay_end < dfhack.getTickCount() then - notification_overlay_instance.subviews.lbl:setText("") + notification_message = "" end end @@ -448,10 +445,8 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if v.default then load_preset(i) - if notification_overlay_instance then - notification_overlay_instance.subviews.lbl:setText("*** Loaded mod list '" .. v.name .. "'!") - notification_overlay_end = dfhack.getTickCount() + 5000 - end + notification_message = "*** Loaded mod list '" .. v.name .. "'!" + notification_overlay_end = dfhack.getTickCount() + 5000 break end From e133c760c304cb83c8e6d240a6544807c285dd9d Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 16 May 2023 10:42:38 -0700 Subject: [PATCH 206/732] Apply suggestions from code review --- docs/gui/mod-manager.rst | 2 +- gui/mod-manager.lua | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 18f4c83139..3d88415dc7 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -2,7 +2,7 @@ gui/mod-manager =============== .. dfhack-tool:: - :summary: Simple modlist manager. + :summary: Save and restore lists of active mods. :tags: dfhack interface Adds an optional overlay to the mod list screen that diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index be16009829..1fcb363b99 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -1,7 +1,6 @@ -- Save and restore lists of active mods. --@ module = true -local argparse = require('argparse') local overlay = require('plugins.overlay') local gui = require('gui') local widgets = require('gui.widgets') @@ -363,7 +362,7 @@ end ModmanageScreen = defclass(ModmanageScreen, gui.ZScreen) ModmanageScreen.ATTRS { - focus_path = "modman_screen", + focus_path = "mod-manager", defocusable = false, } From c6cdd92b83c2da23d00694b3bf6e5bc91f126174 Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 16 May 2023 10:48:42 -0700 Subject: [PATCH 207/732] Update gui/mod-manager.lua --- 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 1fcb363b99..ace0c39b88 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -374,7 +374,8 @@ end ModmanageOverlay = defclass(ModmanageOverlay, overlay.OverlayWidget) ModmanageOverlay.ATTRS { - frame = { w=30, h=1 }, + frame = { w=16, h=3 }, + frame_style = gui.MEDIUM_FRAME, default_pos = { x=5, y=-5 }, viewscreens = { "new_region/Mods" }, default_enabled=true, From 4eb6172f9fb0d07a7e253be88b9bf5b14baccde1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 16 May 2023 12:37:23 -0700 Subject: [PATCH 208/732] add option for full text search for list filters --- gui/control-panel.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index c0051c49ac..0afbeecc01 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -78,6 +78,8 @@ local PREFERENCES = { desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.'}, SCROLL_DELAY_MS={label='Mouse scroll repeat delay (ms)', type='int', default=20, min=5, desc='The delay between events when holding the mouse button down on a scrollbar, in ms.'}, + FILTER_FULL_TEXT={label='DFHack list filters search full text', type='bool', default=false, + desc='Whether to search for a match in the full text (true) or just at the start of words (false).'}, }, } @@ -627,18 +629,16 @@ function Preferences:refresh() for ctx_name,settings in pairs(PREFERENCES) do local ctx_env = require(ctx_name) for id,spec in pairs(settings) do + local label = ('%s (%s)'):format(spec.label, ctx_env[id]) local text = { {tile=BUTTON_PEN_LEFT}, {tile=CONFIGURE_PEN_CENTER}, {tile=BUTTON_PEN_RIGHT}, ' ', - spec.label, - ' (', - tostring(ctx_env[id]), - ')', + label, } table.insert(choices, - {text=text, desc=spec.desc, search_key=id, + {text=text, desc=spec.desc, search_key=label, ctx_env=ctx_env, id=id, spec=spec}) end end From dc4ef22be7d08aa6a5f6d7a7266e78ee31c2b31b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 16 May 2023 14:32:06 -0700 Subject: [PATCH 209/732] add black background when freezing game to avoid strobing also add better handling of "scr" target --- docs/gui/gm-editor.rst | 16 +++--- gui/gm-editor.lua | 107 ++++++++++++++++++++++++++++++++--------- 2 files changed, 94 insertions(+), 29 deletions(-) diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index 54229e6d22..a16862d465 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -32,7 +32,9 @@ Examples ``gui/gm-editor world.items.all`` Opens the editor on the items list. ``gui/gm-editor --freeze scr`` - Opens the editor on the current viewscreen data and prevents it from getting updates while you have the editor open. + Opens the editor on the current DF viewscreen data (bypassing any DFHack + layers) and prevents the underlying viewscreen from getting updates while + you have the editor open. Options ------- @@ -40,12 +42,12 @@ Options ``-f``, ``--freeze`` Freeze the underlying viewscreen so that it does not receive any updates. This allows you to be sure that whatever you are inspecting or modifying - will not be changed by the game until you are done with it. Note that this - will also prevent any rendering refreshes, so the background may strobe, - and if you drag the `gui/gm-editor` window around then you will see visual - artifacts. You might want to maximize the gm-editor window by - double-clicking in the title bar, otherwise the stobe effect of the - background might get tiring on the eyes. + will not be read or changed by the game until you are done with it. Note + that this will also prevent any rendering refreshes, so the background is + replaced with a blank screen. You can open multiple instances of + `gui/gm-editor` as usual when the game is frozen. The black background will + disappear when the last `gui/gm-editor` window that was opened with the + ``--freeze`` option is dismissed. Screenshot ---------- diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 5eed34057d..615e9e87a5 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -1,4 +1,5 @@ -- Interface powered memory object editor. +--@module=true local gui = require 'gui' local json = require 'json' @@ -634,39 +635,92 @@ function GmEditorUi:postUpdateLayout() save_config({frame = self.frame}) end +FreezeScreen = defclass(FreezeScreen, gui.Screen) +FreezeScreen.ATTRS{ + focus_path='gm-editor/freeze', +} + +function FreezeScreen:init() + self:addviews{ + widgets.Panel{ + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.Label{ + frame={t=0, l=1}, + auto_width=true, + text='gui/gm-editor has paused all game functions', + }, + widgets.Label{ + frame={t=0, r=1}, + auto_width=true, + text='gui/gm-editor has paused all game functions', + }, + widgets.Label{ + frame={}, + auto_width=true, + text='gui/gm-editor has paused all game functions', + }, + widgets.Label{ + frame={b=0, l=1}, + auto_width=true, + text='gui/gm-editor has paused all game functions', + }, + widgets.Label{ + frame={b=0, r=1}, + auto_width=true, + text='gui/gm-editor has paused all game functions', + }, + }, + }, + } + freeze_screen = self +end + +function FreezeScreen:onDismiss() + freeze_screen = nil +end + GmScreen = defclass(GmScreen, gui.ZScreen) GmScreen.ATTRS { focus_path='gm-editor', freeze=false, } +local function has_frozen_view() + for view in pairs(views) do + if view.freeze then + return true + end + end + return false +end + function GmScreen:init(args) local target = args.target if not target then qerror('Target not found') end - self.force_pause = self.freeze - self:addviews{GmEditorUi{target=target}} -end - -function GmScreen:onIdle() - if not self.freeze then - GmScreen.super.onIdle(self) - elseif self.force_pause and dfhack.isMapLoaded() then - df.global.pause_state = true - end -end - -function GmScreen:render(dc) if self.freeze then - GmScreen.super.super.render(self, dc) - else - GmScreen.super.render(self, dc) + self.force_pause = true + if not has_frozen_view() then + FreezeScreen{}:show() + -- raise existing views above the freeze screen + for view in pairs(views) do + view:raise() + end + end end + self:addviews{GmEditorUi{target=target}} + views[self] = true end function GmScreen:onDismiss() views[self] = nil + if freeze_screen then + if not has_frozen_view() then + freeze_screen:dismiss() + end + end end local function get_editor(args) @@ -679,18 +733,27 @@ local function get_editor(args) if args[1]=="dialog" then dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, "", function(entry) - local view = GmScreen{freeze=freeze, target=eval(entry)}:show() - views[view] = true + GmScreen{freeze=freeze, target=eval(entry)}:show() end) elseif args[1]=="free" then - return GmScreen{freeze=freeze, target=df.reinterpret_cast(df[args[2]],args[3])}:show() + GmScreen{freeze=freeze, target=df.reinterpret_cast(df[args[2]],args[3])}:show() + elseif args[1]=="scr" then + -- this will not work for more complicated expressions, like scr.fieldname, but + -- it should capture the most common case + GmScreen{freeze=freeze, target=dfhack.gui.getDFViewscreen(true)}:show() else - return GmScreen{freeze=freeze, target=eval(args[1])}:show() + GmScreen{freeze=freeze, target=eval(args[1])}:show() end else - return GmScreen{freeze=freeze, target=getTargetFromScreens()}:show() + GmScreen{freeze=freeze, target=getTargetFromScreens()}:show() end end views = views or {} -views[get_editor{...}] = true +freeze_screen = freeze_screen or nil + +if dfhack_flags.module then + return +end + +get_editor{...} From 1e2b27ec9094f118a628ef23edd3adaaa9a5ec8b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 16 May 2023 17:18:04 -0700 Subject: [PATCH 210/732] use dynamic texpos values --- gui/overlay.lua | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/gui/overlay.lua b/gui/overlay.lua index fedf15b8e4..f5d33c0244 100644 --- a/gui/overlay.lua +++ b/gui/overlay.lua @@ -9,6 +9,7 @@ local overlay = require('plugins.overlay') local DIALOG_WIDTH = 59 local LIST_HEIGHT = 14 +local HIGHLIGHT_TILE = df.global.init.load_bar_texpos[1] local SHADOW_FRAME = copyall(gui.PANEL_FRAME) SHADOW_FRAME.signature_pen = false @@ -16,14 +17,14 @@ SHADOW_FRAME.signature_pen = false local to_pen = dfhack.pen.parse local HIGHLIGHT_FRAME = { - t_frame_pen = to_pen{tile=902, ch=205, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, - l_frame_pen = to_pen{tile=908, ch=186, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, - b_frame_pen = to_pen{tile=916, ch=205, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, - r_frame_pen = to_pen{tile=910, ch=186, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, - lt_frame_pen = to_pen{tile=901, ch=201, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, - lb_frame_pen = to_pen{tile=915, ch=200, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, - rt_frame_pen = to_pen{tile=903, ch=187, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, - rb_frame_pen = to_pen{tile=917, ch=188, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, + t_frame_pen = to_pen{tile=df.global.init.texpos_border_n, ch=205, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, + l_frame_pen = to_pen{tile=df.global.init.texpos_border_w, ch=186, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, + b_frame_pen = to_pen{tile=df.global.init.texpos_border_s, ch=205, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, + r_frame_pen = to_pen{tile=df.global.init.texpos_border_e, ch=186, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, + lt_frame_pen = to_pen{tile=df.global.init.texpos_border_nw, ch=201, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, + lb_frame_pen = to_pen{tile=df.global.init.texpos_border_sw, ch=200, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, + rt_frame_pen = to_pen{tile=df.global.init.texpos_border_ne, ch=187, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, + rb_frame_pen = to_pen{tile=df.global.init.texpos_border_se, ch=188, fg=COLOR_GREEN, bg=COLOR_BLACK, tile_fg=COLOR_LIGHTGREEN}, signature_pen=false, } @@ -31,14 +32,14 @@ local function make_highlight_frame_style(frame) local frame_style = copyall(HIGHLIGHT_FRAME) local fg, bg = COLOR_GREEN, COLOR_LIGHTGREEN if frame.t then - frame_style.t_frame_pen = to_pen{tile=779, ch=205, fg=fg, bg=bg} + frame_style.t_frame_pen = to_pen{tile=HIGHLIGHT_TILE, ch=205, fg=fg, bg=bg} elseif frame.b then - frame_style.b_frame_pen = to_pen{tile=779, ch=205, fg=fg, bg=bg} + frame_style.b_frame_pen = to_pen{tile=HIGHLIGHT_TILE, ch=205, fg=fg, bg=bg} end if frame.l then - frame_style.l_frame_pen = to_pen{tile=779, ch=186, fg=fg, bg=bg} + frame_style.l_frame_pen = to_pen{tile=HIGHLIGHT_TILE, ch=186, fg=fg, bg=bg} elseif frame.r then - frame_style.r_frame_pen = to_pen{tile=779, ch=186, fg=fg, bg=bg} + frame_style.r_frame_pen = to_pen{tile=HIGHLIGHT_TILE, ch=186, fg=fg, bg=bg} end return frame_style end From 26c09c4a7979c4eaca6746de8f5c51f66fc8e04a Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 16 May 2023 17:23:14 -0700 Subject: [PATCH 211/732] Apply suggestions from code review --- gui/control-panel.lua | 2 -- gui/launcher.lua | 2 -- 2 files changed, 4 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 900268866e..9487f77b15 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -833,8 +833,6 @@ function ControlPanel:init() }, on_select=self:callback('set_page'), get_cur_page=function() return self.subviews.pages:getSelected() end, - key='CUSTOM_ALT_T', - key_back='CUSTOM_ALT_R', }, widgets.Pages{ view_id='pages', diff --git a/gui/launcher.lua b/gui/launcher.lua index f3917678be..a976e7a117 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -357,8 +357,6 @@ function HelpPanel:init() }, on_select=function(idx) self.subviews.pages:setSelected(idx) end, get_cur_page=function() return self.subviews.pages:getSelected() end, - key='CUSTOM_ALT_T', - key_back='CUSTOM_ALT_R', }, widgets.Pages{ view_id='pages', From 5281ed0425fef2a47c7822ed1ea30bc3ad785921 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 17 May 2023 11:51:41 -0700 Subject: [PATCH 212/732] change wording on frame badge and background --- gui/gm-editor.lua | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 615e9e87a5..f52dbb6833 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -634,6 +634,12 @@ end function GmEditorUi:postUpdateLayout() save_config({frame = self.frame}) end +function GmEditorUi:onRenderFrame(dc, rect) + GmEditorUi.super.onRenderFrame(self, dc, rect) + if self.parent_view.freeze then + dc:seek(rect.x1+2, rect.y2):string(' GAME SUSPENDED ', COLOR_RED) + end +end FreezeScreen = defclass(FreezeScreen, gui.Screen) FreezeScreen.ATTRS{ @@ -648,27 +654,27 @@ function FreezeScreen:init() widgets.Label{ frame={t=0, l=1}, auto_width=true, - text='gui/gm-editor has paused all game functions', + text='Dwarf Fortress is currently suspended by gui/gm-editor', }, widgets.Label{ frame={t=0, r=1}, auto_width=true, - text='gui/gm-editor has paused all game functions', + text='Dwarf Fortress is currently suspended by gui/gm-editor', }, widgets.Label{ frame={}, auto_width=true, - text='gui/gm-editor has paused all game functions', + text='Dwarf Fortress is currently suspended by gui/gm-editor', }, widgets.Label{ frame={b=0, l=1}, auto_width=true, - text='gui/gm-editor has paused all game functions', + text='Dwarf Fortress is currently suspended by gui/gm-editor', }, widgets.Label{ frame={b=0, r=1}, auto_width=true, - text='gui/gm-editor has paused all game functions', + text='Dwarf Fortress is currently suspended by gui/gm-editor', }, }, }, From a022ec0a8ef3ae7009122d400526838bd92787de Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 17 May 2023 17:23:00 -0700 Subject: [PATCH 213/732] add gui/autodump --- docs/gui/autodump.rst | 29 +++ gui/autodump.lua | 401 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 430 insertions(+) create mode 100644 docs/gui/autodump.rst create mode 100644 gui/autodump.lua diff --git a/docs/gui/autodump.rst b/docs/gui/autodump.rst new file mode 100644 index 0000000000..a41cf839bf --- /dev/null +++ b/docs/gui/autodump.rst @@ -0,0 +1,29 @@ +gui/autodump +============ + +.. dfhack-tool:: + :summary: Teleport or destroy items. + :tags: fort armok items + +This is a general point and click interface for teleporting or destroying +items. By default, it will teleport items you have marked for dumping, but if +you draw boxes around items on the map, it will act on the selected items +instead. Double-click anywhere on the map to teleport the items there. Be wary +(or excited) that if you teleport the items into an unsupported position (e.g. +mid-air), then they will become projectiles and fall. + +Usage +----- + +:: + + gui/autodump + +Destroying items +---------------- + +This tool also allows you to destroy the target items instead of teleporting +them. When you click the destroy button (or hit the hotkey), `gui/autodump` +will force-pause the game and enable an "Undo" button, just in case you want +those items back. Once you exit the `gui/autodump` tool, those items will be +unrecoverable. diff --git a/gui/autodump.lua b/gui/autodump.lua new file mode 100644 index 0000000000..aac8db367c --- /dev/null +++ b/gui/autodump.lua @@ -0,0 +1,401 @@ +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local widgets = require('gui.widgets') + +local function get_dims(pos1, pos2) + local width, height, depth = math.abs(pos1.x - pos2.x) + 1, + math.abs(pos1.y - pos2.y) + 1, + math.abs(pos1.z - pos2.z) + 1 + return width, height, depth +end + +local function is_good_item(item, include_forbidden, include_in_job) + if not item then return false end + if not item.flags.on_ground or item.flags.garbage_collect or + item.flags.hostile or item.flags.on_fire or item.flags.trader or + item.flags.in_building or item.flags.construction or item.flags.spider_web then + return false + end + if item.flags.forbid and not include_forbidden then return false end + if item.flags.in_job and not include_in_job then return false end + return true +end + +----------------- +-- Autodump +-- + +Autodump = defclass(Autodump, widgets.Window) +Autodump.ATTRS { + frame_title='Autodump', + frame={w=47, h=18, r=2, t=18}, + resizable=true, + resize_min={h=10}, + autoarrange_subviews=true, +} + +function Autodump:init() + self.mark = nil + self.prev_help_text = '' + self.destroyed_items = {} + self:reset_selected_state() -- sets self.selected_* + self:refresh_dump_items() -- sets self.dump_items + self:reset_double_click() -- sets self.last_map_click_ms and self.last_map_click_pos + + self:addviews{ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap=self:callback('get_help_text'), + }, + widgets.Panel{frame={h=1}}, + widgets.Panel{ + frame={h=2}, + subviews={ + widgets.Label{ + frame={l=0, t=0}, + text={ + 'Selected area: ', + {text=self:callback('get_selection_area_text')} + }, + }, + }, + visible=function() return self.mark end, + }, + widgets.HotkeyLabel{ + frame={l=0}, + label='Dump to tile under mouse cursor', + key='CUSTOM_CTRL_D', + auto_width=true, + on_activate=self:callback('do_dump'), + enabled=function() return dfhack.gui.getMousePos() end, + }, + widgets.HotkeyLabel{ + frame={l=0}, + label='Destroy items', + key='CUSTOM_CTRL_Y', + auto_width=true, + on_activate=self:callback('do_destroy'), + enabled=function() return #self.dump_items > 0 or #self.selected_items.list > 0 end, + }, + widgets.HotkeyLabel{ + frame={l=0}, + label='Undo destroy items', + key='CUSTOM_CTRL_Z', + auto_width=true, + on_activate=self:callback('undo_destroy'), + enabled=function() return #self.destroyed_items > 0 end, + }, + widgets.HotkeyLabel{ + frame={l=0}, + label='Select all items on this z-level', + key='CUSTOM_CTRL_A', + auto_width=true, + on_activate=function() + self:select_box(self:get_bounds( + {x=0, y=0, z=df.global.window_z}, + {x=df.global.world.map.x_count-1, + y=df.global.world.map.y_count-1, + z=df.global.window_z})) + self:updateLayout() + end, + }, + widgets.HotkeyLabel{ + frame={l=0}, + label='Clear selected items', + key='CUSTOM_CTRL_C', + auto_width=true, + on_activate=self:callback('reset_selected_state'), + enabled=function() return #self.selected_items.list > 0 end, + }, + widgets.Panel{frame={h=1}}, + widgets.ToggleHotkeyLabel{ + view_id='include_forbidden', + frame={l=0}, + label='Include forbidden items', + key='CUSTOM_CTRL_F', + auto_width=true, + initial_option=false, + on_change=self:callback('refresh_dump_items'), + }, + widgets.ToggleHotkeyLabel{ + view_id='include_in_job', + frame={l=0}, + label='Include items claimed by jobs', + key='CUSTOM_CTRL_J', + auto_width=true, + initial_option=false, + on_change=self:callback('refresh_dump_items'), + }, + widgets.ToggleHotkeyLabel{ + view_id='mark_as_forbidden', + frame={l=0}, + label='Forbid after teleporting', + key='CUSTOM_CTRL_M', + auto_width=true, + initial_option=false, + }, + } +end + +function Autodump:reset_double_click() + self.last_map_click_ms = 0 + self.last_map_click_pos = {} +end + +function Autodump:reset_selected_state() + self.selected_items = {list={}, set={}} + self.selected_coords = {} -- z -> y -> x -> true + self.selected_bounds = {} -- z -> bounds rect + if next(self.subviews) then + self:updateLayout() + end +end + +function Autodump:refresh_dump_items() + local dump_items = {} + local include_forbidden = false + local include_in_job = false + if next(self.subviews) then + include_forbidden = self.subviews.include_forbidden:getOptionValue() + include_in_job = self.subviews.include_in_job:getOptionValue() + end + for _,item in ipairs(df.global.world.items.all) do + if not is_good_item(item, include_forbidden, include_in_job) then goto continue end + if item.flags.dump then + table.insert(dump_items, item) + end + ::continue:: + end + self.dump_items = dump_items + if next(self.subviews) then + self:updateLayout() + end +end + +function Autodump:get_help_text() + local ret = 'Double click on a tile to teleport' + if #self.selected_items.list > 0 then + ret = ('%s %d highlighted item(s).'):format(ret, #self.selected_items.list) + else + ret = ('%s %d item(s) marked for dumping.'):format(ret, #self.dump_items) + end + if ret ~= self.prev_help_text then + self.prev_help_text = ret + end + return ret +end + +function Autodump:get_selection_area_text() + local mark = self.mark + if not mark then return '' end + local cursor = dfhack.gui.getMousePos() or {x=mark.x, y=mark.y, z=df.global.window_z} + return ('%dx%dx%d'):format(get_dims(mark, cursor)) +end + +function Autodump:get_bounds(cursor, mark) + cursor = cursor or self.mark + mark = mark or self.mark or cursor + if not mark then return end + + return { + x1=math.min(cursor.x, mark.x), + x2=math.max(cursor.x, mark.x), + y1=math.min(cursor.y, mark.y), + y2=math.max(cursor.y, mark.y), + z1=math.min(cursor.z, mark.z), + z2=math.max(cursor.z, mark.z) + } +end + +function Autodump:select_items_in_block(block, bounds) + local include_forbidden = self.subviews.include_forbidden:getOptionValue() + local include_in_job = self.subviews.include_in_job:getOptionValue() + for _,item_id in ipairs(block.items) do + local item = df.item.find(item_id) + if not is_good_item(item, include_forbidden, include_in_job) then + goto continue + end + local x, y, z = dfhack.items.getPosition(item) + if not x then goto continue end + if not self.selected_items.set[item_id] and + x >= bounds.x1 and x <= bounds.x2 and + y >= bounds.y1 and y <= bounds.y2 then + self.selected_items.set[item_id] = true + table.insert(self.selected_items.list, item) + ensure_key(ensure_key(self.selected_coords, z), y)[x] = true + local selected_bounds = ensure_key(self.selected_bounds, z, + {x1=x, x2=x, y1=y, y2=y}) + selected_bounds.x1 = math.min(selected_bounds.x1, x) + selected_bounds.x2 = math.max(selected_bounds.x2, x) + selected_bounds.y1 = math.min(selected_bounds.y1, y) + selected_bounds.y2 = math.max(selected_bounds.y2, y) + end + ::continue:: + end +end + +function Autodump:select_box(bounds) + if not bounds then return end + local seen_blocks = {} + for z=bounds.z1,bounds.z2 do + for y=bounds.y1,bounds.y2 do + for x=bounds.x1,bounds.x2 do + local block = dfhack.maps.getTileBlock(xyz2pos(x, y, z)) + local block_str = tostring(block) + if not seen_blocks[block_str] then + seen_blocks[block_str] = true + self:select_items_in_block(block, bounds) + end + end + end + end +end + +function Autodump:onInput(keys) + if Autodump.super.onInput(self, keys) then return true end + if keys._MOUSE_R_DOWN and self.mark then + self.mark = nil + self:updateLayout() + return true + elseif keys._MOUSE_L_DOWN then + if self:getMouseFramePos() then return true end + local pos = dfhack.gui.getMousePos() + if not pos then + self:reset_double_click() + return false + end + local now_ms = dfhack.getTickCount() + if same_xyz(pos, self.last_map_click_pos) and + now_ms - self.last_map_click_ms <= widgets.DOUBLE_CLICK_MS then + self:reset_double_click() + self:do_dump(pos) + self.mark = nil + self:updateLayout() + return true + end + self.last_map_click_ms = now_ms + self.last_map_click_pos = pos + if self.mark then + self:select_box(self:get_bounds(pos)) + self:reset_double_click() + self.mark = nil + self:updateLayout() + return true + end + self.mark = pos + self:updateLayout() + return true + end +end + +local to_pen = dfhack.pen.parse +local CURSOR_PEN = to_pen{ch='o', fg=COLOR_BLUE, + tile=dfhack.screen.findGraphicsTile('CURSORS', 5, 22)} +local BOX_PEN = to_pen{ch='X', fg=COLOR_GREEN, + tile=dfhack.screen.findGraphicsTile('CURSORS', 0, 0)} +local SELECTED_PEN = to_pen{ch='I', fg=COLOR_GREEN, + tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} + +function Autodump:onRenderFrame(dc, rect) + Autodump.super.onRenderFrame(self, dc, rect) + + local highlight_coords = self.selected_coords[df.global.window_z] + if highlight_coords then + local function get_overlay_pen(pos) + if safe_index(highlight_coords, pos.y, pos.x) then + return SELECTED_PEN + end + end + guidm.renderMapOverlay(get_overlay_pen, self.selected_bounds[df.global.window_z]) + end + + -- draw selection box and cursor (blinking when in ascii mode) + local cursor = dfhack.gui.getMousePos() + local selection_bounds = self:get_bounds(cursor) + if selection_bounds and (dfhack.screen.inGraphicsMode() or gui.blink_visible(500)) then + guidm.renderMapOverlay( + function() return self.mark and BOX_PEN or CURSOR_PEN end, + selection_bounds) + end +end + +function Autodump:do_dump(pos) + pos = pos or dfhack.gui.getMousePos() + if not pos then return end + local tileattrs = df.tiletype.attrs[dfhack.maps.getTileType(pos)] + local basic_shape = df.tiletype_shape.attrs[tileattrs.shape].basic_shape + print(basic_shape, df.tiletype_shape_basic[basic_shape]) + local on_ground = basic_shape == df.tiletype_shape_basic.Floor or + basic_shape == df.tiletype_shape_basic.Stair or + basic_shape == df.tiletype_shape_basic.Ramp + local items = #self.selected_items.list > 0 and self.selected_items.list or self.dump_items + print(('dumping %d items at (%d, %d, %d):'):format(#items, pos.x, pos.y, pos.z)) + local mark_as_forbidden = self.subviews.mark_as_forbidden:getOptionValue() + for _,item in ipairs(items) do + if dfhack.items.moveToGround(item, pos) then + item.flags.dump = false + if mark_as_forbidden then + item.flags.forbid = true + end + if not on_ground then + dfhack.items.makeProjectile(item) + end + else + print(('Could not move item: %s from (%d, %d, %d)'):format( + dfhack.items.getDescription(item, 0, true), + item.pos.x, item.pos.y, item.pos.z)) + end + end + self:refresh_dump_items() + self:reset_selected_state() + self:updateLayout() +end + +function Autodump:do_destroy() + self.parent_view.force_pause = true + local items = #self.selected_items.list > 0 and self.selected_items.list or self.dump_items + print(('destroying %d items'):format(#items)) + for _,item in ipairs(items) do + table.insert(self.destroyed_items, {item=item, flags=copyall(item.flags)}) + item.flags.garbage_collect = true + item.flags.forbid = true + item.flags.hidden = true + end + self:refresh_dump_items() + self:reset_selected_state() + self:updateLayout() +end + +function Autodump:undo_destroy() + print(('undestroying %d items'):format(#self.destroyed_items)) + for _,item_spec in ipairs(self.destroyed_items) do + local item = item_spec.item + item.flags.garbage_collect = false + item.flags.forbid = item_spec.flags.forbid + item.flags.hidden = item_spec.flags.hidden + end + self.destroyed_items = {} + self:refresh_dump_items() + self.parent_view.force_pause = false +end + +----------------- +-- AutodumpScreen +-- + +AutodumpScreen = defclass(AutodumpScreen, gui.ZScreen) +AutodumpScreen.ATTRS { + focus_path='autodump', + pass_movement_keys=true, + pass_mouse_clicks=false, +} + +function AutodumpScreen:init() + self:addviews{Autodump{}} +end + +function AutodumpScreen:onDismiss() + view = nil +end + +view = view and view:raise() or AutodumpScreen{}:show() From bcc8fa63b9b414cc54d78fcf2d8eca973a44c844 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 17 May 2023 17:27:35 -0700 Subject: [PATCH 214/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e22dad1dfb..022d191932 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ that repo. - `light-aquifers-only`: reinstated: convert heavy aquifers to light - `necronomicon`: search fort for items containing the secrets of life and death - `gui/mod-manager`: automatically restore your list of active mods when generating new worlds +- `gui/autodump`: point and click item teleportation and destruction interface ## Fixes - `gui/design`: Fix building and stairs designation From 97557b9239a5ad8297f6a32651b78fd9dbb333ec Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 17 May 2023 17:47:09 -0700 Subject: [PATCH 215/732] sync spreadsheet to docs --- docs/necronomicon.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/necronomicon.rst b/docs/necronomicon.rst index c08ea20c71..f64622380f 100644 --- a/docs/necronomicon.rst +++ b/docs/necronomicon.rst @@ -3,7 +3,7 @@ necronomicon .. dfhack-tool:: :summary: Find books that contain the secrets of life and death. - :tags: fort items + :tags: fort inspection productivity items Lists all books in the fortress that contain the secrets to life and death. To find the books in fortress mode, go to the Written content submenu in Objects (O). From 34f85209027c871537ddcb6762c7c9214ef8ca9a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 18 May 2023 17:12:11 -0700 Subject: [PATCH 216/732] be looser about tile types where machines go strictly, they need a gear assembly nearby to support them, but I think quickfort can assume that you're doing it "right" (otherwise the machine will deconstruct after being built) --- changelog.txt | 1 + internal/quickfort/build.lua | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 022d191932..759b8626b9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -23,6 +23,7 @@ that repo. ## Fixes - `gui/design`: Fix building and stairs designation +- `quickfort`: fixed detection of tiles where machines are allowed (e.g. water wheels *can* be built on stairs if there is a machine support nearby) ## Misc Improvements - `gui/create-item`: ask for number of items to spawn by default diff --git a/internal/quickfort/build.lua b/internal/quickfort/build.lua index 48bc4bd754..19e43c9ce4 100644 --- a/internal/quickfort/build.lua +++ b/internal/quickfort/build.lua @@ -92,7 +92,9 @@ local function is_valid_tile_has_space_or_is_ramp(pos) end local function is_valid_tile_machine(pos) - return is_valid_tile_has_space_or_is_ramp(pos) + local shape = df.tiletype.attrs[dfhack.maps.getTileType(pos)].shape + local basic_shape = df.tiletype_shape.attrs[shape].basic_shape + return is_valid_tile_has_space_or_is_ramp(pos) or basic_shape == df.tiletype_shape_basic.Stair end -- ramps are ok everywhere except under the anchor point of directional bridges @@ -312,7 +314,7 @@ local function make_ns_ew_entry(name, building_type, long_dim_min, long_dim_max, min_width=width_min, max_width=width_max, min_height=height_min, max_height=height_max, direction=vertical and 1 or 0, - is_valid_tile_fn=is_valid_tile_has_space, -- impeded by ramps + is_valid_tile_fn=is_valid_tile_machine, transform=transform} end local function make_water_wheel_entry(vertical) From 73f9be3efeb9fdbb7b764456620b35f1d5cf7e42 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 18 May 2023 17:32:29 -0700 Subject: [PATCH 217/732] don't consider itemless tiles as errors for blueprints that affect items --- changelog.txt | 1 + internal/quickfort/dig.lua | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/changelog.txt b/changelog.txt index 022d191932..a78cee719e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ that repo. - `gui/design`: Fix building and stairs designation ## Misc Improvements +- `quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them - `gui/create-item`: ask for number of items to spawn by default - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for the gm-editor window itself) diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index e6e3b6643e..7fa3160630 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -375,46 +375,47 @@ local function get_items_at(pos, include_buildings) return items end -local function do_item_flag(pos, flag_name, flag_value, include_buildings) - local items = get_items_at(pos, include_buildings) - if #items == 0 then return nil end +local function do_item_flag(digctx, flag_name, flag_value, include_buildings) + if digctx.flags.hidden then return nil end + local items = get_items_at(digctx.pos, include_buildings) + if #items == 0 then return function() end end -- noop, but not an error return function() for _,item in ipairs(items) do item.flags[flag_name] = flag_value end end end local function do_claim(digctx) - return do_item_flag(digctx.pos, "forbid", values.item_claimed, true) + return do_item_flag(digctx, "forbid", values.item_claimed, true) end local function do_forbid(digctx) - return do_item_flag(digctx.pos, "forbid", values.item_forbidden, true) + return do_item_flag(digctx, "forbid", values.item_forbidden, true) end local function do_melt(digctx) -- the game appears to autoremove the flag from unmeltable items, so we -- don't actually need to do any filtering here - return do_item_flag(digctx.pos, "melt", values.item_melted, false) + return do_item_flag(digctx, "melt", values.item_melted, false) end local function do_remove_melt(digctx) - return do_item_flag(digctx.pos, "melt", values.item_unmelted, false) + return do_item_flag(digctx, "melt", values.item_unmelted, false) end local function do_dump(digctx) - return do_item_flag(digctx.pos, "dump", values.item_dumped, false) + return do_item_flag(digctx, "dump", values.item_dumped, false) end local function do_remove_dump(digctx) - return do_item_flag(digctx.pos, "dump", values.item_undumped, false) + return do_item_flag(digctx, "dump", values.item_undumped, false) end local function do_hide(digctx) - return do_item_flag(digctx.pos, "hidden", values.item_hidden, true) + return do_item_flag(digctx, "hidden", values.item_hidden, true) end local function do_unhide(digctx) - return do_item_flag(digctx.pos, "hidden", values.item_unhidden, true) + return do_item_flag(digctx, "hidden", values.item_unhidden, true) end local function do_traffic_high(digctx) From da4166bc1836963a405b55055ded30c7b0e61411 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 18 May 2023 17:54:15 -0700 Subject: [PATCH 218/732] fix carving restrictions under buildings --- changelog.txt | 1 + internal/quickfort/dig.lua | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index 022d191932..2123e97eb4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -22,6 +22,7 @@ that repo. - `gui/autodump`: point and click item teleportation and destruction interface ## Fixes +- `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath passable tiles of buildings - `gui/design`: Fix building and stairs designation ## Misc Improvements diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index e6e3b6643e..cd17f8bc0b 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -281,8 +281,7 @@ local function do_smooth(digctx) if is_construction(digctx.tileattrs) or not is_hard(digctx.tileattrs) or is_smooth(digctx.tileattrs) or - (not is_floor(digctx.tileattrs) and - not is_wall(digctx.tileattrs)) then + not (is_floor(digctx.tileattrs) or is_wall(digctx.tileattrs)) then return nil end return function() digctx.flags.smooth = values.tile_smooth end @@ -290,8 +289,8 @@ end local function do_engrave(digctx) if digctx.flags.hidden or - is_construction(digctx.tileattrs) or not is_smooth(digctx.tileattrs) or + not (is_floor(digctx.tileattrs) or is_wall(digctx.tileattrs)) or digctx.engraving ~= nil then return nil end @@ -710,9 +709,18 @@ local function do_run_impl(zlevel, grid, ctx) get_track_direction(extent_x, extent_y, extent.width, extent.height)) local digctx = init_dig_ctx(ctx, extent_pos, direction) - -- can't dig through buildings - if digctx.occupancy.building ~= 0 then - goto inner_continue + if db_entry.action == do_smooth or db_entry.action == do_engrave or + db_entry.action == do_track then + -- can only smooth passable tiles + if digctx.occupancy.building > df.tile_building_occ.Passable and + digctx.occupancy.building ~= df.tile_building_occ.Dynamic then + goto inner_continue + end + else + -- can't dig through buildings + if digctx.occupancy.building ~= 0 then + goto inner_continue + end end local action_fn = dig_tile(digctx, db_entry) quickfort_preview.set_preview_tile(ctx, extent_pos, From 07cedb1290bcfd292dd6518ff5b0b0519e6d07ed Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 12:50:50 -0700 Subject: [PATCH 219/732] Apply suggestions from code review --- fix/stuck-instruments.lua | 52 +++++++++++++-------------------------- gui/control-panel.lua | 2 +- 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/fix/stuck-instruments.lua b/fix/stuck-instruments.lua index 8b4080fb4e..25b9dd9d32 100644 --- a/fix/stuck-instruments.lua +++ b/fix/stuck-instruments.lua @@ -1,47 +1,20 @@ -- Fixes instruments that never got played during a performance -local help = [====[ +local argparse = require('argparse') -fix/stuck-instruments -===================== - -Fixes instruments that were picked up for a performance, but were instead -simulated and are now stuck permanently in a job that no longer exists. - -This works around the issue encountered with :bug:`9485`, and should be run -if you notice any instruments lying on the ground that seem to be stuck in a -job. - -Run ``fix/stuck-instruments -n`` or ``fix/stuck-instruments --dry-run`` to -list how many instruments would be fixed without performing the action. - -]====] - - -function fixInstruments(args) - local dry_run = false +function fixInstruments(opts) local fixed = 0 - for _, arg in pairs(args) do - if args[1]:match('-h') or args[1]:match('help') then - print(dfhack.script_help()) - return - elseif args[1]:match('-n') or args[1]:match('dry') then - dry_run = true - end - end for _, item in ipairs(df.global.world.items.other.INSTRUMENT) do for i, ref in pairs(item.general_refs) do if ref:getType() == df.general_ref_type.ACTIVITY_EVENT then local activity = df.activity_entry.find(ref.activity_id) if not activity then - if not dry_run then + if not opts.dry_run then --remove dead activity reference + item.general_refs[i]:delete() item.general_refs:erase(i) - if item.flags.in_job then - --remove stuck in_job flag if true - item.flags.in_job = false - end + item.flags.in_job = false end fixed = fixed + 1 break @@ -50,7 +23,7 @@ function fixInstruments(args) end end - if fixed > 0 or dry_run then + if fixed > 0 or opts.dry_run then print(("%s %d stuck instruments."):format( dry_run and "Found" or "Fixed", fixed @@ -58,7 +31,16 @@ function fixInstruments(args) end end +local opts = {} -if not dfhack_flags.module then - fixInstruments{...} +local positionals = argparse.processArgsGetopt({...}, { + { 'h', 'help', handler = function() options.help = true end }, + { 'n', 'dry-run', handler = function() options.dry_run = true end }, +}) + +if positionals[1] == 'help' or opts.help then + print(dfhack.script_help()) + return end + +fixInstruments(opts) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 6c397dd528..67a09a057d 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -97,7 +97,7 @@ local REPEATS = { ['combine']={ desc='Combine partial stacks in stockpiles into full stacks.', command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, - ['fixInstruments']={ + ['stuck-instruments']={ desc='Fix activity references on stuck instruments to make them usable again.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, ['general-strike']={ From 45e43a0e7db6fb83c6cf3492b77ad7f151628ac8 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 12:54:45 -0700 Subject: [PATCH 220/732] Update fix/stuck-instruments.lua --- fix/stuck-instruments.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fix/stuck-instruments.lua b/fix/stuck-instruments.lua index 25b9dd9d32..6c4c35b7c7 100644 --- a/fix/stuck-instruments.lua +++ b/fix/stuck-instruments.lua @@ -34,8 +34,8 @@ end local opts = {} local positionals = argparse.processArgsGetopt({...}, { - { 'h', 'help', handler = function() options.help = true end }, - { 'n', 'dry-run', handler = function() options.dry_run = true end }, + { 'h', 'help', handler = function() opts.help = true end }, + { 'n', 'dry-run', handler = function() opts.dry_run = true end }, }) if positionals[1] == 'help' or opts.help then From a5ad329d5892ad44fc57b6341b7f90e43604be25 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 12:55:34 -0700 Subject: [PATCH 221/732] Update fix/stuck-instruments.lua --- fix/stuck-instruments.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix/stuck-instruments.lua b/fix/stuck-instruments.lua index 6c4c35b7c7..2324cd85b4 100644 --- a/fix/stuck-instruments.lua +++ b/fix/stuck-instruments.lua @@ -25,7 +25,7 @@ function fixInstruments(opts) if fixed > 0 or opts.dry_run then print(("%s %d stuck instruments."):format( - dry_run and "Found" or "Fixed", + opts.dry_run and "Found" or "Fixed", fixed )) end From 8a26bfb02eefd5838d37024ab71afd9885659444 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 12:56:11 -0700 Subject: [PATCH 222/732] Update fix/stuck-instruments.lua --- fix/stuck-instruments.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix/stuck-instruments.lua b/fix/stuck-instruments.lua index 2324cd85b4..e90d13ce13 100644 --- a/fix/stuck-instruments.lua +++ b/fix/stuck-instruments.lua @@ -24,7 +24,7 @@ function fixInstruments(opts) end if fixed > 0 or opts.dry_run then - print(("%s %d stuck instruments."):format( + print(("%s %d stuck instrument(s)."):format( opts.dry_run and "Found" or "Fixed", fixed )) From f15191bb701b178ca697c635885a13703eb13ecb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 19 May 2023 13:01:52 -0700 Subject: [PATCH 223/732] add changelog entry for #635 --- changelog.txt | 1 + fix/stuck-instruments.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 1d7f00037b..f90e82f19c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. - `modtools/create-item`: reinstated: commandline and API interface for creating items - `light-aquifers-only`: reinstated: convert heavy aquifers to light - `necronomicon`: search fort for items containing the secrets of life and death +- `fix/stuck-instruments`: fix instruments that are attached to invalid jobs, making them unusable - `gui/mod-manager`: automatically restore your list of active mods when generating new worlds - `gui/autodump`: point and click item teleportation and destruction interface diff --git a/fix/stuck-instruments.lua b/fix/stuck-instruments.lua index e90d13ce13..0526efb769 100644 --- a/fix/stuck-instruments.lua +++ b/fix/stuck-instruments.lua @@ -10,6 +10,7 @@ function fixInstruments(opts) if ref:getType() == df.general_ref_type.ACTIVITY_EVENT then local activity = df.activity_entry.find(ref.activity_id) if not activity then + print(('Found stuck instrument: %s'):format(dfhack.items.getDescription(item, 0, true))) if not opts.dry_run then --remove dead activity reference item.general_refs[i]:delete() From 869cd1844837f0ec51ef4623c3c5ae9e7e876cb1 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 13:11:20 -0700 Subject: [PATCH 224/732] Create diplomacy.lua move from diplo.lua --- diplomacy.lua | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 diplomacy.lua diff --git a/diplomacy.lua b/diplomacy.lua new file mode 100644 index 0000000000..7d918b41e2 --- /dev/null +++ b/diplomacy.lua @@ -0,0 +1,128 @@ +@@ -0,0 +1,79 @@ +--[[ +diplo - Quick "Diplomacy" +Without arguments: + Print list of civs the player civ has diplomatic relations to. + This list might not match the ingame civ list, but is what matters. Does not include 'no contact' civs, etc. + Also shows if relations match between player civ and other civ - a mismatch can potentially happen 'naturally', but is important when trying to make peace proper, as both civs need to be at peace with the other. +With arguments: + diplo CIV_ID RELATION + Changes the relation to the civ identified by CIV_ID to the one specified in RELATION, making sure that this is mutual. + CIV_ID can be 'all' to change relations with all civs. + RELATION can be 'peace'/0 or 'war'/1. + Civ list is shown afterwards. +]] + +local args = {...} + +-- player civ references: +local p_civ_id = df.global.plotinfo.civ_id +local p_civ = df.historical_entity.find(df.global.plotinfo.civ_id) + +-- get list of civs: +function get_civ_list() + local civ_list = {} + for _, entity in pairs(p_civ.relations.diplomacy) do + local cur_civ_id = entity.group_id + local cur_civ = df.historical_entity.find(cur_civ_id) + if cur_civ.type == 0 then + -- if true then + rel_str = "" + if entity.relation == 0 then + rel_str = "0 (Peace)" + elseif entity.relation == 1 then + rel_str = "1 (War)" + end + matched = "No" + for _, entity2 in pairs(cur_civ.relations.diplomacy) do + if entity2.group_id == p_civ_id and entity2.relation == entity.relation then + matched = "Yes" + end + end + table.insert(civ_list, { + cur_civ_id, + rel_str, + matched, + dfhack.TranslateName(cur_civ.name, true) + }) + end + end + return civ_list +end + +-- output civ list: +function output_civ_list() + local civ_list = get_civ_list() + if not next(civ_list) then + print("Your civilisation has no diplomatic relations! This means something is going wrong, as it should have at least a relation to itself.") + else + print(([[%4s %12s %8s %30s]]):format("ID", "Relation", "Matched", "Name")) + for _, civ in pairs(civ_list) do + print(([[%4s %12s %8s %30s]]):format(civ[1], civ[2], civ[3], civ[4])) + end + end +end + +-- change relation: +function change_relation(civ_id, relation) + print("Changing relation with " .. civ_id .. " to " .. relation) + for _, entity in pairs(p_civ.relations.diplomacy) do + local cur_civ_id = entity.group_id + local cur_civ = df.historical_entity.find(cur_civ_id) + if cur_civ.type == 0 and cur_civ_id == civ_id then + entity.relation = relation + for _, entity2 in pairs(cur_civ.relations.diplomacy) do + if entity2.group_id == p_civ_id then + entity2.relation = relation + end + end + end + end +end + +-- parse relation string args: +function relation_parse(rel_str) + if rel_str:lower() == "peace" then + return 0 + elseif rel_str:lower() == "war" then + return 1 + elseif rel_str == "0" then + return 0 + elseif rel_str == "1" then + return 1 + else + print(dfhack.script_help()) + qerror("Cannot parse relation: " .. rel_str) + end +end + +-- handle 'all' civ argument: +function handle_all(arg1, arg2) + if arg1:lower() == "all" then + local civ_list = get_civ_list() + for _, civ in pairs(civ_list) do + if civ[1] ~= p_civ_id then + change_relation(civ[1], arg2) + end + end + else + change_relation(tonumber(arg1), arg2) + end +end + +-- if no civ ID is entered, just output list of civs: +if not args[1] then + output_civ_list() + return +end + +-- make sure that there is a relation to change to: +if not args[2] then + print(dfhack.script_help()) + qerror("Missing relation!") +end + +-- change relation(s) according to args: +handle_all(args[1], relation_parse(args[2])) +output_civ_list() +return From 4cded3208ed04d64c9d0fb476dc0603b475b77aa Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 13:11:48 -0700 Subject: [PATCH 225/732] Delete diplo.lua --- diplo.lua | 128 ------------------------------------------------------ 1 file changed, 128 deletions(-) delete mode 100644 diplo.lua diff --git a/diplo.lua b/diplo.lua deleted file mode 100644 index 3334ca14eb..0000000000 --- a/diplo.lua +++ /dev/null @@ -1,128 +0,0 @@ -@@ -0,0 +1,79 @@ ---[[ -diplo - Quick "Diplomacy" -Without arguments: - Print list of civs the player civ has diplomatic relations to. - This list might not match the ingame civ list, but is what matters. Does not include 'no contact' civs, etc. - Also shows if relations match between player civ and other civ - a mismatch can potentially happen 'naturally', but is important when trying to make peace proper, as both civs need to be at peace with the other. -With arguments: - diplo CIV_ID RELATION - Changes the relation to the civ identified by CIV_ID to the one specified in RELATION, making sure that this is mutual. - CIV_ID can be 'all' to change relations with all civs. - RELATION can be 'peace'/0 or 'war'/1. - Civ list is shown afterwards. -]] - -local args = {...} - --- player civ references: -local p_civ_id = df.global.plotinfo.civ_id -local p_civ = df.historical_entity.find(df.global.plotinfo.civ_id) - --- get list of civs: -function get_civ_list() - local civ_list = {} - for _, entity in pairs(p_civ.relations.diplomacy) do - local cur_civ_id = entity.group_id - local cur_civ = df.historical_entity.find(cur_civ_id) - if cur_civ.type == 0 then - -- if true then - rel_str = "" - if entity.relation == 0 then - rel_str = "0 (Peace)" - elseif entity.relation == 1 then - rel_str = "1 (War)" - end - matched = "No" - for _, entity2 in pairs(cur_civ.relations.diplomacy) do - if entity2.group_id == p_civ_id and entity2.relation == entity.relation then - matched = "Yes" - end - end - table.insert(civ_list, { - cur_civ_id, - rel_str, - matched, - dfhack.TranslateName(cur_civ.name, true) - }) - end - end - return civ_list -end - --- output civ list: -function output_civ_list() - local civ_list = get_civ_list() - if not next(civ_list) then - print("Your civilisation has no diplomatic relations! This means something is going wrong, as it should have at least a relation to itself.") - else - print(([[%4s %12s %8s %30s]]):format("ID", "Relation", "Matched", "Name")) - for _, civ in pairs(civ_list) do - print(([[%4s %12s %8s %30s]]):format(civ[1], civ[2], civ[3], civ[4])) - end - end -end - --- change relation: -function change_relation(civ_id, relation) - print("Changing relation with " .. civ_id .. " to " .. relation) - for _, entity in pairs(p_civ.relations.diplomacy) do - local cur_civ_id = entity.group_id - local cur_civ = df.historical_entity.find(cur_civ_id) - if cur_civ.type == 0 and cur_civ_id == civ_id then - entity.relation = relation - for _, entity2 in pairs(cur_civ.relations.diplomacy) do - if entity2.group_id == p_civ_id then - entity2.relation = relation - end - end - end - end -end - --- parse relation string args: -function relation_parse(rel_str) - if rel_str:lower() == "peace" then - return 0 - elseif rel_str:lower() == "war" then - return 1 - elseif rel_str == "0" then - return 0 - elseif rel_str == "1" then - return 1 - else - print(dfhack.script_help()) - qerror("Cannot parse relation: " .. rel_str) - end -end - --- handle 'all' civ argument: -function handle_all(arg1, arg2) - if arg1:lower() == "all" then - local civ_list = get_civ_list() - for _, civ in pairs(civ_list) do - if civ[1] ~= p_civ_id then - change_relation(civ[1], arg2) - end - end - else - change_relation(tonumber(arg1), arg2) - end -end - --- if no civ ID is entered, just output list of civs: -if not args[1] then - output_civ_list() - return -end - --- make sure that there is a relation to change to: -if not args[2] then - print(dfhack.script_help()) - qerror("Missing relation!") -end - --- change relation(s) according to args: -handle_all(args[1], relation_parse(args[2])) -output_civ_list() -return From 826c7534a39321df42be0f691ef93f68afeb52c7 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 13:24:38 -0700 Subject: [PATCH 226/732] Create diplomacy.rst --- docs/diplomacy.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/diplomacy.rst diff --git a/docs/diplomacy.rst b/docs/diplomacy.rst new file mode 100644 index 0000000000..af558e49cb --- /dev/null +++ b/docs/diplomacy.rst @@ -0,0 +1,29 @@ +diplomacy +========= + +.. dfhack-tool:: + :summary: View or alter diplomatic relationships. + :tags: fort armok inspection military + +This tool can report on or modify the diplomatic relationships (i.e. war vs. +peace) you have with other contacted civilizations. Note that a civilization +is only at peace if **both** you are at peace with them **and** they are at +peace with you. + +Usage +----- + +:: + + diplomacy + diplomacy + +Examples +-------- + +``diplomacy`` + See current diplomatic relationships between you and all other contacted + civs. +``diplomacy 224 war`` + Changes both your stance towards civilization 224 and their stance towards + you to War. From 918b1e15cca7ea4010698a52841fa16fb0f95e5d Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 13:28:46 -0700 Subject: [PATCH 227/732] Update diplomacy.lua --- diplomacy.lua | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/diplomacy.lua b/diplomacy.lua index 7d918b41e2..7baee75ab6 100644 --- a/diplomacy.lua +++ b/diplomacy.lua @@ -1,18 +1,3 @@ -@@ -0,0 +1,79 @@ ---[[ -diplo - Quick "Diplomacy" -Without arguments: - Print list of civs the player civ has diplomatic relations to. - This list might not match the ingame civ list, but is what matters. Does not include 'no contact' civs, etc. - Also shows if relations match between player civ and other civ - a mismatch can potentially happen 'naturally', but is important when trying to make peace proper, as both civs need to be at peace with the other. -With arguments: - diplo CIV_ID RELATION - Changes the relation to the civ identified by CIV_ID to the one specified in RELATION, making sure that this is mutual. - CIV_ID can be 'all' to change relations with all civs. - RELATION can be 'peace'/0 or 'war'/1. - Civ list is shown afterwards. -]] - local args = {...} -- player civ references: @@ -29,9 +14,9 @@ function get_civ_list() -- if true then rel_str = "" if entity.relation == 0 then - rel_str = "0 (Peace)" + rel_str = "Peace" elseif entity.relation == 1 then - rel_str = "1 (War)" + rel_str = "War" end matched = "No" for _, entity2 in pairs(cur_civ.relations.diplomacy) do @@ -56,7 +41,7 @@ function output_civ_list() if not next(civ_list) then print("Your civilisation has no diplomatic relations! This means something is going wrong, as it should have at least a relation to itself.") else - print(([[%4s %12s %8s %30s]]):format("ID", "Relation", "Matched", "Name")) + print(([[%4s %12s %8s %30s]]):format("ID", "Relation", "Mutual", "Name")) for _, civ in pairs(civ_list) do print(([[%4s %12s %8s %30s]]):format(civ[1], civ[2], civ[3], civ[4])) end @@ -65,7 +50,7 @@ end -- change relation: function change_relation(civ_id, relation) - print("Changing relation with " .. civ_id .. " to " .. relation) + print("Changing relation with " .. civ_id .. " to " .. (relation == 0 and "Peace" or "War")) for _, entity in pairs(p_civ.relations.diplomacy) do local cur_civ_id = entity.group_id local cur_civ = df.historical_entity.find(cur_civ_id) @@ -82,15 +67,11 @@ end -- parse relation string args: function relation_parse(rel_str) - if rel_str:lower() == "peace" then - return 0 - elseif rel_str:lower() == "war" then - return 1 - elseif rel_str == "0" then + if rel_str == "0" or rel_str:lower() == "peace" then return 0 - elseif rel_str == "1" then + elseif rel_str == "1" or rel_str:lower() == "war" then return 1 - else + else print(dfhack.script_help()) qerror("Cannot parse relation: " .. rel_str) end @@ -119,10 +100,9 @@ end -- make sure that there is a relation to change to: if not args[2] then print(dfhack.script_help()) - qerror("Missing relation!") + qerror("Please specify 'peace' or 'war'") end -- change relation(s) according to args: handle_all(args[1], relation_parse(args[2])) output_civ_list() -return From 4a41215f2e79e9d083047f333ea04d231c94fb8e Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 13:30:29 -0700 Subject: [PATCH 228/732] document 'all' param --- docs/diplomacy.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/diplomacy.rst b/docs/diplomacy.rst index af558e49cb..c735a479cc 100644 --- a/docs/diplomacy.rst +++ b/docs/diplomacy.rst @@ -16,6 +16,7 @@ Usage :: diplomacy + diplomacy all diplomacy Examples @@ -24,6 +25,8 @@ Examples ``diplomacy`` See current diplomatic relationships between you and all other contacted civs. -``diplomacy 224 war`` +``diplomacy 224 peace`` Changes both your stance towards civilization 224 and their stance towards - you to War. + you to peace. +``diplomacy all war`` + Induce the entire world to declare war on your civilization. From 4340095352fb1f3e494e0a6c5e38625c2bf09186 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 19 May 2023 13:32:32 -0700 Subject: [PATCH 229/732] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index f90e82f19c..83351899d2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,6 +14,7 @@ that repo. # Future ## New Scripts +- `diplomacy`: view or alter diplomatic relationships - `exportlegends`: reinstated: export extended legends information for external browsing - `modtools/create-item`: reinstated: commandline and API interface for creating items - `light-aquifers-only`: reinstated: convert heavy aquifers to light From 6ebe2d7ce482ed8ce054ff1eac21c94815487c2c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 19 May 2023 15:08:51 -0700 Subject: [PATCH 230/732] add missing return --- unforbid.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/unforbid.lua b/unforbid.lua index df269594d6..1daf1e41e1 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -50,6 +50,7 @@ local positionals = argparse.processArgsGetopt(args, { if positionals[1] == nil or positionals[1] == 'help' or options.help then print(dfhack.script_help()) + return end if positionals[1] == 'all' then From 86b083cdf5847128e88197834eb953c1716c50a8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 19 May 2023 15:13:24 -0700 Subject: [PATCH 231/732] remove debug print --- gui/control-panel.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 67a09a057d..6a27fa6e93 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -360,7 +360,6 @@ end local function get_first_word(text) local word = text:trim():split(' +')[1] if word:startswith(':') then word = word:sub(2) end - print(text, word) return word end From 8a4251743006107b9a6b3a4affffdad5bd8de9be Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 19 May 2023 17:56:17 -0700 Subject: [PATCH 232/732] fix incorrect weapon being implicated in a death --- deathcause.lua | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index a968678713..7939898597 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -1,5 +1,5 @@ -- show death cause of a creature -local utils = require 'utils' +local utils = require('utils') local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES -- Creates a table of all items at the given location optionally matching a given item type @@ -81,8 +81,14 @@ function displayDeathUnit(unit) print(str .. '.') end -function getItemTypeName(item_type) - return df.global.world.raws.itemdefs.all[item_type].name +-- returns the item description if the item still exists; otherwise +-- returns the weapon name +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 + end + return dfhack.items.getDescription(item, 0, false) end function displayDeathEventHistFigUnit(histfig_unit, event) @@ -103,9 +109,9 @@ function displayDeathEventHistFigUnit(histfig_unit, event) if event.weapon then if event.weapon.item_type == df.item_type.WEAPON then - str = str .. (", using a %s"):format(getItemTypeName(event.weapon.item_subtype)) + str = str .. (", using a %s"):format(getWeaponName(event.weapon.item, event.weapon.item_subtype)) elseif event.weapon.shooter_item_type == df.item_type.WEAPON then - str = str .. (", shot by a %s"):format(getItemTypeName(event.weapon.shooter_item_subtype)) + str = str .. (", shot by a %s"):format(getWeaponName(event.weapon.shooter_item, event.weapon.shooter_item_subtype)) end end @@ -143,8 +149,8 @@ function displayDeathHistFig(histfig) end end -local selected_item = dfhack.gui.getSelectedItem() -local selected_unit = dfhack.gui.getSelectedUnit() +local selected_item = dfhack.gui.getSelectedItem(true) +local selected_unit = dfhack.gui.getSelectedUnit(true) local hist_figure_id if not selected_unit and (not selected_item or selected_item:getType() ~= df.item_type.CORPSE) then @@ -159,7 +165,7 @@ if not selected_unit and (not selected_item or selected_item:getType() ~= df.ite end if not selected_unit and not selected_item then - qerror("Please select a corpse in the loo'k' menu, or a unit in the 'u'nitlist screen") + qerror("Please select a corpse") end if selected_item then From 35714ff5104f934eebeb469744f27fb0fee4c7e3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 19 May 2023 17:57:58 -0700 Subject: [PATCH 233/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 83351899d2..4dd420d54f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ that repo. ## Fixes - `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath passable tiles of buildings +- `deathcause`: fix incorrect weapon sometimes being reported - `gui/design`: Fix building and stairs designation - `quickfort`: fixed detection of tiles where machines are allowed (e.g. water wheels *can* be built on stairs if there is a machine support nearby) From 4048d65f714ce6e972e1c6a29a55f5a76cca6fdd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 20 May 2023 03:56:23 -0700 Subject: [PATCH 234/732] properly rotate carved track tiles --- changelog.txt | 1 + internal/quickfort/dig.lua | 115 +++++++++++++++++++++++++++++++------ 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/changelog.txt b/changelog.txt index 83351899d2..a2f44caf37 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ that repo. - `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath passable tiles of buildings - `gui/design`: Fix building and stairs designation - `quickfort`: fixed detection of tiles where machines are allowed (e.g. water wheels *can* be built on stairs if there is a machine support nearby) +- `quickfort`: fixed rotation of blueprints with carved track tiles ## Misc Improvements - `quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index 610b5d6d78..cd63c918c2 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -17,6 +17,7 @@ local quickfort_map = reqscript('internal/quickfort/map') local quickfort_parse = reqscript('internal/quickfort/parse') local quickfort_preview = reqscript('internal/quickfort/preview') local quickfort_set = reqscript('internal/quickfort/set') +local quickfort_transform = reqscript('internal/quickfort/transform') local log = quickfort_common.log @@ -437,11 +438,82 @@ local function do_traffic_restricted(digctx) return function() digctx.flags.traffic = values.traffic_restricted end end -local function track_alias_entry(directions) +local unit_vectors = quickfort_transform.unit_vectors +local unit_vectors_revmap = quickfort_transform.unit_vectors_revmap + +local track_end_data = { + N=unit_vectors.north, + E=unit_vectors.east, + S=unit_vectors.south, + W=unit_vectors.west +} +local track_end_revmap = { + [unit_vectors_revmap.north]='N', + [unit_vectors_revmap.east]='E', + [unit_vectors_revmap.south]='S', + [unit_vectors_revmap.west]='W' +} + +local track_through_data = { + NS=unit_vectors.north, + EW=unit_vectors.east +} +local track_through_revmap = { + [unit_vectors_revmap.north]='NS', + [unit_vectors_revmap.east]='EW', + [unit_vectors_revmap.south]='NS', + [unit_vectors_revmap.west]='EW' +} + +local track_corner_data = { + NE={x=1, y=-2}, + NW={x=-2, y=-1}, + SE={x=2, y=1}, + SW={x=-1, y=2} +} +local track_corner_revmap = { + ['x=1, y=-2'] = 'NE', + ['x=2, y=-1'] = 'NE', + ['x=2, y=1'] = 'SE', + ['x=1, y=2'] = 'SE', + ['x=-1, y=2'] = 'SW', + ['x=-2, y=1'] = 'SW', + ['x=-2, y=-1'] = 'NW', + ['x=-1, y=-2'] = 'NW' +} + +local track_tee_data = { + NSE={x=1, y=-2}, + NEW={x=-2, y=-1}, + SEW={x=2, y=1}, + NSW={x=-1, y=2} +} +local track_tee_revmap = { + ['x=1, y=-2'] = 'NSE', + ['x=2, y=-1'] = 'NEW', + ['x=2, y=1'] = 'SEW', + ['x=1, y=2'] = 'NSE', + ['x=-1, y=2'] = 'NSW', + ['x=-2, y=1'] = 'SEW', + ['x=-2, y=-1'] = 'NEW', + ['x=-1, y=-2'] = 'NSW' +} + +local function make_transform_track_fn(vector, revmap) + return function(ctx) + return 'track' .. quickfort_transform.resolve_transformed_vector(ctx, vector, revmap) + end +end +local function make_track_entry(name, data, revmap) + local transform = nil + if data and revmap then + transform = make_transform_track_fn(data[name], revmap) + end return {action=do_track, use_priority=true, can_clobber_engravings=true, - direction={single_tile=true, north=directions.north, - south=directions.south, east=directions.east, - west=directions.west}} + direction={single_tile=true, north=name:find('N'), + south=name:find('S'), east=name:find('E'), + west=name:find('W')}, + transform=transform} end local dig_db = { @@ -478,24 +550,25 @@ local dig_db = { ol={action=do_traffic_low}, ['or']={action=do_traffic_restricted}, -- single-tile track aliases - trackN=track_alias_entry{north=true}, - trackS=track_alias_entry{south=true}, - trackE=track_alias_entry{east=true}, - trackW=track_alias_entry{west=true}, - trackNS=track_alias_entry{north=true, south=true}, - trackNE=track_alias_entry{north=true, east=true}, - trackNW=track_alias_entry{north=true, west=true}, - trackSE=track_alias_entry{south=true, east=true}, - trackSW=track_alias_entry{south=true, west=true}, - trackEW=track_alias_entry{east=true, west=true}, - trackNSE=track_alias_entry{north=true, south=true, east=true}, - trackNSW=track_alias_entry{north=true, south=true, west=true}, - trackNEW=track_alias_entry{north=true, east=true, west=true}, - trackSEW=track_alias_entry{south=true, east=true, west=true}, - trackNSEW=track_alias_entry{north=true, south=true, east=true, west=true}, + trackN=make_track_entry('N', track_end_data, track_end_revmap), + trackS=make_track_entry('S', track_end_data, track_end_revmap), + trackE=make_track_entry('E', track_end_data, track_end_revmap), + trackW=make_track_entry('W', track_end_data, track_end_revmap), + trackNS=make_track_entry('NS', track_through_data, track_through_revmap), + trackEW=make_track_entry('EW', track_through_data, track_through_revmap), + trackNE=make_track_entry('NE', track_corner_data, track_corner_revmap), + trackNW=make_track_entry('NW', track_corner_data, track_corner_revmap), + trackSE=make_track_entry('SE', track_corner_data, track_corner_revmap), + trackSW=make_track_entry('SW', track_corner_data, track_corner_revmap), + trackNSE=make_track_entry('NSE', track_tee_data, track_tee_revmap), + trackNSW=make_track_entry('NSW', track_tee_data, track_tee_revmap), + trackNEW=make_track_entry('NEW', track_tee_data, track_tee_revmap), + trackSEW=make_track_entry('SEW', track_tee_data, track_tee_revmap), + trackNSEW=make_track_entry('NSEW'), } -- add trackramp aliases for the track aliases +-- (trackramps are just tracks carved over ramps) dig_db.trackrampN = dig_db.trackN dig_db.trackrampS = dig_db.trackS dig_db.trackrampE = dig_db.trackE @@ -675,6 +748,10 @@ local function do_run_impl(zlevel, grid, ctx) stats.invalid_keys.value = stats.invalid_keys.value + 1 goto continue end + if db_entry.transform then + db_entry = copyall(db_entry) + db_entry.direction = dig_db[db_entry.transform(ctx)].direction + end if db_entry.action == do_track and not db_entry.direction and math.abs(extent.width) == 1 and math.abs(extent.height) == 1 then From b741455deb88962cf4c027ac9cd5c028ebb31e1b Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 20 May 2023 04:41:06 -0700 Subject: [PATCH 235/732] Update gm-editor.lua --- gui/gm-editor.lua | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index db15042f6c..663d57685d 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -29,6 +29,7 @@ end)() local keybindings_raw = { {name='toggle_ro', key="CUSTOM_CTRL_D",desc="Toggle between read-only and read-write"}, + {name='autoupdate', key="CUSTOM_ALT_A",desc="See live updates of changing values"}, {name='offset', key="CUSTOM_ALT_O",desc="Show current items offset"}, {name='find', key="CUSTOM_F",desc="Find a value by entering a predicate"}, {name='find_id', key="CUSTOM_I",desc="Find object with this ID, using ref-target if available"}, @@ -41,7 +42,6 @@ local keybindings_raw = { {name='gotopos', key="CUSTOM_G",desc="Move map view to location of target"}, {name='help', key="STRING_A063",desc="Show this help"}, {name='displace', key="STRING_A093",desc="Open reference offseted by index"}, - {name='autoupdate', key="CUSTOM_ALT_A",desc="Automatically keep values updated"}, --{name='NOT_USED', key="SEC_SELECT",desc="Edit selected entry as a number (for enums)"}, --not a binding... } @@ -143,8 +143,7 @@ function GmEditorUi:init(args) subviews={ mainList, widgets.Label{text={{text="",id="name"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, - widgets.EditField{frame={l=1,t=2,h=1},label_text="Search",key=keybindings.start_filter.key,key_sep='(): ',on_change=self:callback('text_input'),view_id="filter_input"}, - widgets.ToggleHotkeyLabel{label="Auto-Update", key=keybindings.autoupdate.key, initial_option=false, view_id = 'lbl_autoupdate', frame={l=1,t=0,yalign=0}}} + widgets.EditField{frame={l=1,t=2,h=1},label_text="Search",key=keybindings.start_filter.key,key_sep='(): ',on_change=self:callback('text_input'),view_id="filter_input"}} ,view_id='page_main'} self:addviews{widgets.Pages{subviews={mainPage,helpPage},view_id="pages"}} @@ -180,9 +179,8 @@ function GmEditorUi:verifyStack(args) if failure then self.stack = {table.unpack(self.stack, 1, last_good_level)} return false - else - return true end + return true end function GmEditorUi:text_input(new_text) self:updateTarget(true,true) @@ -379,7 +377,7 @@ function GmEditorUi:editSelectedEnum(index,choice) end function GmEditorUi:openReinterpret(key) local trg=self:currentTarget() - dialog.showInputPrompt(tostring(trg_key),"Enter new type:",COLOR_WHITE, + dialog.showInputPrompt(tostring(self:getSelectedKey()),"Enter new type:",COLOR_WHITE, "",function(choice) local ntype=df[choice] self:pushTarget(df.reinterpret_cast(ntype,trg.target[key])) @@ -524,6 +522,9 @@ function GmEditorUi:onInput(keys) self.read_only = not self.read_only self:updateTitles() return true + elseif keys[keybindings.autoupdate.key] then + self.autoupdate = not self.autoupdate + return true elseif keys[keybindings.offset.key] then local trg=self:currentTarget() local _,stoff=df.sizeof(trg.target) @@ -602,6 +603,7 @@ end function GmEditorUi:updateTarget(preserve_pos,reindex) self:verifyStack() local trg=self:currentTarget() + if not trg then return end local filter=self.subviews.filter_input.text:lower() if reindex then @@ -626,13 +628,15 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) for k,v in pairs(trg.keys) do table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=2,text=getStringValue(trg,v)}}}) end - local last_pos + local last_selected, last_top if preserve_pos then - last_pos=self.subviews.list_main:getSelected() + last_selected=self.subviews.list_main:getSelected() + last_top=self.subviews.list_main.page_top end self.subviews.list_main:setChoices(t) - if last_pos then - self.subviews.list_main:setSelected(last_pos) + if last_selected then + self.subviews.list_main:setSelected(last_selected) + self.subviews.list_main:on_scrollbar(last_top) else self.subviews.list_main:setSelected(trg.selected) end @@ -682,6 +686,9 @@ function GmEditorUi:onRenderFrame(dc, rect) if self.parent_view.freeze then dc:seek(rect.x1+2, rect.y2):string(' GAME SUSPENDED ', COLOR_RED) end + if self.autoupdate and self.next_refresh_ms <= dfhack.getTickCount() then + self:updateTarget(true, true) + end end FreezeScreen = defclass(FreezeScreen, gui.Screen) @@ -729,12 +736,6 @@ function FreezeScreen:onDismiss() freeze_screen = nil end -function GmEditorUi:onRenderBody() - if self.subviews.lbl_autoupdate:getOptionValue() and self.next_refresh_ms <= dfhack.getTickCount() then - self:updateTarget() - end -end - GmScreen = defclass(GmScreen, gui.ZScreen) GmScreen.ATTRS { focus_path='gm-editor', From 66987cfdc8bfe39e7df7ce287dfa2726d3d3174c Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 20 May 2023 04:43:34 -0700 Subject: [PATCH 236/732] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 83351899d2..1541f788a6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ that repo. - `gui/create-item`: ask for number of items to spawn by default - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for the gm-editor window itself) +- `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused ## Removed From 40e517eb844779b4ac1ba6fe6d226f2d613d0b8c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 20 May 2023 18:05:58 -0700 Subject: [PATCH 237/732] if nothing matches current search, that's not a verification error ref: #597 --- gui/gm-editor.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 663d57685d..53ae73be66 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -160,6 +160,7 @@ function GmEditorUi:verifyStack(args) local keys = level.keys local selection = level.selected local sel_key = keys[selection] + if not sel_key then goto continue end local next_by_ref local status, _ = pcall( function() @@ -175,6 +176,7 @@ function GmEditorUi:verifyStack(args) failure = true break end + ::continue:: end if failure then self.stack = {table.unpack(self.stack, 1, last_good_level)} @@ -603,7 +605,6 @@ end function GmEditorUi:updateTarget(preserve_pos,reindex) self:verifyStack() local trg=self:currentTarget() - if not trg then return end local filter=self.subviews.filter_input.text:lower() if reindex then From 96aa373013f2a6b9f49fed195354a2ded9cbd0ae Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 20 May 2023 16:03:38 -0700 Subject: [PATCH 238/732] support quickfort place mode --- changelog.txt | 3 +- gui/blueprint.lua | 34 ++++++++------ gui/quickfort.lua | 4 ++ internal/quickfort/parse.lua | 4 +- internal/quickfort/place.lua | 88 ++++++++++++------------------------ 5 files changed, 58 insertions(+), 75 deletions(-) diff --git a/changelog.txt b/changelog.txt index 9690c3f5d8..f69ec346d2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,7 +31,8 @@ that repo. - `quickfort`: fixed rotation of blueprints with carved track tiles ## Misc Improvements -- `quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them +- `gui/quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them +- `gui/quickfort`: place (stockpile layout) mode is now supported. note that detailed stockpile configurations were part of query mode and are not yet supported - `gui/create-item`: ask for number of items to spawn by default - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for the gm-editor window itself) diff --git a/gui/blueprint.lua b/gui/blueprint.lua index 78cee5ca56..0335e1b19f 100644 --- a/gui/blueprint.lua +++ b/gui/blueprint.lua @@ -166,39 +166,39 @@ function PhasesPanel:init() widgets.Panel{ frame={h=1}, subviews={widgets.ToggleHotkeyLabel{view_id='dig_phase', - frame={t=0, l=0}, key='CUSTOM_D', label='dig', + frame={t=0, l=0, w=19}, key='CUSTOM_D', label='dig', initial_option=self:get_default('dig'), label_width=9}, widgets.ToggleHotkeyLabel{view_id='carve_phase', - frame={t=0, l=19}, key='CUSTOM_SHIFT_D', label='carve', + frame={t=0, l=19, w=19}, key='CUSTOM_SHIFT_D', label='carve', initial_option=self:get_default('carve')}, }}, widgets.Panel{ frame={h=1}, subviews={widgets.ToggleHotkeyLabel{view_id='construct_phase', - frame={t=0, l=0}, key='CUSTOM_SHIFT_B', + frame={t=0, l=0, w=19}, key='CUSTOM_SHIFT_B', label='construct', initial_option=self:get_default('construct')}, widgets.ToggleHotkeyLabel{view_id='build_phase', - frame={t=0, l=19}, key='CUSTOM_B', label='build', + frame={t=0, l=19, w=19}, key='CUSTOM_B', label='build', initial_option=self:get_default('build')}}}, --- widgets.Panel{frame={h=1}, --- subviews={widgets.ToggleHotkeyLabel{view_id='place_phase', --- frame={t=0, l=0}, --- key='CUSTOM_P', label='place', --- initial_option=self:get_default('place')}, + widgets.Panel{frame={h=1}, + subviews={widgets.ToggleHotkeyLabel{view_id='place_phase', + frame={t=0, l=0, w=19}, + key='CUSTOM_P', label='place', + initial_option=self:get_default('place')}, -- widgets.ToggleHotkeyLabel{view_id='zone_phase', --- frame={t=0, l=15}, +-- frame={t=0, l=15, w=19}, -- key='CUSTOM_Z', label='zone', -- initial_option=self:get_default('zone'), -- label_width=5} --- }}, + }}, -- widgets.Panel{frame={h=1}, -- subviews={widgets.ToggleHotkeyLabel{view_id='query_phase', --- frame={t=0, l=0}, +-- frame={t=0, l=0, w=19}, -- key='CUSTOM_Q', label='query', -- initial_option=self:get_default('query')}, -- widgets.ToggleHotkeyLabel{view_id='rooms_phase', --- frame={t=0, l=15}, +-- frame={t=0, l=15, w=19}, -- key='CUSTOM_SHIFT_Q', label='rooms', -- initial_option=self:get_default('rooms')} -- }}, @@ -531,6 +531,14 @@ function Blueprint:commit(pos) -- set cursor to top left corner of the *uppermost* z-level local bounds = self:get_bounds() + if not bounds then + dialogs.MessageBox{ + frame_title='Error', + text='Ensure blueprint bounds are set' + }:show() + return + end + table.insert(params, ('--cursor=%d,%d,%d') :format(bounds.x1, bounds.y1, bounds.z2)) diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 4290a61688..47e39a573e 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -556,6 +556,8 @@ function Quickfort:refresh_preview() end local to_pen = dfhack.pen.parse +local CURSOR_PEN = to_pen{ch='o', fg=COLOR_BLUE, + tile=dfhack.screen.findGraphicsTile('CURSORS', 5, 22)} local GOOD_PEN = to_pen{ch='x', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, @@ -584,6 +586,7 @@ function Quickfort:onRenderFrame(dc, rect) if not tiles[cursor.z] then return end local function get_overlay_pen(pos) + if same_xyz(pos, cursor) then return CURSOR_PEN end local preview_tile = quickfort_preview.get_preview_tile(tiles, pos) if preview_tile == nil then return end return preview_tile and GOOD_PEN or BAD_PEN @@ -612,6 +615,7 @@ function Quickfort:commit() end function Quickfort:do_command(command, dry_run, post_fn) + self.dirty = true print(('executing via gui/quickfort: quickfort %s'):format( quickfort_parse.format_command( command, self.blueprint_name, self.section_name, dry_run))) diff --git a/internal/quickfort/parse.lua b/internal/quickfort/parse.lua index 6b50ee61b3..4b5e5a33e0 100644 --- a/internal/quickfort/parse.lua +++ b/internal/quickfort/parse.lua @@ -12,7 +12,7 @@ local quickfort_transform = reqscript('internal/quickfort/transform') valid_modes = utils.invert({ 'dig', 'build', --- 'place', + 'place', -- 'zone', -- 'query', -- 'config', @@ -290,7 +290,7 @@ local function parse_modeline(modeline, filename, modeline_id) if not modeline then return nil end local _, mode_end, mode = string.find(modeline, '^#([%l]+)') -- remove this as these modes become supported - if mode == 'place' or mode == 'zone' or mode == 'query' or mode == 'config' then + if mode == 'zone' or mode == 'query' or mode == 'config' then mode = 'ignore' end if not mode or not valid_modes[mode] then return nil end diff --git a/internal/quickfort/place.lua b/internal/quickfort/place.lua index 7588529cab..c165ce16fe 100644 --- a/internal/quickfort/place.lua +++ b/internal/quickfort/place.lua @@ -16,10 +16,10 @@ end require('dfhack.buildings') -- loads additional functions into dfhack.buildings local utils = require('utils') +local stockpiles = require('plugins.stockpiles') local quickfort_common = reqscript('internal/quickfort/common') local quickfort_building = reqscript('internal/quickfort/building') local quickfort_orders = reqscript('internal/quickfort/orders') -local quickfort_query = reqscript('internal/quickfort/query') local quickfort_set = reqscript('internal/quickfort/set') local log = quickfort_common.log @@ -56,24 +56,24 @@ local stockpile_template = { } local stockpile_db = { - a={label='Animal', indices={0}}, - f={label='Food', indices={1}, want_barrels=true}, - u={label='Furniture', indices={2}}, - n={label='Coins', indices={7}, want_bins=true}, - y={label='Corpses', indices={3}}, - r={label='Refuse', indices={4}}, - s={label='Stone', indices={5}, want_wheelbarrows=true}, - w={label='Wood', indices={13}}, - e={label='Gem', indices={9}, want_bins=true}, - b={label='Bar/Block', indices={8}, want_bins=true}, - h={label='Cloth', indices={12}, want_bins=true}, - l={label='Leather', indices={11}, want_bins=true}, - z={label='Ammo', indices={6}, want_bins=true}, - S={label='Sheets', indices={16}, want_bins=true}, - g={label='Finished Goods', indices={10}, want_bins=true}, - p={label='Weapons', indices={14}, want_bins=true}, - d={label='Armor', indices={15}, want_bins=true}, - c={label='Custom', indices={}} + a={label='Animal', categories={'animals'}}, + f={label='Food', categories={'food'}, want_barrels=true}, + u={label='Furniture', categories={'furniture'}}, + n={label='Coins', categories={'coins'}, want_bins=true}, + y={label='Corpses', categories={'corpses'}}, + r={label='Refuse', categories={'refuse'}}, + s={label='Stone', categories={'stone'}, want_wheelbarrows=true}, + w={label='Wood', categories={'wood'}}, + e={label='Gem', categories={'gems'}, want_bins=true}, + b={label='Bar/Block', categories={'bars_blocks'}, want_bins=true}, + h={label='Cloth', categories={'cloth'}, want_bins=true}, + l={label='Leather', categories={'leather'}, want_bins=true}, + z={label='Ammo', categories={'ammo'}, want_bins=true}, + S={label='Sheets', categories={'sheets'}, want_bins=true}, + g={label='Finished Goods', categories={'finished_goods'}, want_bins=true}, + p={label='Weapons', categories={'weapons'}, want_bins=true}, + d={label='Armor', categories={'armor'}, want_bins=true}, + c={label='Custom', categories={}} } for _, v in pairs(stockpile_db) do utils.assign(v, stockpile_template) end @@ -82,7 +82,7 @@ local function add_resource_digit(cur_val, digit) end local function custom_stockpile(_, keys) - local labels, indices = {}, {} + local labels, categories = {}, {} local want_bins, want_barrels, want_wheelbarrows = false, false, false local num_bins, num_barrels, num_wheelbarrows = nil, nil, nil local prev_key, in_digits = nil, false @@ -105,7 +105,7 @@ local function custom_stockpile(_, keys) end if not rawget(stockpile_db, k) then return nil end table.insert(labels, stockpile_db[k].label) - table.insert(indices, stockpile_db[k].indices[1]) + table.insert(categories, stockpile_db[k].categories[1]) want_bins = want_bins or stockpile_db[k].want_bins want_barrels = want_barrels or stockpile_db[k].want_barrels want_wheelbarrows = @@ -118,7 +118,7 @@ local function custom_stockpile(_, keys) end local stockpile_data = { label=table.concat(labels, '+'), - indices=indices, + categories=categories, want_bins=want_bins, want_barrels=want_barrels, want_wheelbarrows=want_wheelbarrows, @@ -132,39 +132,11 @@ end setmetatable(stockpile_db, {__index=custom_stockpile}) - -local function init_stockpile_settings(zlevel, stockpile_query_grid, ctx) - local saved_verbosity = quickfort_common.verbose - quickfort_common.verbose = false - quickfort_query.do_run(zlevel, stockpile_query_grid, ctx) - quickfort_common.verbose = saved_verbosity -end - -local function get_stockpile_query_text(db_entry) - local text = '' - for _,index in ipairs(db_entry.indices) do - text = text .. string.format('s{Down %d}e^', index) - end - return text -end - -local function queue_stockpile_settings_init(s, db_entry, stockpile_query_grid) - local query_x, query_y - for extent_x, col in ipairs(s.extent_grid) do - for extent_y, in_extent in ipairs(col) do - if in_extent then - query_x = s.pos.x + extent_x - 1 - query_y = s.pos.y + extent_y - 1 - break - end - end - if active_x then break end - end - if not stockpile_query_grid[query_y] then - stockpile_query_grid[query_y] = {} +local function configure_stockpile(bld, db_entry) + for _,cat in ipairs(db_entry.categories) do + local name = ('library/cat_%s'):format(cat) + stockpiles.import_stockpile(name, {id=bld.id, mode='enable', filters={}}) end - stockpile_query_grid[query_y][query_x] = - {cell='generated',text=get_stockpile_query_text(db_entry)} end local function init_containers(db_entry, ntiles, fields) @@ -201,7 +173,7 @@ local function init_containers(db_entry, ntiles, fields) end end -local function create_stockpile(s, stockpile_query_grid, dry_run) +local function create_stockpile(s, dry_run) local db_entry = stockpile_db[s.type] log('creating %s stockpile at map coordinates (%d, %d, %d), defined from' .. ' spreadsheet cells: %s', @@ -219,7 +191,7 @@ local function create_stockpile(s, stockpile_query_grid, dry_run) -- is supposed to prevent this from ever happening error(string.format('unable to place stockpile: %s', err)) end - queue_stockpile_settings_init(s, db_entry, stockpile_query_grid) + configure_stockpile(bld, db_entry) return ntiles end @@ -244,17 +216,15 @@ function do_run(zlevel, grid, ctx) quickfort_building.check_tiles_and_extents( ctx, stockpiles, stockpile_db) - local stockpile_query_grid = {} local dry_run = ctx.dry_run for _, s in ipairs(stockpiles) do if s.pos then - local ntiles = create_stockpile(s, stockpile_query_grid, dry_run) + local ntiles = create_stockpile(s, dry_run) stats.place_tiles.value = stats.place_tiles.value + ntiles stats.place_designated.value = stats.place_designated.value + 1 end end if dry_run then return end - init_stockpile_settings(zlevel, stockpile_query_grid, ctx) dfhack.job.checkBuildingsNow() end From 702e83c65d41507bf324f03d7e1c3e60a6ddf830 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 20 May 2023 16:35:57 -0700 Subject: [PATCH 239/732] stockpiles are no longer limited to 31x31 --- internal/quickfort/place.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/quickfort/place.lua b/internal/quickfort/place.lua index c165ce16fe..1af2ef1f2e 100644 --- a/internal/quickfort/place.lua +++ b/internal/quickfort/place.lua @@ -50,7 +50,7 @@ local function is_valid_stockpile_extent(s) end local stockpile_template = { - has_extents=true, min_width=1, max_width=31, min_height=1, max_height=31, + has_extents=true, min_width=1, max_width=math.huge, min_height=1, max_height=math.huge, is_valid_tile_fn = is_valid_stockpile_tile, is_valid_extent_fn = is_valid_stockpile_extent } From 277916d05258ac64aab7b4f523b1dcb1556cb87a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 21 May 2023 14:25:12 -0700 Subject: [PATCH 240/732] allow armor to be made out of leather --- changelog.txt | 1 + gui/create-item.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 9690c3f5d8..aae5ccf786 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,7 @@ that repo. ## Fixes - `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath passable tiles of buildings - `deathcause`: fix incorrect weapon sometimes being reported +- `gui/create-item`: allow armor to be made out of leather when using the restrictive filters - `gui/design`: Fix building and stairs designation - `quickfort`: fixed detection of tiles where machines are allowed (e.g. water wheels *can* be built on stairs if there is a machine support nearby) - `quickfort`: fixed rotation of blueprints with carved track tiles diff --git a/gui/create-item.lua b/gui/create-item.lua index decf3143db..95384cd3ec 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -81,7 +81,7 @@ local function getRestrictiveMatFilter(itemType, opts) return (mat.flags.ITEMS_AMMO) end, ARMOR = function(mat, parent, typ, idx) - return (mat.flags.ITEMS_ARMOR) + return (mat.flags.ITEMS_ARMOR or mat.flags.LEATHER) end, INSTRUMENT = function(mat, parent, typ, idx) return (mat.flags.ITEMS_HARD) From d305ede5067e9f8bb1cfeab6d746ac39a659a590 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 21 May 2023 18:32:39 -0700 Subject: [PATCH 241/732] don't verify consistency of bitfield flags ref: #597 --- gui/gm-editor.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 53ae73be66..480dd380aa 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -149,13 +149,14 @@ function GmEditorUi:init(args) self:addviews{widgets.Pages{subviews={mainPage,helpPage},view_id="pages"}} self:pushTarget(args.target) end -function GmEditorUi:verifyStack(args) +function GmEditorUi:verifyStack() local failure = false local last_good_level = nil for i, level in pairs(self.stack) do local obj=level.target + if obj._kind == "bitfield" then goto continue end local keys = level.keys local selection = level.selected From 884e9f84cf34bf20b5e527fb4b06487a4cc3dcdd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 21 May 2023 10:57:00 -0700 Subject: [PATCH 242/732] add basic sandbox script --- gui/sandbox.lua | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 gui/sandbox.lua diff --git a/gui/sandbox.lua b/gui/sandbox.lua new file mode 100644 index 0000000000..7718b7934d --- /dev/null +++ b/gui/sandbox.lua @@ -0,0 +1,117 @@ +local gui = require('gui') +local widgets = require('gui.widgets') + +--------------------- +-- Sandbox +-- + +Sandbox = defclass(Sandbox, widgets.Window) +Sandbox.ATTRS { + frame_title='Arena Sandbox', + frame={r=2, t=18, w=40, h=10}, +} + +function Sandbox:init() + self:addviews{ + widgets.WrappedLabel{ + text_to_wrap='Use the buttons at the bottom of the screen to create units, trees, or fluids. \n\nClose this window to return to your fort.' + } + } +end + +function Sandbox:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + -- close any open UI elements + df.global.game.main_interface.arena_unit.open = false + df.global.game.main_interface.arena_tree.open = false + df.global.game.main_interface.bottom_mode_selected = -1 + end + return Sandbox.super.onInput(self, keys) +end + +--------------------- +-- SandboxScreen +-- + +SandboxScreen = defclass(SandboxScreen, gui.ZScreen) +SandboxScreen.ATTRS { + focus_path='sandbox', + force_pause=true, + pass_movement_keys=true, +} + +function SandboxScreen:init() + local arena = df.global.world.arena + local arena_unit = df.global.game.main_interface.arena_unit + local arena_tree = df.global.game.main_interface.arena_tree + + -- races + arena.race:resize(0) + arena.caste:resize(0) + arena.creature_cnt:resize(0) + arena.type = -1 + arena_unit.race = 0 + arena_unit.caste = 0 + arena_unit.races_filtered:resize(0) + arena_unit.races_all:resize(0) + arena_unit.castes_filtered:resize(0) + arena_unit.castes_all:resize(0) + for i, cre in ipairs(df.global.world.raws.creatures.all) do + arena.creature_cnt:insert('#', 0) + for caste in ipairs(cre.caste) do + -- the real interface sorts these alphabetically + arena.race:insert('#', i) + arena.caste:insert('#', caste) + end + end + + -- interactions + arena.interactions:resize(0) + arena.interaction = -1 + arena_unit.interactions:resize(0) + arena_unit.interaction = -1 + for _, inter in ipairs(df.global.world.raws.interactions) do + for _, effect in ipairs(inter.effects) do + if #effect.arena_name > 0 then + arena.interactions:insert('#', effect) + end + end + end + + -- skills + arena.skills:resize(0) + arena.skill_levels:resize(0) + arena_unit.skills:resize(0) + arena_unit.skill_levels:resize(0) + for i in ipairs(df.job_skill) do + if i >= 0 then + arena.skills:insert('#', i) + arena.skill_levels:insert('#', 0) + end + end + + -- trees + arena.tree_types:resize(0) + arena.tree_age = 100 + arena_tree.tree_types_filtered:resize(0) + arena_tree.tree_types_all:resize(0) + arena_tree.age = 100 + for _, tree in ipairs(df.global.world.raws.plants.trees) do + arena.tree_types:insert('#', tree) + end + + df.global.gametype = df.game_type.DWARF_ARENA + + self:addviews{Sandbox{}} +end + +function SandboxScreen:onDismiss() + df.global.gametype = df.game_type.DWARF_MAIN + view = nil +end + +if df.global.gametype ~= df.game_type.DWARF_MAIN then + qerror('must have a fort loaded') +end + +view = view and view:raise() or SandboxScreen{}:show() From b3329868b4b9755b93c0e628c56d8aa9c112fc22 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 21 May 2023 11:06:38 -0700 Subject: [PATCH 243/732] add docs for gui/sandbox --- docs/gui/sandbox.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/gui/sandbox.rst diff --git a/docs/gui/sandbox.rst b/docs/gui/sandbox.rst new file mode 100644 index 0000000000..c34a73a5c7 --- /dev/null +++ b/docs/gui/sandbox.rst @@ -0,0 +1,22 @@ +gui/sandbox +=========== + +.. dfhack-tool:: + :summary: Create units, trees, liquids, snow, and mud. + :tags: fort armok animals map plants units + +This tool brings up the arena creation interface while you're in fort mode. You +can create units (with arbitrary skillsets) and trees, spawn liquids, and paint +the ground with snow or mud. You can also use this tool to clean the map of +spatters (like snow, mud, or blood). Note that the weather controls do not have +an effect on your fort. + +See `clean` for other ways to clean the map of spatters and `gui/liquids` for a +more focused interface for creating liquids. + +Usage +----- + +:: + + gui/sandbox From f635e8d5609668443a08a6467f334d4f5693e219 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 21 May 2023 18:40:09 -0700 Subject: [PATCH 244/732] friendliness selection --- docs/gui/sandbox.rst | 7 +++ docs/makeown.rst | 2 +- gui/sandbox.lua | 101 ++++++++++++++++++++++++++++++++++++++++--- makeown.lua | 20 +++++---- 4 files changed, 115 insertions(+), 15 deletions(-) diff --git a/docs/gui/sandbox.rst b/docs/gui/sandbox.rst index c34a73a5c7..f4e7ac9fd9 100644 --- a/docs/gui/sandbox.rst +++ b/docs/gui/sandbox.rst @@ -11,6 +11,13 @@ the ground with snow or mud. You can also use this tool to clean the map of spatters (like snow, mud, or blood). Note that the weather controls do not have an effect on your fort. +You can choose whether spawned units are: + +- hostile (default) +- independent/wild +- friendly +- citizens/pets + See `clean` for other ways to clean the map of spatters and `gui/liquids` for a more focused interface for creating liquids. diff --git a/docs/makeown.rst b/docs/makeown.rst index 013c5a141b..b47f78eb34 100644 --- a/docs/makeown.rst +++ b/docs/makeown.rst @@ -6,7 +6,7 @@ makeown :tags: fort armok units Select a unit in the UI and run this tool to converts that unit to be a fortress -citizen. It also removes their foreign affiliation, if any. +citizen (if sentient). It also removes their foreign affiliation, if any. Usage ----- diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 7718b7934d..63e76f6c19 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -1,6 +1,14 @@ local gui = require('gui') +local makeown = reqscript('makeown') local widgets = require('gui.widgets') +local DISPOSITIONS = { + HOSTILE = 1, + WILD = 2, + FRIENDLY = 3, + FORT = 4, +} + --------------------- -- Sandbox -- @@ -8,14 +16,34 @@ local widgets = require('gui.widgets') Sandbox = defclass(Sandbox, widgets.Window) Sandbox.ATTRS { frame_title='Arena Sandbox', - frame={r=2, t=18, w=40, h=10}, + frame={r=2, t=18, w=40, h=15}, + autoarrange_subviews=true, } function Sandbox:init() self:addviews{ widgets.WrappedLabel{ - text_to_wrap='Use the buttons at the bottom of the screen to create units, trees, or fluids. \n\nClose this window to return to your fort.' - } + frame={l=0}, + text_to_wrap='Use the buttons at the bottom of the screen to create units, trees, or fluids. \n\nClick on this window to focus and then right click to close and return to your fort.' + }, + widgets.Panel{frame={h=1}}, + widgets.WrappedLabel{ + frame={l=0}, + text_pen=COLOR_GREY, + text_to_wrap='When returning to fort mode, mark created units as:' + }, + widgets.CycleHotkeyLabel{ + view_id='disposition', + frame={l=0}, + key='CUSTOM_SHIFT_D', + key_back='CUSTOM_SHIFT_A', + options={ + {label='hostile', value=DISPOSITIONS.HOSTILE, pen=COLOR_RED}, + {label='independent/wild', value=DISPOSITIONS.WILD, pen=COLOR_YELLOW}, + {label='friendly', value=DISPOSITIONS.FRIENDLY, pen=COLOR_GREEN}, + {label='citizens/pets', value=DISPOSITIONS.FORT, pen=COLOR_BLUE}, + }, + }, } end @@ -37,10 +65,9 @@ SandboxScreen = defclass(SandboxScreen, gui.ZScreen) SandboxScreen.ATTRS { focus_path='sandbox', force_pause=true, - pass_movement_keys=true, } -function SandboxScreen:init() +local function init_arena() local arena = df.global.world.arena local arena_unit = df.global.game.main_interface.arena_unit local arena_tree = df.global.game.main_interface.arena_tree @@ -99,15 +126,77 @@ function SandboxScreen:init() for _, tree in ipairs(df.global.world.raws.plants.trees) do arena.tree_types:insert('#', tree) end +end - df.global.gametype = df.game_type.DWARF_ARENA +local function is_sentient(unit) + local caste_flags = unit.enemy.caste_flags + return caste_flags.CAN_SPEAK or caste_flags.CAN_LEARN +end + +local function finalize_sentient(unit, disposition) + + if disposition == DISPOSITIONS.HOSTILE then + unit.flags1.marauder = true; + elseif disposition == DISPOSITIONS.WILD then + unit.flags2.visitor = true + unit.flags3.guest = true + unit.animal.leave_countdown = 20000 + elseif disposition == DISPOSITIONS.FRIENDLY then + -- noop; units are created friendly by default + elseif disposition == DISPOSITIONS.FORT then + makeown.make_own(unit) + end +end + +local function finalize_animal(unit, disposition) + if disposition == DISPOSITIONS.HOSTILE then + unit.flags1.active_invader = true; + unit.flags1.marauder = true; + unit.flags4.agitated_wilderness_creature = true + elseif disposition == DISPOSITIONS.WILD then + unit.flags2.roaming_wilderness_population_source = true + unit.animal.leave_countdown = 20000 + elseif disposition == DISPOSITIONS.FRIENDLY then + -- noop; units are created friendly by default + elseif disposition == DISPOSITIONS.FORT then + makeown.make_own(unit) + unit.flags1.tame = true + unit.training_level = df.animal_training_level.Domesticated + end +end + +local function finalize_units(first_created_unit_id, disposition) + print(first_created_unit_id, disposition) + -- unit->flags4.bits.agitated_wilderness_creature + for unit_id=first_created_unit_id,df.global.unit_next_id-1 do + local unit = df.unit.find(unit_id) + if not unit then goto continue end + unit.profession = df.profession.STANDARD + unit.name.has_name = false + if is_sentient(unit) then + finalize_sentient(unit, disposition) + else + finalize_animal(unit, disposition) + end + ::continue:: + end +end + +function SandboxScreen:init() + self.first_created_unit_id = df.global.unit_next_id + + init_arena() self:addviews{Sandbox{}} + + df.global.gametype = df.game_type.DWARF_ARENA end function SandboxScreen:onDismiss() df.global.gametype = df.game_type.DWARF_MAIN view = nil + finalize_units(self.first_created_unit_id, + self.subviews.disposition:getOptionValue()) end if df.global.gametype ~= df.game_type.DWARF_MAIN then diff --git a/makeown.lua b/makeown.lua index 1609c6fb67..339743e90f 100644 --- a/makeown.lua +++ b/makeown.lua @@ -3,6 +3,7 @@ make_own(unit) -- removes foreign flags, sets civ_id to fort civ_id, and sets clothes ownership make_citizen(unit) -- called by make_own if unit.race == fort race --]] +--@module=true local utils = require 'utils' @@ -65,7 +66,7 @@ local function entity_link(hf, eid, do_event, add, replace_idx) end if do_event then - event = add and df.history_event_add_hf_entity_linkst:new() or df.history_event_remove_hf_entity_linkst:new() + local event = add and df.history_event_add_hf_entity_linkst:new() or df.history_event_remove_hf_entity_linkst:new() event.year = df.global.cur_year event.seconds = df.global.cur_year_tick event.civ = eid @@ -81,7 +82,7 @@ end local function change_state(hf, site_id, pos) hf.info.whereabouts.whereabouts_type = 1 -- state? arrived? hf.info.whereabouts.site = site_id - event = df.history_event_change_hf_statest:new() + local event = df.history_event_change_hf_statest:new() event.year = df.global.cur_year event.seconds = df.global.cur_year_tick event.id = df.global.hist_event_next_id @@ -101,12 +102,10 @@ function make_citizen(unit) local civ_id = df.global.plotinfo.civ_id --get civ id local group_id = df.global.plotinfo.group_id --get group id local site_id = df.global.plotinfo.site_id --get site id - local events = df.global.world.history.events --get events local fortent = df.historical_entity.find(group_id) --get fort's entity local civent = df.historical_entity.find(civ_id) - local event local region_pos = df.world_site.find(site_id).pos -- used with state events and hf state local hf @@ -116,7 +115,7 @@ function make_citizen(unit) if not hf then --if its not a histfig then make it a histfig --new_hf = true - hf = df.historical_figure.new() + hf = df.new(df.historical_figure) hf.id = df.global.hist_figure_next_id df.global.hist_figure_next_id = df.global.hist_figure_next_id+1 hf.profession = unit.profession @@ -128,7 +127,7 @@ function make_citizen(unit) hf.born_seconds = unit.birth_time hf.curse_year = unit.curse_year hf.curse_seconds = unit.curse_time - hf.birth_year_bias=unit.bias_birth_bias + hf.birth_year_bias=unit.birth_year_bias hf.birth_time_bias=unit.birth_time_bias hf.old_year = unit.old_year hf.old_seconds = unit.old_time @@ -291,11 +290,16 @@ function make_own(unit) fix_clothing_ownership(unit) - if unit.hist_figure_id > -1 and unit.hist_figure_id2 > -1 then --previously this just checked if the units race was the same as the - make_citizen(unit) --player's civ, but now it checks if the creature is a histfig + local caste_flags = unit.enemy.caste_flags + if caste_flags.CAN_SPEAK or caste_flags.CAN_LEARN then + make_citizen(unit) end end +if dfhack_flags.module then + return +end + unit = dfhack.gui.getSelectedUnit() if not unit then qerror('No unit selected!') From f9a798d26134ccc2abe85867994380049c7e2d74 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 21 May 2023 19:25:51 -0700 Subject: [PATCH 245/732] mark spawned animal as non-regional --- gui/sandbox.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 63e76f6c19..40862ae8b3 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -155,6 +155,7 @@ local function finalize_animal(unit, disposition) unit.flags4.agitated_wilderness_creature = true elseif disposition == DISPOSITIONS.WILD then unit.flags2.roaming_wilderness_population_source = true + unit.flags2.roaming_wilderness_population_source_not_a_map_feature = true unit.animal.leave_countdown = 20000 elseif disposition == DISPOSITIONS.FRIENDLY then -- noop; units are created friendly by default From 3c413b7d59845f0267d9f66408ab4b88920d1666 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 22 May 2023 01:03:48 -0700 Subject: [PATCH 246/732] blank out the arena interface and make the sandbox ui always focused --- docs/gui/sandbox.rst | 13 +- gui/sandbox.lua | 275 ++++++++++++++++++++++++++++++------------- 2 files changed, 195 insertions(+), 93 deletions(-) diff --git a/docs/gui/sandbox.rst b/docs/gui/sandbox.rst index f4e7ac9fd9..44da321698 100644 --- a/docs/gui/sandbox.rst +++ b/docs/gui/sandbox.rst @@ -2,14 +2,10 @@ gui/sandbox =========== .. dfhack-tool:: - :summary: Create units, trees, liquids, snow, and mud. - :tags: fort armok animals map plants units + :summary: Create units, trees, or items. + :tags: fort armok animals items map plants units -This tool brings up the arena creation interface while you're in fort mode. You -can create units (with arbitrary skillsets) and trees, spawn liquids, and paint -the ground with snow or mud. You can also use this tool to clean the map of -spatters (like snow, mud, or blood). Note that the weather controls do not have -an effect on your fort. +This tool provides a spawning interface for units, trees, and/or items. Units can be created with arbitrary skillsets, and trees can be created either as saplings or as fully grown (depending on the age you set). You can choose whether spawned units are: @@ -18,9 +14,6 @@ You can choose whether spawned units are: - friendly - citizens/pets -See `clean` for other ways to clean the map of spatters and `gui/liquids` for a -more focused interface for creating liquids. - Usage ----- diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 40862ae8b3..c08e1c9d1f 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -16,45 +16,200 @@ local DISPOSITIONS = { Sandbox = defclass(Sandbox, widgets.Window) Sandbox.ATTRS { frame_title='Arena Sandbox', - frame={r=2, t=18, w=40, h=15}, - autoarrange_subviews=true, + frame={r=2, t=18, w=26, h=24}, + frame_inset=0, } +local function is_sentient(unit) + local caste_flags = unit.enemy.caste_flags + return caste_flags.CAN_SPEAK or caste_flags.CAN_LEARN +end + +local function finalize_sentient(unit, disposition) + + if disposition == DISPOSITIONS.HOSTILE then + unit.flags1.marauder = true; + elseif disposition == DISPOSITIONS.WILD then + unit.flags2.visitor = true + unit.flags3.guest = true + unit.animal.leave_countdown = 20000 + elseif disposition == DISPOSITIONS.FRIENDLY then + -- noop; units are created friendly by default + elseif disposition == DISPOSITIONS.FORT then + makeown.make_own(unit) + end +end + +local function finalize_animal(unit, disposition) + if disposition == DISPOSITIONS.HOSTILE then + unit.flags1.active_invader = true; + unit.flags1.marauder = true; + unit.flags4.agitated_wilderness_creature = true + elseif disposition == DISPOSITIONS.WILD then + unit.flags2.roaming_wilderness_population_source = true + unit.flags2.roaming_wilderness_population_source_not_a_map_feature = true + unit.animal.leave_countdown = 20000 + elseif disposition == DISPOSITIONS.FRIENDLY then + -- noop; units are created friendly by default + elseif disposition == DISPOSITIONS.FORT then + makeown.make_own(unit) + unit.flags1.tame = true + unit.training_level = df.animal_training_level.Domesticated + end +end + +local function finalize_units(first_created_unit_id, disposition) + print(first_created_unit_id, disposition) + -- unit->flags4.bits.agitated_wilderness_creature + for unit_id=first_created_unit_id,df.global.unit_next_id-1 do + local unit = df.unit.find(unit_id) + if not unit then goto continue end + unit.profession = df.profession.STANDARD + unit.name.has_name = false + if is_sentient(unit) then + finalize_sentient(unit, disposition) + else + finalize_animal(unit, disposition) + end + ::continue:: + end +end + function Sandbox:init() + self.spawn_group = 1 + self.first_unit_id = df.global.unit_next_id + self:addviews{ - widgets.WrappedLabel{ - frame={l=0}, - text_to_wrap='Use the buttons at the bottom of the screen to create units, trees, or fluids. \n\nClick on this window to focus and then right click to close and return to your fort.' - }, - widgets.Panel{frame={h=1}}, - widgets.WrappedLabel{ - frame={l=0}, - text_pen=COLOR_GREY, - text_to_wrap='When returning to fort mode, mark created units as:' + widgets.ResizingPanel{ + frame={t=0}, + frame_style=gui.FRAME_INTERIOR, + frame_inset=1, + autoarrange_subviews=1, + subviews={ + widgets.Label{ + frame={l=0}, + text={ + 'Spawn group #', + {text=function() return self.spawn_group end}, + NEWLINE, + ' unit', + {text=function() return df.global.unit_next_id - self.first_unit_id == 1 and '' or 's' end}, ': ', + {text=function() return df.global.unit_next_id - self.first_unit_id end}, + }, + }, + widgets.Panel{frame={h=1}}, + widgets.CycleHotkeyLabel{ + view_id='disposition', + frame={l=0}, + key='CUSTOM_SHIFT_D', + key_back='CUSTOM_SHIFT_A', + label='Unit disposition', + label_below=true, + options={ + {label='hostile', value=DISPOSITIONS.HOSTILE, pen=COLOR_RED}, + {label='independent/wild', value=DISPOSITIONS.WILD, pen=COLOR_YELLOW}, + {label='friendly', value=DISPOSITIONS.FRIENDLY, pen=COLOR_GREEN}, + {label='citizens/pets', value=DISPOSITIONS.FORT, pen=COLOR_BLUE}, + }, + }, + widgets.Panel{frame={h=1}}, + widgets.HotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_U', + label="Spawn unit", + on_activate=function() + df.global.enabler.mouse_lbut = 0 + view:sendInputToParent{ARENA_CREATE_CREATURE=true} + end, + }, + widgets.Panel{frame={h=1}}, + widgets.HotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_G', + label='Start new group', + on_activate=self:callback('finalize_group'), + enabled=function() return df.global.unit_next_id ~= self.first_unit_id end, + }, + }, }, - widgets.CycleHotkeyLabel{ - view_id='disposition', - frame={l=0}, - key='CUSTOM_SHIFT_D', - key_back='CUSTOM_SHIFT_A', - options={ - {label='hostile', value=DISPOSITIONS.HOSTILE, pen=COLOR_RED}, - {label='independent/wild', value=DISPOSITIONS.WILD, pen=COLOR_YELLOW}, - {label='friendly', value=DISPOSITIONS.FRIENDLY, pen=COLOR_GREEN}, - {label='citizens/pets', value=DISPOSITIONS.FORT, pen=COLOR_BLUE}, + widgets.ResizingPanel{ + frame={t=10}, + frame_style=gui.FRAME_INTERIOR, + frame_inset=1, + autoarrange_subviews=1, + subviews={ + widgets.HotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_T', + label="Spawn tree", + on_activate=function() + df.global.enabler.mouse_lbut = 0 + view:sendInputToParent{ARENA_CREATE_TREE=true} + end, + }, + widgets.HotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_I', + label="Create item", + on_activate=function() dfhack.run_script('gui/create-item') end + }, }, }, + widgets.HotkeyLabel{ + frame={l=1, b=0}, + key='LEAVESCREEN', + label="Return to fortress", + on_activate=function() + repeat until not self:onInput{LEAVESCREEN=true} + view:dismiss() + end, + }, } end function Sandbox:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys._MOUSE_R_DOWN and self:getMouseFramePos() then -- close any open UI elements df.global.game.main_interface.arena_unit.open = false df.global.game.main_interface.arena_tree.open = false df.global.game.main_interface.bottom_mode_selected = -1 + return false end - return Sandbox.super.onInput(self, keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if df.global.game.main_interface.arena_unit.open or + df.global.game.main_interface.arena_tree.open or + df.global.game.main_interface.bottom_mode_selected ~= -1 then + view:sendInputToParent{LEAVESCREEN=true} + return true + else + return false + end + end + if not Sandbox.super.onInput(self, keys) then + view:sendInputToParent(keys) + end + return true +end + +function Sandbox:finalize_group() + finalize_units(self.first_unit_id, + self.subviews.disposition:getOptionValue()) + + self.spawn_group = self.spawn_group + 1 + self.first_unit_id = df.global.unit_next_id +end + +--------------------- +-- InterfaceMask +-- + +InterfaceMask = defclass(InterfaceMask, widgets.Panel) +InterfaceMask.ATTRS{ + frame_background=gui.TRANSPARENT_PEN, +} + +function InterfaceMask:onInput(keys) + return keys._MOUSE_L and self:getMousePos() end --------------------- @@ -65,6 +220,7 @@ SandboxScreen = defclass(SandboxScreen, gui.ZScreen) SandboxScreen.ATTRS { focus_path='sandbox', force_pause=true, + defocusable=false, } local function init_arena() @@ -128,67 +284,21 @@ local function init_arena() end end -local function is_sentient(unit) - local caste_flags = unit.enemy.caste_flags - return caste_flags.CAN_SPEAK or caste_flags.CAN_LEARN -end - -local function finalize_sentient(unit, disposition) - - if disposition == DISPOSITIONS.HOSTILE then - unit.flags1.marauder = true; - elseif disposition == DISPOSITIONS.WILD then - unit.flags2.visitor = true - unit.flags3.guest = true - unit.animal.leave_countdown = 20000 - elseif disposition == DISPOSITIONS.FRIENDLY then - -- noop; units are created friendly by default - elseif disposition == DISPOSITIONS.FORT then - makeown.make_own(unit) - end -end - -local function finalize_animal(unit, disposition) - if disposition == DISPOSITIONS.HOSTILE then - unit.flags1.active_invader = true; - unit.flags1.marauder = true; - unit.flags4.agitated_wilderness_creature = true - elseif disposition == DISPOSITIONS.WILD then - unit.flags2.roaming_wilderness_population_source = true - unit.flags2.roaming_wilderness_population_source_not_a_map_feature = true - unit.animal.leave_countdown = 20000 - elseif disposition == DISPOSITIONS.FRIENDLY then - -- noop; units are created friendly by default - elseif disposition == DISPOSITIONS.FORT then - makeown.make_own(unit) - unit.flags1.tame = true - unit.training_level = df.animal_training_level.Domesticated - end -end - -local function finalize_units(first_created_unit_id, disposition) - print(first_created_unit_id, disposition) - -- unit->flags4.bits.agitated_wilderness_creature - for unit_id=first_created_unit_id,df.global.unit_next_id-1 do - local unit = df.unit.find(unit_id) - if not unit then goto continue end - unit.profession = df.profession.STANDARD - unit.name.has_name = false - if is_sentient(unit) then - finalize_sentient(unit, disposition) - else - finalize_animal(unit, disposition) - end - ::continue:: - end -end - function SandboxScreen:init() - self.first_created_unit_id = df.global.unit_next_id - init_arena() - self:addviews{Sandbox{}} + self:addviews{ + Sandbox{ + view_id='sandbox', + }, + InterfaceMask{ + frame={l=17, r=38, t=0, h=3}, + frame_background=gui.CLEAR_PEN, + }, + InterfaceMask{ + frame={l=0, r=0, b=0, h=3}, + }, + } df.global.gametype = df.game_type.DWARF_ARENA end @@ -196,8 +306,7 @@ end function SandboxScreen:onDismiss() df.global.gametype = df.game_type.DWARF_MAIN view = nil - finalize_units(self.first_created_unit_id, - self.subviews.disposition:getOptionValue()) + self.subviews.sandbox:finalize_group() end if df.global.gametype ~= df.game_type.DWARF_MAIN then From 64b293940e441ebd58fe6e31331e5894abe2dcb6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 22 May 2023 01:20:44 -0700 Subject: [PATCH 247/732] layout tweaking --- gui/sandbox.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index c08e1c9d1f..7c931548e7 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -16,8 +16,8 @@ local DISPOSITIONS = { Sandbox = defclass(Sandbox, widgets.Window) Sandbox.ATTRS { frame_title='Arena Sandbox', - frame={r=2, t=18, w=26, h=24}, - frame_inset=0, + frame={r=2, t=18, w=26, h=20}, + frame_inset={b=1}, } local function is_sentient(unit) @@ -83,7 +83,7 @@ function Sandbox:init() widgets.ResizingPanel{ frame={t=0}, frame_style=gui.FRAME_INTERIOR, - frame_inset=1, + frame_inset={l=1, r=1}, autoarrange_subviews=1, subviews={ widgets.Label{ @@ -133,9 +133,9 @@ function Sandbox:init() }, }, widgets.ResizingPanel{ - frame={t=10}, + frame={t=11}, frame_style=gui.FRAME_INTERIOR, - frame_inset=1, + frame_inset={l=1, r=1}, autoarrange_subviews=1, subviews={ widgets.HotkeyLabel{ From 7cca7f399d17e22f16e49508d84aebcdef210f9d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 22 May 2023 05:22:58 -0700 Subject: [PATCH 248/732] handle ESC for the arena interface (which doesn't handle it itself) --- gui/sandbox.lua | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 7c931548e7..2ec942cf2d 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -59,8 +59,6 @@ local function finalize_animal(unit, disposition) end local function finalize_units(first_created_unit_id, disposition) - print(first_created_unit_id, disposition) - -- unit->flags4.bits.agitated_wilderness_creature for unit_id=first_created_unit_id,df.global.unit_next_id-1 do local unit = df.unit.find(unit_id) if not unit then goto continue end @@ -167,28 +165,33 @@ function Sandbox:init() } end +local function is_arena_action_in_progress() + return df.global.game.main_interface.arena_unit.open or + df.global.game.main_interface.arena_tree.open or + df.global.game.main_interface.bottom_mode_selected ~= -1 +end + +local function clear_arena_action() + -- close any open arena UI elements + df.global.game.main_interface.arena_unit.open = false + df.global.game.main_interface.arena_tree.open = false + df.global.game.main_interface.bottom_mode_selected = -1 +end + function Sandbox:onInput(keys) if keys._MOUSE_R_DOWN and self:getMouseFramePos() then - -- close any open UI elements - df.global.game.main_interface.arena_unit.open = false - df.global.game.main_interface.arena_tree.open = false - df.global.game.main_interface.bottom_mode_selected = -1 + clear_arena_action() return false end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then - if df.global.game.main_interface.arena_unit.open or - df.global.game.main_interface.arena_tree.open or - df.global.game.main_interface.bottom_mode_selected ~= -1 then - view:sendInputToParent{LEAVESCREEN=true} + if is_arena_action_in_progress() then + clear_arena_action() return true else return false end end - if not Sandbox.super.onInput(self, keys) then - view:sendInputToParent(keys) - end - return true + return Sandbox.super.onInput(self, keys) end function Sandbox:finalize_group() @@ -291,10 +294,6 @@ function SandboxScreen:init() Sandbox{ view_id='sandbox', }, - InterfaceMask{ - frame={l=17, r=38, t=0, h=3}, - frame_background=gui.CLEAR_PEN, - }, InterfaceMask{ frame={l=0, r=0, b=0, h=3}, }, From e0e08030ddb056f9ace572ae5c7abc36846d2d75 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 22 May 2023 05:40:10 -0700 Subject: [PATCH 249/732] don't list vermin for spawning --- gui/sandbox.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 2ec942cf2d..88c5b586c3 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -243,12 +243,14 @@ local function init_arena() arena_unit.castes_filtered:resize(0) arena_unit.castes_all:resize(0) for i, cre in ipairs(df.global.world.raws.creatures.all) do + if cre.flags.VERMIN_GROUNDER or cre.flags.VERMIN_SOIL then goto continue end arena.creature_cnt:insert('#', 0) for caste in ipairs(cre.caste) do -- the real interface sorts these alphabetically arena.race:insert('#', i) arena.caste:insert('#', caste) end + ::continue:: end -- interactions From 05494bd4d2a2a4f450544f012dcf3d47b4175190 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 22 May 2023 10:51:20 -0700 Subject: [PATCH 250/732] support assigning equipment --- gui/sandbox.lua | 132 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 88c5b586c3..245103b464 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -1,6 +1,8 @@ local gui = require('gui') +local materials = require('gui.materials') local makeown = reqscript('makeown') local widgets = require('gui.widgets') +local utils = require('utils') local DISPOSITIONS = { HOSTILE = 1, @@ -191,7 +193,9 @@ function Sandbox:onInput(keys) return false end end - return Sandbox.super.onInput(self, keys) + if not Sandbox.super.onInput(self, keys) then + view:sendInputToParent(keys) + end end function Sandbox:finalize_group() @@ -226,10 +230,74 @@ SandboxScreen.ATTRS { defocusable=false, } +local RAWS = df.global.world.raws +local MAT_TABLE = RAWS.mat_table + +-- elements of df.entity_sell_category +local EQUIPMENT_TYPES = { + Weapons={itemdefs=RAWS.itemdefs.weapons, + item_type=df.item_type.WEAPON, + def_filter=function(def) return not def.flags.TRAINING end, + mat_filter=function(mat) return mat.flags.ITEMS_WEAPON and mat.flags.ITEMS_METAL end}, + TrainingWeapons={itemdefs=RAWS.itemdefs.weapons, + item_type=df.item_type.WEAPON, + def_filter=function(def) return def.flags.TRAINING end, + mat_filter=function(mat) return mat.flags.WOOD end}, + Ammo={itemdefs=RAWS.itemdefs.ammo, + item_type=df.item_type.AMMO, + mat_filter=function(mat) return mat.flags.ITEMS_AMMO end}, + Bodywear={itemdefs=RAWS.itemdefs.armor, + item_type=df.item_type.ARMOR, + want_leather=true, + mat_filter=function(mat) return mat.flags.ITEMS_ARMOR or mat.flags.LEATHER end}, + Headwear={itemdefs=RAWS.itemdefs.helms, + item_type=df.item_type.HELM, + want_leather=true, + mat_filter=function(mat) return mat.flags.ITEMS_ARMOR or mat.flags.LEATHER end}, + Handwear={itemdefs=RAWS.itemdefs.gloves, + item_type=df.item_type.GLOVES, + want_leather=true, + mat_filter=function(mat) return mat.flags.ITEMS_ARMOR or mat.flags.LEATHER end}, + Footwear={itemdefs=RAWS.itemdefs.shoes, + item_type=df.item_type.SHOES, + want_leather=true, + mat_filter=function(mat) return mat.flags.ITEMS_ARMOR or mat.flags.LEATHER end}, + Legwear={itemdefs=RAWS.itemdefs.pants, + item_type=df.item_type.PANTS, + want_leather=true, + mat_filter=function(mat) return mat.flags.ITEMS_ARMOR or mat.flags.LEATHER end}, + Shields={itemdefs=RAWS.itemdefs.shields, + item_type=df.item_type.SHIELD, + want_leather=true, + mat_filter=function(mat) return mat.flags.ITEMS_ARMOR or mat.flags.LEATHER end}, + Tools={itemdefs=RAWS.itemdefs.tools, + item_type=df.item_type.TOOL, + mat_filter=function(mat) return mat.flags.ITEMS_HARD end}, +} + +local function scan_organic(cat, vec, start_idx, base, do_insert) + local indexes = MAT_TABLE.organic_indexes[cat] + for idx = start_idx,#indexes-1 do + local matindex = indexes[idx] + local organic = vec[matindex] + for offset, mat in ipairs(organic.material) do + if do_insert(mat, base + offset, matindex) then + print('index', matindex) + pcall(function() print(organic.creature_id) end) + pcall(function() print(organic.id) end) + print(organic.material[offset].id) + return matindex + end + end + end + return 0 +end + local function init_arena() local arena = df.global.world.arena local arena_unit = df.global.game.main_interface.arena_unit local arena_tree = df.global.game.main_interface.arena_tree + local leather_index_hint, plant_index_hint = 0, 0 -- races arena.race:resize(0) @@ -242,11 +310,10 @@ local function init_arena() arena_unit.races_all:resize(0) arena_unit.castes_filtered:resize(0) arena_unit.castes_all:resize(0) - for i, cre in ipairs(df.global.world.raws.creatures.all) do + for i, cre in ipairs(RAWS.creatures.all) do if cre.flags.VERMIN_GROUNDER or cre.flags.VERMIN_SOIL then goto continue end arena.creature_cnt:insert('#', 0) for caste in ipairs(cre.caste) do - -- the real interface sorts these alphabetically arena.race:insert('#', i) arena.caste:insert('#', caste) end @@ -254,11 +321,14 @@ local function init_arena() end -- interactions + -- note this doesn't actually come up with anything in vanilla. normal arena + -- mode reads from the files in data/vanilla/interaction examples/ where some + -- usable insteractions exist arena.interactions:resize(0) arena.interaction = -1 arena_unit.interactions:resize(0) arena_unit.interaction = -1 - for _, inter in ipairs(df.global.world.raws.interactions) do + for _, inter in ipairs(RAWS.interactions) do for _, effect in ipairs(inter.effects) do if #effect.arena_name > 0 then arena.interactions:insert('#', effect) @@ -278,13 +348,65 @@ local function init_arena() end end + -- equipment + -- this is slow, so optimize for speed: + -- - use pre-allocated structures if possible + -- - don't scan past the basic metals + -- - only scan until we fine one kind of thing. we don't need 1000 types of leather + -- or 40 types of wood + -- - remember the last matched material and try that again for the next item type + for idx, list in ipairs(arena.item_types.list) do + local list_size = 0 + local data = EQUIPMENT_TYPES[df.entity_sell_category[idx]] + if not data then goto continue end + for _,itemdef in ipairs(data.itemdefs) do + if data.def_filter and not data.def_filter(itemdef) then goto inner_continue end + local do_insert = function(mat, mattype, matindex) + if data.mat_filter and not data.mat_filter(mat) then return end + local element = { + item_type=data.item_type, + item_subtype=itemdef.subtype, + mattype=mattype, + matindex=matindex, + unk_c=1} + if #list > list_size then + utils.assign(list[list_size], element) + else + element.new = df.embark_item_choice.T_list + list:insert('#', element) + end + list_size = list_size + 1 + return true + end + -- if there is call for glass tools, uncomment this + -- for i in ipairs(df.builtin_mats) do + -- do_insert(MAT_TABLE.builtin[i], i, -1) + -- end + for i, mat in ipairs(RAWS.inorganics) do + do_insert(mat.material, 0, i) + -- stop at the first "special" metal. we don't need more than that + if mat.flags.DEEP_SPECIAL then break end + end + if data.want_leather then + leather_index_hint = scan_organic(df.organic_mat_category.Leather, RAWS.creatures.all, leather_index_hint, materials.CREATURE_BASE, do_insert) + end + plant_index_hint = scan_organic(df.organic_mat_category.Wood, RAWS.plants.all, plant_index_hint, materials.PLANT_BASE, do_insert) + ::inner_continue:: + end + ::continue:: + for list_idx=list_size,#list-1 do + df.delete(list[list_idx]) + end + list:resize(list_size) + end + -- trees arena.tree_types:resize(0) arena.tree_age = 100 arena_tree.tree_types_filtered:resize(0) arena_tree.tree_types_all:resize(0) arena_tree.age = 100 - for _, tree in ipairs(df.global.world.raws.plants.trees) do + for _, tree in ipairs(RAWS.plants.trees) do arena.tree_types:insert('#', tree) end end From 25d69daea0ecf995b15f28ef569b949a00500e30 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 23 May 2023 14:24:29 -0700 Subject: [PATCH 251/732] support undead invaders; clean up interface --- gui/sandbox.lua | 165 +++++++++++++++++++++++++++++++----------------- 1 file changed, 106 insertions(+), 59 deletions(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 245103b464..083ad32575 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -1,14 +1,16 @@ local gui = require('gui') local materials = require('gui.materials') local makeown = reqscript('makeown') +local syndrome_util = require('syndrome-util') local widgets = require('gui.widgets') local utils = require('utils') local DISPOSITIONS = { HOSTILE = 1, - WILD = 2, - FRIENDLY = 3, - FORT = 4, + HOSTILE_UNDEAD = 2, + WILD = 3, + FRIENDLY = 4, + FORT = 5, } --------------------- @@ -20,6 +22,7 @@ Sandbox.ATTRS { frame_title='Arena Sandbox', frame={r=2, t=18, w=26, h=20}, frame_inset={b=1}, + interface_masks=DEFAULT_NIL, } local function is_sentient(unit) @@ -29,7 +32,8 @@ end local function finalize_sentient(unit, disposition) - if disposition == DISPOSITIONS.HOSTILE then + if disposition == DISPOSITIONS.HOSTILE or disposition == DISPOSITIONS.HOSTILE_UNDEAD then + unit.flags1.active_invader = true; unit.flags1.marauder = true; elseif disposition == DISPOSITIONS.WILD then unit.flags2.visitor = true @@ -43,9 +47,7 @@ local function finalize_sentient(unit, disposition) end local function finalize_animal(unit, disposition) - if disposition == DISPOSITIONS.HOSTILE then - unit.flags1.active_invader = true; - unit.flags1.marauder = true; + if disposition == DISPOSITIONS.HOSTILE or disposition == DISPOSITIONS.HOSTILE_UNDEAD then unit.flags4.agitated_wilderness_creature = true elseif disposition == DISPOSITIONS.WILD then unit.flags2.roaming_wilderness_population_source = true @@ -60,11 +62,28 @@ local function finalize_animal(unit, disposition) end end -local function finalize_units(first_created_unit_id, disposition) +local function is_arena_action_in_progress() + return df.global.game.main_interface.arena_unit.open or + df.global.game.main_interface.arena_tree.open or + df.global.game.main_interface.bottom_mode_selected ~= -1 +end + +local function clear_arena_action() + -- close any open arena UI elements + df.global.game.main_interface.arena_unit.open = false + df.global.game.main_interface.arena_tree.open = false + df.global.game.main_interface.bottom_mode_selected = -1 +end + +local function finalize_units(first_created_unit_id, disposition, syndrome) for unit_id=first_created_unit_id,df.global.unit_next_id-1 do local unit = df.unit.find(unit_id) if not unit then goto continue end unit.profession = df.profession.STANDARD + if syndrome then + syndrome_util.infectWithSyndrome(unit, syndrome) + unit.flags1.zombie = true; + end unit.name.has_name = false if is_sentient(unit) then finalize_sentient(unit, disposition) @@ -83,46 +102,32 @@ function Sandbox:init() widgets.ResizingPanel{ frame={t=0}, frame_style=gui.FRAME_INTERIOR, - frame_inset={l=1, r=1}, autoarrange_subviews=1, subviews={ widgets.Label{ frame={l=0}, text={ - 'Spawn group #', + 'Unit group #', {text=function() return self.spawn_group end}, NEWLINE, ' unit', - {text=function() return df.global.unit_next_id - self.first_unit_id == 1 and '' or 's' end}, ': ', + {text=function() return df.global.unit_next_id - self.first_unit_id == 1 and '' or 's' end}, + ' in group: ', {text=function() return df.global.unit_next_id - self.first_unit_id end}, }, }, widgets.Panel{frame={h=1}}, - widgets.CycleHotkeyLabel{ - view_id='disposition', - frame={l=0}, - key='CUSTOM_SHIFT_D', - key_back='CUSTOM_SHIFT_A', - label='Unit disposition', - label_below=true, - options={ - {label='hostile', value=DISPOSITIONS.HOSTILE, pen=COLOR_RED}, - {label='independent/wild', value=DISPOSITIONS.WILD, pen=COLOR_YELLOW}, - {label='friendly', value=DISPOSITIONS.FRIENDLY, pen=COLOR_GREEN}, - {label='citizens/pets', value=DISPOSITIONS.FORT, pen=COLOR_BLUE}, - }, - }, - widgets.Panel{frame={h=1}}, widgets.HotkeyLabel{ frame={l=0}, key='CUSTOM_SHIFT_U', label="Spawn unit", on_activate=function() df.global.enabler.mouse_lbut = 0 + clear_arena_action() view:sendInputToParent{ARENA_CREATE_CREATURE=true} + df.global.game.main_interface.arena_unit.editing_filter = true end, }, - widgets.Panel{frame={h=1}}, widgets.HotkeyLabel{ frame={l=0}, key='CUSTOM_SHIFT_G', @@ -130,12 +135,27 @@ function Sandbox:init() on_activate=self:callback('finalize_group'), enabled=function() return df.global.unit_next_id ~= self.first_unit_id end, }, + widgets.Panel{frame={h=1}}, + widgets.CycleHotkeyLabel{ + view_id='disposition', + frame={l=0}, + key='CUSTOM_SHIFT_D', + key_back='CUSTOM_SHIFT_A', + label='Group disposition', + label_below=true, + options={ + {label='hostile', value=DISPOSITIONS.HOSTILE, pen=COLOR_LIGHTRED}, + {label='hostile (undead)', value=DISPOSITIONS.HOSTILE_UNDEAD, pen=COLOR_RED}, + {label='independent/wild', value=DISPOSITIONS.WILD, pen=COLOR_YELLOW}, + {label='friendly', value=DISPOSITIONS.FRIENDLY, pen=COLOR_GREEN}, + {label='citizens/pets', value=DISPOSITIONS.FORT, pen=COLOR_BLUE}, + }, + }, }, }, widgets.ResizingPanel{ frame={t=11}, frame_style=gui.FRAME_INTERIOR, - frame_inset={l=1, r=1}, autoarrange_subviews=1, subviews={ widgets.HotkeyLabel{ @@ -144,7 +164,9 @@ function Sandbox:init() label="Spawn tree", on_activate=function() df.global.enabler.mouse_lbut = 0 + clear_arena_action() view:sendInputToParent{ARENA_CREATE_TREE=true} + df.global.game.main_interface.arena_tree.editing_filter = true end, }, widgets.HotkeyLabel{ @@ -158,7 +180,7 @@ function Sandbox:init() widgets.HotkeyLabel{ frame={l=1, b=0}, key='LEAVESCREEN', - label="Return to fortress", + label="Return to game", on_activate=function() repeat until not self:onInput{LEAVESCREEN=true} view:dismiss() @@ -167,19 +189,6 @@ function Sandbox:init() } end -local function is_arena_action_in_progress() - return df.global.game.main_interface.arena_unit.open or - df.global.game.main_interface.arena_tree.open or - df.global.game.main_interface.bottom_mode_selected ~= -1 -end - -local function clear_arena_action() - -- close any open arena UI elements - df.global.game.main_interface.arena_unit.open = false - df.global.game.main_interface.arena_tree.open = false - df.global.game.main_interface.bottom_mode_selected = -1 -end - function Sandbox:onInput(keys) if keys._MOUSE_R_DOWN and self:getMouseFramePos() then clear_arena_action() @@ -193,14 +202,43 @@ function Sandbox:onInput(keys) return false end end - if not Sandbox.super.onInput(self, keys) then - view:sendInputToParent(keys) + if Sandbox.super.onInput(self, keys) then + return true + end + if keys._MOUSE_L then + for _,mask_panel in ipairs(self.interface_masks) do + if mask_panel:getMousePos() then return true end + end + end + view:sendInputToParent(keys) +end + +function Sandbox:find_zombie_syndrome() + if self.zombie_syndrome then return self.zombie_syndrome end + for _,syn in ipairs(df.global.world.raws.syndromes.all) do + if #syn.syn_class == 0 then goto continue end + if syn.syn_class[0] ~= 'ZOMBIE' then goto continue end + for _,effect in ipairs(syn.ce) do + if df.creature_interaction_effect_add_simple_flagst.is_instance(effect) and + effect.tags1.OPPOSED_TO_LIFE then + self.zombie_syndrome = syn + return syn + end + end + ::continue:: end + dfhack.printerr('ZOMBIE syndrome not found; not marking as undead') end function Sandbox:finalize_group() + local syndrome = nil + if self.subviews.disposition:getOptionValue() == DISPOSITIONS.HOSTILE_UNDEAD then + syndrome = self:find_zombie_syndrome() + end + finalize_units(self.first_unit_id, - self.subviews.disposition:getOptionValue()) + self.subviews.disposition:getOptionValue(), + syndrome) self.spawn_group = self.spawn_group + 1 self.first_unit_id = df.global.unit_next_id @@ -282,10 +320,6 @@ local function scan_organic(cat, vec, start_idx, base, do_insert) local organic = vec[matindex] for offset, mat in ipairs(organic.material) do if do_insert(mat, base + offset, matindex) then - print('index', matindex) - pcall(function() print(organic.creature_id) end) - pcall(function() print(organic.id) end) - print(organic.material[offset].id) return matindex end end @@ -310,14 +344,20 @@ local function init_arena() arena_unit.races_all:resize(0) arena_unit.castes_filtered:resize(0) arena_unit.castes_all:resize(0) + local arena_creatures = {} for i, cre in ipairs(RAWS.creatures.all) do - if cre.flags.VERMIN_GROUNDER or cre.flags.VERMIN_SOIL then goto continue end + if not cre.flags.VERMIN_GROUNDER and not cre.flags.VERMIN_SOIL then + table.insert(arena_creatures, {race=i, cre=cre}) + end + end + table.sort(arena_creatures, + function(a, b) return string.lower(a.cre.name[0]) < string.lower(b.cre.name[0]) end) + for _, cre_data in ipairs(arena_creatures) do arena.creature_cnt:insert('#', 0) - for caste in ipairs(cre.caste) do - arena.race:insert('#', i) + for caste in ipairs(cre_data.cre.caste) do + arena.race:insert('#', cre_data.race) arena.caste:insert('#', caste) end - ::continue:: end -- interactions @@ -414,26 +454,33 @@ end function SandboxScreen:init() init_arena() + local mask_panel = widgets.Panel{ + subviews={ + InterfaceMask{frame={t=18, r=2, w=22, h=6}}, + InterfaceMask{frame={l=0, r=0, b=0, h=3}}, + }, + } + self:addviews{ + mask_panel, Sandbox{ view_id='sandbox', - }, - InterfaceMask{ - frame={l=0, r=0, b=0, h=3}, + interface_masks=mask_panel.subviews, }, } + self.prev_gametype = df.global.gametype df.global.gametype = df.game_type.DWARF_ARENA end function SandboxScreen:onDismiss() - df.global.gametype = df.game_type.DWARF_MAIN + df.global.gametype = self.prev_gametype view = nil self.subviews.sandbox:finalize_group() end -if df.global.gametype ~= df.game_type.DWARF_MAIN then - qerror('must have a fort loaded') +if not dfhack.isWorldLoaded() then + qerror('gui/sandbox must have a world loaded') end view = view and view:raise() or SandboxScreen{}:show() From f63ebc0ae81111f2ad3eb96c1250ce8587993124 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 23 May 2023 14:27:54 -0700 Subject: [PATCH 252/732] update docs --- docs/gui/sandbox.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/gui/sandbox.rst b/docs/gui/sandbox.rst index 44da321698..50f45c10b4 100644 --- a/docs/gui/sandbox.rst +++ b/docs/gui/sandbox.rst @@ -5,15 +5,23 @@ gui/sandbox :summary: Create units, trees, or items. :tags: fort armok animals items map plants units -This tool provides a spawning interface for units, trees, and/or items. Units can be created with arbitrary skillsets, and trees can be created either as saplings or as fully grown (depending on the age you set). +This tool provides a spawning interface for units, trees, and/or items. Units +can be created with arbitrary skillsets, and trees can be created either as +saplings or as fully grown (depending on the age you set). The item creation +interface is the same as `gui/create-item`. You can choose whether spawned units are: - hostile (default) +- hostile undead - independent/wild - friendly - citizens/pets +Note that if you create new citizens and you're not using `autolabor`, you'll +have to got into the labors screen and make at least one change (any change) to +get DF to assign them labors. Otherwise they'll stand around with "No job". + Usage ----- From 4773388db74e3711f563c82940ae2c3448345194 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 23 May 2023 14:27:59 -0700 Subject: [PATCH 253/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index f69ec346d2..e0c562eb0c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -22,6 +22,7 @@ that repo. - `fix/stuck-instruments`: fix instruments that are attached to invalid jobs, making them unusable - `gui/mod-manager`: automatically restore your list of active mods when generating new worlds - `gui/autodump`: point and click item teleportation and destruction interface +- `gui/sandbox`: creation interface for units, trees, and items ## Fixes - `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath passable tiles of buildings From 578c22d88c005e1f9706405e70552a39f3a78c34 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 23 May 2023 15:08:18 -0700 Subject: [PATCH 254/732] remove reference to arena; don't let clicks bleed through --- gui/sandbox.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 083ad32575..e745995970 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -19,7 +19,7 @@ local DISPOSITIONS = { Sandbox = defclass(Sandbox, widgets.Window) Sandbox.ATTRS { - frame_title='Arena Sandbox', + frame_title='Armok\'s Sandbox', frame={r=2, t=18, w=26, h=20}, frame_inset={b=1}, interface_masks=DEFAULT_NIL, @@ -206,6 +206,7 @@ function Sandbox:onInput(keys) return true end if keys._MOUSE_L then + if self:getMouseFramePos() then return true end for _,mask_panel in ipairs(self.interface_masks) do if mask_panel:getMousePos() then return true end end From dd87a2df156b5cf776999ead61f3694ba1410b1b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 23 May 2023 15:11:02 -0700 Subject: [PATCH 255/732] remove space --- gui/sandbox.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index e745995970..5dc7d18e36 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -31,7 +31,6 @@ local function is_sentient(unit) end local function finalize_sentient(unit, disposition) - if disposition == DISPOSITIONS.HOSTILE or disposition == DISPOSITIONS.HOSTILE_UNDEAD then unit.flags1.active_invader = true; unit.flags1.marauder = true; From 04e1e09a75e4ce3d9b7a2b98ab99a0cb45eff3b9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 23 May 2023 22:26:49 -0700 Subject: [PATCH 256/732] address feedback --- gui/sandbox.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 5dc7d18e36..db7c6508ee 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -216,18 +216,16 @@ end function Sandbox:find_zombie_syndrome() if self.zombie_syndrome then return self.zombie_syndrome end for _,syn in ipairs(df.global.world.raws.syndromes.all) do - if #syn.syn_class == 0 then goto continue end - if syn.syn_class[0] ~= 'ZOMBIE' then goto continue end for _,effect in ipairs(syn.ce) do - if df.creature_interaction_effect_add_simple_flagst.is_instance(effect) and - effect.tags1.OPPOSED_TO_LIFE then + if df.creature_interaction_effect_add_simple_flagst:is_instance(effect) and + effect.tags1.OPPOSED_TO_LIFE and effect['end'] == -1 then self.zombie_syndrome = syn return syn end end ::continue:: end - dfhack.printerr('ZOMBIE syndrome not found; not marking as undead') + dfhack.printerr('permanent syndrome with OPPOSED_TO_LIFE not found; not marking as undead') end function Sandbox:finalize_group() @@ -265,6 +263,7 @@ SandboxScreen = defclass(SandboxScreen, gui.ZScreen) SandboxScreen.ATTRS { focus_path='sandbox', force_pause=true, + pass_pause=false, defocusable=false, } @@ -346,7 +345,7 @@ local function init_arena() arena_unit.castes_all:resize(0) local arena_creatures = {} for i, cre in ipairs(RAWS.creatures.all) do - if not cre.flags.VERMIN_GROUNDER and not cre.flags.VERMIN_SOIL then + if not cre.flags.VERMIN_GROUNDER and not cre.flags.VERMIN_SOIL and cre.graphics then table.insert(arena_creatures, {race=i, cre=cre}) end end From 4393568117eaa5b74872c4d98e07dee1d58f3e48 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 24 May 2023 12:39:21 -0700 Subject: [PATCH 257/732] change default size of gui/launcher --- gui/launcher.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index a976e7a117..cd454c8451 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -588,7 +588,7 @@ function LauncherUI:init(args) else new_frame = config.data if not next(new_frame) then - new_frame = {l=5, r=25, t=5, b=5} + new_frame = {w=110, h=36} end end main_panel.frame = new_frame From 71264d9c7f79489aaefb2d087c704ac4143a3bf1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 24 May 2023 13:01:18 -0700 Subject: [PATCH 258/732] reinstate orders generation using code borrowed from stockflow --- changelog.txt | 1 + gui/quickfort.lua | 8 +- internal/quickfort/command.lua | 7 +- internal/quickfort/orders.lua | 11 +- internal/quickfort/stockflow.lua | 640 +++++++++++++++++++++++++++++++ 5 files changed, 654 insertions(+), 13 deletions(-) create mode 100644 internal/quickfort/stockflow.lua diff --git a/changelog.txt b/changelog.txt index f714286053..29e7426c96 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ that repo. ## Misc Improvements - `gui/quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them - `gui/quickfort`: place (stockpile layout) mode is now supported. note that detailed stockpile configurations were part of query mode and are not yet supported +- `gui/quickfort`: you can now generate manager orders for items required to complete bluerpints - `gui/create-item`: ask for number of items to spawn by default - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for the gm-editor window itself) diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 47e39a573e..695d431f9d 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -350,13 +350,13 @@ function Quickfort:init() return #transformations == 0 and 'No transform' or table.concat(transformations, ', ') end}}}}}, widgets.HotkeyLabel{key='CUSTOM_O', label='Generate manager orders', - active=function() return self.blueprint_name and false end, - enabled=function() return self.blueprint_name and false end, + active=function() return self.blueprint_name end, + enabled=function() return self.blueprint_name end, on_activate=self:callback('do_command', 'orders')}, widgets.HotkeyLabel{key='CUSTOM_SHIFT_O', label='Preview manager orders', - active=function() return self.blueprint_name and false end, - enabled=function() return self.blueprint_name and false end, + active=function() return self.blueprint_name end, + enabled=function() return self.blueprint_name end, on_activate=self:callback('do_command', 'orders', true)}, widgets.HotkeyLabel{key='CUSTOM_SHIFT_U', label='Undo blueprint', active=function() return self.blueprint_name end, diff --git a/internal/quickfort/command.lua b/internal/quickfort/command.lua index ce73888f32..a78e233ee1 100644 --- a/internal/quickfort/command.lua +++ b/internal/quickfort/command.lua @@ -22,7 +22,7 @@ end local command_switch = { run='do_run', - -- orders='do_orders', -- until we get stockflow working + orders='do_orders', undo='do_undo', } @@ -185,7 +185,7 @@ local function do_one_command(command, cursor, blueprint_name, section_name, if command == 'orders' or mode == 'notes' or mode == 'config' then cursor = {x=0, y=0, z=0} else - qerror('please position the game cursor at the blueprint start ' .. + qerror('please position the keyboard cursor at the blueprint start ' .. 'location or use the --cursor option') end end @@ -234,9 +234,6 @@ end function do_command(args) for _,command in ipairs(args.commands) do if not command or not command_switch[command] then - if command == 'orders' then - qerror('orders functionality not updated yet') - end qerror(string.format('invalid command: "%s"', command)) end end diff --git a/internal/quickfort/orders.lua b/internal/quickfort/orders.lua index 3050aae0c5..8b73fc784a 100644 --- a/internal/quickfort/orders.lua +++ b/internal/quickfort/orders.lua @@ -11,10 +11,13 @@ end local quickfort_common = reqscript('internal/quickfort/common') -local ok, stockflow = pcall(require, 'plugins.stockflow') -if not ok then - stockflow = nil -end +-- local ok, stockflow = pcall(require, 'plugins.stockflow') +-- if not ok then +-- stockflow = nil +-- end + +-- use our own copy of stockflow logic until stockflow becomes available again +local stockflow = reqscript('internal/quickfort/stockflow') local log = quickfort_common.log diff --git a/internal/quickfort/stockflow.lua b/internal/quickfort/stockflow.lua new file mode 100644 index 0000000000..52fa743118 --- /dev/null +++ b/internal/quickfort/stockflow.lua @@ -0,0 +1,640 @@ +-- copy of stockflow logic for use until stockflow becomes available +--@ module = true + +local function reaction_entry(reactions, job_type, values, name) + if not job_type then + -- Perhaps df.job_type.something returned nil for an unknown job type. + -- We could warn about it; in any case, don't add it to the list. + return + end + + local order = df.manager_order:new() + -- These defaults differ from the newly created order's. + order:assign{ + job_type = job_type, + item_type = -1, + item_subtype = -1, + mat_type = -1, + mat_index = -1, + } + + if values then + -- Override default attributes. + order:assign(values) + end + + table.insert(reactions, { + name = name or df.job_type.attrs[job_type].caption, + order = order, + }) +end + +local function resource_reactions(reactions, job_type, mat_info, keys, items, options) + local values = {} + for key, value in pairs(mat_info.management) do + values[key] = value + end + + for _, itemid in ipairs(keys) do + local itemdef = items[itemid] + local start = options.verb or mat_info.verb or "Make" + if options.adjective then + start = start.." "..itemdef.adjective + end + + if (not options.permissible) or options.permissible(itemdef) then + local item_name = " "..itemdef[options.name_field or "name"] + if options.capitalize then + item_name = string.gsub(item_name, " .", string.upper) + end + + values.item_subtype = itemid + reaction_entry(reactions, job_type, values, start.." "..mat_info.adjective..item_name) + end + end +end + +local function material_reactions(reactions, itemtypes, mat_info) + -- Expects a list of {job_type, verb, item_name} tuples. + for _, row in ipairs(itemtypes) do + local line = row[2].." "..mat_info.adjective + if row[3] then + line = line.." "..row[3] + end + + reaction_entry(reactions, row[1], mat_info.management, line) + end +end + +local function clothing_reactions(reactions, mat_info, filter) + local resources = df.historical_entity.find(df.global.plotinfo.civ_id).resources + local itemdefs = df.global.world.raws.itemdefs + local job_types = df.job_type + resource_reactions(reactions, job_types.MakeArmor, mat_info, resources.armor_type, itemdefs.armor, {permissible = filter}) + resource_reactions(reactions, job_types.MakePants, mat_info, resources.pants_type, itemdefs.pants, {permissible = filter}) + resource_reactions(reactions, job_types.MakeGloves, mat_info, resources.gloves_type, itemdefs.gloves, {permissible = filter}) + resource_reactions(reactions, job_types.MakeHelm, mat_info, resources.helm_type, itemdefs.helms, {permissible = filter}) + resource_reactions(reactions, job_types.MakeShoes, mat_info, resources.shoes_type, itemdefs.shoes, {permissible = filter}) +end + +-- Find the reaction types that should be listed in the management interface. +function collect_reactions() + -- The sequence here tries to match the native manager screen. + -- It should also be possible to collect the sequence from somewhere native, + -- but I currently can only find it while the job selection screen is active. + -- Even that list doesn't seem to include their names. + local result = {} + + -- Caching the enumeration might not be important, but saves lookups. + local job_types = df.job_type + + local materials = { + rock = { + adjective = "rock", + management = {mat_type = 0}, + }, + } + + for _, name in ipairs{"wood", "cloth", "leather", "silk", "yarn", "bone", "shell", "tooth", "horn", "pearl"} do + materials[name] = { + adjective = name, + management = {material_category = {[name] = true}}, + } + end + + materials.wood.adjective = "wooden" + materials.tooth.adjective = "ivory/tooth" + materials.leather.clothing_flag = "LEATHER" + materials.shell.short = true + materials.pearl.short = true + + -- Collection and Entrapment + reaction_entry(result, job_types.CollectWebs) + reaction_entry(result, job_types.CollectSand) + reaction_entry(result, job_types.CollectClay) + reaction_entry(result, job_types.CatchLiveLandAnimal) + reaction_entry(result, job_types.CatchLiveFish) + + -- Cutting, encrusting, and metal extraction. + local rock_types = df.global.world.raws.inorganics + for rock_id = #rock_types-1, 0, -1 do + local material = rock_types[rock_id].material + local rock_name = material.state_adj.Solid + if material.flags.IS_STONE or material.flags.IS_GEM then + reaction_entry(result, job_types.CutGems, { + mat_type = 0, + mat_index = rock_id, + }, "Cut "..rock_name) + + reaction_entry(result, job_types.EncrustWithGems, { + mat_type = 0, + mat_index = rock_id, + item_category = {finished_goods = true}, + }, "Encrust Finished Goods With "..rock_name) + + reaction_entry(result, job_types.EncrustWithGems, { + mat_type = 0, + mat_index = rock_id, + item_category = {furniture = true}, + }, "Encrust Furniture With "..rock_name) + + reaction_entry(result, job_types.EncrustWithGems, { + mat_type = 0, + mat_index = rock_id, + item_category = {ammo = true}, + }, "Encrust Ammo With "..rock_name) + end + + if #rock_types[rock_id].metal_ore.mat_index > 0 then + reaction_entry(result, job_types.SmeltOre, {mat_type = 0, mat_index = rock_id}, "Smelt "..rock_name.." Ore") + end + + if #rock_types[rock_id].thread_metal.mat_index > 0 then + reaction_entry(result, job_types.ExtractMetalStrands, {mat_type = 0, mat_index = rock_id}) + end + end + + -- Glass cutting and encrusting, with different job numbers. + -- We could search the entire table, but glass is less subject to raws. + local glass_types = df.global.world.raws.mat_table.builtin + local glasses = {} + for glass_id = 3, 5 do + local material = glass_types[glass_id] + local glass_name = material.state_adj.Solid + if material.flags.IS_GLASS then + -- For future use. + table.insert(glasses, { + adjective = glass_name, + management = {mat_type = glass_id}, + }) + + reaction_entry(result, job_types.CutGlass, {mat_type = glass_id}, "Cut "..glass_name) + + reaction_entry(result, job_types.EncrustWithGlass, { + mat_type = glass_id, + item_category = {finished_goods = true}, + }, "Encrust Finished Goods With "..glass_name) + + reaction_entry(result, job_types.EncrustWithGlass, { + mat_type = glass_id, + item_category = {furniture = true}, + }, "Encrust Furniture With "..glass_name) + + reaction_entry(result, job_types.EncrustWithGlass, { + mat_type = glass_id, + item_category = {ammo = true}, + }, "Encrust Ammo With "..glass_name) + end + end + + -- Dyeing + reaction_entry(result, job_types.DyeThread) + reaction_entry(result, job_types.DyeCloth) + + -- Sew Image + local cloth_mats = {materials.cloth, materials.silk, materials.yarn, materials.leather} + for _, material in ipairs(cloth_mats) do + material_reactions(result, {{job_types.SewImage, "Sew", "Image"}}, material) + material.cloth = true + end + + for _, spec in ipairs{materials.bone, materials.shell, materials.tooth, materials.horn, materials.pearl} do + material_reactions(result, {{job_types.DecorateWith, "Decorate With"}}, spec) + end + + reaction_entry(result, job_types.MakeTotem) + reaction_entry(result, job_types.ButcherAnimal) + reaction_entry(result, job_types.MillPlants) + reaction_entry(result, job_types.MakePotashFromLye) + reaction_entry(result, job_types.MakePotashFromAsh) + + -- Kitchen + reaction_entry(result, job_types.PrepareMeal, {mat_type = 2}, "Prepare Easy Meal") + reaction_entry(result, job_types.PrepareMeal, {mat_type = 3}, "Prepare Fine Meal") + reaction_entry(result, job_types.PrepareMeal, {mat_type = 4}, "Prepare Lavish Meal") + + -- Brew Drink + reaction_entry(result, job_types.BrewDrink) + + -- Weaving + reaction_entry(result, job_types.WeaveCloth, {material_category = {plant = true}}, "Weave Thread into Cloth") + reaction_entry(result, job_types.WeaveCloth, {material_category = {silk = true}}, "Weave Thread into Silk") + reaction_entry(result, job_types.WeaveCloth, {material_category = {yarn = true}}, "Weave Yarn into Cloth") + + -- Extracts, farmer's workshop, and wood burning + reaction_entry(result, job_types.ExtractFromPlants) + reaction_entry(result, job_types.ExtractFromRawFish) + reaction_entry(result, job_types.ExtractFromLandAnimal) + reaction_entry(result, job_types.PrepareRawFish) + reaction_entry(result, job_types.MakeCheese) + reaction_entry(result, job_types.MilkCreature) + reaction_entry(result, job_types.ShearCreature) + reaction_entry(result, job_types.SpinThread, {material_category = {strand = true}}) + reaction_entry(result, job_types.MakeLye) + reaction_entry(result, job_types.ProcessPlants) + reaction_entry(result, job_types.ProcessPlantsBag) + reaction_entry(result, job_types.ProcessPlantsVial) + reaction_entry(result, job_types.ProcessPlantsBarrel) + reaction_entry(result, job_types.MakeCharcoal) + reaction_entry(result, job_types.MakeAsh) + + -- Reactions defined in the raws. + -- Not all reactions are allowed to the civilization. + -- That includes "Make sharp rock" by default. + local entity = df.historical_entity.find(df.global.plotinfo.civ_id) + if not entity then + -- No global civilization; arena mode? + -- Anyway, skip remaining reactions, since many depend on the civ. + return result + end + + for _, reaction_id in ipairs(entity.entity_raw.workshops.permitted_reaction_id) do + local reaction = df.global.world.raws.reactions.reactions[reaction_id] + local name = string.gsub(reaction.name, "^.", string.upper) + reaction_entry(result, job_types.CustomReaction, {reaction_name = reaction.code}, name) + end + + -- Reactions generated by the game. + for _, reaction in ipairs(df.global.world.raws.reactions.reactions) do + if reaction.source_enid == entity.id then + local name = string.gsub(reaction.name, "^.", string.upper) + reaction_entry(result, job_types.CustomReaction, {reaction_name = reaction.code}, name) + end + end + + -- Metal forging + local itemdefs = df.global.world.raws.itemdefs + for rock_id = 0, #rock_types - 1 do + local material = rock_types[rock_id].material + local rock_name = material.state_adj.Solid + local mat_flags = { + adjective = rock_name, + management = {mat_type = 0, mat_index = rock_id}, + verb = "Forge", + } + + if material.flags.IS_METAL then + reaction_entry(result, job_types.StudWith, mat_flags.management, "Stud With "..rock_name) + + if material.flags.ITEMS_WEAPON then + -- Todo: Are these really the right flags to check? + resource_reactions(result, job_types.MakeWeapon, mat_flags, entity.resources.weapon_type, itemdefs.weapons, { + permissible = (function(itemdef) return itemdef.skill_ranged == -1 end), + }) + + -- Is this entirely disconnected from the entity? + material_reactions(result, {{job_types.MakeBallistaArrowHead, "Forge", "Ballista Arrow Head"}}, mat_flags) + + resource_reactions(result, job_types.MakeTrapComponent, mat_flags, entity.resources.trapcomp_type, itemdefs.trapcomps, { + adjective = true, + }) + + resource_reactions(result, job_types.AssembleSiegeAmmo, mat_flags, entity.resources.siegeammo_type, itemdefs.siege_ammo, { + verb = "Assemble", + }) + end + + if material.flags.ITEMS_WEAPON_RANGED then + resource_reactions(result, job_types.MakeWeapon, mat_flags, entity.resources.weapon_type, itemdefs.weapons, { + permissible = (function(itemdef) return itemdef.skill_ranged >= 0 end), + }) + end + + if material.flags.ITEMS_DIGGER then + -- Todo: Ranged or training digging weapons? + resource_reactions(result, job_types.MakeWeapon, mat_flags, entity.resources.digger_type, itemdefs.weapons, { + }) + end + + if material.flags.ITEMS_AMMO then + resource_reactions(result, job_types.MakeAmmo, mat_flags, entity.resources.ammo_type, itemdefs.ammo, { + name_field = "name_plural", + }) + end + + if material.flags.ITEMS_ANVIL then + material_reactions(result, {{job_types.ForgeAnvil, "Forge", "Anvil"}}, mat_flags) + end + + if material.flags.ITEMS_ARMOR then + local metalclothing = (function(itemdef) return itemdef.props.flags.METAL end) + clothing_reactions(result, mat_flags, metalclothing) + resource_reactions(result, job_types.MakeShield, mat_flags, entity.resources.shield_type, itemdefs.shields, { + }) + end + + if material.flags.ITEMS_SOFT then + local metalclothing = (function(itemdef) return itemdef.props.flags.SOFT and not itemdef.props.flags.METAL end) + clothing_reactions(result, mat_flags, metalclothing) + end + + resource_reactions(result, job_types.MakeTool, mat_flags, entity.resources.tool_type, itemdefs.tools, { + permissible = (function(itemdef) return ((material.flags.ITEMS_HARD and itemdef.flags.HARD_MAT) or (material.flags.ITEMS_METAL and itemdef.flags.METAL_MAT)) and not itemdef.flags.NO_DEFAULT_JOB end), + capitalize = true, + }) + + if material.flags.ITEMS_HARD then + material_reactions(result, { + {job_types.ConstructDoor, "Construct", "Door"}, + {job_types.ConstructFloodgate, "Construct", "Floodgate"}, + {job_types.ConstructHatchCover, "Construct", "Hatch Cover"}, + {job_types.ConstructGrate, "Construct", "Grate"}, + {job_types.ConstructThrone, "Construct", "Throne"}, + {job_types.ConstructCoffin, "Construct", "Sarcophagus"}, + {job_types.ConstructTable, "Construct", "Table"}, + {job_types.ConstructSplint, "Construct", "Splint"}, + {job_types.ConstructCrutch, "Construct", "Crutch"}, + {job_types.ConstructArmorStand, "Construct", "Armor Stand"}, + {job_types.ConstructWeaponRack, "Construct", "Weapon Rack"}, + {job_types.ConstructCabinet, "Construct", "Cabinet"}, + {job_types.MakeGoblet, "Forge", "Goblet"}, + {job_types.MakeInstrument, "Forge", "Instrument"}, + {job_types.MakeToy, "Forge", "Toy"}, + {job_types.ConstructStatue, "Construct", "Statue"}, + {job_types.ConstructBlocks, "Construct", "Blocks"}, + {job_types.MakeAnimalTrap, "Forge", "Animal Trap"}, + {job_types.MakeBarrel, "Forge", "Barrel"}, + {job_types.MakeBucket, "Forge", "Bucket"}, + {job_types.ConstructBin, "Construct", "Bin"}, + {job_types.MakePipeSection, "Forge", "Pipe Section"}, + {job_types.MakeCage, "Forge", "Cage"}, + {job_types.MintCoins, "Mint", "Coins"}, + {job_types.ConstructChest, "Construct", "Chest"}, + {job_types.MakeFlask, "Forge", "Flask"}, + {job_types.MakeChain, "Forge", "Chain"}, + {job_types.MakeCrafts, "Make", "Crafts"}, + {job_types.MakeFigurine, "Make", "Figurine"}, + {job_types.MakeAmulet, "Make", "Amulet"}, + {job_types.MakeScepter, "Make", "Scepter"}, + {job_types.MakeCrown, "Make", "Crown"}, + {job_types.MakeRing, "Make", "Ring"}, + {job_types.MakeEarring, "Make", "Earring"}, + {job_types.MakeBracelet, "Make", "Bracelet"}, + {job_types.MakeGem, "Make Large", "Gem"}, + {job_types.ConstructMechanisms, "Construct", "Mechanisms"}, + }, mat_flags) + end + + if material.flags.ITEMS_SOFT then + material_reactions(result, { + {job_types.MakeBackpack, "Make", "Backpack"}, + {job_types.MakeQuiver, "Make", "Quiver"}, + {job_types.ConstructCatapultParts, "Construct", "Catapult Parts"}, + {job_types.ConstructBallistaParts, "Construct", "Ballista Parts"}, + }, mat_flags) + end + end + end + + -- Traction Bench + reaction_entry(result, job_types.ConstructTractionBench) + + -- Non-metal weapons + resource_reactions(result, job_types.MakeWeapon, materials.wood, entity.resources.weapon_type, itemdefs.weapons, { + permissible = (function(itemdef) return itemdef.skill_ranged >= 0 end), + }) + + resource_reactions(result, job_types.MakeWeapon, materials.wood, entity.resources.training_weapon_type, itemdefs.weapons, { + }) + + resource_reactions(result, job_types.MakeWeapon, materials.bone, entity.resources.weapon_type, itemdefs.weapons, { + permissible = (function(itemdef) return itemdef.skill_ranged >= 0 end), + }) + + resource_reactions(result, job_types.MakeWeapon, materials.rock, entity.resources.weapon_type, itemdefs.weapons, { + permissible = (function(itemdef) return itemdef.flags.CAN_STONE end), + }) + + -- Wooden items + -- Closely related to the ITEMS_HARD list. + material_reactions(result, { + {job_types.ConstructDoor, "Construct", "Door"}, + {job_types.ConstructFloodgate, "Construct", "Floodgate"}, + {job_types.ConstructHatchCover, "Construct", "Hatch Cover"}, + {job_types.ConstructGrate, "Construct", "Grate"}, + {job_types.ConstructThrone, "Construct", "Chair"}, + {job_types.ConstructCoffin, "Construct", "Casket"}, + {job_types.ConstructTable, "Construct", "Table"}, + {job_types.ConstructArmorStand, "Construct", "Armor Stand"}, + {job_types.ConstructWeaponRack, "Construct", "Weapon Rack"}, + {job_types.ConstructCabinet, "Construct", "Cabinet"}, + {job_types.MakeGoblet, "Make", "Cup"}, + {job_types.MakeInstrument, "Make", "Instrument"}, + }, materials.wood) + + resource_reactions(result, job_types.MakeTool, materials.wood, entity.resources.tool_type, itemdefs.tools, { + -- permissible = (function(itemdef) return itemdef.flags.WOOD_MAT and not itemdef.flags.NO_DEFAULT_JOB end), + permissible = (function(itemdef) return not itemdef.flags.NO_DEFAULT_JOB end), + capitalize = true, + }) + + material_reactions(result, { + {job_types.MakeToy, "Make", "Toy"}, + {job_types.ConstructBlocks, "Construct", "Blocks"}, + {job_types.ConstructSplint, "Construct", "Splint"}, + {job_types.ConstructCrutch, "Construct", "Crutch"}, + {job_types.MakeAnimalTrap, "Make", "Animal Trap"}, + {job_types.MakeBarrel, "Make", "Barrel"}, + {job_types.MakeBucket, "Make", "Bucket"}, + {job_types.ConstructBin, "Construct", "Bin"}, + {job_types.MakeCage, "Make", "Cage"}, + {job_types.MakePipeSection, "Make", "Pipe Section"}, + }, materials.wood) + + resource_reactions(result, job_types.MakeTrapComponent, materials.wood, entity.resources.trapcomp_type, itemdefs.trapcomps, { + permissible = (function(itemdef) return itemdef.flags.WOOD end), + adjective = true, + }) + + -- Rock items + material_reactions(result, { + {job_types.ConstructDoor, "Construct", "Door"}, + {job_types.ConstructFloodgate, "Construct", "Floodgate"}, + {job_types.ConstructHatchCover, "Construct", "Hatch Cover"}, + {job_types.ConstructGrate, "Construct", "Grate"}, + {job_types.ConstructThrone, "Construct", "Throne"}, + {job_types.ConstructCoffin, "Construct", "Coffin"}, + {job_types.ConstructTable, "Construct", "Table"}, + {job_types.ConstructArmorStand, "Construct", "Armor Stand"}, + {job_types.ConstructWeaponRack, "Construct", "Weapon Rack"}, + {job_types.ConstructCabinet, "Construct", "Cabinet"}, + {job_types.MakeGoblet, "Make", "Mug"}, + {job_types.MakeInstrument, "Make", "Instrument"}, + }, materials.rock) + + resource_reactions(result, job_types.MakeTool, materials.rock, entity.resources.tool_type, itemdefs.tools, { + permissible = (function(itemdef) return itemdef.flags.HARD_MAT end), + capitalize = true, + }) + + material_reactions(result, { + {job_types.MakeToy, "Make", "Toy"}, + {job_types.ConstructQuern, "Construct", "Quern"}, + {job_types.ConstructMillstone, "Construct", "Millstone"}, + {job_types.ConstructSlab, "Construct", "Slab"}, + {job_types.ConstructStatue, "Construct", "Statue"}, + {job_types.ConstructBlocks, "Construct", "Blocks"}, + }, materials.rock) + + -- Glass items + for _, mat_info in ipairs(glasses) do + material_reactions(result, { + {job_types.ConstructDoor, "Construct", "Portal"}, + {job_types.ConstructFloodgate, "Construct", "Floodgate"}, + {job_types.ConstructHatchCover, "Construct", "Hatch Cover"}, + {job_types.ConstructGrate, "Construct", "Grate"}, + {job_types.ConstructThrone, "Construct", "Throne"}, + {job_types.ConstructCoffin, "Construct", "Coffin"}, + {job_types.ConstructTable, "Construct", "Table"}, + {job_types.ConstructArmorStand, "Construct", "Armor Stand"}, + {job_types.ConstructWeaponRack, "Construct", "Weapon Rack"}, + {job_types.ConstructCabinet, "Construct", "Cabinet"}, + {job_types.MakeGoblet, "Make", "Goblet"}, + {job_types.MakeInstrument, "Make", "Instrument"}, + }, mat_info) + + resource_reactions(result, job_types.MakeTool, mat_info, entity.resources.tool_type, itemdefs.tools, { + permissible = (function(itemdef) return itemdef.flags.HARD_MAT end), + capitalize = true, + }) + + material_reactions(result, { + {job_types.MakeToy, "Make", "Toy"}, + {job_types.ConstructStatue, "Construct", "Statue"}, + {job_types.ConstructBlocks, "Construct", "Blocks"}, + {job_types.MakeCage, "Make", "Terrarium"}, + {job_types.MakePipeSection, "Make", "Tube"}, + }, mat_info) + + resource_reactions(result, job_types.MakeTrapComponent, mat_info, entity.resources.trapcomp_type, itemdefs.trapcomps, { + adjective = true, + }) + end + + -- Bed, specified as wooden. + reaction_entry(result, job_types.ConstructBed, materials.wood.management) + + -- Windows + for _, mat_info in ipairs(glasses) do + material_reactions(result, { + {job_types.MakeWindow, "Make", "Window"}, + }, mat_info) + end + + -- Rock Mechanisms + reaction_entry(result, job_types.ConstructMechanisms, materials.rock.management) + + resource_reactions(result, job_types.AssembleSiegeAmmo, materials.wood, entity.resources.siegeammo_type, itemdefs.siege_ammo, { + verb = "Assemble", + }) + + for _, mat_info in ipairs(glasses) do + material_reactions(result, { + {job_types.MakeRawGlass, "Make Raw", nil}, + }, mat_info) + end + + material_reactions(result, { + {job_types.MakeBackpack, "Make", "Backpack"}, + {job_types.MakeQuiver, "Make", "Quiver"}, + }, materials.leather) + + for _, material in ipairs(cloth_mats) do + clothing_reactions(result, material, (function(itemdef) return itemdef.props.flags[material.clothing_flag or "SOFT"] end)) + end + + -- Boxes, Bags, and Ropes + local boxmats = { + {mats = {materials.wood}, box = "Chest"}, + {mats = {materials.rock}, box = "Coffer"}, + {mats = glasses, box = "Box", flask = "Vial"}, + {mats = {materials.cloth}, box = "Bag", chain = "Rope"}, + {mats = {materials.leather}, box = "Bag", flask = "Waterskin"}, + {mats = {materials.silk, materials.yarn}, box = "Bag", chain = "Rope"}, + } + for _, boxmat in ipairs(boxmats) do + for _, mat in ipairs(boxmat.mats) do + material_reactions(result, {{job_types.ConstructChest, "Construct", boxmat.box}}, mat) + if boxmat.chain then + material_reactions(result, {{job_types.MakeChain, "Make", boxmat.chain}}, mat) + end + if boxmat.flask then + material_reactions(result, {{job_types.MakeFlask, "Make", boxmat.flask}}, mat) + end + end + end + + -- Crafts + for _, mat in ipairs{ + materials.wood, + materials.rock, + materials.cloth, + materials.leather, + materials.shell, + materials.bone, + materials.silk, + materials.tooth, + materials.horn, + materials.pearl, + materials.yarn, + } do + material_reactions(result, { + {job_types.MakeCrafts, "Make", "Crafts"}, + {job_types.MakeAmulet, "Make", "Amulet"}, + {job_types.MakeBracelet, "Make", "Bracelet"}, + {job_types.MakeEarring, "Make", "Earring"}, + }, mat) + + if not mat.cloth then + material_reactions(result, { + {job_types.MakeCrown, "Make", "Crown"}, + {job_types.MakeFigurine, "Make", "Figurine"}, + {job_types.MakeRing, "Make", "Ring"}, + {job_types.MakeGem, "Make Large", "Gem"}, + }, mat) + + if not mat.short then + material_reactions(result, { + {job_types.MakeScepter, "Make", "Scepter"}, + }, mat) + end + end + end + + -- Siege engine parts + reaction_entry(result, job_types.ConstructCatapultParts, materials.wood.management) + reaction_entry(result, job_types.ConstructBallistaParts, materials.wood.management) + + for _, mat in ipairs{materials.wood, materials.bone} do + resource_reactions(result, job_types.MakeAmmo, mat, entity.resources.ammo_type, itemdefs.ammo, { + name_field = "name_plural", + }) + end + + -- BARRED and SCALED as flag names don't quite seem to fit, here. + clothing_reactions(result, materials.bone, (function(itemdef) return itemdef.props.flags.BARRED end)) + clothing_reactions(result, materials.shell, (function(itemdef) return itemdef.props.flags.SCALED end)) + + for _, mat in ipairs{materials.wood, materials.leather} do + resource_reactions(result, job_types.MakeShield, mat, entity.resources.shield_type, itemdefs.shields, {}) + end + + -- Melt a Metal Object + reaction_entry(result, job_types.MeltMetalObject) + + return result +end + +-- Place a new copy of the order onto the manager's queue. +function create_orders(order, amount) + local new_order = order:new() + amount = math.floor(amount) + new_order.amount_left = amount + new_order.amount_total = amount + -- Todo: Create in a validated state if the fortress is small enough? + new_order.status.validated = false + new_order.status.active = false + new_order.id = df.global.world.manager_order_next_id + df.global.world.manager_order_next_id = df.global.world.manager_order_next_id + 1 + df.global.world.manager_orders:insert('#', new_order) +end From 02c0031fc1c6b825784c68bde3f1626c05e1f67c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 29 May 2023 22:16:29 -0700 Subject: [PATCH 259/732] get assign-minecarts working --- assign-minecarts.lua | 27 ++++----------------------- changelog.txt | 7 ++++--- docs/assign-minecarts.rst | 6 +++--- 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/assign-minecarts.lua b/assign-minecarts.lua index cb8da7c834..5554e69022 100644 --- a/assign-minecarts.lua +++ b/assign-minecarts.lua @@ -2,31 +2,12 @@ --@ module = true local argparse = require('argparse') -local quickfort = reqscript('quickfort') - --- ensures the list of available minecarts has been calculated by the game -local function refresh_ui_hauling_vehicles() - local qfdata - if #df.global.plotinfo.hauling.routes > 0 then - -- if there is an existing route, move to the vehicle screen and back - -- out to force the game to scan for assignable minecarts - qfdata = 'hv^^' - else - -- if no current routes, create a route, move to the vehicle screen, - -- back out, and remove the route. The extra "px" is in the string in - -- case the user has the confirm plugin enabled. "p" pauses the plugin - -- and "x" retries the route deletion. - qfdata = 'hrv^xpx^' - end - quickfort.apply_blueprint{mode='config', data=qfdata} -end function get_free_vehicles() - refresh_ui_hauling_vehicles() local free_vehicles = {} - for _,minecart in ipairs(df.global.plotinfo.hauling.vehicles) do - if minecart and minecart.route_id == -1 then - table.insert(free_vehicles, minecart) + for _,vehicle in ipairs(df.global.world.vehicles.active) do + if vehicle and vehicle.route_id == -1 then + table.insert(free_vehicles, vehicle) end end return free_vehicles @@ -136,7 +117,7 @@ local function all(quiet) end end -local function do_help() +local function do_help(_) print(dfhack.script_help()) end diff --git a/changelog.txt b/changelog.txt index 29e7426c96..f4890d1d1b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,14 +15,15 @@ that repo. ## New Scripts - `diplomacy`: view or alter diplomatic relationships -- `exportlegends`: reinstated: export extended legends information for external browsing -- `modtools/create-item`: reinstated: commandline and API interface for creating items -- `light-aquifers-only`: reinstated: convert heavy aquifers to light +- `exportlegends`: (reinstated\) export extended legends information for external browsing +- `modtools/create-item`: (reinstated) commandline and API interface for creating items +- `light-aquifers-only`: (reinstated) convert heavy aquifers to light - `necronomicon`: search fort for items containing the secrets of life and death - `fix/stuck-instruments`: fix instruments that are attached to invalid jobs, making them unusable - `gui/mod-manager`: automatically restore your list of active mods when generating new worlds - `gui/autodump`: point and click item teleportation and destruction interface - `gui/sandbox`: creation interface for units, trees, and items +- `assign-minecarts`: (reinstated) quickly assign minecarts to hauling routes ## Fixes - `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath passable tiles of buildings diff --git a/docs/assign-minecarts.rst b/docs/assign-minecarts.rst index abd7ff856f..6512e4f79d 100644 --- a/docs/assign-minecarts.rst +++ b/docs/assign-minecarts.rst @@ -3,10 +3,10 @@ assign-minecarts .. dfhack-tool:: :summary: Assign minecarts to hauling routes. - :tags: unavailable fort productivity + :tags: fort productivity -This script allows you to assign minecarts to hauling routes without having to -use the in-game interface. +This script allows you to quickly assign minecarts to hauling routes without +having to go through the in-game interface. Note that a hauling route must have at least one stop defined before a minecart can be assigned to it. From 1afb5669f0ad8b74b996b14ab453ff8e8468340c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 1 Jun 2023 10:45:55 -0700 Subject: [PATCH 260/732] remove tools; add quivers --- gui/sandbox.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index db7c6508ee..dcb91e5c85 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -283,6 +283,10 @@ local EQUIPMENT_TYPES = { Ammo={itemdefs=RAWS.itemdefs.ammo, item_type=df.item_type.AMMO, mat_filter=function(mat) return mat.flags.ITEMS_AMMO end}, + Quivers={itemdefs={{subtype=-1}}, + item_type=df.item_type.QUIVER, + want_leather=true, + mat_filter=function(mat) return mat.flags.LEATHER end}, Bodywear={itemdefs=RAWS.itemdefs.armor, item_type=df.item_type.ARMOR, want_leather=true, @@ -307,9 +311,6 @@ local EQUIPMENT_TYPES = { item_type=df.item_type.SHIELD, want_leather=true, mat_filter=function(mat) return mat.flags.ITEMS_ARMOR or mat.flags.LEATHER end}, - Tools={itemdefs=RAWS.itemdefs.tools, - item_type=df.item_type.TOOL, - mat_filter=function(mat) return mat.flags.ITEMS_HARD end}, } local function scan_organic(cat, vec, start_idx, base, do_insert) From 315c7afc20651ac0fe1700d720f71796190f198e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 1 Jun 2023 11:19:06 -0700 Subject: [PATCH 261/732] update changelog for 50.08-r2 --- changelog.txt | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index f4890d1d1b..4ee27f60f4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -13,20 +13,30 @@ that repo. # Future +## New Scripts + +## Fixes + +## Misc Improvements + +## Removed + +# 50.08-r2 + ## New Scripts - `diplomacy`: view or alter diplomatic relationships -- `exportlegends`: (reinstated\) export extended legends information for external browsing +- `exportlegends`: (reinstated) export extended legends information for external browsing - `modtools/create-item`: (reinstated) commandline and API interface for creating items - `light-aquifers-only`: (reinstated) convert heavy aquifers to light - `necronomicon`: search fort for items containing the secrets of life and death -- `fix/stuck-instruments`: fix instruments that are attached to invalid jobs, making them unusable +- `fix/stuck-instruments`: fix instruments that are attached to invalid jobs, making them unusable. turn on automatic fixing in `gui/control-panel` in the ``Maintenance`` tab. - `gui/mod-manager`: automatically restore your list of active mods when generating new worlds -- `gui/autodump`: point and click item teleportation and destruction interface -- `gui/sandbox`: creation interface for units, trees, and items +- `gui/autodump`: point and click item teleportation and destruction interface (available only if ``armok`` tools are shown) +- `gui/sandbox`: creation interface for units, trees, and items (available only if ``armok`` tools are shown) - `assign-minecarts`: (reinstated) quickly assign minecarts to hauling routes ## Fixes -- `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath passable tiles of buildings +- `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath walkable tiles of buildings - `deathcause`: fix incorrect weapon sometimes being reported - `gui/create-item`: allow armor to be made out of leather when using the restrictive filters - `gui/design`: Fix building and stairs designation @@ -39,11 +49,9 @@ that repo. - `gui/quickfort`: you can now generate manager orders for items required to complete bluerpints - `gui/create-item`: ask for number of items to spawn by default - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. -- `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for the gm-editor window itself) +- `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) - `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused -## Removed - # 50.08-r1 ## Fixes From 67fb7cd83ccf98b2169c9a6888675f7dfb5bf2e0 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sun, 4 Jun 2023 15:42:55 +0200 Subject: [PATCH 262/732] suspendmanager: Make it a class - track the job statuses --- changelog.txt | 1 + suspend.lua | 8 ++- suspendmanager.lua | 122 +++++++++++++++++++++++++++++++++------------ unsuspend.lua | 10 ++-- 4 files changed, 105 insertions(+), 36 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4ee27f60f4..4951b59524 100644 --- a/changelog.txt +++ b/changelog.txt @@ -51,6 +51,7 @@ that repo. - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) - `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused +- `suspendmanager`: internal to be more flexible # 50.08-r1 diff --git a/suspend.lua b/suspend.lua index 2456f009bc..bbbe4e4293 100644 --- a/suspend.lua +++ b/suspend.lua @@ -16,8 +16,14 @@ if help then return end +-- Only initialize suspendmanager if we want to suspend blocking jobs +manager = onlyblocking and suspendmanager.SuspendManager{preventBlocking=true} or nil +if manager then + manager:refresh() +end + suspendmanager.foreach_construction_job(function (job) - if not onlyblocking or suspendmanager.isBlocking(job) then + if not manager or manager:shouldBeSuspended(job) then suspendmanager.suspend(job) end end) diff --git a/suspendmanager.lua b/suspendmanager.lua index 605f18543a..fbeb3d4bc1 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -16,23 +16,64 @@ end local GLOBAL_KEY = 'suspendmanager' -- used for state change hooks and persistence enabled = enabled or false -preventblocking = preventblocking == nil and true or preventblocking eventful.enableEvent(eventful.eventType.JOB_INITIATED, 10) eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 10) +--- List of reasons for a job to be suspended +---@enum reason +REASON = { + --- The job is under water and dwarves will suspend the job when starting it + UNDER_WATER = 1, + --- The job is planned by buildingplan, but not yet ready to start + BUILDINGPLAN = 2, + --- Fuzzy risk detection of jobs blocking each other in shapes like corners + RISK_BLOCKING = 3, +} + +REASON_TEXT = { + [REASON.UNDER_WATER] = 'underwater', + [REASON.BUILDINGPLAN] = 'planned', + [REASON.RISK_BLOCKING] = 'blocking' +} + +--- Suspension reasons from an external source +--- SuspendManager does not actively suspend such jobs, but +--- will not unsuspend them +EXTERNAL_REASONS = { + [REASON.UNDER_WATER]=true, + [REASON.BUILDINGPLAN]=true, +} + +---@class SuspendManager +---@field preventBlocking boolean +---@field suspensions table +SuspendManager = defclass(SuspendManager) +SuspendManager.ATTRS { + --- When enabled, suspendmanager also tries to suspend blocking jobs, + --- when not enabled, it only cares about avoiding unsuspending jobs suspended externally + preventBlocking = false, + + --- Current job suspensions with their reasons + suspensions = {} +} + +--- SuspendManager instance kept between frames +---@type SuspendManager +Instance = Instance or SuspendManager{preventBlocking=true} + function isEnabled() return enabled end function preventBlockingEnabled() - return preventblocking + return Instance.preventBlocking end local function persist_state() persist.GlobalTable[GLOBAL_KEY] = json.encode({ enabled=enabled, - prevent_blocking=preventblocking, + prevent_blocking=Instance.preventBlocking, }) end @@ -41,9 +82,9 @@ end function update_setting(setting, value) if setting == "preventblocking" then if (value == "true" or value == true) then - preventblocking = true + Instance.preventBlocking = true elseif (value == "false" or value == false) then - preventblocking = false + Instance.preventBlocking = false else qerror(tostring(value) .. " is not a valid value for preventblocking, it must be true or false") end @@ -162,7 +203,7 @@ local function riskOfStuckConstructionAt(pos) end --- Return true if this job is at risk of blocking another one -function isBlocking(job) +local function riskBlocking(job) -- Not a construction job, no risk if job.job_type ~= df.job_type.ConstructBuilding then return false end @@ -186,43 +227,62 @@ function isBlocking(job) return false end ---- Return true with a reason if a job should be suspended. ---- It optionally takes in account the risk of creating stuck ---- construction buildings +--- Return the reason for suspending a job or nil if it should not be suspended --- @param job job ---- @param accountblocking boolean -function shouldBeSuspended(job, accountblocking) - if accountblocking and isBlocking(job) then - return true, 'blocking' +--- @return reason? +function SuspendManager:shouldBeSuspended(job) + local reason = self.suspensions[job.id] + if reason and EXTERNAL_REASONS[reason] then + -- don't actively suspend external reasons for suspension + return nil end - return false, nil + return reason end ---- Return true with a reason if a job should not be unsuspended. -function shouldStaySuspended(job, accountblocking) - -- External reasons to be suspended +--- Return the reason for keeping a job suspended or nil if it can be unsuspended +--- @param job job +--- @return reason? +function SuspendManager:shouldStaySuspended(job) + return self.suspensions[job.id] +end - if dfhack.maps.getTileFlags(job.pos).flow_size > 1 then - return true, 'underwater' - end +--- Recompute the list of suspended jobs +function SuspendManager:refresh() + self.suspensions = {} - local bld = dfhack.job.getHolder(job) - if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then - return true, 'buildingplan' - end + for _,job in utils.listpairs(df.global.world.jobs.list) do + -- External reasons to suspend a job + if job.job_type == df.job_type.ConstructBuilding then + if dfhack.maps.getTileFlags(job.pos).flow_size > 1 then + self.suspensions[job.id]=REASON.UNDER_WATER + end - -- Internal reasons to be suspended, determined by suspendmanager - return shouldBeSuspended(job, accountblocking) + local bld = dfhack.job.getHolder(job) + if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then + self.suspensions[job.id]=REASON.BUILDINGPLAN + end + end + + if not self.preventBlocking then goto continue end + + -- Internal reasons to suspend a job + if riskBlocking(job) then + self.suspensions[job.id]=REASON.RISK_BLOCKING + end + + ::continue:: + end end local function run_now() + Instance:refresh() foreach_construction_job(function(job) if job.flags.suspend then - if not shouldStaySuspended(job, preventblocking) then + if not Instance:shouldStaySuspended(job) then unsuspend(job) end else - if shouldBeSuspended(job, preventblocking) then + if Instance:shouldBeSuspended(job) then suspend(job) end end @@ -231,7 +291,7 @@ end --- @param job job local function on_job_change(job) - if preventblocking then + if Instance.preventBlocking then -- Note: This method could be made incremental by taking in account the -- changed job run_now() @@ -262,7 +322,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') enabled = (persisted_data or {enabled=false})['enabled'] - preventblocking = (persisted_data or {prevent_blocking=true})['prevent_blocking'] + Instance.preventBlocking = (persisted_data or {prevent_blocking=true})['prevent_blocking'] update_triggers() end @@ -294,7 +354,7 @@ local function main(args) update_setting(positionals[2], positionals[3]) elseif command == nil then print(string.format("suspendmanager is currently %s", (enabled and "enabled" or "disabled"))) - if preventblocking then + if Instance.preventBlocking then print("It is configured to prevent construction jobs from blocking each others") else print("It is configured to unsuspend all jobs") diff --git a/unsuspend.lua b/unsuspend.lua index 9351564d5d..87951d3294 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -186,12 +186,14 @@ argparse.processArgsGetopt({...}, { local skipped_counts = {} local unsuspended_count = 0 +local manager = suspendmanager.SuspendManager{preventBlocking=skipblocking} +manager:refresh() suspendmanager.foreach_construction_job(function(job) if not job.flags.suspend then return end - local skip,reason=suspendmanager.shouldStaySuspended(job, skipblocking) - if skip then - skipped_counts[reason] = (skipped_counts[reason] or 0) + 1 + local skip_reason=manager:shouldStaySuspended(job, skipblocking) + if skip_reason then + skipped_counts[skip_reason] = (skipped_counts[skip_reason] or 0) + 1 return end suspendmanager.unsuspend(job) @@ -200,7 +202,7 @@ end) if not quiet then for reason,count in pairs(skipped_counts) do - print(string.format('Not unsuspending %d %s job(s)', count, reason)) + print(string.format('Not unsuspending %d %s job(s)', count, suspendmanager.REASON_TEXT[reason])) end if unsuspended_count > 0 then From c356aebcde32199fa7c1d32b65ed5c96d008c1f9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 5 Jun 2023 17:41:40 -0700 Subject: [PATCH 263/732] bump to 50.08-r3 --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 4ee27f60f4..e62aa79475 100644 --- a/changelog.txt +++ b/changelog.txt @@ -21,6 +21,8 @@ that repo. ## Removed +# 50.08-r3 + # 50.08-r2 ## New Scripts From 570fcfa6a3af1d0831e3f14e15f45548e8896bec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 01:11:32 +0000 Subject: [PATCH 264/732] [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.22.0 → 0.23.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.22.0...0.23.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59dac64677..4b3cca5564 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.22.0 + rev: 0.23.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From ab6fd7a8c57c51184a39979fc5993b0ddabf487f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 5 Jun 2023 13:01:16 -0700 Subject: [PATCH 265/732] blocks can be made out of wood --- changelog.txt | 1 + gui/create-item.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index e62aa79475..e5181f9bf1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,7 @@ that repo. ## New Scripts ## Fixes +- `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters ## Misc Improvements diff --git a/gui/create-item.lua b/gui/create-item.lua index 95384cd3ec..58e9ede789 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -97,7 +97,7 @@ local function getRestrictiveMatFilter(itemType, opts) mat.id == 'POTASH' or mat.id == 'ASH' or mat.id == 'PEARLASH') end, BLOCKS = function(mat, parent, typ, idx) - return mat.flags.IS_STONE or mat.flags.IS_METAL or mat.flags.IS_GLASS + return mat.flags.IS_STONE or mat.flags.IS_METAL or mat.flags.IS_GLASS or mat.flags.WOOD end, } for k, v in ipairs { 'GOBLET', 'FLASK', 'TOY', 'RING', 'CROWN', 'SCEPTER', 'FIGURINE', 'TOOL' } do From 75fa8de6dc48a4a388f328d3d8947d25b54ea4f0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 5 Jun 2023 03:57:38 -0700 Subject: [PATCH 266/732] add option to clear the trader flag upon teleport --- docs/gui/autodump.rst | 5 +++++ gui/autodump.lua | 47 ++++++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/docs/gui/autodump.rst b/docs/gui/autodump.rst index a41cf839bf..d20ca0021f 100644 --- a/docs/gui/autodump.rst +++ b/docs/gui/autodump.rst @@ -12,6 +12,11 @@ instead. Double-click anywhere on the map to teleport the items there. Be wary (or excited) that if you teleport the items into an unsupported position (e.g. mid-air), then they will become projectiles and fall. +There are options to include or exclude forbidden items, items that are +currently tagged as being used by an active job, and items dropped by traders. +If trader items are included, the ``trader`` flag will be cleared upon teleport +so the items can be used. + Usage ----- diff --git a/gui/autodump.lua b/gui/autodump.lua index aac8db367c..5600d4ad76 100644 --- a/gui/autodump.lua +++ b/gui/autodump.lua @@ -9,15 +9,16 @@ local function get_dims(pos1, pos2) return width, height, depth end -local function is_good_item(item, include_forbidden, include_in_job) +local function is_good_item(item, include) if not item then return false end if not item.flags.on_ground or item.flags.garbage_collect or - item.flags.hostile or item.flags.on_fire or item.flags.trader or - item.flags.in_building or item.flags.construction or item.flags.spider_web then + item.flags.hostile or item.flags.on_fire or item.flags.in_building or + item.flags.construction or item.flags.spider_web then return false end - if item.flags.forbid and not include_forbidden then return false end - if item.flags.in_job and not include_in_job then return false end + if item.flags.forbid and not include.forbidden then return false end + if item.flags.in_job and not include.in_job then return false end + if item.flags.trader and not include.trader then return false end return true end @@ -28,7 +29,7 @@ end Autodump = defclass(Autodump, widgets.Window) Autodump.ATTRS { frame_title='Autodump', - frame={w=47, h=18, r=2, t=18}, + frame={w=48, h=18, r=2, t=18}, resizable=true, resize_min={h=10}, autoarrange_subviews=true, @@ -126,6 +127,15 @@ function Autodump:init() initial_option=false, on_change=self:callback('refresh_dump_items'), }, + widgets.ToggleHotkeyLabel{ + view_id='include_trader', + frame={l=0}, + label='Include items dropped by traders', + key='CUSTOM_CTRL_T', + auto_width=true, + initial_option=false, + on_change=self:callback('refresh_dump_items'), + }, widgets.ToggleHotkeyLabel{ view_id='mark_as_forbidden', frame={l=0}, @@ -151,16 +161,21 @@ function Autodump:reset_selected_state() end end -function Autodump:refresh_dump_items() - local dump_items = {} - local include_forbidden = false - local include_in_job = false +function Autodump:get_include() + local include = {forbidden=false, in_job=false, trader=false} if next(self.subviews) then - include_forbidden = self.subviews.include_forbidden:getOptionValue() - include_in_job = self.subviews.include_in_job:getOptionValue() + include.forbidden = self.subviews.include_forbidden:getOptionValue() + include.in_job = self.subviews.include_in_job:getOptionValue() + include.trader = self.subviews.include_trader:getOptionValue() end + return include +end + +function Autodump:refresh_dump_items() + local dump_items = {} + local include = self:get_include() for _,item in ipairs(df.global.world.items.all) do - if not is_good_item(item, include_forbidden, include_in_job) then goto continue end + if not is_good_item(item, include) then goto continue end if item.flags.dump then table.insert(dump_items, item) end @@ -208,11 +223,10 @@ function Autodump:get_bounds(cursor, mark) end function Autodump:select_items_in_block(block, bounds) - local include_forbidden = self.subviews.include_forbidden:getOptionValue() - local include_in_job = self.subviews.include_in_job:getOptionValue() + local include = self:get_include() for _,item_id in ipairs(block.items) do local item = df.item.find(item_id) - if not is_good_item(item, include_forbidden, include_in_job) then + if not is_good_item(item, include) then goto continue end local x, y, z = dfhack.items.getPosition(item) @@ -334,6 +348,7 @@ function Autodump:do_dump(pos) for _,item in ipairs(items) do if dfhack.items.moveToGround(item, pos) then item.flags.dump = false + item.flags.trader = false if mark_as_forbidden then item.flags.forbid = true end From 06439681c5bbed8de4925a643da4ed90ee65fcfd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 6 Jun 2023 09:59:53 -0700 Subject: [PATCH 267/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e62aa79475..478e82a764 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes ## Misc Improvements +- `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants ## Removed From 5b42765d0b38c8d80b5273da0e398b624d467be9 Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 6 Jun 2023 11:16:33 -0700 Subject: [PATCH 268/732] Update changelog.txt --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index bfd890d526..5ba868473f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -55,7 +55,6 @@ that repo. - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) - `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused -- `suspendmanager`: internal to be more flexible # 50.08-r1 From 2bdcdfb375ee3995348670de6eecff6c931625ee Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sun, 4 Jun 2023 20:18:43 +0200 Subject: [PATCH 269/732] [suspendmanager] Protect floor designation from being erased --- changelog.txt | 1 + docs/suspendmanager.rst | 3 +++ suspendmanager.lua | 55 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 5ba868473f..3be5bb9dba 100644 --- a/changelog.txt +++ b/changelog.txt @@ -55,6 +55,7 @@ that repo. - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) - `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused +- `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased # 50.08-r1 diff --git a/docs/suspendmanager.rst b/docs/suspendmanager.rst index c4b70f46f0..3f74780d78 100644 --- a/docs/suspendmanager.rst +++ b/docs/suspendmanager.rst @@ -11,6 +11,9 @@ This tool will watch your active jobs and: items temporarily in the way, or worker dwarves getting scared by wildlife - suspend construction jobs that would prevent a dwarf from reaching an adjacent construction job, such as when building a wall corner. +- suspend construction jobs on top of a smoothing, engraving or track carving + job. This prevent the construction job to be completed first, which would + erase the other Usage ----- diff --git a/suspendmanager.lua b/suspendmanager.lua index fbeb3d4bc1..3b4dd41f91 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -29,12 +29,15 @@ REASON = { BUILDINGPLAN = 2, --- Fuzzy risk detection of jobs blocking each other in shapes like corners RISK_BLOCKING = 3, + --- Building job on top of an erasable designation (smoothing, carving, ...) + ERASE_DESIGNATION = 4, } REASON_TEXT = { [REASON.UNDER_WATER] = 'underwater', [REASON.BUILDINGPLAN] = 'planned', - [REASON.RISK_BLOCKING] = 'blocking' + [REASON.RISK_BLOCKING] = 'blocking', + [REASON.ERASE_DESIGNATION] = 'designation', } --- Suspension reasons from an external source @@ -133,6 +136,13 @@ local BUILDING_IMPASSABLE = { [df.building_type.BarsVertical]=true, } +--- Designation job type that are erased if a building is built on top of it +local ERASABLE_DESIGNATION = { + [df.job_type.CarveTrack]=true, + [df.job_type.SmoothFloor]=true, + [df.job_type.DetailFloor]=true, +} + --- Check if a building is blocking once constructed ---@param building building_constructionst|building ---@return boolean @@ -227,6 +237,26 @@ local function riskBlocking(job) return false end +--- Return true if the building overlaps with a tile with a designation flag +---@param building building +local function buildingOnDesignation(building) + local z = building.z + for x=building.x1,building.x2 do + for y=building.y1,building.y2 do + local flags, occupancy = dfhack.maps.getTileFlags(x,y,z) + if flags.dig ~= df.tile_dig_designation.No or + flags.smooth > 0 or + occupancy.carve_track_north or + occupancy.carve_track_east or + occupancy.carve_track_south or + occupancy.carve_track_west + then + return true + end + end + end +end + --- Return the reason for suspending a job or nil if it should not be suspended --- @param job job --- @return reason? @@ -270,6 +300,29 @@ function SuspendManager:refresh() self.suspensions[job.id]=REASON.RISK_BLOCKING end + -- First designation protection check: tile with designation flag + if job.job_type == df.job_type.ConstructBuilding then + ---@type building + local building = dfhack.job.getHolder(job) + if building then + if buildingOnDesignation(building) then + self.suspensions[job.id]=REASON.ERASE_DESIGNATION + end + end + end + + -- Second designation protection check: designation job + if ERASABLE_DESIGNATION[job.job_type] then + local building = dfhack.buildings.findAtTile(job.pos) + if building ~= nil then + for _,building_job in ipairs(building.jobs) do + if building_job.job_type == df.job_type.ConstructBuilding then + self.suspensions[building_job.id]=REASON.ERASE_DESIGNATION + end + end + end + end + ::continue:: end end From b9edc387837473896dc052d473119a241721aec5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 2 Jun 2023 13:14:07 -0700 Subject: [PATCH 270/732] add obviously important tasks to prioritize --- prioritize.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prioritize.lua b/prioritize.lua index ed04094d50..188c7e8dc4 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -22,13 +22,13 @@ local DEFAULT_JOB_TYPES = { 'SeekInfant', 'SetBone', 'Surgery', 'Suture', -- ensure prisoners and animals are tended to quickly -- (Animal/prisoner storage already covered by 'StoreItemInStockpile' above) - 'SlaughterAnimal', + 'SlaughterAnimal', 'PenLargeAnimal', 'LoadCageTrap', -- ensure noble tasks never get starved 'InterrogateSubject', 'ManageWorkOrders', 'ReportCrime', 'TradeAtDepot', -- get tasks done quickly that might block the player from getting on to -- the next thing they want to do 'BringItemToDepot', 'DestroyBuilding', 'DumpItem', 'FellTree', - 'RemoveConstruction', + 'RemoveConstruction', 'PullLever' } -- set of job types that we are watching. maps job_type (as a number) to From 51bfc252bb2facb8c0385a41f32c07294aa6047a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 01:47:51 -0700 Subject: [PATCH 271/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 5ba868473f..6425573667 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ that repo. ## Misc Improvements - `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants +- `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types ## Removed From e68f4aea4c7ddd0e14fe5642c59a840d5de12e29 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 Jun 2023 23:44:31 -0700 Subject: [PATCH 272/732] add new config options --- gui/control-panel.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 6a27fa6e93..84a23b5fa1 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -36,9 +36,14 @@ local FORT_SERVICES = { } local FORT_AUTOSTART = { + 'autobutcher target 50 50 14 2 BIRD_GOOSE', + 'autobutcher target 50 50 14 2 BIRD_TURKEY', + 'autobutcher target 50 50 14 2 BIRD_CHICKEN', + 'autofarm threshold 150 grass_tail_pig', 'ban-cooking all', 'buildingplan set boulders false', 'buildingplan set logs false', + 'fix/blood-del fort', 'light-aquifers-only fort', } for _,v in ipairs(FORT_SERVICES) do @@ -814,7 +819,7 @@ end ControlPanel = defclass(ControlPanel, widgets.Window) ControlPanel.ATTRS { frame_title='DFHack Control Panel', - frame={w=55, h=36}, + frame={w=61, h=36}, resizable=true, resize_min={h=28}, autoarrange_subviews=true, From 6007990c0ce5604d3ffe45914bc89a64061feb3c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 01:51:12 -0700 Subject: [PATCH 273/732] update changelog --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 5ba868473f..676ae4181f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,8 @@ that repo. - `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters ## Misc Improvements +- `gui/control-panel`: add some popular startup configuration commands for `autobutcher` and `autofarm` +- `gui/control-panel`: add option for running `fix/blood-del` on new forts (enabled by default) - `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants ## Removed From 1d4018c12fb2f06198e31c1411f6b6e4d5526a31 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 03:13:54 -0700 Subject: [PATCH 274/732] update quickfort entrypoints and docs --- docs/gui/quickfort.rst | 8 ++++--- docs/quickfort.rst | 51 +++++++++++++++++------------------------- gui/quickfort.lua | 15 ++++++++----- quickfort.lua | 47 +++++++++++++++++++++----------------- 4 files changed, 62 insertions(+), 59 deletions(-) diff --git a/docs/gui/quickfort.rst b/docs/gui/quickfort.rst index 4874af0e8d..39e322c131 100644 --- a/docs/gui/quickfort.rst +++ b/docs/gui/quickfort.rst @@ -2,14 +2,16 @@ gui/quickfort ============= .. dfhack-tool:: - :summary: Apply pre-designed blueprints to your fort. + :summary: Apply layout blueprints to your fort. :tags: fort design productivity buildings map stockpiles This is the graphical interface for the `quickfort` script. Once you load a blueprint, you will see a highlight over the tiles that will be modified. You can use the mouse cursor to reposition the blueprint and the hotkeys to rotate and repeat the blueprint up or down z-levels. Once you are satisfied, -click the mouse or hit :kbd:`Enter` to apply the blueprint to the map. +click the mouse or hit :kbd:`Enter` to apply the blueprint to the map. You can +apply the blueprint as many times as you wish to different spots on the map. +Right click or hit :kbd:`Esc` to stop. Usage ----- @@ -26,7 +28,7 @@ dialog is shown where you can select a blueprint to load. You can also type search terms in the dialog and the list of matching blueprints will be filtered as you type. You can search for directory names, file names, -blueprint labels, modes, or comments. Note that, depending on the active list +blueprint labels, modes, or comments. Note that, depending on the active filters, the id numbers in the list may not be contiguous. To rotate or flip the blueprint around, enable transformations with :kbd:`t` and diff --git a/docs/quickfort.rst b/docs/quickfort.rst index 4d3467d4ac..bf9dec67e4 100644 --- a/docs/quickfort.rst +++ b/docs/quickfort.rst @@ -2,7 +2,7 @@ quickfort ========= .. dfhack-tool:: - :summary: Apply pre-designed blueprints to your fort. + :summary: Apply layout blueprints to your fort. :tags: fort design productivity buildings map stockpiles Quickfort reads stored blueprint files and applies them to the game map. @@ -24,13 +24,6 @@ There are many ready-to-use blueprints in the so you can use this tool productively even if you haven't created any blueprints yourself. -.. admonition:: Note - - quickfort is still in the process of being updated for the new version of - DF. Stockpiles (the "place" mode), zones (the "zone" mode), building - configuration (the "query" mode), and game configuration (the "config" mode) - are not yet supported. - Usage ----- @@ -74,10 +67,9 @@ Usage :orders: Uses the manager interface to queue up workorders to manufacture items needed by the specified blueprint(s). :undo: Applies the inverse of the specified blueprint. Dig tiles are - undesignated, buildings are canceled or removed (depending on their - construction status), and stockpiles/zones are removed. There is no - effect for query and config blueprints since they can contain - arbitrary key sequences that are not reversible. + undesignated, buildings are canceled or scheduled for destruction + (depending on their construction status), and stockpiles/zones are + removed. Examples -------- @@ -93,7 +85,7 @@ Examples List all the blueprints that have both "dreamfort" and "help" as keywords. ``quickfort run library/dreamfort.csv`` Run the first blueprint in the ``library/dreamfort.csv`` file (which happens - to be the blueprint that displays the help). + to be the "notes" blueprint that displays the help). ``quickfort run library/pump_stack.csv -n /dig --repeat up,80 --transform ccw,flipv`` Dig a pump stack through 160 z-levels up from the current cursor location (each repetition of the ``library/pump_stack.csv -n /dig`` blueprint is 2 @@ -117,15 +109,15 @@ Command options ```` can be zero or more of: ``-c``, ``--cursor ,,`` - Use the specified map coordinates instead of the current map cursor for the - the blueprint start position. If this option is specified, then an active - game map cursor is not necessary. + Use the specified map coordinates instead of the current keyboard map + cursor for the the blueprint start position. If this option is specified, + then an active keyboard map cursor is not necessary. ``-d``, ``--dry-run`` Go through all the motions and print statistics on what would be done, but don't actually change any game state. ``--preserve-engravings `` - Don't designate tiles for digging if they have an engraving with at least - the specified quality. Valid values for ``quality`` are: ``None``, + Don't designate tiles for digging/carving if they have an engraving with at + least the specified quality. Valid values for ``quality`` are: ``None``, ``Ordinary``, ``WellCrafted``, ``FinelyCrafted``, ``Superior``, ``Exceptional``, and ``Masterful``. Specify ``None`` to ignore engravings when designating tiles. Note that if ``Masterful`` tiles are dug out, the @@ -154,8 +146,8 @@ Transformations All transformations are anchored at the blueprint start cursor position. This is the upper left corner by default, but it can be modified if the blueprint has a -`start() modeline marker `. This just means that the blueprint -tile that would normally appear under your cursor will still appear under your +`start() modeline marker `. This means that the blueprint tile +that would normally appear under your cursor will still appear under your cursor, regardless of how the blueprint is rotated or flipped. ```` is one of: @@ -213,8 +205,8 @@ statistics structure is a map of stat ids to ``{label=string, value=number}``. ``data`` (required) A sparse map populated such that ``data[z][y][x]`` yields the blueprint text that should be applied to the tile at map coordinate ``(x, y, z)``. You can - also just pass a string and it will be interpreted as the value of - ``data[0][0][0]``. + also just pass a string instead of a table and it will be interpreted as + the value of ``data[0][0][0]``. ``command`` The quickfort command to execute, e.g. ``run``, ``orders``, etc. Defaults to ``run``. @@ -225,12 +217,12 @@ statistics structure is a map of stat ids to ``{label=string, value=number}``. specified, defaults to ``{x=0, y=0, z=0}``, which means that the coordinates in the ``data`` map are used without shifting. ``aliases`` - A map of query blueprint aliases names to their expansions. If not - specified, defaults to ``{}``. + A map of blueprint alias names to their expansions. If not specified, + defaults to ``{}``. ``preserve_engravings`` - Don't designate tiles for digging if they have an engraving with at least - the specified quality. Value is a ``df.item_quality`` enum name or value, or - the string ``None`` (or, equivalently, ``-1``) to indicate that no + Don't designate tiles for digging or carving if they have an engraving with + at least the specified quality. Value is a ``df.item_quality`` enum name or + value, or the string ``None`` (or, equivalently, ``-1``) to indicate that no engravings should be preserved. Defaults to ``df.item_quality.Masterful``. ``dry_run`` Just calculate statistics, such as how many tiles are outside the boundaries @@ -240,12 +232,11 @@ statistics structure is a map of stat ids to ``{label=string, value=number}``. API usage example:: - local guidm = require('gui.dwarfmode') local quickfort = reqscript('quickfort') - -- dig a 10x10 block at the cursor position + -- dig a 10x10 block at the mouse cursor position quickfort.apply_blueprint{mode='dig', data='d(10x10)', - pos=guidm.getCursorPos()} + pos=dfhack.gui.getMousePos()} -- dig a 10x10 block starting at coordinate x=30, y=40, z=50 quickfort.apply_blueprint{mode='dig', data={[50]={[40]={[30]='d(10x10)'}}}} diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 695d431f9d..4ebabae8b1 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -1,6 +1,9 @@ -- A GUI front-end for quickfort --@ module = true +-- reload changed transitive dependencies +reqscript('quickfort').refresh_scripts() + local quickfort_command = reqscript('internal/quickfort/command') local quickfort_list = reqscript('internal/quickfort/list') local quickfort_map = reqscript('internal/quickfort/map') @@ -8,7 +11,6 @@ local quickfort_parse = reqscript('internal/quickfort/parse') local quickfort_preview = reqscript('internal/quickfort/preview') local quickfort_transform = reqscript('internal/quickfort/transform') -local argparse = require('argparse') local dialogs = require('gui.dialogs') local gui = require('gui') local guidm = require('gui.dwarfmode') @@ -35,7 +37,7 @@ transformations = transformations or {} -- displays blueprint details, such as the full modeline and comment, that -- otherwise might be truncated for length in the blueprint selection list -local BlueprintDetails = defclass(BlueprintDetails, dialogs.MessageBox) +BlueprintDetails = defclass(BlueprintDetails, dialogs.MessageBox) BlueprintDetails.ATTRS{ focus_path='quickfort/dialog/details', frame_title='Details', @@ -62,7 +64,7 @@ end -- blueprint selection dialog, shown when the script starts or when a user wants -- to load a new blueprint into the ui -local BlueprintDialog = defclass(BlueprintDialog, dialogs.ListBox) +BlueprintDialog = defclass(BlueprintDialog, dialogs.ListBox) BlueprintDialog.ATTRS{ focus_path='quickfort/dialog', frame_title='Load quickfort blueprint', @@ -616,11 +618,12 @@ end function Quickfort:do_command(command, dry_run, post_fn) self.dirty = true - print(('executing via gui/quickfort: quickfort %s'):format( + print(('executing via gui/quickfort: quickfort %s --cursor=%d,%d,%d'):format( quickfort_parse.format_command( - command, self.blueprint_name, self.section_name, dry_run))) + command, self.blueprint_name, self.section_name, dry_run), + self.saved_cursor.x, self.saved_cursor.y, self.saved_cursor.z)) local ctx = self:run_quickfort_command(command, dry_run, false) - quickfort_command.finish_command(ctx, self.section_name) + quickfort_command.finish_commands(ctx) if command == 'run' then if #ctx.messages > 0 then self._dialog = dialogs.showMessage( diff --git a/quickfort.lua b/quickfort.lua index fef56e72dc..70e991f6e1 100644 --- a/quickfort.lua +++ b/quickfort.lua @@ -3,31 +3,38 @@ local argparse = require('argparse') --- reqscript all internal files here, even if they're not directly used by this --- top-level file. this ensures modified transitive dependencies are properly --- reloaded when this script is run. -local quickfort_aliases = reqscript('internal/quickfort/aliases') local quickfort_api = reqscript('internal/quickfort/api') -local quickfort_build = reqscript('internal/quickfort/build') -local quickfort_building = reqscript('internal/quickfort/building') local quickfort_command = reqscript('internal/quickfort/command') local quickfort_common = reqscript('internal/quickfort/common') -local quickfort_config = reqscript('internal/quickfort/config') -local quickfort_dig = reqscript('internal/quickfort/dig') -local quickfort_keycodes = reqscript('internal/quickfort/keycodes') local quickfort_list = reqscript('internal/quickfort/list') -local quickfort_map = reqscript('internal/quickfort/map') -local quickfort_meta = reqscript('internal/quickfort/meta') -local quickfort_notes = reqscript('internal/quickfort/notes') -local quickfort_orders = reqscript('internal/quickfort/orders') -local quickfort_parse = reqscript('internal/quickfort/parse') -local quickfort_place = reqscript('internal/quickfort/place') -local quickfort_preview = reqscript('internal/quickfort/preview') -local quickfort_query = reqscript('internal/quickfort/query') -local quickfort_reader = reqscript('internal/quickfort/reader') local quickfort_set = reqscript('internal/quickfort/set') -local quickfort_transform = reqscript('internal/quickfort/transform') -local quickfort_zone = reqscript('internal/quickfort/zone') + +function refresh_scripts() + -- reqscript all internal files here, even if they're not directly used by this + -- top-level file. this ensures modified transitive dependencies are properly + -- reloaded when this script is run. + reqscript('internal/quickfort/aliases') + reqscript('internal/quickfort/api') + reqscript('internal/quickfort/build') + reqscript('internal/quickfort/building') + reqscript('internal/quickfort/command') + reqscript('internal/quickfort/common') + reqscript('internal/quickfort/dig') + reqscript('internal/quickfort/keycodes') + reqscript('internal/quickfort/list') + reqscript('internal/quickfort/map') + reqscript('internal/quickfort/meta') + reqscript('internal/quickfort/notes') + reqscript('internal/quickfort/orders') + reqscript('internal/quickfort/parse') + reqscript('internal/quickfort/place') + reqscript('internal/quickfort/preview') + reqscript('internal/quickfort/reader') + reqscript('internal/quickfort/set') + reqscript('internal/quickfort/transform') + reqscript('internal/quickfort/zone') +end +refresh_scripts() -- public API function apply_blueprint(params) From 275f3530bca5503c43930e630498901228bdecad Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 03:14:11 -0700 Subject: [PATCH 275/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 5ba868473f..f97df1d163 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ that repo. ## Misc Improvements - `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants +- `quickfort`: now handles zones, locations, stockpile configuration, hauling routes, and more ## Removed From 60f63fc1e87d0f2c98f02cb769ba79dd44fd0a70 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 03:24:02 -0700 Subject: [PATCH 276/732] coalesce orders from multiple blueprints and order them by quantity --- internal/quickfort/command.lua | 71 ++++++++++++++++++++-------------- internal/quickfort/orders.lua | 36 +++++++++++------ 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/internal/quickfort/command.lua b/internal/quickfort/command.lua index a78e233ee1..c5bd0e3e9e 100644 --- a/internal/quickfort/command.lua +++ b/internal/quickfort/command.lua @@ -29,20 +29,26 @@ local command_switch = { local default_transform_fn = function(pos) return pos end -- returns map of values that start the same for all contexts -local function make_ctx_base() +local function make_ctx_base(prev_ctx) + prev_ctx = prev_ctx or { + order_specs={}, + stats={out_of_bounds={label='Tiles outside map boundary', value=0}, + invalid_keys={label='Invalid key sequences', value=0}}, + messages={}, + } return { zmin=30000, zmax=0, transform_fn=default_transform_fn, - stats={out_of_bounds={label='Tiles outside map boundary', value=0}, - invalid_keys={label='Invalid key sequences', value=0}}, - messages={}, + order_specs=prev_ctx.order_specs, + stats=prev_ctx.stats, + messages=prev_ctx.messages, } end -local function make_ctx(command, blueprint_name, cursor, aliases, quiet, +local function make_ctx(prev_ctx, command, blueprint_name, cursor, aliases, quiet, dry_run, preview, preserve_engravings) - local ctx = make_ctx_base() + local ctx = make_ctx_base(prev_ctx) local params = { command=command, blueprint_name=blueprint_name, @@ -58,7 +64,7 @@ local function make_ctx(command, blueprint_name, cursor, aliases, quiet, end -- see make_ctx() above for which params can be specified -function init_ctx(params) +function init_ctx(params, prev_ctx) if not params.command or not command_switch[params.command] then error(('invalid command: "%s"'):format(params.command)) end @@ -70,6 +76,7 @@ function init_ctx(params) end return make_ctx( + prev_ctx, params.command, params.blueprint_name, copyall(params.cursor), -- copy since we modify this during processing @@ -159,17 +166,18 @@ function do_command_section(ctx, section_name, modifiers) local filepath = quickfort_list.get_blueprint_filepath(ctx.blueprint_name) local first_modeline = do_apply_modifiers(filepath, sheet_name, label, ctx, modifiers) - if first_modeline and first_modeline.message then + if first_modeline and first_modeline.message and ctx.command == 'run' then table.insert(ctx.messages, first_modeline.message) end end -function finish_command(ctx, section_name) - if ctx.command == 'orders' then quickfort_orders.create_orders(ctx) end +function finish_commands(ctx) + quickfort_orders.create_orders(ctx) + for _,message in ipairs(ctx.messages) do + print('* '..message) + end if not ctx.quiet then - print(('%s successfully completed'):format( - quickfort_parse.format_command(ctx.command, ctx.blueprint_name, - section_name, ctx.dry_run))) + print('Blueprint statistics:') for _,stat in pairs(ctx.stats) do if stat.always or stat.value > 0 then print((' %s: %d'):format(stat.label, stat.value)) @@ -178,11 +186,11 @@ function finish_command(ctx, section_name) end end -local function do_one_command(command, cursor, blueprint_name, section_name, +local function do_one_command(prev_ctx, command, cursor, blueprint_name, section_name, mode, quiet, dry_run, preserve_engravings, modifiers) if not cursor then - if command == 'orders' or mode == 'notes' or mode == 'config' then + if command == 'orders' or mode == 'notes' then cursor = {x=0, y=0, z=0} else qerror('please position the keyboard cursor at the blueprint start ' .. @@ -190,45 +198,49 @@ local function do_one_command(command, cursor, blueprint_name, section_name, end end - local ctx = init_ctx{ + local ctx = init_ctx({ command=command, blueprint_name=blueprint_name, cursor=cursor, aliases=quickfort_list.get_aliases(blueprint_name), quiet=quiet, dry_run=dry_run, - preserve_engravings=preserve_engravings} + preserve_engravings=preserve_engravings}, prev_ctx) do_command_section(ctx, section_name, modifiers) - finish_command(ctx, section_name) - if command == 'run' then - for _,message in ipairs(ctx.messages) do - print('* '..message) - end + if not ctx.quiet then + print(('%s successfully completed'):format( + quickfort_parse.format_command(ctx.command, ctx.blueprint_name, + section_name, ctx.dry_run))) end + return ctx end local function do_bp_name(commands, cursor, bp_name, sec_names, quiet, dry_run, preserve_engravings, modifiers) + local ctx for _,sec_name in ipairs(sec_names) do local mode = quickfort_list.get_blueprint_mode(bp_name, sec_name) for _,command in ipairs(commands) do - do_one_command(command, cursor, bp_name, sec_name, mode, quiet, + ctx = do_one_command(ctx, command, cursor, bp_name, sec_name, mode, quiet, dry_run, preserve_engravings, modifiers) end end + return ctx end local function do_list_num(commands, cursor, list_nums, quiet, dry_run, preserve_engravings, modifiers) + local ctx for _,list_num in ipairs(list_nums) do local bp_name, sec_name, mode = quickfort_list.get_blueprint_by_number(list_num) for _,command in ipairs(commands) do - do_one_command(command, cursor, bp_name, sec_name, mode, quiet, + ctx = do_one_command(ctx, command, cursor, bp_name, sec_name, mode, quiet, dry_run, preserve_engravings, modifiers) end end + return ctx end function do_command(args) @@ -279,14 +291,15 @@ function do_command(args) function() quickfort_common.verbose = false end, function() local ok, list_nums = pcall(argparse.numberList, blueprint_name) + local ctx if not ok then - do_bp_name(args.commands, cursor, blueprint_name, section_names, - quiet, dry_run, preserve_engravings, - modifiers) + ctx = do_bp_name(args.commands, cursor, blueprint_name, section_names, + quiet, dry_run, preserve_engravings, modifiers) else - do_list_num(args.commands, cursor, list_nums, quiet, dry_run, - preserve_engravings, modifiers) + ctx = do_list_num(args.commands, cursor, list_nums, quiet, dry_run, + preserve_engravings, modifiers) end + finish_commands(ctx) end) end diff --git a/internal/quickfort/orders.lua b/internal/quickfort/orders.lua index 8b73fc784a..53adbcae21 100644 --- a/internal/quickfort/orders.lua +++ b/internal/quickfort/orders.lua @@ -128,14 +128,28 @@ local function get_num_items(b) return math.floor(num_tiles/4) + 1 end +local function create_order(ctx, label, order_spec) + local quantity = math.ceil(order_spec.quantity) + log('ordering %d %s', quantity, label) + if not ctx.dry_run and stockflow then + stockflow.create_orders(order_spec.order, quantity) + table.insert(ctx.stats, {label=('Ordered '..label), value=quantity, is_order=true}) + else + table.insert(ctx.stats, {label=('Would order '..label), value=quantity, is_order=true}) + end +end + +-- sort by quantity so workshops that have smaller numbers of allowed general orders +-- can contribute to the larger item orders function create_orders(ctx) - for k,order_spec in pairs(ctx.order_specs or {}) do - local quantity = math.ceil(order_spec.quantity) - log('ordering %d %s', quantity, k) - if not ctx.dry_run and stockflow then - stockflow.create_orders(order_spec.order, quantity) - end - table.insert(ctx.stats, {label=k, value=quantity, is_order=true}) + if not ctx.order_specs then return end + local orders = {} + for label,spec in pairs(ctx.order_specs) do + table.insert(orders, {label=label, spec=spec}) + end + table.sort(orders, function(a,b) return a.spec.quantity > b.spec.quantity end) + for _,order in ipairs(orders) do + create_order(ctx, order.label, order.spec) end end @@ -158,11 +172,11 @@ function enqueue_additional_order(ctx, label) inc_order_spec(order_specs, 1, get_reactions(), label) end -function enqueue_building_orders(buildings, building_db, ctx) +function enqueue_building_orders(buildings, ctx) local order_specs = ensure_order_specs(ctx) local reactions = get_reactions() for _, b in ipairs(buildings) do - local db_entry = building_db[b.type] + local db_entry = b.db_entry log('processing %s, defined from spreadsheet cell(s): %s', db_entry.label, table.concat(b.cells, ', ')) local filters = dfhack.buildings.getFiltersByType( @@ -182,8 +196,8 @@ function enqueue_building_orders(buildings, building_db, ctx) if filter.quantity == -1 then filter.quantity = get_num_items(b) end if filter.flags2 and filter.flags2.building_material then -- rock blocks get produced at a ratio of 4:1 - filter.quantity = filter.quantity or 1 - filter.quantity = filter.quantity / 4 + -- note that this can be a fraction; math.ceil() is used in create_orders to compensate + filter.quantity = (filter.quantity or 1) / 4 end process_filter(order_specs, filter, reactions) end From 1d469acf94f21e745260d05b82773273cd33a764 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 03:27:28 -0700 Subject: [PATCH 277/732] handle buildings that can overlay (i.e. zones) --- internal/quickfort/building.lua | 154 ++++++++++++++++++++++---------- 1 file changed, 106 insertions(+), 48 deletions(-) diff --git a/internal/quickfort/building.lua b/internal/quickfort/building.lua index d88dd30510..d92e0326b4 100644 --- a/internal/quickfort/building.lua +++ b/internal/quickfort/building.lua @@ -66,32 +66,46 @@ end -- populates seen_grid coordinates with the building id so we can build an -- extent_grid later. spreadsheet cells that define extents (e.g. a(5x5)) create -- buildings separate from adjacent cells, even if they have the same type. -local function flood_fill(ctx, grid, x, y, seen_grid, data, db, aliases) - if seen_grid[x] and seen_grid[x][y] then return 0 end - if not grid[y] or not grid[y][x] then return 0 end +local function flood_fill(ctx, grid, seen_cells, x, y, data, db, aliases) + local seen_grid = data.seen_grid + if safe_index(seen_grid, x, y) then return 0 end + if not safe_index(grid, y, x) then return 0 end local cell, text = grid[y][x].cell, grid[y][x].text + if seen_cells[cell] then return 0 end local keys, extent = quickfort_parse.parse_cell(ctx, text) if aliases[string.lower(keys)] then keys = aliases[string.lower(keys)] end local db_entry = db[keys] if not db_entry then - if not seen_grid[x] then seen_grid[x] = {} end - seen_grid[x][y] = true -- seen, but not part of any building - dfhack.printerr(string.format('invalid key sequence in cell %s: "%s"', - cell, text)) + ensure_key(seen_grid, x)[y] = true -- seen, but not part of any building + dfhack.printerr(('invalid key sequence in cell %s: "%s"'):format(cell, text)) return 1 end if db_entry.transform then - keys = db_entry.transform(ctx) + keys = db_entry:transform(ctx) + db_entry = keys and db[keys] or nil + if not db_entry then + ensure_key(seen_grid, x)[y] = true -- seen, but not part of any building + dfhack.printerr(('invalid transformed key sequence in cell %s: "%s"->"%s"'):format(cell, text, keys)) + return 1 + end + end + if data.db_entry and (data.db_entry.label ~= db_entry.label or extent.specified) then + return 0 end - if data.type and (data.type ~= keys or extent.specified) then return 0 end log('mapping spreadsheet cell %s with text "%s"', cell, text) - if not data.type then data.type = keys end + if data.db_entry then + if data.db_entry.merge_fn then data.db_entry:merge_fn(db_entry) end + else + data.db_entry = copyall(db_entry) + end table.insert(data.cells, cell) + seen_cells[cell] = true + -- note that extent width and height can be negative: they can + -- describe a box from any corner for tgt_x=math.min(x,x+extent.width+1),math.max(x+extent.width-1,x) do for tgt_y=math.min(y,y+extent.height+1),math.max(y+extent.height-1,y) do - if not seen_grid[tgt_x] then seen_grid[tgt_x] = {} end -- this may overlap with another building, but that's handled later - seen_grid[tgt_x][tgt_y] = data.id + ensure_key(seen_grid, tgt_x)[tgt_y] = data.id if tgt_x < data.x_min then data.x_min = tgt_x end if tgt_x > data.x_max then data.x_max = tgt_x end if tgt_y < data.y_min then data.y_min = tgt_y end @@ -99,14 +113,14 @@ local function flood_fill(ctx, grid, x, y, seen_grid, data, db, aliases) end end if extent.specified then return 0 end - return flood_fill(ctx, grid, x-1, y-1, seen_grid, data, db, aliases) + - flood_fill(ctx, grid, x-1, y, seen_grid, data, db, aliases) + - flood_fill(ctx, grid, x-1, y+1, seen_grid, data, db, aliases) + - flood_fill(ctx, grid, x, y-1, seen_grid, data, db, aliases) + - flood_fill(ctx, grid, x, y+1, seen_grid, data, db, aliases) + - flood_fill(ctx, grid, x+1, y-1, seen_grid, data, db, aliases) + - flood_fill(ctx, grid, x+1, y, seen_grid, data, db, aliases) + - flood_fill(ctx, grid, x+1, y+1, seen_grid, data, db, aliases) + return flood_fill(ctx, grid, seen_cells, x-1, y-1, data, db, aliases) + + flood_fill(ctx, grid, seen_cells, x-1, y, data, db, aliases) + + flood_fill(ctx, grid, seen_cells, x-1, y+1, data, db, aliases) + + flood_fill(ctx, grid, seen_cells, x, y-1, data, db, aliases) + + flood_fill(ctx, grid, seen_cells, x, y+1, data, db, aliases) + + flood_fill(ctx, grid, seen_cells, x+1, y-1, data, db, aliases) + + flood_fill(ctx, grid, seen_cells, x+1, y, data, db, aliases) + + flood_fill(ctx, grid, seen_cells, x+1, y+1, data, db, aliases) end local function swap_id_and_trim_chunk(chunk, seen_grid, from_id) @@ -114,7 +128,7 @@ local function swap_id_and_trim_chunk(chunk, seen_grid, from_id) local y_min, y_max = chunk.y_max,chunk.y_min for x=chunk.x_min,chunk.x_max do for y=chunk.y_min,chunk.y_max do - if seen_grid[x] and seen_grid[x][y] == from_id then + if safe_index(seen_grid, x, y) == from_id then seen_grid[x][y] = chunk.id x_min, x_max = math.min(x_min, x), math.max(x_max, x) y_min, y_max = math.min(y_min, y), math.max(y_max, y) @@ -130,10 +144,11 @@ end -- (think of a solid block of staggered workshops of the same type). instead, -- scan the edges and break off pieces as we find them. scan in an order that -- results in the same chunking regardless of any applied transformations. -local function chunk_extents(data_tables, seen_grid, db, invert) +local function chunk_extents(data_tables, invert) local chunks = {} for i, data in ipairs(data_tables) do - local db_entry = db[data.type] + local seen_grid = data.seen_grid + local db_entry = data.db_entry local max_width = db_entry.max_width local max_height = db_entry.max_height local width = data.x_max - data.x_min + 1 @@ -154,7 +169,7 @@ local function chunk_extents(data_tables, seen_grid, db, invert) end for x=startx,endx,stepx do for y=starty,endy,stepy do - if not seen_grid[x] or seen_grid[x][y] ~= data.id then + if safe_index(seen_grid, x, y) ~= data.id then goto inner_continue end chunk = copyall(data) @@ -194,9 +209,10 @@ end -- expand multi-tile buildings that are less than their min dimensions around -- their current center. if the blueprint has been transformed, ensures the -- expansion respects the original orientation. -local function expand_buildings(data_tables, seen_grid, db, invert) +local function expand_buildings(data_tables, invert) for _, data in ipairs(data_tables) do - local db_entry = db[data.type] + local seen_grid = data.seen_grid + local db_entry = data.db_entry if db_entry.has_extents then goto continue end local width = data.x_max - data.x_min + 1 local height = data.y_max - data.y_min + 1 @@ -233,7 +249,8 @@ local function expand_buildings(data_tables, seen_grid, db, invert) end end -local function build_extent_grid(seen_grid, data) +local function build_extent_grid(data) + local seen_grid = data.seen_grid local extent_grid, num_tiles = {}, 0 for x=data.x_min,data.x_max do local extent_x = x - data.x_min + 1 @@ -252,46 +269,87 @@ local function build_extent_grid(seen_grid, data) num_tiles == width * height end +local function merge_building_data(to_data, from_data) + to_data.db_entry:merge_fn(from_data.db_entry) + for _,cell in ipairs(from_data.cells) do + table.insert(to_data.cells, cell) + end + to_data.x_min = math.min(to_data.x_min, from_data.x_min) + to_data.x_max = math.max(to_data.x_max, from_data.x_max) + to_data.y_min = math.min(to_data.y_min, from_data.y_min) + to_data.y_max = math.max(to_data.y_max, from_data.y_max) + for x=from_data.x_min,from_data.x_max do + for y=from_data.y_min,from_data.y_max do + if safe_index(from_data.seen_grid, x, y) == from_data.id then + ensure_key(to_data.seen_grid, x)[y] = to_data.id + end + end + end +end + +local function dump_data_tables(desc, data_tables, common_seen_grid, non_occluding) + if non_occluding then + for _,data in ipairs(data_tables) do + logfn(dump_seen_grid, desc, data.seen_grid, #data_tables) + end + else + logfn(dump_seen_grid, desc, common_seen_grid, #data_tables) + end +end + -- build boundaries and extent maps from blueprint grid input -function init_buildings(ctx, zlevel, grid, buildings, db, aliases) +function init_buildings(ctx, zlevel, grid, buildings, db, aliases, non_occluding) local invalid_keys = 0 local data_tables = {} - local seen_grid = {} -- [x][y] -> id + local global_labels = {} -- label -> id + local common_seen_grid = {} -- [x][y] -> id + local seen_cells = {} -- str -> true aliases = aliases or {} for y, row in pairs(grid) do - for x, cell_and_text in pairs(row) do - if seen_grid[x] and seen_grid[x][y] then goto continue end + for x in pairs(row) do + if safe_index(common_seen_grid, x, y) then goto continue end local data = { - id=#data_tables+1, type=nil, cells={}, + id=#data_tables+1, cells={}, db_entry=nil, + seen_grid=non_occluding and {} or common_seen_grid, x_min=30000, x_max=-30000, y_min=30000, y_max=-30000 } invalid_keys = invalid_keys + - flood_fill(ctx, grid, x, y, seen_grid, data, db, aliases) - if data.type then table.insert(data_tables, data) end + flood_fill(ctx, grid, seen_cells, x, y, data, db, aliases) + if data.db_entry then + if data.db_entry.global_label then + local prev_id = global_labels[data.db_entry.global_label] + if prev_id then + merge_building_data(data_tables[prev_id], data) + goto continue + end + global_labels[data.db_entry.global_label] = data.id + end + table.insert(data_tables, data) + end ::continue:: end end - logfn(dump_seen_grid, 'after edge detection', seen_grid, #data_tables) + dump_data_tables('after edge detection', data_tables, common_seen_grid, non_occluding) local tvec = ctx.transform_fn({x=1, y=-2}, true) local invert = { x=tvec.x == -1 or tvec.x == 2, y=tvec.y == -1 or tvec.y == 2 } - data_tables = chunk_extents(data_tables, seen_grid, db, invert) - logfn(dump_seen_grid, 'after chunking', seen_grid, #data_tables) - expand_buildings(data_tables, seen_grid, db, invert) - logfn(dump_seen_grid, 'after expansion', seen_grid, #data_tables) + data_tables = chunk_extents(data_tables, invert) + dump_data_tables('after chunking', data_tables, common_seen_grid, non_occluding) + expand_buildings(data_tables, invert) + dump_data_tables('after expansion', data_tables, common_seen_grid, non_occluding) for _, data in ipairs(data_tables) do - local extent_grid, is_solid = build_extent_grid(seen_grid, data) - if not db[data.type].has_extents and not is_solid then + local extent_grid, is_solid = build_extent_grid(data) + if not data.db_entry.has_extents and not is_solid then dfhack.printerr( ('space needed for "%s" is taken by adjacent structures ' .. '(defined in spreadsheet cells: %s)'):format( - db[data.type].label, table.concat(data.cells, ', '))) + data.db_entry.label, table.concat(data.cells, ', '))) else table.insert(buildings, - {type=data.type, - cells=data.cells, + {cells=data.cells, + db_entry=data.db_entry, pos=xyz2pos(data.x_min, data.y_min, zlevel), width=data.x_max-data.x_min+1, height=data.y_max-data.y_min+1, @@ -369,12 +427,12 @@ end -- check bounds against size limits and map edges, adjust pos, width, height, -- and extent_grid accordingly. nulls or zeroes out config for buildings that -- are cropped below their minimum dimensions -function crop_to_bounds(ctx, buildings, db) +function crop_to_bounds(ctx, buildings) local out_of_bounds_tiles = 0 local bounds = ctx.bounds or quickfort_map.MapBoundsChecker{} for _, b in ipairs(buildings) do if not b.pos then goto continue end - local prev_oob, db_entry = out_of_bounds_tiles, db[b.type] + local prev_oob, db_entry = out_of_bounds_tiles, b.db_entry -- if zlevel is out of bounds, the whole extent is out of bounds if not bounds:is_on_map_z(b.pos.z) then out_of_bounds_tiles = out_of_bounds_tiles + @@ -456,11 +514,11 @@ end -- check tiles for validity, adjust the extent_grid, and checks the validity of -- the adjusted extent_grid. marks building as invalid if the extent_grid is -- invalid. -function check_tiles_and_extents(ctx, buildings, db) +function check_tiles_and_extents(ctx, buildings) local occupied_tiles = 0 for _, b in ipairs(buildings) do if not b.pos then goto continue end - local db_entry = db[b.type] + local db_entry = b.db_entry local owns_preview = false for extent_x=1,b.width do local col = b.extent_grid[extent_x] From cd5924d9ed875cdd0ce3c53ddf77f78a59667b5c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 03:30:37 -0700 Subject: [PATCH 278/732] common parsing logic for new properties syntax --- internal/quickfort/parse.lua | 67 ++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/internal/quickfort/parse.lua b/internal/quickfort/parse.lua index 4b5e5a33e0..1e29010e7f 100644 --- a/internal/quickfort/parse.lua +++ b/internal/quickfort/parse.lua @@ -5,6 +5,7 @@ if not dfhack_flags.module then qerror('this script cannot be called directly') end +local argparse = require('argparse') local utils = require('utils') local quickfort_reader = reqscript('internal/quickfort/reader') local quickfort_transform = reqscript('internal/quickfort/transform') @@ -13,9 +14,7 @@ valid_modes = utils.invert({ 'dig', 'build', 'place', --- 'zone', --- 'query', --- 'config', + 'zone', 'meta', 'notes', 'ignore', @@ -289,8 +288,8 @@ returns nil if the modeline is invalid. local function parse_modeline(modeline, filename, modeline_id) if not modeline then return nil end local _, mode_end, mode = string.find(modeline, '^#([%l]+)') - -- remove this as these modes become supported - if mode == 'zone' or mode == 'query' or mode == 'config' then + -- ignore no-longer-supported blueprint modes + if mode == 'query' or mode == 'config' then mode = 'ignore' end if not mode or not valid_modes[mode] then return nil end @@ -722,6 +721,63 @@ function parse_extended_token(text, startpos) return token, params, repetitions, next_token_pos end +-- parses a blueprint key sequence optionally followed by a label. returns a map of {keys=string, label=string} +-- and the start position of the next token. label is either a string of at least length 1 or nil +function parse_token_and_label(text, startpos, token_pattern) + token_pattern = token_pattern or alias_pattern + local _, endpos, token, label = text:find('^%s*('..token_pattern..')/('..alias_pattern..')%s*', startpos) + if not endpos then + _, endpos, token = text:find('^%s*('..token_pattern..')%s*', startpos) + end + if not endpos then + return nil, startpos + end + return {token=token, label=label}, endpos+1 +end + +-- parses a sequence starting with '{' and ending with the matching '}' that +-- delimit properties that modify a blueprint element. Properties are in the same +-- format as parse_extended_token above. +-- returns params as map, start position of the next token as int +function parse_properties(text, startpos) + local _, endpos, properties = text:find('^%s*(%b{})%s*', startpos) + if not properties then return {}, startpos end + return get_params(properties, 2), endpos+1 +end + +local stockpile_config_spec_pattern = '[%w_]+' + +local function get_next_stockpile_transformation(text, startpos) + local _, e, op, name = text:find('^%s*([%+%-=])%s*('..stockpile_config_spec_pattern..')%s*', startpos) + if not e then return nil, startpos end + local mode = 'set' + if op == '+' then mode = 'enable' + elseif op == '-' then mode = 'disable' + end + local filters + if text:sub(e+1, e+1) == '/' then + local _, filter_end_pos, filter_str = text:find('^([^%+%-=]+)', e+2) + if filter_end_pos then + e = filter_end_pos + filters = argparse.stringList(filter_str) + end + end + return {mode=mode, name=name, filters=filters}, e+1 +end + +-- returns a list of stockpile transformations and the start position of the next token as int +function parse_stockpile_transformations(text, startpos) + local transformations = {} + local _, e = text:find('^%s*:%s*', startpos) + if not e then return transformations, startpos end + local transformation, next_token_start_pos = get_next_stockpile_transformation(text, e+1) + while transformation do + table.insert(transformations, transformation) + transformation, next_token_start_pos = get_next_stockpile_transformation(text, next_token_start_pos) + end + return transformations, next_token_start_pos +end + if dfhack.internal.IN_TEST then unit_test_hooks = { parse_cell=parse_cell, @@ -757,7 +813,6 @@ if dfhack.internal.IN_TEST then parse_alias_separate=parse_alias_separate, parse_alias_combined=parse_alias_combined, get_sheet_metadata=get_sheet_metadata, - make_transform_fn=make_transform_fn, get_extended_token=get_extended_token, get_token=get_token, get_next_param=get_next_param, From 2f6f036ebdb53330797668aa11804520364062f2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 03:31:08 -0700 Subject: [PATCH 279/732] updated build and hauling route logic --- internal/quickfort/build.lua | 264 +++++++++++++++++++++++++++++++---- 1 file changed, 237 insertions(+), 27 deletions(-) diff --git a/internal/quickfort/build.lua b/internal/quickfort/build.lua index 19e43c9ce4..e951b56992 100644 --- a/internal/quickfort/build.lua +++ b/internal/quickfort/build.lua @@ -15,11 +15,15 @@ if not dfhack_flags.module then qerror('this script cannot be called directly') end +local argparse = require('argparse') local utils = require('utils') local quickfort_common = reqscript('internal/quickfort/common') local quickfort_building = reqscript('internal/quickfort/building') local quickfort_orders = reqscript('internal/quickfort/orders') +local quickfort_parse = reqscript('internal/quickfort/parse') +local quickfort_place = reqscript('internal/quickfort/place') local quickfort_transform = reqscript('internal/quickfort/transform') +local stockpiles = require('plugins.stockpiles') local ok, buildingplan = pcall(require, 'plugins.buildingplan') if not ok then @@ -199,14 +203,121 @@ end -- ************ the database ************ -- -- +local function do_hive_props(db_entry, props) + if props.do_install == 'true' then + ensure_key(db_entry.props, 'hive_flags').do_install = true + props.do_install = nil + end + if props.do_gather == 'true' then + ensure_key(db_entry.props, 'hive_flags').do_gather = true + props.do_gather = nil + end +end + +local function do_farm_props(db_entry, props) + if props.seasonal_fertilize == 'true' then + ensure_key(db_entry.props, 'farm_flags').seasonal_fertilize = true + props.seasonal_fertilize = nil + end +end + +local function do_workshop_furnace_props(db_entry, props) + if props.take_from then + db_entry.links.take_from = argparse.stringList(props.take_from) + props.take_from = nil + end + if props.give_to then + db_entry.links.give_to = argparse.stringList(props.give_to) + props.give_to = nil + end +end + +local function do_trackstop_props(db_entry, props) + if props.take_from then + ensure_key(db_entry, 'route').from_names = argparse.stringList(props.take_from) + props.take_from = nil + end + if props.route then + ensure_key(db_entry, 'route').name = props.route + props.route = nil + end +end + +local hauling = df.global.plotinfo.hauling + +-- adds a new stop to the named route, or creates a new stop in a new route +-- if name is not given or a route with that name is not found +-- returns the stop +local function add_stop(name, pos, adjustments) + local route + if name then + for _,r in ipairs(hauling.routes) do + if string.lower(r.name) == name:lower() then + route = r + break + end + end + end + if not route then + hauling.routes:insert('#', { + new=df.hauling_route, + id=hauling.next_id, + name=name, + }) + hauling.next_id = hauling.next_id + 1 + route = hauling.routes[#hauling.routes-1] + end + local stop_id = 1 + if #route.stops > 0 then + stop_id = route.stops[#route.stops-1].id + 1 + end + route.stops:insert('#', { + new=df.hauling_stop, + id=stop_id, + pos=pos, + }) + stockpiles.import_route('library/everything', route.id, stop_id, 'set') + for _, adj in ipairs(adjustments) do + log('applying stockpile preset: %s %s', adj.mode, adj.name) + stockpiles.import_route(adj.name, route.id, stop_id, adj.mode, adj.filters) + end + return route.stops[#route.stops-1] +end + +local function do_trackstop_adjust(db_entry, bld) + if not db_entry.route then return end + local stop = add_stop(db_entry.route.name, + xyz2pos(bld.centerx, bld.centery, bld.z), db_entry.adjustments) + if db_entry.route.from_names then + local from_names = {} + for _,from_name in ipairs(db_entry.route.from_names) do + from_names[from_name:lower()] = true + end + for _, pile in ipairs(df.global.world.buildings.other.STOCKPILE) do + local name = string.lower(pile.name) + if from_names[name] then + stop.stockpiles:insert('#', { + new=df.route_stockpile_link, + building_id=pile.id, + mode={take=true}, + }) + pile.linked_stops:insert('#', stop) + end + end + end +end + local unit_vectors = quickfort_transform.unit_vectors local unit_vectors_revmap = quickfort_transform.unit_vectors_revmap local function make_transform_building_fn(vector, revmap, post_fn) - return function(ctx) + return function(db_entry, ctx) local keys = quickfort_transform.resolve_transformed_vector( ctx, vector, revmap) if post_fn then keys = post_fn(keys) end + if db_entry.transform_suffix then + keys = keys .. db_entry.transform_suffix + end return keys end end @@ -239,9 +350,9 @@ local function make_bridge_entry(direction) type=df.building_type.Bridge, direction=direction, min_width=1, - max_width=10, + max_width=31, min_height=1, - max_height=10, + max_height=31, is_valid_tile_fn=is_valid_tile_bridge, transform=transform} end @@ -323,7 +434,7 @@ local function make_water_wheel_entry(vertical) end local function make_horizontal_axle_entry(vertical) return make_ns_ew_entry('Horizontal Axle', df.building_type.AxleHorizontal, - 1, 10, horizontal_axle_revmap, vertical) + 1, 31, horizontal_axle_revmap, vertical) end local roller_data = { @@ -359,8 +470,8 @@ local function make_roller_entry(direction, speed) return { label=('Rollers (%s)'):format(roller_data_entry.label), type=df.building_type.Rollers, - min_width=1, max_width=roller_data_entry.vertical and 1 or 10, - min_height=1, max_height=roller_data_entry.vertical and 10 or 1, + min_width=1, max_width=roller_data_entry.vertical and 1 or 31, + min_height=1, max_height=roller_data_entry.vertical and 31 or 1, direction=direction, fields={speed=speed}, is_valid_tile_fn=is_valid_tile_machine, @@ -411,7 +522,9 @@ local function make_trackstop_entry(direction, friction) subtype=df.trap_type.TrackStop, fields=fields, transform=transform, - additional_orders={'wooden minecart'} + additional_orders={'wooden minecart'}, + props_fn=do_trackstop_props, + adjust_fn=do_trackstop_adjust, } end @@ -573,7 +686,7 @@ local building_db = { R={label='Traction Bench', type=df.building_type.TractionBench, additional_orders={'table', 'mechanisms', 'cloth rope'}}, N={label='Nest Box', type=df.building_type.NestBox}, - ['{Alt}h']={label='Hive', type=df.building_type.Hive}, + ['{Alt}h']={label='Hive', type=df.building_type.Hive, props_fn=do_hive_props}, ['{Alt}a']={label='Offering Place', type=df.building_type.OfferingPlace}, ['{Alt}c']={label='Bookcase', type=df.building_type.Bookcase}, F={label='Display Furniture', type=df.building_type.DisplayFurniture}, @@ -589,7 +702,8 @@ local building_db = { type=df.building_type.FarmPlot, has_extents=true, no_extents_if_solid=true, is_valid_tile_fn=is_valid_tile_dirt, - is_valid_extent_fn=is_extent_nonempty}, + is_valid_extent_fn=is_extent_nonempty, + props_fn=do_farm_props}, o={label='Paved Road', type=df.building_type.RoadPaved, has_extents=true, no_extents_if_solid=true, is_valid_extent_fn=is_extent_nonempty}, @@ -723,23 +837,18 @@ local building_db = { CSddddaaaa=make_trackstop_entry({dump_x_shift=-1}, 10), Ts={label='Stone-Fall Trap', type=df.building_type.Trap, subtype=df.trap_type.StoneFallTrap}, - -- TODO: by default a weapon trap is configured with a single weapon. - -- maybe add Tw1 through Tw10 for choosing how many weapons? - -- material preferences can help here for choosing weapon types. - Tw={label='Weapon Trap', + Tw={label='Weapon Trap', props_fn=do_weapon_trap_props, type=df.building_type.Trap, subtype=df.trap_type.WeaponTrap}, Tl={label='Lever', type=df.building_type.Trap, subtype=df.trap_type.Lever, additional_orders={'mechanisms', 'mechanisms'}}, - -- TODO: lots of configuration here with no natural order. may need - -- special-case logic when we read the keys. - Tp={label='Pressure Plate', + Tp={label='Pressure Plate', props_fn=do_pressure_plate_props, type=df.building_type.Trap, subtype=df.trap_type.PressurePlate}, Tc={label='Cage Trap', type=df.building_type.Trap, subtype=df.trap_type.CageTrap, additional_orders={'wooden cage'}}, - -- TODO: Same as weapon trap above - TS={label='Upright Spear/Spike', type=df.building_type.Weapon}, + TS={label='Upright Spear/Spike', type=df.building_type.Weapon, + props_fn=do_weapon_trap_props}, -- tracks (CT...). there aren't any shortcut keys in the UI so we use the -- aliases from python quickfort trackN=make_track_entry('N', track_end_data, track_end_revmap, false), @@ -790,15 +899,43 @@ local building_db = { trackrampNSEW=make_track_entry('NSEW', nil, nil, true) } +local function ensure_data(db_entry) + if not db_entry.props then + db_entry.props = {} + end + if not db_entry.links then + db_entry.links = {give_to={}, take_from={}} + end + if not db_entry.adjustments then + db_entry.adjustments = {} + end +end + +local function merge_db_entries(self, other) + if self.label ~= other.label then + error(('cannot merge db entries of different types: %s != %s'):format(self.label, other.label)) + end + ensure_data(self) + ensure_data(other) + utils.assign(self.props, other.props) + for _,adj in ipairs(other.adjustments) do + table.insert(self.adjustments, adj) + end +end + -- fill in default values if they're not already specified for _, v in pairs(building_db) do + v.merge_fn = merge_db_entries if v.has_extents then if not v.min_width then - v.min_width, v.max_width, v.min_height, v.max_height = 1, 10, 1, 10 + v.min_width, v.max_width, v.min_height, v.max_height = 1, 31, 1, 31 end elseif v.type == df.building_type.Workshop or v.type == df.building_type.Furnace or v.type == df.building_type.SiegeEngine then + if not v.props_fn and v.type ~= df.building_type.SiegeEngine then + v.props_fn = do_workshop_furnace_props + end if not v.min_width then v.min_width, v.max_width, v.min_height, v.max_height = 3, 3, 3, 3 end @@ -917,12 +1054,49 @@ local building_aliases = { ['~s']='{Alt}s', } +local build_key_pattern = '~?%w+' + +local function custom_building(_, keys) + local token_and_label, props_start_pos = quickfort_parse.parse_token_and_label(keys, 1, build_key_pattern) + -- properties and adjustments may hide the alias from the building.init_buildings algorithm + -- so we might have to do our own mapping here + local resolved_alias = building_aliases[token_and_label.token:lower()] + local db_entry = rawget(building_db, resolved_alias or token_and_label.token) + if not db_entry then + return nil + end + db_entry = copyall(db_entry) + db_entry.transform_suffix = keys:sub(props_start_pos) + if token_and_label.label then + db_entry.label = ('%s/%s'):format(db_entry.label, token_and_label.label) + db_entry.transform_suffix = ('/%s%s'):format(token_and_label.label, db_entry.transform_suffix) + end + ensure_data(db_entry) + local props, next_token_pos = quickfort_parse.parse_properties(keys, props_start_pos) + if props.name then + db_entry.props.name = props.name + props.name = nil + end + if db_entry.props_fn then db_entry:props_fn(props) end + for k,v in pairs(props) do + dfhack.printerr(('unhandled property: "%s"="%s"'):format(k, v)) + end + + local adjustments = quickfort_parse.parse_stockpile_transformations(keys, next_token_pos) + if adjustments then + db_entry.adjustments = adjustments + end + return db_entry +end + +setmetatable(building_db, {__index=custom_building}) + -- -- ************ command logic functions ************ -- -- -local function create_building(b, dry_run) - local db_entry = building_db[b.type] +local function create_building(b, cache, dry_run) + local db_entry = b.db_entry log('creating %dx%d %s at map coordinates (%d, %d, %d), defined from ' .. 'spreadsheet cells: %s', b.width, b.height, db_entry.label, b.pos.x, b.pos.y, b.pos.z, @@ -945,6 +1119,33 @@ local function create_building(b, dry_run) -- is supposed to prevent this from ever happening error(string.format('unable to place %s: %s', db_entry.label, err)) end + ensure_data(db_entry) + utils.assign(bld, db_entry.props) + if db_entry.adjust_fn then + db_entry:adjust_fn(bld) + end + for _,recipient in ipairs(db_entry.links.give_to) do + cache.piles = cache.piles or quickfort_place.get_stockpiles_by_name() + if cache.piles[recipient] then + for _,to in ipairs(cache.piles[recipient]) do + utils.insert_sorted(bld.profile.links.give_to_pile, to, 'id') + utils.insert_sorted(to.links.take_from_workshop, bld, 'id') + end + else + dfhack.printerr(('cannot find stockpile named "%s" to give to'):format(recipient)) + end + end + for _,supplier in ipairs(db_entry.links.take_from) do + cache.piles = cache.piles or quickfort_place.get_stockpiles_by_name() + if cache.piles[supplier] then + for _,from in ipairs(cache.piles[supplier]) do + utils.insert_sorted(bld.profile.links.take_from_pile, from, 'id') + utils.insert_sorted(from.links.give_to_workshop, bld, 'id') + end + else + dfhack.printerr(('cannot find stockpile named "%s" to take from'):format(supplier)) + end + end if buildingplan and buildingplan.isEnabled() and buildingplan.isPlannableBuilding( db_entry.type, db_entry.subtype or -1, @@ -982,9 +1183,10 @@ function do_run(zlevel, grid, ctx) quickfort_building.check_tiles_and_extents( ctx, buildings, building_db) + local cache = {} for _, b in ipairs(buildings) do if b.pos then - create_building(b, ctx.dry_run) + create_building(b, cache, ctx.dry_run) stats.build_designated.value = stats.build_designated.value + 1 end end @@ -999,7 +1201,7 @@ function do_orders(zlevel, grid, ctx) stats.invalid_keys.value = stats.invalid_keys.value + quickfort_building.init_buildings( ctx, zlevel, grid, buildings, building_db, building_aliases) - quickfort_orders.enqueue_building_orders(buildings, building_db, ctx) + quickfort_orders.enqueue_building_orders(buildings, ctx) end local function is_queued_for_destruction(bld) @@ -1023,6 +1225,9 @@ function do_undo(zlevel, grid, ctx) stats.invalid_keys.value + quickfort_building.init_buildings( ctx, zlevel, grid, buildings, building_db, building_aliases) + -- ensure we don't delete the currently selected building, which causes crashes. + local selected_bld = dfhack.gui.getSelectedBuilding(true) + for _, s in ipairs(buildings) do for extent_x, col in ipairs(s.extent_grid) do for extent_y, in_extent in ipairs(col) do @@ -1040,11 +1245,16 @@ function do_undo(zlevel, grid, ctx) stats.build_marked.value = stats.build_marked.value + 1 end - elseif dfhack.buildings.deconstruct(bld) then - stats.build_undesignated.value = - stats.build_undesignated.value + 1 else - stats.build_marked.value = stats.build_marked.value + 1 + if bld == selected_bld then + dfhack.printerr('cannot remove actively selected building.') + dfhack.printerr('please deselect the building and try again.') + elseif dfhack.buildings.deconstruct(bld) then + stats.build_undesignated.value = + stats.build_undesignated.value + 1 + else + stats.build_marked.value = stats.build_marked.value + 1 + end end end ::continue:: From 658e84df64adc462bfedc8866a13579d8efbb08a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 03:31:41 -0700 Subject: [PATCH 280/732] updated stockpile property and configuration logic --- internal/quickfort/place.lua | 318 +++++++++++++++++++++++++++++------ 1 file changed, 263 insertions(+), 55 deletions(-) diff --git a/internal/quickfort/place.lua b/internal/quickfort/place.lua index 1af2ef1f2e..2eff9e5df3 100644 --- a/internal/quickfort/place.lua +++ b/internal/quickfort/place.lua @@ -15,11 +15,13 @@ if not dfhack_flags.module then end require('dfhack.buildings') -- loads additional functions into dfhack.buildings +local argparse = require('argparse') local utils = require('utils') local stockpiles = require('plugins.stockpiles') local quickfort_common = reqscript('internal/quickfort/common') local quickfort_building = reqscript('internal/quickfort/building') local quickfort_orders = reqscript('internal/quickfort/orders') +local quickfort_parse = reqscript('internal/quickfort/parse') local quickfort_set = reqscript('internal/quickfort/set') local log = quickfort_common.log @@ -49,10 +51,40 @@ local function is_valid_stockpile_extent(s) return false end +local function ensure_data(db_entry) + if not db_entry.links then + db_entry.links = {give_to={}, take_from={}} + end + if not db_entry.props then + db_entry.props = {} + end + if not db_entry.adjustments then + db_entry.adjustments = {} + end +end + +local function merge_db_entries(self, other) + if self.label ~= other.label then + error(('cannot merge db entries of different types: %s != %s'):format(self.label, other.label)) + end + ensure_data(self) + utils.assign(self.props, other.props or {}) + for adj in pairs(other.adjustments or {}) do + self.adjustments[adj] = true + end + for _, to in ipairs(other.links and other.links.give_to or {}) do + table.insert(self.links.give_to, to) + end + for _, from in ipairs(other.links and other.links.take_from or {}) do + table.insert(self.links.take_from, from) + end +end + local stockpile_template = { has_extents=true, min_width=1, max_width=math.huge, min_height=1, max_height=math.huge, is_valid_tile_fn = is_valid_stockpile_tile, - is_valid_extent_fn = is_valid_stockpile_extent + is_valid_extent_fn = is_valid_stockpile_extent, + merge_fn = merge_db_entries, } local stockpile_db = { @@ -65,7 +97,7 @@ local stockpile_db = { s={label='Stone', categories={'stone'}, want_wheelbarrows=true}, w={label='Wood', categories={'wood'}}, e={label='Gem', categories={'gems'}, want_bins=true}, - b={label='Bar/Block', categories={'bars_blocks'}, want_bins=true}, + b={label='Bars and Blocks', categories={'bars_blocks'}, want_bins=true}, h={label='Cloth', categories={'cloth'}, want_bins=true}, l={label='Leather', categories={'leather'}, want_bins=true}, z={label='Ammo', categories={'ammo'}, want_bins=true}, @@ -77,11 +109,20 @@ local stockpile_db = { } for _, v in pairs(stockpile_db) do utils.assign(v, stockpile_template) end +local place_key_pattern = '%w+' + +local function parse_keys(keys) + local token_and_label, props_start_pos = quickfort_parse.parse_token_and_label(keys, 1, place_key_pattern) + local props, next_token_pos = quickfort_parse.parse_properties(keys, props_start_pos) + local adjustments = quickfort_parse.parse_stockpile_transformations(keys, next_token_pos) + return token_and_label, props, adjustments +end + local function add_resource_digit(cur_val, digit) return (cur_val * 10) + digit end -local function custom_stockpile(_, keys) +local function make_db_entry(keys) local labels, categories = {}, {} local want_bins, want_barrels, want_wheelbarrows = false, false, false local num_bins, num_barrels, num_wheelbarrows = nil, nil, nil @@ -89,11 +130,11 @@ local function custom_stockpile(_, keys) for k in keys:gmatch('.') do local digit = tonumber(k) if digit and prev_key then - local db_entry = rawget(stockpile_db, prev_key) - if db_entry.want_bins then + local raw_db_entry = rawget(stockpile_db, prev_key) + if raw_db_entry.want_bins then if not in_digits then num_bins = 0 end num_bins = add_resource_digit(num_bins, digit) - elseif db_entry.want_barrels then + elseif raw_db_entry.want_barrels then if not in_digits then num_barrels = 0 end num_barrels = add_resource_digit(num_barrels, digit) else @@ -116,7 +157,7 @@ local function custom_stockpile(_, keys) in_digits = false ::continue:: end - local stockpile_data = { + local db_entry = { label=table.concat(labels, '+'), categories=categories, want_bins=want_bins, @@ -126,8 +167,78 @@ local function custom_stockpile(_, keys) num_barrels=num_barrels, num_wheelbarrows=num_wheelbarrows } - utils.assign(stockpile_data, stockpile_template) - return stockpile_data + utils.assign(db_entry, stockpile_template) + return db_entry +end + +local function custom_stockpile(_, keys) + local token_and_label, props, adjustments = parse_keys(keys) + local db_entry = make_db_entry(token_and_label.token) + if not db_entry then return nil end + if token_and_label.label then + db_entry.label = ('%s/%s'):format(db_entry.label, token_and_label.label) + end + ensure_data(db_entry) + if next(adjustments) then + db_entry.adjustments[adjustments] = true + end + + -- convert from older parsing style to properties + db_entry.props.max_barrels = db_entry.num_barrels + db_entry.num_barrels = nil + db_entry.props.max_bins = db_entry.num_bins + db_entry.num_bins = nil + db_entry.props.max_wheelbarrows = db_entry.num_wheelbarrows + db_entry.num_wheelbarrows = nil + + -- alias properties + if props.quantum == 'true' then + props.links_only = 'true' + props.containers = 0 + props.quantum = nil + end + if props.containers then + props.barrels = props.containers + props.bins = props.containers + props.wheelbarrows = props.containers + props.containers = nil + end + + -- actual properties + if props.barrels then + db_entry.props.max_barrels = tonumber(props.barrels) + props.barrels = nil + end + if props.bins then + db_entry.props.max_bins = tonumber(props.bins) + props.bins = nil + end + if props.wheelbarrows then + db_entry.props.max_wheelbarrows = tonumber(props.wheelbarrows) + props.wheelbarrows = nil + end + if props.links_only == 'true' then + db_entry.props.use_links_only = 1 + props.links_only = nil + end + if props.name then + db_entry.props.name = props.name + props.name = nil + end + if props.take_from then + db_entry.links.take_from = argparse.stringList(props.take_from) + props.take_from = nil + end + if props.give_to then + db_entry.links.give_to = argparse.stringList(props.give_to) + props.give_to = nil + end + + for k,v in pairs(props) do + dfhack.printerr(('unhandled property: "%s"="%s"'):format(k, v)) + end + + return db_entry end setmetatable(stockpile_db, {__index=custom_stockpile}) @@ -135,53 +246,48 @@ setmetatable(stockpile_db, {__index=custom_stockpile}) local function configure_stockpile(bld, db_entry) for _,cat in ipairs(db_entry.categories) do local name = ('library/cat_%s'):format(cat) - stockpiles.import_stockpile(name, {id=bld.id, mode='enable', filters={}}) + log('enabling stockpile category: %s', cat) + stockpiles.import_stockpile(name, {id=bld.id, mode='enable'}) + end + for adjlist in pairs(db_entry.adjustments) do + for _,adj in ipairs(adjlist) do + log('applying stockpile preset: %s %s (filters=)', adj.mode, adj.name, table.concat(adj.filters or {}, ',')) + stockpiles.import_stockpile(adj.name, {id=bld.id, mode=adj.mode, filters=adj.filters}) + end end end -local function init_containers(db_entry, ntiles, fields) - if db_entry.want_barrels then - local max_barrels = db_entry.num_barrels or +local function init_containers(db_entry, ntiles) + if db_entry.want_barrels or db_entry.props.max_barrels then + local max_barrels = db_entry.props.max_barrels or quickfort_set.get_setting('stockpiles_max_barrels') - if max_barrels < 0 or max_barrels >= ntiles then - fields.max_barrels = ntiles - else - fields.max_barrels = max_barrels - end - log('barrels set to %d', fields.max_barrels) + db_entry.props.max_barrels = (max_barrels < 0 or max_barrels >= ntiles) and ntiles or max_barrels + log('barrels set to %d', db_entry.props.max_barrels) end - if db_entry.want_bins then - local max_bins = db_entry.num_bins or + if db_entry.want_bins or db_entry.props.max_bins then + local max_bins = db_entry.props.max_bins or quickfort_set.get_setting('stockpiles_max_bins') - if max_bins < 0 or max_bins >= ntiles then - fields.max_bins = ntiles - else - fields.max_bins = max_bins - end - log('bins set to %d', fields.max_bins) + db_entry.props.max_bins = (max_bins < 0 or max_bins >= ntiles) and ntiles or max_bins + log('bins set to %d', db_entry.props.max_bins) end - if db_entry.want_wheelbarrows or db_entry.num_wheelbarrows then - local max_wb = db_entry.num_wheelbarrows or + if db_entry.want_wheelbarrows or db_entry.props.max_wheelbarrows then + local max_wb = db_entry.props.max_wheelbarrows or quickfort_set.get_setting('stockpiles_max_wheelbarrows') if max_wb < 0 then max_wb = 1 end - if max_wb >= ntiles - 1 then - fields.max_wheelbarrows = ntiles - 1 - else - fields.max_wheelbarrows = max_wb - end - log('wheelbarrows set to %d', fields.max_wheelbarrows) + db_entry.props.max_wheelbarrows = (max_wb >= ntiles - 1) and ntiles-1 or max_wb + log('wheelbarrows set to %d', db_entry.props.max_wheelbarrows) end end -local function create_stockpile(s, dry_run) - local db_entry = stockpile_db[s.type] +local function create_stockpile(s, link_data, dry_run) + local db_entry = s.db_entry log('creating %s stockpile at map coordinates (%d, %d, %d), defined from' .. ' spreadsheet cells: %s', db_entry.label, s.pos.x, s.pos.y, s.pos.z, table.concat(s.cells, ', ')) local extents, ntiles = quickfort_building.make_extents(s, dry_run) local fields = {room={x=s.pos.x, y=s.pos.y, width=s.width, height=s.height, extents=extents}} - init_containers(db_entry, ntiles, fields) + init_containers(db_entry, ntiles) if dry_run then return ntiles end local bld, err = dfhack.buildings.constructBuilding{ type=df.building_type.Stockpile, abstract=true, pos=s.pos, @@ -191,10 +297,101 @@ local function create_stockpile(s, dry_run) -- is supposed to prevent this from ever happening error(string.format('unable to place stockpile: %s', err)) end + utils.assign(bld, db_entry.props) configure_stockpile(bld, db_entry) + if db_entry.props.name then + table.insert(ensure_key(link_data.piles, db_entry.props.name), bld) + end + for _,recipient in ipairs(db_entry.links.give_to) do + log('giving to: "%s"', recipient) + table.insert(link_data.nodes, {from=bld, to=recipient}) + end + for _,supplier in ipairs(db_entry.links.take_from) do + log('taking from: "%s"', supplier) + table.insert(link_data.nodes, {from=supplier, to=bld}) + end return ntiles end +function get_stockpiles_by_name() + local piles = {} + for _, pile in ipairs(df.global.world.buildings.other.STOCKPILE) do + if #pile.name > 0 then + table.insert(ensure_key(piles, pile.name), pile) + end + end + return piles +end + +local function get_workshops_by_name() + local shops = {} + for _, shop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do + if #shop.name > 0 then + table.insert(ensure_key(shops, shop.name), shop) + end + end + return shops +end + +local function get_pile_targets(name, peer_piles, all_piles) + if peer_piles[name] then return peer_piles[name], all_piles end + all_piles = all_piles or get_stockpiles_by_name() + return all_piles[name], all_piles +end + +local function get_shop_targets(name, all_shops) + all_shops = all_shops or get_workshops_by_name() + return all_shops[name], all_shops +end + +-- will link to stockpiles created in this blueprint +-- if no match, will search all stockpiles +-- if no match, will search all workshops +local function link_stockpiles(link_data) + local all_piles, all_shops + for _,node in ipairs(link_data.nodes) do + if type(node.from) == 'string' then + local name = node.from + node.from, all_piles = get_pile_targets(name, link_data.piles, all_piles) + if node.from then + for _,from in ipairs(node.from) do + utils.insert_sorted(from.links.give_to_pile, node.to, 'id') + utils.insert_sorted(node.to.links.take_from_pile, from, 'id') + end + else + node.from, all_shops = get_shop_targets(name, all_shops) + if node.from then + for _,from in ipairs(node.from) do + utils.insert_sorted(from.profile.links.give_to_pile, node.to, 'id') + utils.insert_sorted(node.to.links.take_from_workshop, from, 'id') + end + else + dfhack.printerr(('cannot find stockpile or workshop named "%s" to take from'):format(name)) + end + end + elseif type(node.to) == 'string' then + local name = node.to + node.to, all_piles = get_pile_targets(name, link_data.piles, all_piles) + if node.to then + for _,to in ipairs(node.to) do + utils.insert_sorted(node.from.links.give_to_pile, to, 'id') + utils.insert_sorted(to.links.take_from_pile, node.from, 'id') + end + else + node.to, all_shops = get_shop_targets(name, all_shops) + if node.to then + for _,to in ipairs(node.to) do + utils.insert_sorted(node.from.links.give_to_workshop, to, 'id') + utils.insert_sorted(to.profile.links.take_from_pile, node.from, 'id') + end + else + dfhack.printerr(('cannot find stockpile or workshop named "%s" to give to'):format(name)) + end + end + end + end +end + function do_run(zlevel, grid, ctx) local stats = ctx.stats stats.place_designated = stats.place_designated or @@ -204,39 +401,42 @@ function do_run(zlevel, grid, ctx) stats.place_occupied = stats.place_occupied or {label='Stockpile tiles skipped (tile occupied)', value=0} - local stockpiles = {} + local piles = {} stats.invalid_keys.value = stats.invalid_keys.value + quickfort_building.init_buildings( - ctx, zlevel, grid, stockpiles, stockpile_db) + ctx, zlevel, grid, piles, stockpile_db) stats.out_of_bounds.value = stats.out_of_bounds.value + quickfort_building.crop_to_bounds( - ctx, stockpiles, stockpile_db) + ctx, piles, stockpile_db) stats.place_occupied.value = stats.place_occupied.value + quickfort_building.check_tiles_and_extents( - ctx, stockpiles, stockpile_db) + ctx, piles, stockpile_db) local dry_run = ctx.dry_run - for _, s in ipairs(stockpiles) do + local link_data = {piles={}, nodes={}} + for _, s in ipairs(piles) do if s.pos then - local ntiles = create_stockpile(s, dry_run) + ensure_data(s.db_entry) + local ntiles = create_stockpile(s, link_data, dry_run) stats.place_tiles.value = stats.place_tiles.value + ntiles stats.place_designated.value = stats.place_designated.value + 1 end end if dry_run then return end + link_stockpiles(link_data) dfhack.job.checkBuildingsNow() end -- enqueues orders only for explicitly requested containers function do_orders(zlevel, grid, ctx) - local stockpiles = {} - quickfort_building.init_buildings( - ctx, zlevel, grid, stockpiles, stockpile_db) - for _, s in ipairs(stockpiles) do - local db_entry = stockpile_db[s.type] + local piles = {} + quickfort_building.init_buildings(ctx, zlevel, grid, piles, stockpile_db) + for _, s in ipairs(piles) do + local db_entry = s.db_entry + ensure_data(db_entry) quickfort_orders.enqueue_container_orders(ctx, - db_entry.num_bins, db_entry.num_barrels, db_entry.num_wheelbarrows) + db_entry.props.max_bins, db_entry.props.max_barrels, db_entry.props.max_wheelbarrows) end end @@ -245,21 +445,29 @@ function do_undo(zlevel, grid, ctx) stats.place_removed = stats.place_removed or {label='Stockpiles removed', value=0, always=true} - local stockpiles = {} + local piles = {} stats.invalid_keys.value = stats.invalid_keys.value + quickfort_building.init_buildings( - ctx, zlevel, grid, stockpiles, stockpile_db) + ctx, zlevel, grid, piles, stockpile_db) + + -- ensure we don't delete the currently selected stockpile, which causes crashes. + local selected_pile = dfhack.gui.getSelectedStockpile(true) - for _, s in ipairs(stockpiles) do + for _, s in ipairs(piles) do for extent_x, col in ipairs(s.extent_grid) do for extent_y, in_extent in ipairs(col) do - if not s.extent_grid[extent_x][extent_y] then goto continue end + if not in_extent then goto continue end local pos = xyz2pos(s.pos.x+extent_x-1, s.pos.y+extent_y-1, s.pos.z) local bld = dfhack.buildings.findAtTile(pos) if bld and bld:getType() == df.building_type.Stockpile then if not ctx.dry_run then - dfhack.buildings.deconstruct(bld) + if bld == selected_pile then + dfhack.printerr('cannot remove actively selected stockpile.') + dfhack.printerr('please deselect the stockpile and try again.') + else + dfhack.buildings.deconstruct(bld) + end end stats.place_removed.value = stats.place_removed.value + 1 end From 1fffb8773488c9ca31f65945c84804fa8a2adf1e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 03:32:08 -0700 Subject: [PATCH 281/732] updated zone and location logic --- internal/quickfort/zone.lua | 572 +++++++++++++++++++++++++----------- 1 file changed, 405 insertions(+), 167 deletions(-) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 3fe6172972..3dc7ab8f94 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -9,71 +9,136 @@ require('dfhack.buildings') -- loads additional functions into dfhack.buildings local utils = require('utils') local quickfort_common = reqscript('internal/quickfort/common') local quickfort_building = reqscript('internal/quickfort/building') -local quickfort_map = reqscript('internal/quickfort/map') local quickfort_parse = reqscript('internal/quickfort/parse') local log = quickfort_common.log local logfn = quickfort_common.logfn +local function parse_pit_pond_props(zone_data, props) + if props.pond == 'true' then + ensure_key(zone_data, 'zone_settings').pit_pond = df.building_civzonest.T_zone_settings.T_pit_pond.top_of_pond + props.pond = nil + end +end + +local function parse_gather_props(zone_data, props) + if props.pick_trees == 'false' then + ensure_keys(zone_data, 'zone_settings', 'gather').pick_trees = false + props.pick_trees = nil + end + if props.pick_shrubs == 'false' then + ensure_keys(zone_data, 'zone_settings', 'gather').pick_shrubs = false + props.pick_shrubs = nil + end + if props.gather_fallen == 'false' then + ensure_keys(zone_data, 'zone_settings', 'gather').gather_fallen = false + props.gather_fallen = nil + end +end + +local function parse_archery_props(zone_data, props) + if not props.shoot_from then return end + local archery = ensure_keys(zone_data, 'zone_settings', 'archery') + if props.shoot_from == 'west' or props.shoot_from == 'left' then + archery.dir_x = 1 + archery.dir_y = 0 + props.shoot_from = nil + elseif props.shoot_from == 'east' or props.shoot_from == 'right' then + archery.dir_x = -1 + archery.dir_y = 0 + props.shoot_from = nil + elseif props.shoot_from == 'north' or props.shoot_from == 'top' then + archery.dir_x = 0 + archery.dir_y = 1 + props.shoot_from = nil + elseif props.shoot_from == 'south' or props.shoot_from == 'bottom' then + archery.dir_x = 0 + archery.dir_y = -1 + props.shoot_from = nil + end +end + +local function parse_tomb_props(zone_data, props) + if props.pets == 'true' then + ensure_keys(zone_data, 'zone_settings', 'tomb').no_pets = false + props.pets = nil + end + if props.citizens == 'false' then + ensure_keys(zone_data, 'zone_settings', 'tomb').no_citizens = true + props.citizens = nil + end +end + local function is_valid_zone_tile(pos) return not dfhack.maps.getTileFlags(pos).hidden end local function is_valid_zone_extent(s) - for extent_x, col in ipairs(s.extent_grid) do - for extent_y, in_extent in ipairs(col) do + for _, col in ipairs(s.extent_grid) do + for _, in_extent in ipairs(col) do if in_extent then return true end end end return false end +local function ensure_data(db_entry) + if not db_entry.data then + db_entry.data = {{}} + utils.assign(db_entry.data[1], db_entry.default_data) + end +end + +local function merge_db_entries(self, other) + if self.label ~= other.label then + error(('cannot merge db entries of different types: %s != %s'):format(self.label, other.label)) + end + ensure_data(self) + ensure_data(other) + for i=1,#self.data do + utils.assign(self.data[i], other.data[i]) + end +end + local zone_template = { - has_extents=true, min_width=1, max_width=31, min_height=1, max_height=31, - is_valid_tile_fn = is_valid_zone_tile, - is_valid_extent_fn = is_valid_zone_extent + has_extents=true, min_width=1, max_width=math.huge, min_height=1, max_height=math.huge, + is_valid_tile_fn=is_valid_zone_tile, + is_valid_extent_fn=is_valid_zone_extent, + merge_fn=merge_db_entries, } local zone_db = { - a={label='Inactive', zone_flags={active=false}}, - w={label='Water Source', zone_flags={water_source=true}}, - f={label='Fishing', zone_flags={fishing=true}}, - g={label='Gather/Pick Fruit', zone_flags={gather=true}}, - d={label='Garbage Dump', zone_flags={garbage_dump=true}}, - n={label='Pen/Pasture', zone_flags={pen_pasture=true}}, - p={label='Pit/Pond', zone_flags={pit_pond=true}}, - s={label='Sand', zone_flags={sand=true}}, - c={label='Clay', zone_flags={clay=true}}, - m={label='Meeting Area', zone_flags={meeting_area=true}}, - h={label='Hospital', zone_flags={hospital=true}}, - t={label='Animal Training', zone_flags={animal_training=true}}, + m={label='Meeting Area', default_data={type=df.civzone_type.MeetingHall}}, + b={label='Bedroom', default_data={type=df.civzone_type.Bedroom}}, + h={label='Dining Hall', default_data={type=df.civzone_type.DiningHall}}, + n={label='Pen/Pasture', default_data={type=df.civzone_type.Pen, + assign={zone_settings={pen={unk=1}}}}}, + p={label='Pit/Pond', props_fn=parse_pit_pond_props, default_data={type=df.civzone_type.Pond, + assign={zone_settings={pit_pond=df.building_civzonest.T_zone_settings.T_pit_pond.top_of_pit}}}}, + w={label='Water Source', default_data={type=df.civzone_type.WaterSource}}, + j={label='Dungeon', default_data={type=df.civzone_type.Dungeon}}, + f={label='Fishing', default_data={type=df.civzone_type.FishingArea}}, + s={label='Sand', default_data={type=df.civzone_type.SandCollection}}, + o={label='Office', default_data={type=df.civzone_type.Office}}, + D={label='Dormitory', default_data={type=df.civzone_type.Dormitory}}, + B={label='Barracks', default_data={type=df.civzone_type.Barracks}}, + a={label='Archery Range', props_fn=parse_archery_props, default_data={type=df.civzone_type.ArcheryRange, + assign={zone_settings={archery={dir_x=1, dir_y=0}}}}}, + d={label='Garbage Dump', default_data={type=df.civzone_type.Dump}}, + t={label='Animal Training', default_data={type=df.civzone_type.AnimalTraining}}, + T={label='Tomb', props_fn=parse_tomb_props, default_data={type=df.civzone_type.Tomb, + assign={zone_settings={tomb={whole=1}}}}}, + g={label='Gather/Pick Fruit', props_fn=parse_gather_props, default_data={type=df.civzone_type.PlantGathering, + assign={zone_settings={gather={pick_trees=true, pick_shrubs=true, gather_fallen=true}}}}}, + c={label='Clay', default_data={type=df.civzone_type.ClayCollection}}, } -for _, v in pairs(zone_db) do utils.assign(v, zone_template) end - -local function parse_pit_pond_subconfig(keys, flags) - for c in keys:gmatch('.') do - if c == 'f' then - flags.is_pond = true - else - qerror(string.format('invalid pit/pond config char: "%s"', c)) - end - end +for _, v in pairs(zone_db) do + utils.assign(v, zone_template) + ensure_key(v.default_data, 'assign').is_active = 8 -- set to active by default end -local function parse_gather_subconfig(keys, flags) - -- all options are on by default; specifying them turns them off - for c in keys:gmatch('.') do - if c == 't' then - flags.pick_trees = false - elseif c == 's' then - flags.pick_shrubs = false - elseif c == 'f' then - flags.gather_fallen = false - else - qerror(string.format('invalid gather config char: "%s"', c)) - end - end -end +-- we may want to offer full name aliases for the single letter ones above +local aliases = {} local hospital_max_values = { thread=1500000, @@ -85,128 +150,298 @@ local hospital_max_values = { soap=15000 } -local function set_hospital_supplies(key, val, flags) - if not hospital_max_values[key] then - qerror(string.format('invalid hospital setting: "%s"', key)) +local valid_locations = { + tavern={new=df.abstract_building_inn_tavernst, + assign={name={type=df.language_name_type.SymbolFood}, + contents={desired_goblets=10, desired_instruments=5, + need_more={goblets=true, instruments=true}}}}, + hospital={new=df.abstract_building_hospitalst, + assign={name={type=df.language_name_type.Hospital}, + contents={desired_splints=5, desired_thread=75000, + desired_cloth=50000, desired_crutches=5, desired_powder=750, + desired_buckets=2, desired_soap=750, need_more={splints=true, + thread=true, cloth=true, crutches=true, powder=true, + buckets=true, soap=true}}}}, + guildhall={new=df.abstract_building_guildhallst, + assign={name={type=df.language_name_type.Guildhall}}}, + library={new=df.abstract_building_libraryst, + assign={name={type=df.language_name_type.Library}, + contents={desired_paper=10, need_more={paper=true}}}}, + temple={new=df.abstract_building_templest, + assign={name={type=df.language_name_type.Temple}, + contents={desired_instruments=5, need_more={instruments=true}}}}, +} +local valid_restrictions = { + visitors={AllowVisitors=true, AllowResidents=true, OnlyMembers=false}, + residents={AllowVisitors=false, AllowResidents=true, OnlyMembers=false}, + citizens={AllowVisitors=false, AllowResidents=false, OnlyMembers=false}, + members={AllowVisitors=false, AllowResidents=false, OnlyMembers=true}, +} +for _, v in pairs(valid_locations) do + ensure_key(v, 'assign').flags = valid_restrictions.visitors + ensure_key(v.assign, 'name').has_name = true + ensure_key(v.assign.name, 'parts_of_speech').resize = false + v.assign.name.parts_of_speech.FirstAdjective = df.part_of_speech.Adjective +end + +local location_occupations = { + tavern={df.occupation_type.TAVERN_KEEPER, df.occupation_type.PERFORMER}, + hospital={df.occupation_type.DOCTOR, df.occupation_type.DIAGNOSTICIAN, + df.occupation_type.SURGEON, df.occupation_type.BONE_DOCTOR}, + guildhall={}, + library={}, + temple={}, +} + +local prop_prefix = 'desired_' + +local function parse_location_props(props) + if not props.location then return nil end + local token_and_label = quickfort_parse.parse_token_and_label(props.location, 1) + props.location = nil + if not valid_locations[token_and_label.token] then + dfhack.printerr(('ignoring invalid location type: "%s"'):format(token_and_label.token)) + return nil end - local val_num = tonumber(val) - if not val_num or val_num < 0 or val_num > hospital_max_values[key] then - qerror(string.format( - 'invalid hospital supply count: "%s". must be between 0 and %d', - val, hospital_max_values[key])) + local location_data = { + type=token_and_label.token, + label=token_and_label.label, + data={}, + } + if props.allow then + if valid_restrictions[props.allow] then + location_data.data.flags = copyall(valid_restrictions[props.allow]) + else + dfhack.printerr(('ignoring invalid allow value: "%s"'):format(props.allow)) + end + props.allow = nil end - flags['max_'..key] = val_num - flags.supplies_needed[key] = val_num > 0 + if location_data.type == 'guildhall' and props.profession then + local profession = df.profession[props.profession:upper()] + if not profession then + dfhack.printerr(('ignoring invalid guildhall profession: "%s"'):format(props.profession)) + else + ensure_key(location_data.data, 'contents').profession = profession + end + props.profession = nil + end + if safe_index(valid_locations[location_data.type], 'assign', 'contents') then + for k in pairs(valid_locations[location_data.type].assign.contents) do + if not k:startswith(prop_prefix) then goto continue end + local short_prop = k:sub(#prop_prefix+1) + if props[short_prop] then + local prop = props[short_prop] + props[short_prop] = nil + local val = tonumber(prop) + if not val or val ~= math.floor(val) or val < 0 then + dfhack.printerr(('ignoring invalid %s value: "%s"'):format(short_prop, prop)) + goto continue + end + if short_prop == 'thread' then val = val * 15000 + elseif short_prop == 'cloth' then val = val * 10000 + elseif short_prop == 'powder' or short_prop == 'soap' then + val = val * 150 + end + ensure_key(location_data.data, 'contents')[k] = val + ensure_keys(location_data.data, 'contents', 'need_more')[short_prop] = (val > 0) + end + ::continue:: + end + end + return location_data end --- full format (all params optional): --- {hospital thread=num cloth=num splints=num crutches=num plaster=num buckets=num soap=num} -local function parse_hospital_subconfig(keys, flags) - local etoken, params = quickfort_parse.parse_extended_token(keys) - if etoken:lower() ~= 'hospital' then - qerror(string.format('invalid hospital settings: "%s"', keys)) +local function get_noble_position_id(positions, noble) + noble = noble:upper() + for _,position in ipairs(positions.own) do + if position.code == noble then return position.id end end - for k,v in pairs(params) do - set_hospital_supplies(k, v, flags) +end + +local function get_assigned_noble_unit(positions, noble_position_id) + for _,assignment in ipairs(positions.assignments) do + if assignment.position_id == noble_position_id then + local histfig = df.historical_figure.find(assignment.histfig) + if not histfig then return end + return df.unit.find(histfig.unit_id) + end end end -local function parse_zone_config(keys, labels, zone_data) - local i = 1 - while i <= #keys do - local c = keys:sub(i, i) - if rawget(zone_db, c) then - local db_entry = zone_db[c] - if (db_entry.zone_flags.pen_pasture or - zone_data.zone_flags.pen_pasture) and - (db_entry.zone_flags.pit_pond or - zone_data.zone_flags.pit_pond) then - qerror("zone cannot be both a pen/pasture and a pit/pond") - end - table.insert(labels, db_entry.label) - utils.assign(zone_data.zone_flags, db_entry.zone_flags) - elseif c == 'P' then - zone_data.pit_flags = {} - parse_pit_pond_subconfig(keys:sub(i+1), zone_data.pit_flags) - break - elseif c == 'G' then - zone_data.gather_flags = {} - parse_gather_subconfig(keys:sub(i+1), zone_data.gather_flags) - break - elseif c == 'H' then - zone_data.hospital = {supplies_needed={}} - parse_hospital_subconfig(keys:sub(i+1), zone_data.hospital) - break +local function get_noble_unit(noble) + local site = df.global.world.world_data.active_site[0] + for _,entity_site_link in ipairs(site.entity_links) do + local gov = df.historical_entity.find(entity_site_link.entity_id) + if not gov or gov.type ~= df.historical_entity_type.SiteGovernment then goto continue end + local noble_position_id = get_noble_position_id(gov.positions, noble) + if not noble_position_id then + dfhack.printerr(('could not find a noble position for: "%s"'):format(noble)) + return + else + return get_assigned_noble_unit(gov.positions, noble_position_id) end - i = i + 1 + ::continue:: end end -local function custom_zone(_, keys) - local labels = {} - local zone_data = {zone_flags={}} - -- subconfig sequences are separated by '^' characters - for zone_config in keys:gmatch('[^^]+') do - parse_zone_config(zone_config, labels, zone_data) - end - zone_data.label = table.concat(labels, '+') - utils.assign(zone_data, zone_template) - return zone_data +local function parse_zone_config(c, props) + if not rawget(zone_db, c) then + return 'Invalid', nil + end + local zone_data = {} + local db_entry = zone_db[c] + utils.assign(zone_data, db_entry.default_data) + zone_data.location = parse_location_props(props) + if props.active == 'false' then + zone_data.is_active = 0 + props.active = nil + end + if props.name then + zone_data.name = props.name + props.name = nil + end + if props.assigned_unit then + zone_data.assigned_unit = get_noble_unit(props.assigned_unit) + if not zone_data.assigned_unit and props.assigned_unit:lower() == 'sheriff' then + zone_data.assigned_unit = get_noble_unit('captain_of_the_guard') + end + if not zone_data.assigned_unit then + dfhack.printerr(('could not find a unit assigned to noble position: "%s"'):format(props.assigned_unit)) + end + props.assigned_unit = nil + end + if db_entry.props_fn then db_entry.props_fn(zone_data, props) end + + for k,v in pairs(props) do + dfhack.printerr(('unhandled property: "%s"="%s"'):format(k, v)) + end + + return db_entry.label, zone_data end -setmetatable(zone_db, {__index=custom_zone}) +local zone_key_pattern = '%a' -- just one letter -local function dump_flags(args) - local flags = args[1] - for k,v in pairs(flags) do - if type(v) ~= 'table' then - print(string.format(' %s: %s', k, v)) +local function custom_zone(_, keys) + local labels, set_global_label = {}, false + local db_entry = {data={}} + local token_and_label, props_start_pos = quickfort_parse.parse_token_and_label(keys, 1, zone_key_pattern) + while token_and_label do + local props, next_token_pos = quickfort_parse.parse_properties(keys, props_start_pos) + local label, zone_data = parse_zone_config(token_and_label.token, props) + if token_and_label.label then + label = ('%s/%s'):format(label, token_and_label.label) + set_global_label = true end + table.insert(labels, label) + table.insert(db_entry.data, zone_data) + token_and_label, props_start_pos = quickfort_parse.parse_token_and_label(keys, next_token_pos, zone_key_pattern) end + db_entry.label = table.concat(labels, '+') + if set_global_label then + db_entry.global_label = db_entry.label + end + utils.assign(db_entry, zone_template) + return db_entry end -local function assign_flags(bld, db_entry, key, dry_run) - local flags = db_entry[key] - if flags then - log('assigning %s:', key) - logfn(dump_flags, flags) - if not dry_run then utils.assign(bld[key], flags) end +setmetatable(zone_db, {__index=custom_zone}) + +local word_table = df.global.world.raws.language.word_table[0][35] + +local function generate_name() + local adj_index = math.random(0, #word_table.words.Adjectives - 1) + local thex_index = math.random(0, #word_table.words.TheX - 1) + return {words={ + resize=false, + FirstAdjective=word_table.words.Adjectives[adj_index], + TheX=word_table.words.TheX[thex_index], + }} +end + +local function set_location(zone, location, ctx) + if location.type == 'guildhall' and not safe_index(location.data, 'contents', 'profession') then + dfhack.printerr('cannot create a guildhall without a specified profession') + return + end + local site = df.global.world.world_data.active_site[0] + local loc_id = nil + if location.label then + loc_id = safe_index(ctx, 'zone', 'locations', location.label) + end + local data = copyall(valid_locations[location.type]) + utils.assign(data, location.data) + if not loc_id then + loc_id = site.next_building_id + local occupations = df.global.world.occupations.all + for _,ot in ipairs(location_occupations[location.type]) do + local occ_id = df.global.occupation_next_id + occupations:insert('#', { + new=df.occupation, + id=occ_id, + type=ot, + location_id=loc_id, + site_id=site.id, + }) + table.insert(ensure_key(data, 'occupations'), occupations[#occupations-1]) + df.global.occupation_next_id = df.global.occupation_next_id + 1 + end + + data.name = generate_name() + data.id = loc_id + data.site_id = site.id + data.pos = copyall(site.pos) + for _,entity_site_link in ipairs(site.entity_links) do + local he = df.historical_entity.find(entity_site_link.entity_id) + if not he or he.type ~= df.historical_entity_type.SiteGovernment then goto continue end + data.site_owner_id = he.id + ::continue:: + end + site.buildings:insert('#', data) + site.next_building_id = site.next_building_id + 1 + -- fix up BitArray flags (which don't seem to get set by the insert above) + local bld = site.buildings[#site.buildings-1] + for flag, val in pairs(data.assign.flags) do + bld.flags[flag] = val + end + if data.flags then + for flag, val in pairs(data.flags) do + bld.flags[flag] = val + end + end + end + zone.site_id = site.id + zone.location_id = loc_id + if location.label then + -- remember this location for future associations in this blueprint + ensure_keys(ctx, 'zone', 'locations')[location.label] = loc_id end end -local function create_zone(zone, dry_run) - local db_entry = zone_db[zone.type] - log('creating %s zone at map coordinates (%d, %d, %d), defined' .. - ' from spreadsheet cells: %s', - db_entry.label, zone.pos.x, zone.pos.y, zone.pos.z, - table.concat(zone.cells, ', ')) +local function create_zone(zone, data, ctx) local extents, ntiles = - quickfort_building.make_extents(zone, dry_run) - local bld, err = nil, nil - if not dry_run then - local fields = {room={x=zone.pos.x, y=zone.pos.y, width=zone.width, - height=zone.height, extents=extents}, - is_room=true} - bld, err = dfhack.buildings.constructBuilding{ - type=df.building_type.Civzone, subtype=df.civzone_type.ActivityZone, - abstract=true, pos=zone.pos, width=zone.width, height=zone.height, - fields=fields} - if not bld then - -- this is an error instead of a qerror since our validity checking - -- is supposed to prevent this from ever happening - error(string.format('unable to designate zone: %s', err)) - end - -- set defaults (should move into constructBuilding) - bld.zone_flags.active = true - bld.gather_flags.pick_trees = true - bld.gather_flags.pick_shrubs = true - bld.gather_flags.gather_fallen = true - end - -- set specified flags - assign_flags(bld, db_entry, 'zone_flags', dry_run) - assign_flags(bld, db_entry, 'pit_flags', dry_run) - assign_flags(bld, db_entry, 'gather_flags', dry_run) - assign_flags(bld, db_entry, 'hospital', dry_run) + quickfort_building.make_extents(zone, ctx.dry_run) + if ctx.dry_run then return ntiles end + local fields = {room={x=zone.pos.x, y=zone.pos.y, width=zone.width, + height=zone.height, extents=extents}} + local bld, err = dfhack.buildings.constructBuilding{ + type=df.building_type.Civzone, subtype=data.type, + abstract=true, pos=zone.pos, width=zone.width, height=zone.height, + fields=fields} + if not bld then + -- this is an error instead of a qerror since our validity checking + -- is supposed to prevent this from ever happening + error(string.format('unable to designate zone: %s', err)) + end + if data.location then + data = copyall(data) + set_location(bld, data.location, ctx) + data.location = nil + end + if data.assigned_unit then + dfhack.buildings.setOwner(bld, data.assigned_unit) + data.assigned_unit = nil + end + utils.assign(bld, data) return ntiles end @@ -222,21 +457,31 @@ function do_run(zlevel, grid, ctx) local zones = {} stats.invalid_keys.value = stats.invalid_keys.value + quickfort_building.init_buildings( - ctx, zlevel, grid, zones, zone_db) + ctx, zlevel, grid, zones, zone_db, aliases, true) stats.out_of_bounds.value = - stats.out_of_bounds.value + quickfort_building.crop_to_bounds( - ctx, zones, zone_db) + stats.out_of_bounds.value + quickfort_building.crop_to_bounds(ctx, zones) stats.zone_occupied.value = stats.zone_occupied.value + - quickfort_building.check_tiles_and_extents( - ctx, zones, zone_db) + quickfort_building.check_tiles_and_extents(ctx, zones) for _,zone in ipairs(zones) do - if zone.pos then - local ntiles = create_zone(zone, ctx.dry_run) + if not zone.pos then goto continue end + local db_entry = zone.db_entry + log('creating %s zone(s) at map coordinates (%d, %d, %d), defined' .. + ' from spreadsheet cells: %s', + db_entry.label, zone.pos.x, zone.pos.y, zone.pos.z, + table.concat(zone.cells, ', ')) + if not db_entry.data then + ensure_data(db_entry) + end + for _,data in ipairs(db_entry.data) do + log('creating zone with properties:') + logfn(printall_recurse, data) + local ntiles = create_zone(zone, data, ctx) stats.zone_tiles.value = stats.zone_tiles.value + ntiles stats.zone_designated.value = stats.zone_designated.value + 1 end + ::continue:: end end @@ -249,9 +494,7 @@ local function get_activity_zones(pos) local civzones = dfhack.buildings.findCivzonesAt(pos) if not civzones then return activity_zones end for _,civzone in ipairs(civzones) do - if civzone.type == df.civzone_type.ActivityZone then - table.insert(activity_zones, civzone) - end + table.insert(activity_zones, civzone) end return activity_zones end @@ -264,31 +507,28 @@ function do_undo(zlevel, grid, ctx) local zones = {} stats.invalid_keys.value = stats.invalid_keys.value + quickfort_building.init_buildings( - ctx, zlevel, grid, zones, zone_db) + ctx, zlevel, grid, zones, zone_db, aliases, true) - -- ensure a zone is not currently selected when we delete it. that causes - -- crashes. note that we move the cursor, but we have to keep the ui mode - -- the same. otherwise the zone stays selected (somehow) in memory. we only - -- move the cursor when we're in mode Zones to avoid having the viewport - -- jump around when it doesn't need to - local restore_cursor = false - if not dry_run and df.global.plotinfo.main.mode == df.ui_sidebar_mode.Zones then - quickfort_map.move_cursor(xyz2pos(-1, -1, ctx.cursor.z)) - restore_cursor = true - end + -- ensure we don't delete the currently selected zone, which causes crashes. + local selected_zone = dfhack.gui.getSelectedCivZone(true) for _, zone in ipairs(zones) do for extent_x, col in ipairs(zone.extent_grid) do for extent_y, in_extent in ipairs(col) do - if not zone.extent_grid[extent_x][extent_y] then goto continue end + if not in_extent then goto continue end local pos = xyz2pos(zone.pos.x+extent_x-1, zone.pos.y+extent_y-1, zone.pos.z) local activity_zones = get_activity_zones(pos) for _,activity_zone in ipairs(activity_zones) do log('removing zone at map coordinates (%d, %d, %d)', pos.x, pos.y, pos.z) - if not dry_run then - dfhack.buildings.deconstruct(activity_zone) + if not ctx.dry_run then + if activity_zone == selected_zone then + dfhack.printerr('cannot remove actively selected zone.') + dfhack.printerr('please deselect the zone and try again.') + else + dfhack.buildings.deconstruct(activity_zone) + end end stats.zone_removed.value = stats.zone_removed.value + 1 end @@ -296,6 +536,4 @@ function do_undo(zlevel, grid, ctx) end end end - - if restore_cursor then quickfort_map.move_cursor(ctx.cursor) end end From b4ea8656a1699694270f4ef9a6b0eb766edb9989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Lul=C3=A9?= <630159+plule@users.noreply.github.com> Date: Wed, 7 Jun 2023 22:22:17 +0200 Subject: [PATCH 282/732] Update docs/suspendmanager.rst --- docs/suspendmanager.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/suspendmanager.rst b/docs/suspendmanager.rst index 3f74780d78..d1a5133f35 100644 --- a/docs/suspendmanager.rst +++ b/docs/suspendmanager.rst @@ -12,8 +12,8 @@ This tool will watch your active jobs and: - suspend construction jobs that would prevent a dwarf from reaching an adjacent construction job, such as when building a wall corner. - suspend construction jobs on top of a smoothing, engraving or track carving - job. This prevent the construction job to be completed first, which would - erase the other + designation. This prevents the construction job from being completed first, + which would erase the designation. Usage ----- From a635dcfc88e848d359675dde7ba6684bae4069af Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Wed, 7 Jun 2023 22:22:41 +0200 Subject: [PATCH 283/732] Move the changelog to the correct release --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 3be5bb9dba..0390e5cdb6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ that repo. ## Misc Improvements - `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants +- `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased ## Removed @@ -55,7 +56,6 @@ that repo. - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) - `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused -- `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased # 50.08-r1 From a4133e1359c539b86b1745ed68c0121e9f63fbb0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 7 Jun 2023 16:15:56 -0700 Subject: [PATCH 284/732] read friction and speed as properties but still support the old way of specifying --- internal/quickfort/build.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/quickfort/build.lua b/internal/quickfort/build.lua index e951b56992..9e79055086 100644 --- a/internal/quickfort/build.lua +++ b/internal/quickfort/build.lua @@ -232,7 +232,24 @@ local function do_workshop_furnace_props(db_entry, props) end end +local function do_roller_props(db_entry, props) + if props.speed and + (props.speed == '50000' or props.speed == '40000' or props.speed == '30000' or + props.speed == '20000' or props.speed == '10000') + then + db_entry.props.speed = tonumber(props.speed) + props.speed = nil + end +end + local function do_trackstop_props(db_entry, props) + if props.friction and + (props.friction == '50000' or props.friction == '10000' or props.friction == '500' or + props.friction == '50' or props.friction == '10') + then + db_entry.props.friction = tonumber(props.friction) + props.friction = nil + end if props.take_from then ensure_key(db_entry, 'route').from_names = argparse.stringList(props.take_from) props.take_from = nil @@ -475,6 +492,7 @@ local function make_roller_entry(direction, speed) direction=direction, fields={speed=speed}, is_valid_tile_fn=is_valid_tile_machine, + props_fn=do_roller_props, transform=transform } end From 901eb30ae8ea8917823b1f80c8fc594b6d680345 Mon Sep 17 00:00:00 2001 From: wsfsbvchr <61580558+wsfsbvchr@users.noreply.github.com> Date: Thu, 8 Jun 2023 15:25:53 +0300 Subject: [PATCH 285/732] Update emigration.lua Fix for emigration.lua not working since the citizenship logic was changed. --- emigration.lua | 206 +++++++++++++++++++++++++++++++------------------ 1 file changed, 130 insertions(+), 76 deletions(-) diff --git a/emigration.lua b/emigration.lua index 47b3325659..909a7e3ca9 100644 --- a/emigration.lua +++ b/emigration.lua @@ -1,27 +1,35 @@ --Allow stressed dwarves to emigrate from the fortress +-- Updated for 0.47.05 by wsfsbvchr -- For 34.11 by IndigoFenix; update and cleanup by PeridexisErrant -- old version: http://dffd.bay12games.com/file.php?id=8404 ---@module = true ---@enable = true +--[====[ +emigration +========== +Allows dwarves to emigrate from the fortress when stressed, +in proportion to how badly stressed they are and adjusted +for who they would have to leave with - a dwarven merchant +being more attractive than leaving alone (or with an elf). +The check is made monthly. -local json = require('json') -local persist = require('persist-table') +A happy dwarf (ie with negative stress) will never emigrate. -local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence +Usage:: -enabled = enabled or false + emigration enable|disable +]====] -function isEnabled() - return enabled -end +enabled = enabled or false -local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled}) +local args = {...} +if args[1] == "enable" then + enabled = true +elseif args[1] == "disable" then + enabled = false end function desireToStay(unit,method,civ_id) -- on a percentage scale - local value = 100 - unit.status.current_soul.personality.stress / 5000 + local value = 100 - unit.status.current_soul.personality.stress_level / 5000 if method == 'merchant' or method == 'diplomat' then if civ_id ~= unit.civ_id then value = value*2 end end if method == 'wild' then @@ -30,24 +38,99 @@ function desireToStay(unit,method,civ_id) end function desert(u,method,civ) - u.following = nil - local line = dfhack.TranslateName(dfhack.units.getVisibleName(u)) .. " has " - if method == 'merchant' then - line = line.."joined the merchants" - u.flags1.merchant = true - u.civ_id = civ - elseif method == 'diplomat' then - line = line.."followed the diplomat" - u.flags1.diplomat = true - u.civ_id = civ - else - line = line.."abandoned the settlement in search of a better life." - u.civ_id = -1 - u.flags1.forest = true - u.animal.leave_countdown = 2 - end - print(line) - dfhack.gui.showAnnouncement(line, COLOR_WHITE) + u.following = nil + local line = dfhack.TranslateName(dfhack.units.getVisibleName(u)) .. " has " + if method == 'merchant' then + line = line.."joined the merchants" + u.flags1.merchant = true + u.civ_id = civ + else + line = line.."abandoned the settlement in search of a better life." + u.civ_id = civ + u.flags1.forest = true + u.flags2.visitor = true + u.animal.leave_countdown = 2 + end + + local hf_id = u.hist_figure_id + local hf = df.historical_figure.find(u.hist_figure_id) + local fort_ent = df.global.ui.main.fortress_entity + local civ_ent = df.historical_entity.find(hf.civ_id) + + local newent_id = -1 + local newsite_id = -1 + + -- free owned rooms + for i = #u.owned_buildings-1, 0, -1 do + local temp_bld = df.building.find(u.owned_buildings[i].id) + dfhack.buildings.setOwner(temp_bld, nil) + end + + -- erase the unit from the fortress entity + for k,v in pairs(fort_ent.histfig_ids) do + if tonumber(v) == hf_id then + df.global.ui.main.fortress_entity.histfig_ids:erase(k) + break + end + end + for k,v in pairs(fort_ent.hist_figures) do + if v.id == hf_id then + df.global.ui.main.fortress_entity.hist_figures:erase(k) + break + end + end + for k,v in pairs(fort_ent.nemesis) do + if v.figure.id == hf_id then + df.global.ui.main.fortress_entity.nemesis:erase(k) + df.global.ui.main.fortress_entity.nemesis_ids:erase(k) + break + end + end + + -- remove the old entity link and create new one to indicate former membership + hf.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = fort_ent.id, link_strength = 100}) + for k,v in pairs(hf.entity_links) do + if v._type == df.histfig_entity_link_memberst and v.entity_id == fort_ent.id then + hf.entity_links:erase(k) + break + end + end + + -- try to find a new entity for the unit to join + for k,v in pairs(civ_ent.entity_links) do + if v.type == 1 and v.target ~= fort_ent.id then + newent_id = v.target + break + end + end + + if newent_id > -1 then + hf.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newent_id, link_strength = 100}) + + -- try to find a new site for the unit to join + for k,v in pairs(df.global.world.entities.all[hf.civ_id].site_links) do + if v.type == 0 and v.target ~= site_id then + newsite_id = v.target + break + end + end + + local newent = df.historical_entity.find(newent_id) + newent.histfig_ids:insert('#', hf_id) + newent.hist_figures:insert('#', hf) + + local hf_event_id = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", {new = df.history_event_add_hf_entity_linkst, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, civ = newent_id, histfig = hf_id, link_type = 0}) + if newsite_id > -1 then + local hf_event_id = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", {new = df.history_event_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, hfid = hf_id, state = 1, reason = -1, site = newsite_id}) + end + end + + print(line) + dfhack.gui.showAnnouncement(line, COLOR_WHITE) end function canLeave(unit) @@ -55,12 +138,7 @@ function canLeave(unit) return false end - for _, skill in pairs(unit.status.current_soul.skills) do - if skill.rating > 14 then return false end - end - - return dfhack.units.isOwnRace(unit) and -- Doubtful check. naturalized citizens - dfhack.units.isOwnCiv(unit) and -- might also want to leave. + return dfhack.units.isCitizen(unit) and dfhack.units.isActive(unit) and not dfhack.units.isOpposedToLife(unit) and not unit.flags1.merchant and @@ -68,7 +146,6 @@ function canLeave(unit) not unit.flags1.chained and dfhack.units.getNoblePositions(unit) == nil and unit.military.squad_id == -1 and - dfhack.units.isCitizen(unit) and dfhack.units.isSane(unit) and not dfhack.units.isBaby(unit) and not dfhack.units.isChild(unit) @@ -96,13 +173,15 @@ function checkmigrationnow() and not unit.flags1.tame then if unit.flags1.merchant then table.insert(merchant_civ_ids, unit.civ_id) end - if unit.flags1.diplomat then table.insert(diplomat_civ_ids, unit.civ_id) end + --if unit.flags1.diplomat then table.insert(diplomat_civ_ids, unit.civ_id) end end end - + + if #merchant_civ_ids == 0 and #diplomat_civ_ids == 0 then + checkForDeserters('wild', df.global.ui.main.fortress_entity.entity_links[0].target) + end for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end - for _, civ_id in pairs(diplomat_civ_ids) do checkForDeserters('diplomat', civ_id) end - checkForDeserters('wild', -1) + --for _, civ_id in pairs(diplomat_civ_ids) do checkForDeserters('diplomat', civ_id) end end local function event_loop() @@ -112,42 +191,17 @@ local function event_loop() end end -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - enabled = false - return - end - - if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return +dfhack.onStateChange.loadEmigration = function(code) + if code==SC_MAP_LOADED then + if enabled then + print("Emigration enabled.") + event_loop() + else + print("Emigration disabled.") + end end - - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') - enabled = (persisted_data or {enabled=false})['enabled'] - event_loop() -end - -if dfhack_flags.module then - return -end - -if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then - dfhack.printerr('emigration needs a loaded fortress map to work') - return -end - -local args = {...} -if dfhack_flags and dfhack_flags.enable then - args = {dfhack_flags.enable_state and 'enable' or 'disable'} end -if args[1] == "enable" then - enabled = true -elseif args[1] == "disable" then - enabled = false -else - return +if dfhack.isMapLoaded() then + dfhack.onStateChange.loadEmigration(SC_MAP_LOADED) end - -event_loop() -persist_state() From ba832ff90f04846dd0e575c0f4f25cb36a0e5afd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 8 Jun 2023 12:53:29 -0700 Subject: [PATCH 286/732] init examples have moved to gui/control-panel --- docs/gui/control-panel.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/gui/control-panel.rst b/docs/gui/control-panel.rst index 19a9c9c5c1..ca968a305f 100644 --- a/docs/gui/control-panel.rst +++ b/docs/gui/control-panel.rst @@ -1,3 +1,5 @@ +.. _dfhack-examples-guide: + gui/control-panel ================= From ce5ad5088644f61c784a9616448ea2793cff7a73 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 8 Jun 2023 13:00:10 -0700 Subject: [PATCH 287/732] move anchor to the appropriate section mostly so the lint header checker is happy --- docs/gui/control-panel.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/gui/control-panel.rst b/docs/gui/control-panel.rst index ca968a305f..a8933087fd 100644 --- a/docs/gui/control-panel.rst +++ b/docs/gui/control-panel.rst @@ -1,5 +1,3 @@ -.. _dfhack-examples-guide: - gui/control-panel ================= @@ -36,6 +34,8 @@ an associated GUI config screen, a gear icon will also appear next to the help icon. Hit :kbd:`Ctrl`:kbd:`G` or click on that icon to launch the relevant configuration interface. +.. _dfhack-examples-guide: + New Fort Autostart Commands --------------------------- From ea11e2a07395311e5eb29d119917485142dc9b19 Mon Sep 17 00:00:00 2001 From: wsfsbvchr <61580558+wsfsbvchr@users.noreply.github.com> Date: Fri, 9 Jun 2023 00:09:00 +0300 Subject: [PATCH 288/732] Update emigration.lua Integrated the changes to existing code. --- emigration.lua | 274 ++++++++++++++++++++++++++----------------------- 1 file changed, 145 insertions(+), 129 deletions(-) diff --git a/emigration.lua b/emigration.lua index 909a7e3ca9..e6e5056126 100644 --- a/emigration.lua +++ b/emigration.lua @@ -1,36 +1,29 @@ --Allow stressed dwarves to emigrate from the fortress --- Updated for 0.47.05 by wsfsbvchr +-- Updated for 0.47.05 and potentially for 50.08 by wsfsbvchr -- For 34.11 by IndigoFenix; update and cleanup by PeridexisErrant -- old version: http://dffd.bay12games.com/file.php?id=8404 ---[====[ -emigration -========== -Allows dwarves to emigrate from the fortress when stressed, -in proportion to how badly stressed they are and adjusted -for who they would have to leave with - a dwarven merchant -being more attractive than leaving alone (or with an elf). -The check is made monthly. +--@module = true +--@enable = true -A happy dwarf (ie with negative stress) will never emigrate. +local json = require('json') +local persist = require('persist-table') -Usage:: - - emigration enable|disable -]====] +local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence enabled = enabled or false -local args = {...} -if args[1] == "enable" then - enabled = true -elseif args[1] == "disable" then - enabled = false +function isEnabled() + return enabled +end + +local function persist_state() + persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled}) end function desireToStay(unit,method,civ_id) -- on a percentage scale - local value = 100 - unit.status.current_soul.personality.stress_level / 5000 - if method == 'merchant' or method == 'diplomat' then + local value = 100 - unit.status.current_soul.personality.stress / 5000 + if method == 'merchant' then if civ_id ~= unit.civ_id then value = value*2 end end if method == 'wild' then value = value*5 end @@ -38,99 +31,99 @@ function desireToStay(unit,method,civ_id) end function desert(u,method,civ) - u.following = nil - local line = dfhack.TranslateName(dfhack.units.getVisibleName(u)) .. " has " - if method == 'merchant' then - line = line.."joined the merchants" - u.flags1.merchant = true - u.civ_id = civ - else - line = line.."abandoned the settlement in search of a better life." - u.civ_id = civ - u.flags1.forest = true - u.flags2.visitor = true - u.animal.leave_countdown = 2 - end - - local hf_id = u.hist_figure_id - local hf = df.historical_figure.find(u.hist_figure_id) - local fort_ent = df.global.ui.main.fortress_entity - local civ_ent = df.historical_entity.find(hf.civ_id) - - local newent_id = -1 - local newsite_id = -1 - - -- free owned rooms - for i = #u.owned_buildings-1, 0, -1 do - local temp_bld = df.building.find(u.owned_buildings[i].id) - dfhack.buildings.setOwner(temp_bld, nil) - end - - -- erase the unit from the fortress entity - for k,v in pairs(fort_ent.histfig_ids) do - if tonumber(v) == hf_id then - df.global.ui.main.fortress_entity.histfig_ids:erase(k) - break - end - end - for k,v in pairs(fort_ent.hist_figures) do - if v.id == hf_id then - df.global.ui.main.fortress_entity.hist_figures:erase(k) - break - end - end - for k,v in pairs(fort_ent.nemesis) do - if v.figure.id == hf_id then - df.global.ui.main.fortress_entity.nemesis:erase(k) - df.global.ui.main.fortress_entity.nemesis_ids:erase(k) - break - end - end - - -- remove the old entity link and create new one to indicate former membership - hf.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = fort_ent.id, link_strength = 100}) - for k,v in pairs(hf.entity_links) do - if v._type == df.histfig_entity_link_memberst and v.entity_id == fort_ent.id then - hf.entity_links:erase(k) - break - end - end - - -- try to find a new entity for the unit to join - for k,v in pairs(civ_ent.entity_links) do - if v.type == 1 and v.target ~= fort_ent.id then - newent_id = v.target - break - end - end - - if newent_id > -1 then - hf.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newent_id, link_strength = 100}) - - -- try to find a new site for the unit to join - for k,v in pairs(df.global.world.entities.all[hf.civ_id].site_links) do - if v.type == 0 and v.target ~= site_id then - newsite_id = v.target - break - end - end - - local newent = df.historical_entity.find(newent_id) - newent.histfig_ids:insert('#', hf_id) - newent.hist_figures:insert('#', hf) - - local hf_event_id = df.global.hist_event_next_id - df.global.hist_event_next_id = df.global.hist_event_next_id+1 - df.global.world.history.events:insert("#", {new = df.history_event_add_hf_entity_linkst, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, civ = newent_id, histfig = hf_id, link_type = 0}) - if newsite_id > -1 then - local hf_event_id = df.global.hist_event_next_id - df.global.hist_event_next_id = df.global.hist_event_next_id+1 - df.global.world.history.events:insert("#", {new = df.history_event_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, hfid = hf_id, state = 1, reason = -1, site = newsite_id}) - end - end - - print(line) - dfhack.gui.showAnnouncement(line, COLOR_WHITE) + u.following = nil + local line = dfhack.TranslateName(dfhack.units.getVisibleName(u)) .. " has " + if method == 'merchant' then + line = line.."joined the merchants" + u.flags1.merchant = true + u.civ_id = civ + else + line = line.."abandoned the settlement in search of a better life." + u.civ_id = civ + u.flags1.forest = true + u.flags2.visitor = true + u.animal.leave_countdown = 2 + end + + local hf_id = u.hist_figure_id + local hf = df.historical_figure.find(u.hist_figure_id) + local fort_ent = df.global.ui.main.fortress_entity + local civ_ent = df.historical_entity.find(hf.civ_id) + + local newent_id = -1 + local newsite_id = -1 + + -- free owned rooms + for i = #u.owned_buildings-1, 0, -1 do + local temp_bld = df.building.find(u.owned_buildings[i].id) + dfhack.buildings.setOwner(temp_bld, nil) + end + + -- erase the unit from the fortress entity + for k,v in ipairs(fort_ent.histfig_ids) do + if v == hf_id then + df.global.ui.main.fortress_entity.histfig_ids:erase(k) + break + end + end + for k,v in ipairs(fort_ent.hist_figures) do + if v.id == hf_id then + df.global.ui.main.fortress_entity.hist_figures:erase(k) + break + end + end + for k,v in ipairs(fort_ent.nemesis) do + if v.figure.id == hf_id then + df.global.ui.main.fortress_entity.nemesis:erase(k) + df.global.ui.main.fortress_entity.nemesis_ids:erase(k) + break + end + end + + -- remove the old entity link and create new one to indicate former membership + hf.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = fort_ent.id, link_strength = 100}) + for k,v in ipairs(hf.entity_links) do + if v._type == df.histfig_entity_link_memberst and v.entity_id == fort_ent.id then + hf.entity_links:erase(k) + break + end + end + + -- try to find a new entity for the unit to join + for k,v in ipairs(civ_ent.entity_links) do + if v.type == df.entity_entity_link_type.CHILD and v.target ~= fort_ent.id then + newent_id = v.target + break + end + end + + if newent_id > -1 then + hf.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newent_id, link_strength = 100}) + + -- try to find a new site for the unit to join + for k,v in ipairs(df.global.world.entities.all[hf.civ_id].site_links) do + if v.type == df.entity_site_link_type.Claim and v.target ~= site_id then + newsite_id = v.target + break + end + end + + local newent = df.historical_entity.find(newent_id) + newent.histfig_ids:insert('#', hf_id) + newent.hist_figures:insert('#', hf) + + local hf_event_id = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", {new = df.history_event_add_hf_entity_linkst, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, civ = newent_id, histfig = hf_id, link_type = 0}) + if newsite_id > -1 then + local hf_event_id = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", {new = df.history_event_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, hfid = hf_id, state = 1, reason = -1, site = newsite_id}) + end + end + + print(line) + dfhack.gui.showAnnouncement(line, COLOR_WHITE) end function canLeave(unit) @@ -163,7 +156,6 @@ end function checkmigrationnow() local merchant_civ_ids = {} --as:number[] - local diplomat_civ_ids = {} --as:number[] local allUnits = df.global.world.units.active for i=0, #allUnits-1 do local unit = allUnits[i] @@ -173,15 +165,14 @@ function checkmigrationnow() and not unit.flags1.tame then if unit.flags1.merchant then table.insert(merchant_civ_ids, unit.civ_id) end - --if unit.flags1.diplomat then table.insert(diplomat_civ_ids, unit.civ_id) end end end - if #merchant_civ_ids == 0 and #diplomat_civ_ids == 0 then + if #merchant_civ_ids == 0 then checkForDeserters('wild', df.global.ui.main.fortress_entity.entity_links[0].target) + else + for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end end - for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end - --for _, civ_id in pairs(diplomat_civ_ids) do checkForDeserters('diplomat', civ_id) end end local function event_loop() @@ -191,17 +182,42 @@ local function event_loop() end end -dfhack.onStateChange.loadEmigration = function(code) - if code==SC_MAP_LOADED then - if enabled then - print("Emigration enabled.") - event_loop() - else - print("Emigration disabled.") - end +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + enabled = false + return end + + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') + enabled = (persisted_data or {enabled=false})['enabled'] + event_loop() end -if dfhack.isMapLoaded() then - dfhack.onStateChange.loadEmigration(SC_MAP_LOADED) +if dfhack_flags.module then + return end + +if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then + dfhack.printerr('emigration needs a loaded fortress map to work') + return +end + +local args = {...} +if dfhack_flags and dfhack_flags.enable then + args = {dfhack_flags.enable_state and 'enable' or 'disable'} +end + +if args[1] == "enable" then + enabled = true +elseif args[1] == "disable" then + enabled = false +else + return +end + +event_loop() +persist_state() From e63b5386019cb1b2898d51754c17d6c32f1e2d69 Mon Sep 17 00:00:00 2001 From: wsfsbvchr <61580558+wsfsbvchr@users.noreply.github.com> Date: Fri, 9 Jun 2023 00:14:17 +0300 Subject: [PATCH 289/732] Update emigration.lua Removed some excess whitespace. --- emigration.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/emigration.lua b/emigration.lua index e6e5056126..3ac5dbfeea 100644 --- a/emigration.lua +++ b/emigration.lua @@ -44,7 +44,7 @@ function desert(u,method,civ) u.flags2.visitor = true u.animal.leave_countdown = 2 end - + local hf_id = u.hist_figure_id local hf = df.historical_figure.find(u.hist_figure_id) local fort_ent = df.global.ui.main.fortress_entity @@ -52,7 +52,7 @@ function desert(u,method,civ) local newent_id = -1 local newsite_id = -1 - + -- free owned rooms for i = #u.owned_buildings-1, 0, -1 do local temp_bld = df.building.find(u.owned_buildings[i].id) @@ -121,7 +121,7 @@ function desert(u,method,civ) df.global.world.history.events:insert("#", {new = df.history_event_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, hfid = hf_id, state = 1, reason = -1, site = newsite_id}) end end - + print(line) dfhack.gui.showAnnouncement(line, COLOR_WHITE) end @@ -167,7 +167,7 @@ function checkmigrationnow() if unit.flags1.merchant then table.insert(merchant_civ_ids, unit.civ_id) end end end - + if #merchant_civ_ids == 0 then checkForDeserters('wild', df.global.ui.main.fortress_entity.entity_links[0].target) else From 54a3dac5a043b097dcf92fd5239304644c43ef04 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:11:21 +0200 Subject: [PATCH 290/732] [suspendmanager] Protect dead-ends from being closed --- changelog.txt | 1 + docs/suspendmanager.rst | 4 +-- suspendmanager.lua | 78 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/changelog.txt b/changelog.txt index a2e60115b6..9736edf4b0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ that repo. - `quickfort`: now handles zones, locations, stockpile configuration, hauling routes, and more - `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased - `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types +- `suspendmanager`: suspend jobs when building high walls or filling corridors ## Removed diff --git a/docs/suspendmanager.rst b/docs/suspendmanager.rst index d1a5133f35..f36c8e3c9c 100644 --- a/docs/suspendmanager.rst +++ b/docs/suspendmanager.rst @@ -9,8 +9,8 @@ This tool will watch your active jobs and: - unsuspend jobs that have become suspended due to inaccessible materials, items temporarily in the way, or worker dwarves getting scared by wildlife -- suspend construction jobs that would prevent a dwarf from reaching an adjacent - construction job, such as when building a wall corner. +- suspend most construction jobs that would prevent a dwarf from reaching another + construction job, such as when building a wall corner or high walls - suspend construction jobs on top of a smoothing, engraving or track carving designation. This prevents the construction job from being completed first, which would erase the designation. diff --git a/suspendmanager.lua b/suspendmanager.lua index 3b4dd41f91..0d6a9b43cb 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -31,6 +31,8 @@ REASON = { RISK_BLOCKING = 3, --- Building job on top of an erasable designation (smoothing, carving, ...) ERASE_DESIGNATION = 4, + --- Blocks a dead end (either a corridor or on top of a wall) + DEADEND = 5, } REASON_TEXT = { @@ -38,6 +40,7 @@ REASON_TEXT = { [REASON.BUILDINGPLAN] = 'planned', [REASON.RISK_BLOCKING] = 'blocking', [REASON.ERASE_DESIGNATION] = 'designation', + [REASON.DEADEND] = 'dead end', } --- Suspension reasons from an external source @@ -155,18 +158,17 @@ local function isImpassable(building) end end ---- True if there is a construction plan to build an unwalkable tile +--- If there is a construction plan to build an unwalkable tile, return the building ---@param pos coord ----@return boolean +---@return building? local function plansToConstructImpassableAt(pos) --- @type building_constructionst|building local building = dfhack.buildings.findAtTile(pos) - if not building then return false end - if building.flags.exists then - -- The building is already created - return false + if not building then return nil end + if not building.flags.exists and isImpassable(building) then + return building end - return isImpassable(building) + return nil end --- Check if the tile can be walked on @@ -237,6 +239,65 @@ local function riskBlocking(job) return false end +--- Analyzes the given job, and if it is at a dead end, follow the "corridor" and +--- mark the jobs containing it as dead end blocking jobs +function SuspendManager:suspendDeadend(start_job) + local building = dfhack.job.getHolder(start_job) + if not building then return end + local pos = {x=building.centerx,y=building.centery,z=building.z} + + -- visited building ids of this potential dead end + local visited = { + [building.id] = true + } + + --- Support dead ends of a maximum length of 1000 + for _=0,1000 do + -- building plan on the way to the exit + ---@type building? + local exit = nil + for _,neighbourPos in pairs(neighbours(pos)) do + if not walkable(neighbourPos) then + -- non walkable neighbour, not an exit + goto continue + end + + local impassablePlan = plansToConstructImpassableAt(neighbourPos) + if not impassablePlan then + -- walkable neighbour with no building scheduled, not in a dead end + return + end + + if visited[impassablePlan.id] then + -- already visited, not an exit + goto continue + end + + if exit then + -- more than one exit, not in a dead end + return + end + + -- the building plan is a candidate to exit + exit = impassablePlan + + ::continue:: + end + + if not exit then return end + + -- exit is the single exit point of this corridor, suspend its construction job + -- and continue the exploration from its position + for _,job in ipairs(exit.jobs) do + if job.job_type == df.job_type.ConstructBuilding then + self.suspensions[job.id] = REASON.DEADEND + end + end + visited[exit.id] = true + pos = {x=exit.centerx,y=exit.centery,z=exit.z} + end +end + --- Return true if the building overlaps with a tile with a designation flag ---@param building building local function buildingOnDesignation(building) @@ -300,6 +361,9 @@ function SuspendManager:refresh() self.suspensions[job.id]=REASON.RISK_BLOCKING end + -- If this job is a dead end, mark jobs leading to it as dead end + self:suspendDeadend(job) + -- First designation protection check: tile with designation flag if job.job_type == df.job_type.ConstructBuilding then ---@type building From 748420a8ce46f7b071ecd6af128fdc371e90f644 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:58:13 +0200 Subject: [PATCH 291/732] missing word in changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 9736edf4b0..d4b16ffec0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,7 +25,7 @@ that repo. - `quickfort`: now handles zones, locations, stockpile configuration, hauling routes, and more - `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased - `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types -- `suspendmanager`: suspend jobs when building high walls or filling corridors +- `suspendmanager`: suspend blocking jobs when building high walls or filling corridors ## Removed From 6401650c8d993fef40340d77a8b62f8c973a7132 Mon Sep 17 00:00:00 2001 From: wsfsbvchr <61580558+wsfsbvchr@users.noreply.github.com> Date: Fri, 9 Jun 2023 09:30:09 +0300 Subject: [PATCH 292/732] Update emigration.lua Adjusted to use of df.global.plotinfo. --- emigration.lua | 92 ++++++++++++++++++++------------------------------ 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/emigration.lua b/emigration.lua index 3ac5dbfeea..c6ab274128 100644 --- a/emigration.lua +++ b/emigration.lua @@ -1,29 +1,35 @@ --Allow stressed dwarves to emigrate from the fortress --- Updated for 0.47.05 and potentially for 50.08 by wsfsbvchr -- For 34.11 by IndigoFenix; update and cleanup by PeridexisErrant -- old version: http://dffd.bay12games.com/file.php?id=8404 ---@module = true ---@enable = true +--[====[ +emigration +========== +Allows dwarves to emigrate from the fortress when stressed, +in proportion to how badly stressed they are and adjusted +for who they would have to leave with - a dwarven merchant +being more attractive than leaving alone (or with an elf). +The check is made monthly. -local json = require('json') -local persist = require('persist-table') +A happy dwarf (ie with negative stress) will never emigrate. -local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence +Usage:: -enabled = enabled or false + emigration enable|disable +]====] -function isEnabled() - return enabled -end +enabled = enabled or false -local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled}) +local args = {...} +if args[1] == "enable" then + enabled = true +elseif args[1] == "disable" then + enabled = false end function desireToStay(unit,method,civ_id) -- on a percentage scale local value = 100 - unit.status.current_soul.personality.stress / 5000 - if method == 'merchant' then + if method == 'merchant' or method == 'diplomat' then if civ_id ~= unit.civ_id then value = value*2 end end if method == 'wild' then value = value*5 end @@ -47,7 +53,7 @@ function desert(u,method,civ) local hf_id = u.hist_figure_id local hf = df.historical_figure.find(u.hist_figure_id) - local fort_ent = df.global.ui.main.fortress_entity + local fort_ent = df.global.plotinfo.main.fortress_entity local civ_ent = df.historical_entity.find(hf.civ_id) local newent_id = -1 @@ -62,20 +68,20 @@ function desert(u,method,civ) -- erase the unit from the fortress entity for k,v in ipairs(fort_ent.histfig_ids) do if v == hf_id then - df.global.ui.main.fortress_entity.histfig_ids:erase(k) + df.global.plotinfo.main.fortress_entity.histfig_ids:erase(k) break end end for k,v in ipairs(fort_ent.hist_figures) do if v.id == hf_id then - df.global.ui.main.fortress_entity.hist_figures:erase(k) + df.global.plotinfo.main.fortress_entity.hist_figures:erase(k) break end end for k,v in ipairs(fort_ent.nemesis) do if v.figure.id == hf_id then - df.global.ui.main.fortress_entity.nemesis:erase(k) - df.global.ui.main.fortress_entity.nemesis_ids:erase(k) + df.global.plotinfo.main.fortress_entity.nemesis:erase(k) + df.global.plotinfo.main.fortress_entity.nemesis_ids:erase(k) break end end @@ -169,10 +175,9 @@ function checkmigrationnow() end if #merchant_civ_ids == 0 then - checkForDeserters('wild', df.global.ui.main.fortress_entity.entity_links[0].target) - else - for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end + checkForDeserters('wild', df.global.plotinfo.main.fortress_entity.entity_links[0].target) end + for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end end local function event_loop() @@ -182,42 +187,17 @@ local function event_loop() end end -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - enabled = false - return - end - - if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return +dfhack.onStateChange.loadEmigration = function(code) + if code==SC_MAP_LOADED then + if enabled then + print("Emigration enabled.") + event_loop() + else + print("Emigration disabled.") + end end - - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') - enabled = (persisted_data or {enabled=false})['enabled'] - event_loop() -end - -if dfhack_flags.module then - return end -if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then - dfhack.printerr('emigration needs a loaded fortress map to work') - return +if dfhack.isMapLoaded() then + dfhack.onStateChange.loadEmigration(SC_MAP_LOADED) end - -local args = {...} -if dfhack_flags and dfhack_flags.enable then - args = {dfhack_flags.enable_state and 'enable' or 'disable'} -end - -if args[1] == "enable" then - enabled = true -elseif args[1] == "disable" then - enabled = false -else - return -end - -event_loop() -persist_state() From cf61aa0b65abd3637bcf9a5cccf1fc70e0ae892e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 Jun 2023 03:23:59 -0700 Subject: [PATCH 293/732] don't leak data into raw db entries --- internal/quickfort/build.lua | 59 ++++++++++++++------------------ internal/quickfort/building.lua | 2 +- internal/quickfort/place.lua | 60 ++++++++++++++------------------- internal/quickfort/zone.lua | 29 ++++++---------- 4 files changed, 62 insertions(+), 88 deletions(-) diff --git a/internal/quickfort/build.lua b/internal/quickfort/build.lua index 9e79055086..af6e4bf02a 100644 --- a/internal/quickfort/build.lua +++ b/internal/quickfort/build.lua @@ -624,7 +624,7 @@ local function make_track_entry(name, data, revmap, is_ramp) end -- grouped by type, generally in ui order -local building_db = { +local building_db_raw = { -- basic building types a={label='Armor Stand', type=df.building_type.Armorstand}, b={label='Bed', type=df.building_type.Bed, @@ -640,13 +640,13 @@ local building_db = { G={label='Floor Grate', type=df.building_type.GrateFloor, is_valid_tile_fn=is_tile_coverable}, B={label='Vertical Bars', type=df.building_type.BarsVertical}, - ['{Alt}b']={label='Floor Bars', type=df.building_type.BarsFloor, + ['~b']={label='Floor Bars', type=df.building_type.BarsFloor, is_valid_tile_fn=is_tile_coverable}, f={label='Cabinet', type=df.building_type.Cabinet}, h={label='Container', type=df.building_type.Box}, r={label='Weapon Rack', type=df.building_type.Weaponrack}, s={label='Statue', type=df.building_type.Statue}, - ['{Alt}s']={label='Slab', type=df.building_type.Slab}, + ['~s']={label='Slab', type=df.building_type.Slab}, t={label='Table', type=df.building_type.Table}, gs=make_bridge_entry(df.building_bridgest.T_direction.Retracting), gw=make_bridge_entry(df.building_bridgest.T_direction.Up), @@ -704,9 +704,9 @@ local building_db = { R={label='Traction Bench', type=df.building_type.TractionBench, additional_orders={'table', 'mechanisms', 'cloth rope'}}, N={label='Nest Box', type=df.building_type.NestBox}, - ['{Alt}h']={label='Hive', type=df.building_type.Hive, props_fn=do_hive_props}, - ['{Alt}a']={label='Offering Place', type=df.building_type.OfferingPlace}, - ['{Alt}c']={label='Bookcase', type=df.building_type.Bookcase}, + ['~h']={label='Hive', type=df.building_type.Hive, props_fn=do_hive_props}, + ['~a']={label='Offering Place', type=df.building_type.OfferingPlace}, + ['~c']={label='Bookcase', type=df.building_type.Bookcase}, F={label='Display Furniture', type=df.building_type.DisplayFurniture}, -- basic building types with extents @@ -917,32 +917,24 @@ local building_db = { trackrampNSEW=make_track_entry('NSEW', nil, nil, true) } -local function ensure_data(db_entry) - if not db_entry.props then - db_entry.props = {} - end - if not db_entry.links then - db_entry.links = {give_to={}, take_from={}} - end - if not db_entry.adjustments then - db_entry.adjustments = {} - end -end - local function merge_db_entries(self, other) if self.label ~= other.label then error(('cannot merge db entries of different types: %s != %s'):format(self.label, other.label)) end - ensure_data(self) - ensure_data(other) utils.assign(self.props, other.props) + for _, to in ipairs(other.links.give_to) do + table.insert(self.links.give_to, to) + end + for _, from in ipairs(other.links.take_from) do + table.insert(self.links.take_from, from) + end for _,adj in ipairs(other.adjustments) do table.insert(self.adjustments, adj) end end -- fill in default values if they're not already specified -for _, v in pairs(building_db) do +for _, v in pairs(building_db_raw) do v.merge_fn = merge_db_entries if v.has_extents then if not v.min_width then @@ -973,8 +965,8 @@ for _, v in pairs(building_db) do end -- case sensitive aliases -building_db.g = building_db.gs -building_db.Ms = building_db.Msu +building_db_raw.g = building_db_raw.gs +building_db_raw.Ms = building_db_raw.Msu -- case insensitive aliases for keys in the db -- this allows us to keep compatibility with the old python quickfort and makes @@ -1065,11 +1057,11 @@ local building_aliases = { trackrampnew='trackrampNEW', trackrampsew='trackrampSEW', trackrampnsew='trackrampNSEW', - ['~h']='{Alt}h', - ['~a']='{Alt}a', - ['~c']='{Alt}c', - ['~b']='{Alt}b', - ['~s']='{Alt}s', + ['{Alt}h']='~h', + ['{Alt}a']='~a', + ['{Alt}c']='~c', + ['{Alt}b']='~b', + ['{Alt}s']='~s', } local build_key_pattern = '~?%w+' @@ -1079,7 +1071,7 @@ local function custom_building(_, keys) -- properties and adjustments may hide the alias from the building.init_buildings algorithm -- so we might have to do our own mapping here local resolved_alias = building_aliases[token_and_label.token:lower()] - local db_entry = rawget(building_db, resolved_alias or token_and_label.token) + local db_entry = rawget(building_db_raw, resolved_alias or token_and_label.token) if not db_entry then return nil end @@ -1089,7 +1081,9 @@ local function custom_building(_, keys) db_entry.label = ('%s/%s'):format(db_entry.label, token_and_label.label) db_entry.transform_suffix = ('/%s%s'):format(token_and_label.label, db_entry.transform_suffix) end - ensure_data(db_entry) + db_entry.props = {} + db_entry.links = {give_to={}, take_from={}} + db_entry.adjustments = {} local props, next_token_pos = quickfort_parse.parse_properties(keys, props_start_pos) if props.name then db_entry.props.name = props.name @@ -1107,6 +1101,7 @@ local function custom_building(_, keys) return db_entry end +local building_db = {} setmetatable(building_db, {__index=custom_building}) -- @@ -1120,8 +1115,7 @@ local function create_building(b, cache, dry_run) b.width, b.height, db_entry.label, b.pos.x, b.pos.y, b.pos.z, table.concat(b.cells, ', ')) if dry_run then return end - local fields = {} - if db_entry.fields then fields = copyall(db_entry.fields) end + local fields = db_entry.fields and copyall(db_entry.fields) or {} local use_extents = db_entry.has_extents and not (db_entry.no_extents_if_solid and is_extent_solid(b)) if use_extents then @@ -1137,7 +1131,6 @@ local function create_building(b, cache, dry_run) -- is supposed to prevent this from ever happening error(string.format('unable to place %s: %s', db_entry.label, err)) end - ensure_data(db_entry) utils.assign(bld, db_entry.props) if db_entry.adjust_fn then db_entry:adjust_fn(bld) diff --git a/internal/quickfort/building.lua b/internal/quickfort/building.lua index d92e0326b4..7c16b84b74 100644 --- a/internal/quickfort/building.lua +++ b/internal/quickfort/building.lua @@ -96,7 +96,7 @@ local function flood_fill(ctx, grid, seen_cells, x, y, data, db, aliases) if data.db_entry then if data.db_entry.merge_fn then data.db_entry:merge_fn(db_entry) end else - data.db_entry = copyall(db_entry) + data.db_entry = db_entry end table.insert(data.cells, cell) seen_cells[cell] = true diff --git a/internal/quickfort/place.lua b/internal/quickfort/place.lua index 2eff9e5df3..9d7a35102d 100644 --- a/internal/quickfort/place.lua +++ b/internal/quickfort/place.lua @@ -51,31 +51,18 @@ local function is_valid_stockpile_extent(s) return false end -local function ensure_data(db_entry) - if not db_entry.links then - db_entry.links = {give_to={}, take_from={}} - end - if not db_entry.props then - db_entry.props = {} - end - if not db_entry.adjustments then - db_entry.adjustments = {} - end -end - local function merge_db_entries(self, other) if self.label ~= other.label then error(('cannot merge db entries of different types: %s != %s'):format(self.label, other.label)) end - ensure_data(self) - utils.assign(self.props, other.props or {}) - for adj in pairs(other.adjustments or {}) do + utils.assign(self.props, other.props) + for adj in pairs(other.adjustments) do self.adjustments[adj] = true end - for _, to in ipairs(other.links and other.links.give_to or {}) do + for _, to in ipairs(other.links.give_to) do table.insert(self.links.give_to, to) end - for _, from in ipairs(other.links and other.links.take_from or {}) do + for _, from in ipairs(other.links.take_from) do table.insert(self.links.take_from, from) end end @@ -87,7 +74,7 @@ local stockpile_template = { merge_fn = merge_db_entries, } -local stockpile_db = { +local stockpile_db_raw = { a={label='Animal', categories={'animals'}}, f={label='Food', categories={'food'}, want_barrels=true}, u={label='Furniture', categories={'furniture'}}, @@ -107,7 +94,7 @@ local stockpile_db = { d={label='Armor', categories={'armor'}, want_bins=true}, c={label='Custom', categories={}} } -for _, v in pairs(stockpile_db) do utils.assign(v, stockpile_template) end +for _, v in pairs(stockpile_db_raw) do utils.assign(v, stockpile_template) end local place_key_pattern = '%w+' @@ -130,7 +117,7 @@ local function make_db_entry(keys) for k in keys:gmatch('.') do local digit = tonumber(k) if digit and prev_key then - local raw_db_entry = rawget(stockpile_db, prev_key) + local raw_db_entry = rawget(stockpile_db_raw, prev_key) if raw_db_entry.want_bins then if not in_digits then num_bins = 0 end num_bins = add_resource_digit(num_bins, digit) @@ -144,13 +131,13 @@ local function make_db_entry(keys) in_digits = true goto continue end - if not rawget(stockpile_db, k) then return nil end - table.insert(labels, stockpile_db[k].label) - table.insert(categories, stockpile_db[k].categories[1]) - want_bins = want_bins or stockpile_db[k].want_bins - want_barrels = want_barrels or stockpile_db[k].want_barrels + if not rawget(stockpile_db_raw, k) then return nil end + table.insert(labels, stockpile_db_raw[k].label) + table.insert(categories, stockpile_db_raw[k].categories[1]) + want_bins = want_bins or stockpile_db_raw[k].want_bins + want_barrels = want_barrels or stockpile_db_raw[k].want_barrels want_wheelbarrows = - want_wheelbarrows or stockpile_db[k].want_wheelbarrows + want_wheelbarrows or stockpile_db_raw[k].want_wheelbarrows prev_key = k -- flag that we're starting a new (potential) digit sequence and we -- should reset the accounting for the relevent resource number @@ -165,7 +152,10 @@ local function make_db_entry(keys) want_wheelbarrows=want_wheelbarrows, num_bins=num_bins, num_barrels=num_barrels, - num_wheelbarrows=num_wheelbarrows + num_wheelbarrows=num_wheelbarrows, + links={give_to={}, take_from={}}, + props={}, + adjustments={}, } utils.assign(db_entry, stockpile_template) return db_entry @@ -178,7 +168,6 @@ local function custom_stockpile(_, keys) if token_and_label.label then db_entry.label = ('%s/%s'):format(db_entry.label, token_and_label.label) end - ensure_data(db_entry) if next(adjustments) then db_entry.adjustments[adjustments] = true end @@ -241,6 +230,7 @@ local function custom_stockpile(_, keys) return db_entry end +local stockpile_db = {} setmetatable(stockpile_db, {__index=custom_stockpile}) local function configure_stockpile(bld, db_entry) @@ -249,7 +239,7 @@ local function configure_stockpile(bld, db_entry) log('enabling stockpile category: %s', cat) stockpiles.import_stockpile(name, {id=bld.id, mode='enable'}) end - for adjlist in pairs(db_entry.adjustments) do + for adjlist in pairs(db_entry.adjustments or {}) do for _,adj in ipairs(adjlist) do log('applying stockpile preset: %s %s (filters=)', adj.mode, adj.name, table.concat(adj.filters or {}, ',')) stockpiles.import_stockpile(adj.name, {id=bld.id, mode=adj.mode, filters=adj.filters}) @@ -297,10 +287,11 @@ local function create_stockpile(s, link_data, dry_run) -- is supposed to prevent this from ever happening error(string.format('unable to place stockpile: %s', err)) end - utils.assign(bld, db_entry.props) + local props = db_entry.props + utils.assign(bld, props) configure_stockpile(bld, db_entry) - if db_entry.props.name then - table.insert(ensure_key(link_data.piles, db_entry.props.name), bld) + if props.name then + table.insert(ensure_key(link_data.piles, props.name), bld) end for _,recipient in ipairs(db_entry.links.give_to) do log('giving to: "%s"', recipient) @@ -417,7 +408,6 @@ function do_run(zlevel, grid, ctx) local link_data = {piles={}, nodes={}} for _, s in ipairs(piles) do if s.pos then - ensure_data(s.db_entry) local ntiles = create_stockpile(s, link_data, dry_run) stats.place_tiles.value = stats.place_tiles.value + ntiles stats.place_designated.value = stats.place_designated.value + 1 @@ -434,9 +424,9 @@ function do_orders(zlevel, grid, ctx) quickfort_building.init_buildings(ctx, zlevel, grid, piles, stockpile_db) for _, s in ipairs(piles) do local db_entry = s.db_entry - ensure_data(db_entry) + local props = db_entry.props or {} quickfort_orders.enqueue_container_orders(ctx, - db_entry.props.max_bins, db_entry.props.max_barrels, db_entry.props.max_wheelbarrows) + props.max_bins, props.max_barrels, props.max_wheelbarrows) end end diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 3dc7ab8f94..52d383563b 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -82,21 +82,14 @@ local function is_valid_zone_extent(s) return false end -local function ensure_data(db_entry) - if not db_entry.data then - db_entry.data = {{}} - utils.assign(db_entry.data[1], db_entry.default_data) - end -end - local function merge_db_entries(self, other) if self.label ~= other.label then error(('cannot merge db entries of different types: %s != %s'):format(self.label, other.label)) end - ensure_data(self) - ensure_data(other) - for i=1,#self.data do - utils.assign(self.data[i], other.data[i]) + if other.data then + for i=1,#self.data do + utils.assign(self.data[i], other.data[i] or {}) + end end end @@ -107,7 +100,7 @@ local zone_template = { merge_fn=merge_db_entries, } -local zone_db = { +local zone_db_raw = { m={label='Meeting Area', default_data={type=df.civzone_type.MeetingHall}}, b={label='Bedroom', default_data={type=df.civzone_type.Bedroom}}, h={label='Dining Hall', default_data={type=df.civzone_type.DiningHall}}, @@ -132,7 +125,7 @@ local zone_db = { assign={zone_settings={gather={pick_trees=true, pick_shrubs=true, gather_fallen=true}}}}}, c={label='Clay', default_data={type=df.civzone_type.ClayCollection}}, } -for _, v in pairs(zone_db) do +for _, v in pairs(zone_db_raw) do utils.assign(v, zone_template) ensure_key(v.default_data, 'assign').is_active = 8 -- set to active by default end @@ -285,11 +278,11 @@ local function get_noble_unit(noble) end local function parse_zone_config(c, props) - if not rawget(zone_db, c) then + if not rawget(zone_db_raw, c) then return 'Invalid', nil end local zone_data = {} - local db_entry = zone_db[c] + local db_entry = zone_db_raw[c] utils.assign(zone_data, db_entry.default_data) zone_data.location = parse_location_props(props) if props.active == 'false' then @@ -344,6 +337,7 @@ local function custom_zone(_, keys) return db_entry end +local zone_db = {} setmetatable(zone_db, {__index=custom_zone}) local word_table = df.global.world.raws.language.word_table[0][35] @@ -471,10 +465,7 @@ function do_run(zlevel, grid, ctx) ' from spreadsheet cells: %s', db_entry.label, zone.pos.x, zone.pos.y, zone.pos.z, table.concat(zone.cells, ', ')) - if not db_entry.data then - ensure_data(db_entry) - end - for _,data in ipairs(db_entry.data) do + for _,data in ipairs(db_entry.data or {}) do log('creating zone with properties:') logfn(printall_recurse, data) local ntiles = create_zone(zone, data, ctx) From 65dec8546555e13b24e5b71272919e7cfdefff10 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 Jun 2023 03:28:29 -0700 Subject: [PATCH 294/732] connect the location back to the zone --- internal/quickfort/zone.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 52d383563b..4f5f0e979a 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -402,6 +402,7 @@ local function set_location(zone, location, ctx) bld.flags[flag] = val end end + bld.contents.building_ids:insert('#', zone.id) end zone.site_id = site.id zone.location_id = loc_id From aa572dab79fbe6498a8d9be39f046cb7d6332714 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 Jun 2023 12:05:11 -0700 Subject: [PATCH 295/732] properly categorize zones attached to locations --- internal/quickfort/zone.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 4f5f0e979a..265bd9ecfc 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -406,6 +406,8 @@ local function set_location(zone, location, ctx) end zone.site_id = site.id zone.location_id = loc_id + -- categorize the zone in the location vector + utils.insert_sorted(df.global.world.buildings.other.LOCATION_ASSIGNED, zone, 'id') if location.label then -- remember this location for future associations in this blueprint ensure_keys(ctx, 'zone', 'locations')[location.label] = loc_id From 2047d678461ec44bcf691b5998fdc1276e9a80f2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 Jun 2023 12:45:44 -0700 Subject: [PATCH 296/732] we don't need to init occupations; bounds check desired items --- internal/quickfort/zone.lua | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 265bd9ecfc..9697db48cd 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -133,16 +133,6 @@ end -- we may want to offer full name aliases for the single letter ones above local aliases = {} -local hospital_max_values = { - thread=1500000, - cloth=1000000, - splints=100, - crutches=100, - plaster=15000, - buckets=100, - soap=15000 -} - local valid_locations = { tavern={new=df.abstract_building_inn_tavernst, assign={name={type=df.language_name_type.SymbolFood}, @@ -177,15 +167,6 @@ for _, v in pairs(valid_locations) do v.assign.name.parts_of_speech.FirstAdjective = df.part_of_speech.Adjective end -local location_occupations = { - tavern={df.occupation_type.TAVERN_KEEPER, df.occupation_type.PERFORMER}, - hospital={df.occupation_type.DOCTOR, df.occupation_type.DIAGNOSTICIAN, - df.occupation_type.SURGEON, df.occupation_type.BONE_DOCTOR}, - guildhall={}, - library={}, - temple={}, -} - local prop_prefix = 'desired_' local function parse_location_props(props) @@ -226,7 +207,7 @@ local function parse_location_props(props) local prop = props[short_prop] props[short_prop] = nil local val = tonumber(prop) - if not val or val ~= math.floor(val) or val < 0 then + if not val or val ~= math.floor(val) or val < 0 or val > 999 then dfhack.printerr(('ignoring invalid %s value: "%s"'):format(short_prop, prop)) goto continue end @@ -366,20 +347,6 @@ local function set_location(zone, location, ctx) utils.assign(data, location.data) if not loc_id then loc_id = site.next_building_id - local occupations = df.global.world.occupations.all - for _,ot in ipairs(location_occupations[location.type]) do - local occ_id = df.global.occupation_next_id - occupations:insert('#', { - new=df.occupation, - id=occ_id, - type=ot, - location_id=loc_id, - site_id=site.id, - }) - table.insert(ensure_key(data, 'occupations'), occupations[#occupations-1]) - df.global.occupation_next_id = df.global.occupation_next_id + 1 - end - data.name = generate_name() data.id = loc_id data.site_id = site.id From 02ac437a3663040712899658dd34bca10d56de57 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 Jun 2023 14:01:54 -0700 Subject: [PATCH 297/732] clean up code --- internal/quickfort/zone.lua | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 9697db48cd..779aea1e84 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -353,9 +353,10 @@ local function set_location(zone, location, ctx) data.pos = copyall(site.pos) for _,entity_site_link in ipairs(site.entity_links) do local he = df.historical_entity.find(entity_site_link.entity_id) - if not he or he.type ~= df.historical_entity_type.SiteGovernment then goto continue end - data.site_owner_id = he.id - ::continue:: + if he and he.type == df.historical_entity_type.SiteGovernment then + data.site_owner_id = he.id + break + end end site.buildings:insert('#', data) site.next_building_id = site.next_building_id + 1 @@ -364,10 +365,8 @@ local function set_location(zone, location, ctx) for flag, val in pairs(data.assign.flags) do bld.flags[flag] = val end - if data.flags then - for flag, val in pairs(data.flags) do - bld.flags[flag] = val - end + for flag, val in pairs(data.flags or {}) do + bld.flags[flag] = val end bld.contents.building_ids:insert('#', zone.id) end From e1cbd625f2358826cba910cb9ac582f7474ff21f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 Jun 2023 15:41:21 -0700 Subject: [PATCH 298/732] ensure assigned_unit defaults to -1 instead of 0 --- internal/quickfort/zone.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 779aea1e84..3f3dc8451b 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -384,8 +384,11 @@ local function create_zone(zone, data, ctx) local extents, ntiles = quickfort_building.make_extents(zone, ctx.dry_run) if ctx.dry_run then return ntiles end - local fields = {room={x=zone.pos.x, y=zone.pos.y, width=zone.width, - height=zone.height, extents=extents}} + local fields = { + assigned_unit_id=-1, + room={x=zone.pos.x, y=zone.pos.y, width=zone.width, height=zone.height, + extents=extents}, + } local bld, err = dfhack.buildings.constructBuilding{ type=df.building_type.Civzone, subtype=data.type, abstract=true, pos=zone.pos, width=zone.width, height=zone.height, From 756350bb6098501963189c12d9b88b2f77fa4c70 Mon Sep 17 00:00:00 2001 From: wsfsbvchr <61580558+wsfsbvchr@users.noreply.github.com> Date: Sat, 10 Jun 2023 13:26:11 +0300 Subject: [PATCH 299/732] Update emigration.lua Proper fix for stuff, hopefully. --- emigration.lua | 106 ++++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/emigration.lua b/emigration.lua index c6ab274128..a242d90307 100644 --- a/emigration.lua +++ b/emigration.lua @@ -1,35 +1,28 @@ --Allow stressed dwarves to emigrate from the fortress -- For 34.11 by IndigoFenix; update and cleanup by PeridexisErrant -- old version: http://dffd.bay12games.com/file.php?id=8404 ---[====[ -emigration -========== -Allows dwarves to emigrate from the fortress when stressed, -in proportion to how badly stressed they are and adjusted -for who they would have to leave with - a dwarven merchant -being more attractive than leaving alone (or with an elf). -The check is made monthly. +--@module = true +--@enable = true -A happy dwarf (ie with negative stress) will never emigrate. +local json = require('json') +local persist = require('persist-table') -Usage:: - - emigration enable|disable -]====] +local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence enabled = enabled or false -local args = {...} -if args[1] == "enable" then - enabled = true -elseif args[1] == "disable" then - enabled = false +function isEnabled() + return enabled +end + +local function persist_state() + persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled}) end function desireToStay(unit,method,civ_id) -- on a percentage scale local value = 100 - unit.status.current_soul.personality.stress / 5000 - if method == 'merchant' or method == 'diplomat' then + if method == 'merchant' then if civ_id ~= unit.civ_id then value = value*2 end end if method == 'wild' then value = value*5 end @@ -50,42 +43,40 @@ function desert(u,method,civ) u.flags2.visitor = true u.animal.leave_countdown = 2 end - local hf_id = u.hist_figure_id local hf = df.historical_figure.find(u.hist_figure_id) - local fort_ent = df.global.plotinfo.main.fortress_entity + local fort_ent = df.global.ui.main.fortress_entity local civ_ent = df.historical_entity.find(hf.civ_id) - local newent_id = -1 local newsite_id = -1 - + -- free owned rooms for i = #u.owned_buildings-1, 0, -1 do local temp_bld = df.building.find(u.owned_buildings[i].id) dfhack.buildings.setOwner(temp_bld, nil) end - + -- erase the unit from the fortress entity for k,v in ipairs(fort_ent.histfig_ids) do if v == hf_id then - df.global.plotinfo.main.fortress_entity.histfig_ids:erase(k) + df.global.ui.main.fortress_entity.histfig_ids:erase(k) break end end for k,v in ipairs(fort_ent.hist_figures) do if v.id == hf_id then - df.global.plotinfo.main.fortress_entity.hist_figures:erase(k) + df.global.ui.main.fortress_entity.hist_figures:erase(k) break end end for k,v in ipairs(fort_ent.nemesis) do if v.figure.id == hf_id then - df.global.plotinfo.main.fortress_entity.nemesis:erase(k) - df.global.plotinfo.main.fortress_entity.nemesis_ids:erase(k) + df.global.ui.main.fortress_entity.nemesis:erase(k) + df.global.ui.main.fortress_entity.nemesis_ids:erase(k) break end end - + -- remove the old entity link and create new one to indicate former membership hf.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = fort_ent.id, link_strength = 100}) for k,v in ipairs(hf.entity_links) do @@ -94,7 +85,7 @@ function desert(u,method,civ) break end end - + -- try to find a new entity for the unit to join for k,v in ipairs(civ_ent.entity_links) do if v.type == df.entity_entity_link_type.CHILD and v.target ~= fort_ent.id then @@ -102,7 +93,7 @@ function desert(u,method,civ) break end end - + if newent_id > -1 then hf.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newent_id, link_strength = 100}) @@ -113,11 +104,9 @@ function desert(u,method,civ) break end end - local newent = df.historical_entity.find(newent_id) newent.histfig_ids:insert('#', hf_id) newent.hist_figures:insert('#', hf) - local hf_event_id = df.global.hist_event_next_id df.global.hist_event_next_id = df.global.hist_event_next_id+1 df.global.world.history.events:insert("#", {new = df.history_event_add_hf_entity_linkst, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, civ = newent_id, histfig = hf_id, link_type = 0}) @@ -127,7 +116,6 @@ function desert(u,method,civ) df.global.world.history.events:insert("#", {new = df.history_event_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, hfid = hf_id, state = 1, reason = -1, site = newsite_id}) end end - print(line) dfhack.gui.showAnnouncement(line, COLOR_WHITE) end @@ -175,9 +163,10 @@ function checkmigrationnow() end if #merchant_civ_ids == 0 then - checkForDeserters('wild', df.global.plotinfo.main.fortress_entity.entity_links[0].target) + checkForDeserters('wild', df.global.ui.main.fortress_entity.entity_links[0].target) + else + for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end end - for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end end local function event_loop() @@ -187,17 +176,42 @@ local function event_loop() end end -dfhack.onStateChange.loadEmigration = function(code) - if code==SC_MAP_LOADED then - if enabled then - print("Emigration enabled.") - event_loop() - else - print("Emigration disabled.") - end +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + enabled = false + return + end + + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return end + + local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') + enabled = (persisted_data or {enabled=false})['enabled'] + event_loop() +end + +if dfhack_flags.module then + return +end + +if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then + dfhack.printerr('emigration needs a loaded fortress map to work') + return end -if dfhack.isMapLoaded() then - dfhack.onStateChange.loadEmigration(SC_MAP_LOADED) +local args = {...} +if dfhack_flags and dfhack_flags.enable then + args = {dfhack_flags.enable_state and 'enable' or 'disable'} end + +if args[1] == "enable" then + enabled = true +elseif args[1] == "disable" then + enabled = false +else + return +end + +event_loop() +persist_state() From 117b0792fc903c73529b73407f8425c153d00712 Mon Sep 17 00:00:00 2001 From: wsfsbvchr <61580558+wsfsbvchr@users.noreply.github.com> Date: Sat, 10 Jun 2023 13:34:44 +0300 Subject: [PATCH 300/732] Update emigration.lua One more fix, maybe now... --- emigration.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/emigration.lua b/emigration.lua index a242d90307..5288f65684 100644 --- a/emigration.lua +++ b/emigration.lua @@ -45,7 +45,7 @@ function desert(u,method,civ) end local hf_id = u.hist_figure_id local hf = df.historical_figure.find(u.hist_figure_id) - local fort_ent = df.global.ui.main.fortress_entity + local fort_ent = df.global.plotinfo.main.fortress_entity local civ_ent = df.historical_entity.find(hf.civ_id) local newent_id = -1 local newsite_id = -1 @@ -59,20 +59,20 @@ function desert(u,method,civ) -- erase the unit from the fortress entity for k,v in ipairs(fort_ent.histfig_ids) do if v == hf_id then - df.global.ui.main.fortress_entity.histfig_ids:erase(k) + df.global.plotinfo.main.fortress_entity.histfig_ids:erase(k) break end end for k,v in ipairs(fort_ent.hist_figures) do if v.id == hf_id then - df.global.ui.main.fortress_entity.hist_figures:erase(k) + df.global.plotinfo.main.fortress_entity.hist_figures:erase(k) break end end for k,v in ipairs(fort_ent.nemesis) do if v.figure.id == hf_id then - df.global.ui.main.fortress_entity.nemesis:erase(k) - df.global.ui.main.fortress_entity.nemesis_ids:erase(k) + df.global.plotinfo.main.fortress_entity.nemesis:erase(k) + df.global.plotinfo.main.fortress_entity.nemesis_ids:erase(k) break end end @@ -163,7 +163,7 @@ function checkmigrationnow() end if #merchant_civ_ids == 0 then - checkForDeserters('wild', df.global.ui.main.fortress_entity.entity_links[0].target) + checkForDeserters('wild', df.global.plotinfo.main.fortress_entity.entity_links[0].target) else for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end end From c6f465a59a0dee659601391ac6a1f6492ff3e792 Mon Sep 17 00:00:00 2001 From: wsfsbvchr <61580558+wsfsbvchr@users.noreply.github.com> Date: Sat, 10 Jun 2023 17:31:03 +0300 Subject: [PATCH 301/732] Update emigration.lua Removed excess whitespace --- emigration.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/emigration.lua b/emigration.lua index 5288f65684..4bad19ca94 100644 --- a/emigration.lua +++ b/emigration.lua @@ -49,13 +49,13 @@ function desert(u,method,civ) local civ_ent = df.historical_entity.find(hf.civ_id) local newent_id = -1 local newsite_id = -1 - + -- free owned rooms for i = #u.owned_buildings-1, 0, -1 do local temp_bld = df.building.find(u.owned_buildings[i].id) dfhack.buildings.setOwner(temp_bld, nil) end - + -- erase the unit from the fortress entity for k,v in ipairs(fort_ent.histfig_ids) do if v == hf_id then @@ -76,7 +76,7 @@ function desert(u,method,civ) break end end - + -- remove the old entity link and create new one to indicate former membership hf.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = fort_ent.id, link_strength = 100}) for k,v in ipairs(hf.entity_links) do @@ -85,7 +85,7 @@ function desert(u,method,civ) break end end - + -- try to find a new entity for the unit to join for k,v in ipairs(civ_ent.entity_links) do if v.type == df.entity_entity_link_type.CHILD and v.target ~= fort_ent.id then @@ -93,7 +93,7 @@ function desert(u,method,civ) break end end - + if newent_id > -1 then hf.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newent_id, link_strength = 100}) From faacd62051bc644d28561c807fc56247f06146b0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 10 Jun 2023 16:43:34 -0700 Subject: [PATCH 302/732] better handling for 2D notes blueprints and represent horizontal spacing intelligently --- internal/quickfort/notes.lua | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/quickfort/notes.lua b/internal/quickfort/notes.lua index ecdeb9923f..aa64168607 100644 --- a/internal/quickfort/notes.lua +++ b/internal/quickfort/notes.lua @@ -12,16 +12,24 @@ local log = quickfort_common.log function do_run(_, grid, ctx) local cells = quickfort_parse.get_ordered_grid_cells(grid) - local lines = {} - local prev_y = nil + local line, lines = {}, {} + local prev_x, prev_y = nil, nil for _,cell in ipairs(cells) do - if prev_y then - for dy = prev_y,cell.y-2 do + if prev_y ~= cell.y and #line > 0 then + table.insert(lines, table.concat(line, ' ')) + for _ = prev_y or cell.y,cell.y-2 do table.insert(lines, '') end + line = {} end - table.insert(lines, cell.text) - prev_y = cell.y + for _ = prev_x or cell.x,cell.x-2 do + table.insert(line, ' ') + end + table.insert(line, cell.text) + prev_x, prev_y = cell.x, cell.y + end + if #line > 0 then + table.insert(lines, table.concat(line, ' ')) end table.insert(ctx.messages, table.concat(lines, '\n')) end From eb5a8b04c6590e76849c09c43066a7846371c168 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 11 Jun 2023 16:22:03 -0700 Subject: [PATCH 303/732] fix temples (units can now pray in them) --- internal/quickfort/zone.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 3f3dc8451b..3066d7df55 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -152,6 +152,7 @@ local valid_locations = { contents={desired_paper=10, need_more={paper=true}}}}, temple={new=df.abstract_building_templest, assign={name={type=df.language_name_type.Temple}, + deity_data={Religion=-1}, contents={desired_instruments=5, need_more={instruments=true}}}}, } local valid_restrictions = { @@ -372,8 +373,9 @@ local function set_location(zone, location, ctx) end zone.site_id = site.id zone.location_id = loc_id - -- categorize the zone in the location vector - utils.insert_sorted(df.global.world.buildings.other.LOCATION_ASSIGNED, zone, 'id') + -- recategorize the civzone as attached to a location + zone:uncategorize() + zone:categorize(true) if location.label then -- remember this location for future associations in this blueprint ensure_keys(ctx, 'zone', 'locations')[location.label] = loc_id From 6d3e3dfaff1d29b7d7045a2c56543221f6f288d1 Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 11 Jun 2023 21:14:47 -0700 Subject: [PATCH 304/732] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 0390e5cdb6..b549f8d6fd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ that repo. ## Fixes - `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters +- `emigration`: reassign home site for emigrating units so they don't just come right back to the fort ## Misc Improvements - `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants From 32ab29298b7cd4405e038fc2481a3e3eb7392dca Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 12 Jun 2023 02:26:31 -0700 Subject: [PATCH 305/732] make properties override configuration --- internal/quickfort/place.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/quickfort/place.lua b/internal/quickfort/place.lua index 9d7a35102d..5a91383578 100644 --- a/internal/quickfort/place.lua +++ b/internal/quickfort/place.lua @@ -288,8 +288,8 @@ local function create_stockpile(s, link_data, dry_run) error(string.format('unable to place stockpile: %s', err)) end local props = db_entry.props - utils.assign(bld, props) configure_stockpile(bld, db_entry) + utils.assign(bld, props) if props.name then table.insert(ensure_key(link_data.piles, props.name), bld) end From 3054fc0a400ed8a459dc93f6c03f58c2916afe15 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 12 Jun 2023 13:15:44 -0700 Subject: [PATCH 306/732] logistics integration for quickfort place mode --- internal/quickfort/place.lua | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/internal/quickfort/place.lua b/internal/quickfort/place.lua index 5a91383578..4da103a016 100644 --- a/internal/quickfort/place.lua +++ b/internal/quickfort/place.lua @@ -56,6 +56,7 @@ local function merge_db_entries(self, other) error(('cannot merge db entries of different types: %s != %s'):format(self.label, other.label)) end utils.assign(self.props, other.props) + utils.assign(self.logistics, other.logistics) for adj in pairs(other.adjustments) do self.adjustments[adj] = true end @@ -156,6 +157,7 @@ local function make_db_entry(keys) links={give_to={}, take_from={}}, props={}, adjustments={}, + logistics={}, } utils.assign(db_entry, stockpile_template) return db_entry @@ -172,6 +174,24 @@ local function custom_stockpile(_, keys) db_entry.adjustments[adjustments] = true end + -- logistics properties + if props.automelt == 'true' then + db_entry.logistics.automelt = true + props.automelt = nil + end + if props.autotrade == 'true' then + db_entry.logistics.autotrade = true + props.autotrade = nil + end + if props.autodump == 'true' then + db_entry.logistics.autodump = true + props.autodump = nil + end + if props.autotrain == 'true' then + db_entry.logistics.autotrain = true + props.autotrain = nil + end + -- convert from older parsing style to properties db_entry.props.max_barrels = db_entry.num_barrels db_entry.num_barrels = nil @@ -301,6 +321,23 @@ local function create_stockpile(s, link_data, dry_run) log('taking from: "%s"', supplier) table.insert(link_data.nodes, {from=supplier, to=bld}) end + if next(db_entry.logistics) then + local logistics_command = {'logistics', 'add', '-s', tostring(bld.stockpile_number)} + if db_entry.logistics.automelt then + table.insert(logistics_command, 'melt') + end + if db_entry.logistics.autotrade then + table.insert(logistics_command, 'trade') + end + if db_entry.logistics.autodump then + table.insert(logistics_command, 'dump') + end + if db_entry.logistics.autotrain then + table.insert(logistics_command, 'train') + end + log('running logistics command: "%s"', table.concat(logistics_command, ' ')) + dfhack.run_command(logistics_command) + end return ntiles end From 876c66e0ab2e91b8c2d8e09689bf5c0f52ce3dcc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 12 Jun 2023 17:08:52 -0700 Subject: [PATCH 307/732] allow max_general_orders to be set for workshops --- internal/quickfort/build.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/quickfort/build.lua b/internal/quickfort/build.lua index af6e4bf02a..53f50b105c 100644 --- a/internal/quickfort/build.lua +++ b/internal/quickfort/build.lua @@ -230,6 +230,10 @@ local function do_workshop_furnace_props(db_entry, props) db_entry.links.give_to = argparse.stringList(props.give_to) props.give_to = nil end + if props.max_general_orders and tonumber(props.max_general_orders) then + ensure_key(db_entry.props, 'profile').max_general_orders = math.max(0, math.min(10, tonumber(props.max_general_orders))) + props.max_general_orders = nil + end end local function do_roller_props(db_entry, props) From c04a70610a289372d0e4e34bdeca7f8d8b6c1554 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Tue, 13 Jun 2023 07:47:52 +0200 Subject: [PATCH 308/732] [suspendmanager] Performance improvements --- suspendmanager.lua | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 0d6a9b43cb..644948d2d8 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -54,6 +54,7 @@ EXTERNAL_REASONS = { ---@class SuspendManager ---@field preventBlocking boolean ---@field suspensions table +---@field lastAutoRunTick integer SuspendManager = defclass(SuspendManager) SuspendManager.ATTRS { --- When enabled, suspendmanager also tries to suspend blocking jobs, @@ -61,7 +62,10 @@ SuspendManager.ATTRS { preventBlocking = false, --- Current job suspensions with their reasons - suspensions = {} + suspensions = {}, + + --- Last tick where it was run automatically + lastAutoRunTick = -1, } --- SuspendManager instance kept between frames @@ -146,6 +150,17 @@ local ERASABLE_DESIGNATION = { [df.job_type.DetailFloor]=true, } +--- Job types that impact suspendmanager +local FILTER_JOB_TYPES = { + [df.job_type.CarveTrack]=true, + [df.job_type.ConstructBuilding]=true, + [df.job_type.DestroyBuilding]=true, + [df.job_type.DetailFloor]=true, + [df.job_type.Dig]=true, + [df.job_type.DigChannel]=true, + [df.job_type.SmoothFloor]=true, +} + --- Check if a building is blocking once constructed ---@param building building_constructionst|building ---@return boolean @@ -408,7 +423,9 @@ end --- @param job job local function on_job_change(job) - if Instance.preventBlocking then + local tick = df.global.cur_year_tick + if Instance.preventBlocking and FILTER_JOB_TYPES[job.job_type] and tick ~= Instance.lastAutoRunTick then + Instance.lastAutoRunTick = tick -- Note: This method could be made incremental by taking in account the -- changed job run_now() From 6c4b255f517973d078ff50969b6995795b1ec38c Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Tue, 13 Jun 2023 08:58:19 +0200 Subject: [PATCH 309/732] More complete list of job to filter --- suspendmanager.lua | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 644948d2d8..483b87f5f5 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -151,14 +151,23 @@ local ERASABLE_DESIGNATION = { } --- Job types that impact suspendmanager -local FILTER_JOB_TYPES = { - [df.job_type.CarveTrack]=true, - [df.job_type.ConstructBuilding]=true, - [df.job_type.DestroyBuilding]=true, - [df.job_type.DetailFloor]=true, - [df.job_type.Dig]=true, - [df.job_type.DigChannel]=true, - [df.job_type.SmoothFloor]=true, +--- Any completed pathable job can impact suspendmanager by allowing or disallowing +--- access to construction job +local FILTER_JOB_TYPES = utils.invert{ + df.job_type.CarveRamp, + df.job_type.CarveTrack, + df.job_type.CarveUpDownStaircase, + df.job_type.CarveUpwardStaircase, + df.job_type.CarveDownwardStaircase, + df.job_type.ConstructBuilding, + df.job_type.DestroyBuilding, + df.job_type.DetailFloor, + df.job_type.Dig, + df.job_type.DigChannel, + df.job_type.FellTree, + df.job_type.SmoothFloor, + df.job_type.RemoveConstruction, + df.job_type.RemoveStairs, } --- Check if a building is blocking once constructed From 2763dc3b5cb30988f5d188b02c3fcd55bc621321 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 14 Jun 2023 16:07:36 -0700 Subject: [PATCH 310/732] remove gui/automelt will be replaced more thoroughly by gui/logistics --- changelog.txt | 1 + docs/gui/automelt.rst | 18 --- gui/automelt.lua | 305 ------------------------------------------ 3 files changed, 1 insertion(+), 323 deletions(-) delete mode 100644 docs/gui/automelt.rst delete mode 100644 gui/automelt.lua diff --git a/changelog.txt b/changelog.txt index 48f3d44bb1..f9cf170fa5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ that repo. - `suspendmanager`: suspend blocking jobs when building high walls or filling corridors ## Removed +- `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile # 50.08-r3 diff --git a/docs/gui/automelt.rst b/docs/gui/automelt.rst deleted file mode 100644 index a5daac341d..0000000000 --- a/docs/gui/automelt.rst +++ /dev/null @@ -1,18 +0,0 @@ -gui/automelt -============ - -.. dfhack-tool:: - :summary: Quickly designate items to be melted. - :tags: fort auto stockpiles - -This is the configuration interface for the `automelt` plugin. You can configure -which stockpiles will be monitored, and have their contents automatically marked -for melting. You can also see how many items are present in monitored stockpiles, -as well as the number of melt-designated items globally. - -Usage ------ - -:: - - gui/automelt diff --git a/gui/automelt.lua b/gui/automelt.lua deleted file mode 100644 index 187aeec03e..0000000000 --- a/gui/automelt.lua +++ /dev/null @@ -1,305 +0,0 @@ --- config ui for automelt - -local gui = require('gui') -local widgets = require('gui.widgets') -local plugin = require('plugins.automelt') - -local PROPERTIES_HEADER = 'Monitor Items Marked ' -local REFRESH_MS = 10000 - --- --- StockpileSettings --- - -StockpileSettings = defclass(StockpileSettings, widgets.Window) -StockpileSettings.ATTRS{ - frame={l=0, t=5, w=56, h=13}, -} - -function StockpileSettings:init() - self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text='Stockpile: ', - }, - widgets.Label{ - view_id='name', - frame={t=0, l=12}, - text_pen=COLOR_GREEN, - }, - widgets.ToggleHotkeyLabel{ - view_id='monitored', - frame={t=2, l=0}, - key='CUSTOM_M', - label='Monitor stockpile', - }, - widgets.HotkeyLabel{ - frame={t=8, l=0}, - key='SELECT', - label='Apply', - on_activate=self:callback('commit'), - }, - } -end - -function StockpileSettings:show(choice, on_commit) - self.data = choice.data - self.on_commit = on_commit - local data = self.data - self.subviews.name:setText(data.name) - self.subviews.monitored:setOption(data.monitored) - self.visible = true - self:setFocus(true) - self:updateLayout() -end - -function StockpileSettings:hide() - self:setFocus(false) - self.visible = false -end - -function StockpileSettings:commit() - local data = { - id=self.data.id, - monitored=self.subviews.monitored:getOptionValue(), - - } - plugin.setStockpileConfig(data) - self:hide() - self.on_commit() -end - -function StockpileSettings:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then - self:hide() - return true - end - StockpileSettings.super.onInput(self, keys) - return true -- we're a modal dialog -end - --- --- Automelt --- - -Automelt = defclass(Automelt, widgets.Window) -Automelt.ATTRS { - frame_title='Automelt', - frame={w=64, h=27}, - resizable=true, - resize_min={h=25}, - hide_unmonitored=DEFAULT_NIL, - manual_hide_unmonitored_touched=DEFAULT_NIL, -} - -function Automelt:init() - local minimal = false - local saved_frame = {w=45, h=8, r=2, t=18} - local saved_resize_min = {w=saved_frame.w, h=saved_frame.h} - local function toggle_minimal() - minimal = not minimal - local swap = self.frame - self.frame = saved_frame - saved_frame = swap - swap = self.resize_min - self.resize_min = saved_resize_min - saved_resize_min = swap - self:updateLayout() - self:refresh_data() - end - local function is_minimal() - return minimal - end - local function is_not_minimal() - return not minimal - end - - self:addviews{ - widgets.ToggleHotkeyLabel{ - view_id='enable_toggle', - frame={t=0, l=0, w=31}, - label='Automelt is', - key='CUSTOM_CTRL_E', - options={{value=true, label='Enabled', pen=COLOR_GREEN}, - {value=false, label='Disabled', pen=COLOR_RED}}, - on_change=function(val) plugin.setEnabled(val) end, - }, - widgets.HotkeyLabel{ - frame={r=0, t=0, w=10}, - key='CUSTOM_ALT_M', - label=string.char(31)..string.char(30), - on_activate=toggle_minimal}, - widgets.Label{ - view_id='minimal_summary', - frame={t=1, l=0, h=4}, - auto_height=false, - visible=is_minimal, - }, - widgets.Label{ - frame={t=3, l=0}, - text='Stockpile', - auto_width=true, - visible=is_not_minimal, - }, - widgets.Label{ - frame={t=3, r=0}, - text=PROPERTIES_HEADER, - auto_width=true, - visible=is_not_minimal, - }, - widgets.List{ - view_id='list', - frame={t=5, l=0, r=0, b=14}, - on_submit=self:callback('configure_stockpile'), - visible=is_not_minimal, - }, - widgets.ToggleHotkeyLabel{ - view_id='hide', - frame={b=11, l=0}, - label='Hide stockpiles with no meltable items: ', - key='CUSTOM_CTRL_H', - initial_option=false, - on_change=function() self:update_choices() end, - visible=is_not_minimal, - }, - widgets.ToggleHotkeyLabel{ - view_id='hide_unmonitored', - frame={b=10, l=0}, - label='Hide unmonitored stockpiles: ', - key='CUSTOM_CTRL_U', - initial_option=self:getDefaultHide(), - on_change=function() - self:update_choices() - end, - visible=is_not_minimal, - }, - widgets.HotkeyLabel{ - frame={b=9, l=0}, - label='Designate items for melting now', - key='CUSTOM_CTRL_D', - on_activate=function() - plugin.automelt_designate() - self:refresh_data() - self:update_choices() - end, - visible=is_not_minimal, - }, - widgets.Label{ - view_id='summary', - frame={b=0, l=0}, - visible=is_not_minimal, - }, - StockpileSettings{ - view_id='stockpile_settings', - visible=false, - }, - } - - self:refresh_data() -end - -function Automelt:hasMonitoredStockpiles() - self.data = plugin.getItemCountsAndStockpileConfigs() - --- check to see if we have any already monitored stockpiles - for _,c in ipairs(self.data.stockpile_configs) do - if c.monitored then - return true - end - end - - return false -end - -function Automelt:getDefaultHide() - return self:hasMonitoredStockpiles() -end - -function Automelt:configure_stockpile(idx, choice) - self.subviews.stockpile_settings:show(choice, function() - self:refresh_data() - self:update_choices() - end) -end - -function Automelt:update_choices() - local list = self.subviews.list - local name_width = list.frame_body.width - #PROPERTIES_HEADER - local fmt = '%-'..tostring(name_width)..'s [%s] %5d %5d ' - local hide_empty = self.subviews.hide:getOptionValue() - local hide_unmonitored = self.subviews.hide_unmonitored:getOptionValue() - local choices = {} - for _,c in ipairs(self.data.stockpile_configs) do - local num_items = self.data.item_counts[c.id] or 0 - if not hide_empty or num_items > 0 then - if not hide_unmonitored or c.monitored then - local text = (fmt):format( - c.name:sub(1,name_width), c.monitored and 'x' or ' ', - num_items or 0, self.data.premarked_item_counts[c.id] or 0) - table.insert(choices, {text=text, data=c}) - end - end - end - self.subviews.list:setChoices(choices) - self.subviews.list:updateLayout() - - -end - -function Automelt:refresh_data() - self.subviews.enable_toggle:setOption(plugin.isEnabled()) - self.data = plugin.getItemCountsAndStockpileConfigs() - - local summary = self.data.summary - local summary_text = { - ' Items in monitored stockpiles: ', tostring(summary.total_items), - NEWLINE, - 'All items marked for melting (monitored piles + global): ', tostring(summary.marked_item_count_total), - NEWLINE, - - } - self.subviews.summary:setText(summary_text) - - local minimal_summary_text = { - ' Items monitored: ', tostring(summary.total_items), NEWLINE, - 'Monitored Items marked for melting: ',tostring(summary.premarked_items), - } - self.subviews.minimal_summary:setText(minimal_summary_text) - - self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS -end - - -function Automelt:postUpdateLayout() - self:update_choices() -end - --- refreshes data every 10 seconds or so -function Automelt:onRenderBody() - if self.next_refresh_ms <= dfhack.getTickCount() then - self:refresh_data() - self:update_choices() - end -end - --- --- AutomeltScreen --- - -AutomeltScreen = defclass(AutomeltScreen, gui.ZScreen) -AutomeltScreen.ATTRS { - focus_path='automelt', -} - -function AutomeltScreen:init() - self:addviews{Automelt{}} -end - -function AutomeltScreen:onDismiss() - view = nil -end - -if not dfhack.isMapLoaded() then - qerror('automelt requires a map to be loaded') -end - -view = view and view:raise() or AutomeltScreen{}:show() From a0b8a5a18dfa5b567573f31e2c1415ad239b17c4 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Thu, 15 Jun 2023 08:56:38 +0200 Subject: [PATCH 311/732] Expand the job filter comment --- suspendmanager.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 483b87f5f5..a0c627fddb 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -152,7 +152,9 @@ local ERASABLE_DESIGNATION = { --- Job types that impact suspendmanager --- Any completed pathable job can impact suspendmanager by allowing or disallowing ---- access to construction job +--- access to construction job. +--- Any job read by suspendmanager such as smoothing and carving can also impact +--- job suspension, since it suspends construction job on top of it local FILTER_JOB_TYPES = utils.invert{ df.job_type.CarveRamp, df.job_type.CarveTrack, From 03cd0803a946e390d941d1216415c36997800597 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 16 Jun 2023 12:40:38 -0700 Subject: [PATCH 312/732] account for wonky interpretation of liquid type --- changelog.txt | 1 + modtools/spawn-liquid.lua | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index f9cf170fa5..508cf247b1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes - `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters - `emigration`: reassign home site for emigrating units so they don't just come right back to the fort +- `gui/liquids`: ensure tile temperature is set correctly when painting water or magma ## Misc Improvements - `gui/control-panel`: add some popular startup configuration commands for `autobutcher` and `autofarm` diff --git a/modtools/spawn-liquid.lua b/modtools/spawn-liquid.lua index b279e8816a..419b0b4595 100644 --- a/modtools/spawn-liquid.lua +++ b/modtools/spawn-liquid.lua @@ -7,10 +7,11 @@ function resetTemperature(position) local map_block = dfhack.maps.getTileBlock(position) local tile = dfhack.maps.getTileFlags(position) - if tile.liquid_type == df.tile_liquid.Water or tile.flow_size == 0 then + -- tile.liquid_type is interpreted as a boolean; 0 (Water) is interpreted as false + if tile.liquid_type == false or tile.liquid_type == df.tile_liquid.Water or tile.flow_size == 0 then map_block.temperature_1[position.x % 16][position.y % 16] = 10015 map_block.temperature_2[position.x % 16][position.y % 16] = 10015 - elseif tile.liquid_type == df.tile_liquid.Magma then + elseif tile.liquid_type == true or tile.liquid_type == df.tile_liquid.Magma then map_block.temperature_1[position.x % 16][position.y % 16] = 12000 map_block.temperature_2[position.x % 16][position.y % 16] = 12000 end From 08bf7646ba681219f79b86d45b48594f68fb5476 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 16 Jun 2023 14:54:33 -0700 Subject: [PATCH 313/732] modify existing jobs if we can when __reduce_amount is requested --- changelog.txt | 1 + docs/workorder.rst | 6 +++-- workorder.lua | 60 ++++++++++++++++++++++++---------------------- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/changelog.txt b/changelog.txt index f9cf170fa5..a53bd3021f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ that repo. - `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased - `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types - `suspendmanager`: suspend blocking jobs when building high walls or filling corridors +- `workorder`: reduce existing orders for automatic shearing and milking jobs when animals are no longer available ## Removed - `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile diff --git a/docs/workorder.rst b/docs/workorder.rst index b26ac16526..b9d628e04c 100644 --- a/docs/workorder.rst +++ b/docs/workorder.rst @@ -73,5 +73,7 @@ Also: ``load(code)(order, orders)`` that must return an integer. A custom field ``__reduce_amount`` can be set if existing open orders should be -taken into account, reducing the new order's ``total_amount`` (possibly all the -way to ``0``). An empty ``amount_total`` implies ``"__reduce_amount": true``. +taken into account. The first matching existing order will be modified to have +the desired quantity remaining. If the desired quantity is negative, the +existing order will be removed. An empty ``amount_total`` implies +``"__reduce_amount": true``. diff --git a/workorder.lua b/workorder.lua index 3da7302a84..487355980c 100644 --- a/workorder.lua +++ b/workorder.lua @@ -5,10 +5,6 @@ -- which is a great place to look up stuff like "How the hell do I find out if -- a creature can be sheared?!!" ---initialized = false -- uncomment this when working with the code -if not initialized then - initialized = true - local function print_help() print(dfhack.script_help()) end @@ -95,27 +91,22 @@ local function orders_match(a, b) return true end --- Reduce the quantity by the number of matching orders in the queue. -local function order_quantity(order, quantity) - local amount = quantity - for _, managed in ipairs(world.manager_orders) do +-- Get the remaining quantity for open matching orders in the queue. +local function cur_order_quantity(order) + local amount, cur_order, cur_idx = 0, nil, nil + for idx, managed in ipairs(world.manager_orders) do if orders_match(order, managed) then -- if infinity, don't plan anything if 0 == managed.amount_total then - return -1 - end - -- if ordered infinity don't reduce - if 0 ~= quantity then - amount = amount - managed.amount_left - if amount <= 0 then - return -1 - end + return 0, managed, idx end + amount = amount + managed.amount_left + cur_order = cur_order or managed + cur_idx = cur_idx or idx end end - return amount + return amount, cur_order, cur_idx end --- ]] -- make sure we have 'WEAPON' not 24. local function ensure_df_string(df_list, key) @@ -408,17 +399,30 @@ local function create_orders(orders) local amount = it.amount_total if it.__reduce_amount then - -- reduce if there are identical orders - -- with some amount_left. - amount = order_quantity(order, amount) + -- modify existing order if possible + local cur_amount, cur_order, cur_order_idx = cur_order_quantity(order) + if cur_order then + if 0 == cur_amount then + amount = -1 + elseif 0 ~= amount then + local diff = amount - cur_order.amount_left + amount = -1 + if verbose then print('adjusting existing order by', diff) end + cur_order.amount_left = cur_order.amount_left + diff + cur_order.amount_total = cur_order.amount_total + diff + if cur_order.amount_left <= 0 then + if verbose then print('negative amount; removing existing order') end + world.manager_orders:erase(cur_order_idx) + cur_order:delete() + end + end + end end if amount < 0 then if verbose then - print(string.format( - "Order %s (%s) not queued: amount reduced from %s to %s.", - it.id, df.job_type[order.job_type], tostring(it.amount_total), tostring(amount) - )) + print(string.format("Order %s (%s) not queued.", + it.id, df.job_type[order.job_type])) end order:delete() else @@ -490,7 +494,9 @@ local function preprocess_orders(orders) print(string.format("order.id: %s; job: %s; .amount_total: %s; .__reduce_amount: %s", order.id, df.job_type[ order.job ], order.amount_total, order.__reduce_amount)) end - if order.amount_total >= 0 then ret[#ret + 1] = order end + if order.amount_total >= 0 or order.__reduce_amount then + ret[#ret + 1] = order + end end return ret @@ -648,7 +654,5 @@ actions = { ["--reset"] = function() initialized = false end, } -end -- `if not initialized ` - -- Lua is beautiful. (actions[ (...) or "?" ] or default_action)(...) From 17370d629f37ca4655363e6f5ab4dca18fdd605e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 16 Jun 2023 17:22:59 -0700 Subject: [PATCH 314/732] move "cursor lock" from keyboard semantics to mouse --- changelog.txt | 1 + gui/quickfort.lua | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index f9cf170fa5..6a70a7c33f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ that repo. - `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased - `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types - `suspendmanager`: suspend blocking jobs when building high walls or filling corridors +- `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them ## Removed - `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 4ebabae8b1..84a75eed45 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -6,7 +6,6 @@ reqscript('quickfort').refresh_scripts() local quickfort_command = reqscript('internal/quickfort/command') local quickfort_list = reqscript('internal/quickfort/list') -local quickfort_map = reqscript('internal/quickfort/map') local quickfort_parse = reqscript('internal/quickfort/parse') local quickfort_preview = reqscript('internal/quickfort/preview') local quickfort_transform = reqscript('internal/quickfort/transform') @@ -388,12 +387,19 @@ function Quickfort:get_blueprint_name() end function Quickfort:get_lock_cursor_label() + if self.cursor_locked and self.saved_cursor.z ~= df.global.window_z then + return 'Zoom to locked position' + end return (self.cursor_locked and 'Unl' or 'L') .. 'ock blueprint position' end function Quickfort:toggle_lock_cursor() if self.cursor_locked then - quickfort_map.move_cursor(self.saved_cursor) + local was_on_different_zlevel = self.saved_cursor.z ~= df.global.window_z + dfhack.gui.revealInDwarfmodeMap(self.saved_cursor) + if was_on_different_zlevel then + return + end end self.cursor_locked = not self.cursor_locked end @@ -588,7 +594,7 @@ function Quickfort:onRenderFrame(dc, rect) if not tiles[cursor.z] then return end local function get_overlay_pen(pos) - if same_xyz(pos, cursor) then return CURSOR_PEN end + if same_xyz(pos, self.saved_cursor) then return CURSOR_PEN end local preview_tile = quickfort_preview.get_preview_tile(tiles, pos) if preview_tile == nil then return end return preview_tile and GOOD_PEN or BAD_PEN From 9f53da577049d62c3d365ff72513ec00451002d9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 17 Jun 2023 22:33:03 -0700 Subject: [PATCH 315/732] use string-based hotkeys instead of action keybindings where we really want static symbols --- internal/gm-unit/editor_skills.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/gm-unit/editor_skills.lua b/internal/gm-unit/editor_skills.lua index 61b450bc43..b97ee60ddc 100644 --- a/internal/gm-unit/editor_skills.lua +++ b/internal/gm-unit/editor_skills.lua @@ -68,20 +68,20 @@ function Editor_Skills:init( args ) choices=skill_list, frame = {t=0, b=3,l=0}, view_id="skills", - edit_ignore_keys={"KEYBOARD_CURSOR_UP_Z", "KEYBOARD_CURSOR_DOWN_Z", "STRING_A047"}, + edit_ignore_keys={"STRING_A045", "STRING_A043", "STRING_A047"}, }, widgets.Label{ frame = { b=0,l=0}, text ={ {text=": remove level ", - key = "KEYBOARD_CURSOR_UP_Z", + key = "STRING_A045", on_activate=self:callback("level_skill",-1)}, {text=": add level ", - key = "KEYBOARD_CURSOR_DOWN_Z", + key = "STRING_A043", on_activate=self:callback("level_skill",1)}, NEWLINE, {text=": show learned only ", - key = "STRING_A047", + key = "STRING_A047", -- / on_activate=function () self.learned_only=not self.learned_only self:update_list(true) From 72ba800723f163f75af05cc6b761c03c3a2fa74d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 18 Jun 2023 01:24:31 -0700 Subject: [PATCH 316/732] clean up code/docs for combine --- combine.lua | 13 +++++++------ docs/combine.rst | 9 +++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/combine.lua b/combine.lua index 53cc9c875f..968c228b08 100644 --- a/combine.lua +++ b/combine.lua @@ -1,4 +1,5 @@ --- Combines food and plant items across stockpiles. +-- Combines items in a stockpile that could be stacked together + local argparse = require('argparse') local utils = require('utils') @@ -178,7 +179,7 @@ local function stack_type_new(type_vals) return stack_type end -local function stacks_add_item(stockpile, stacks, stack_type, item, container, contained_count) +local function stacks_add_item(stockpile, stacks, stack_type, item, container) -- add an item to the matching comp_items table; based on comp_key. local comp_key = '' @@ -202,7 +203,7 @@ local function stacks_add_item(stockpile, stacks, stack_type, item, container, c stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_size) end - local new_comp_item_item = comp_item_add_item(stockpile, stack_type, stack_type.comp_items[comp_key], item, container, contained_count) + local new_comp_item_item = comp_item_add_item(stockpile, stack_type, stack_type.comp_items[comp_key], item, container) if new_comp_item_item then stack_type.before_stacks = stack_type.before_stacks + 1 stack_type.item_qty = stack_type.item_qty + item.stack_size @@ -393,7 +394,7 @@ local function isValidPart(item) item.material_amount.Yarn > 0)) end -local function stacks_add_items(stockpile, stacks, items, container, contained_count, ind) +local function stacks_add_items(stockpile, stacks, items, container, ind) -- loop through each item and add it to the matching stack[type_id].comp_items table -- recursively calls itself to add contained items if not ind then ind = '' end @@ -407,7 +408,7 @@ local function stacks_add_items(stockpile, stacks, items, container, contained_c if stack_type and not item:isSand() and not item:isPlaster() and isValidPart(item) then if not isRestrictedItem(item) then - stacks_add_item(stockpile, stacks, stack_type, item, container, contained_count) + stacks_add_item(stockpile, stacks, stack_type, item, container) if typesThatUseCreatures[df.item_type[type_id]] then local raceRaw = df.global.world.raws.creatures.all[item.race] @@ -435,7 +436,7 @@ local function stacks_add_items(stockpile, stacks, items, container, contained_c stacks.containers[item.id].before_size = #contained_items stacks.containers[item.id].description = utils.getItemDescription(item, 1) log(4, (' %sContainer:%s <%6d> #items:%5d\n'):format(ind, utils.getItemDescription(item), item.id, count, item:isSandBearing())) - stacks_add_items(stockpile, stacks, contained_items, item, count, ind .. ' ') + stacks_add_items(stockpile, stacks, contained_items, item, ind .. ' ') -- excluded item types else diff --git a/docs/combine.rst b/docs/combine.rst index fae44bbbfd..8f8889090a 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -2,7 +2,7 @@ combine ======= .. dfhack-tool:: - :summary: Combine stacks of food and plants. + :summary: Combine items that can be stacked together. :tags: fort productivity items plants stockpiles Usage @@ -63,7 +63,7 @@ Options ``powders``: POWDERS_MISC - ``seeds``: SEEDS + ``seed``: SEEDS ``-q``, ``--quiet`` Only print changes instead of a summary of all processed stockpiles. @@ -85,5 +85,6 @@ The following categories are defined: 3. Ammo, grouped by ammo type, material, and quality. If the ammo is a masterwork, it is also grouped by who created it. 4. Anything else, grouped by item type and material -Each category has a default stack size of 30 unless a larger stack already exists in your fort. -In that case the largest existing stack size is used. +Each category has a default stack size of 30 unless a larger stack already +exists "naturally" in your fort. In that case the largest existing stack size +is used. From 0812828064c5084e631315c1761ae264b6753883 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 18 Jun 2023 01:54:03 -0700 Subject: [PATCH 317/732] sync spreadsheet to docs --- docs/devel/sc.rst | 2 +- docs/gaydar.rst | 2 +- docs/modtools/add-syndrome.rst | 2 +- docs/region-pops.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/devel/sc.rst b/docs/devel/sc.rst index 3a7df80547..06320b9525 100644 --- a/docs/devel/sc.rst +++ b/docs/devel/sc.rst @@ -3,7 +3,7 @@ devel/sc .. dfhack-tool:: :summary: Scan DF structures for errors. - :tags: unavailable dev + :tags: dev Size Check: scans structures for invalid vectors, misaligned structures, and unidentified enum values. diff --git a/docs/gaydar.rst b/docs/gaydar.rst index 9067a19098..ee40541155 100644 --- a/docs/gaydar.rst +++ b/docs/gaydar.rst @@ -3,7 +3,7 @@ gaydar .. dfhack-tool:: :summary: Shows the sexual orientation of units. - :tags: unavailable fort inspection animals units + :tags: fort inspection animals units ``gaydar`` is useful for social engineering or checking the viability of livestock breeding programs. diff --git a/docs/modtools/add-syndrome.rst b/docs/modtools/add-syndrome.rst index 5492db23c7..12fa435715 100644 --- a/docs/modtools/add-syndrome.rst +++ b/docs/modtools/add-syndrome.rst @@ -3,7 +3,7 @@ modtools/add-syndrome .. dfhack-tool:: :summary: Add and remove syndromes from units. - :tags: unavailable dev + :tags: dev This allows adding and removing syndromes from units. diff --git a/docs/region-pops.rst b/docs/region-pops.rst index 9627b7fd18..6c1857fc02 100644 --- a/docs/region-pops.rst +++ b/docs/region-pops.rst @@ -3,7 +3,7 @@ region-pops .. dfhack-tool:: :summary: Change regional animal populations. - :tags: unavailable fort inspection animals + :tags: fort inspection animals This tool can show or modify the populations of animals in the region. From a49f6119d1b7608b9955f1f4a2569d5d2943e97a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 19 Jun 2023 12:56:32 -0700 Subject: [PATCH 318/732] don't duplicate messages when repeating blueprints --- changelog.txt | 1 + internal/quickfort/command.lua | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 326ecb147d..319d7403b3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,7 @@ that repo. - `suspendmanager`: suspend blocking jobs when building high walls or filling corridors - `workorder`: reduce existing orders for automatic shearing and milking jobs when animals are no longer available - `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them +- `gui/quickfort`: only display post-blueprint messages once when repeating the blueprint up or down z-levels ## Removed - `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile diff --git a/internal/quickfort/command.lua b/internal/quickfort/command.lua index c5bd0e3e9e..1b5a85caa1 100644 --- a/internal/quickfort/command.lua +++ b/internal/quickfort/command.lua @@ -35,6 +35,7 @@ local function make_ctx_base(prev_ctx) stats={out_of_bounds={label='Tiles outside map boundary', value=0}, invalid_keys={label='Invalid key sequences', value=0}}, messages={}, + messages_set={}, } return { zmin=30000, @@ -43,6 +44,7 @@ local function make_ctx_base(prev_ctx) order_specs=prev_ctx.order_specs, stats=prev_ctx.stats, messages=prev_ctx.messages, + messages_set=prev_ctx.messages_set, } end @@ -166,8 +168,11 @@ function do_command_section(ctx, section_name, modifiers) local filepath = quickfort_list.get_blueprint_filepath(ctx.blueprint_name) local first_modeline = do_apply_modifiers(filepath, sheet_name, label, ctx, modifiers) - if first_modeline and first_modeline.message and ctx.command == 'run' then + if first_modeline and first_modeline.message and ctx.command == 'run' + and not ctx.messages_set[first_modeline.message] + then table.insert(ctx.messages, first_modeline.message) + ctx.messages_set[first_modeline.message] = true end end From 42ffc6052e18ba3fefddb659eecaef0fcec72a4b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 19 Jun 2023 13:10:37 -0700 Subject: [PATCH 319/732] protect against infinite loops --- changelog.txt | 1 + internal/quickfort/meta.lua | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/changelog.txt b/changelog.txt index 326ecb147d..b893b023c6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ that repo. - `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types - `suspendmanager`: suspend blocking jobs when building high walls or filling corridors - `workorder`: reduce existing orders for automatic shearing and milking jobs when animals are no longer available +- `gui/quickfort`: protect against meta blueprints recursing infinitely if they include themselves - `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them ## Removed diff --git a/internal/quickfort/meta.lua b/internal/quickfort/meta.lua index df2dcf77b0..cc3ef70ce7 100644 --- a/internal/quickfort/meta.lua +++ b/internal/quickfort/meta.lua @@ -44,11 +44,16 @@ local function do_meta(zlevel, grid, ctx) stats.meta_blueprints = stats.meta_blueprints or {label='Blueprints applied', value=0, always=true} + ensure_keys(ctx, 'meta', 'parents') -- context history for infinite loop protection + -- use get_ordered_grid_cells() to ensure we process blueprints in exactly -- the declared order (pairs() over the grid makes no such guarantee) for _, cell in ipairs(quickfort_parse.get_ordered_grid_cells(grid)) do local section_name, extra = get_section_name(cell.cell, cell.text, ctx.sheet_name) + if ctx.meta.parents[section_name] then + qerror(('infinite loop detected in blueprint: "%s"'):format(section_name)) + end local modifiers = quickfort_parse.get_meta_modifiers(extra, ctx.blueprint_name) local repeat_str = '' @@ -58,8 +63,10 @@ local function do_meta(zlevel, grid, ctx) (modifiers.repeat_zoff > 0) and 'up' or 'down') end log('applying blueprint%s: "%s"', repeat_str, section_name) + ctx.meta.parents[section_name] = true local ok, err = pcall(quickfort_command.do_command_section, ctx, section_name, modifiers) + ctx.meta.parents[section_name] = nil if ok then stats.meta_blueprints.value = stats.meta_blueprints.value + modifiers.repeat_count From bf39c684e1649b4824234bd70b7f8b2bda84b6e7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 19 Jun 2023 13:23:25 -0700 Subject: [PATCH 320/732] traffic restrictions can be applied anywhere --- changelog.txt | 1 + internal/quickfort/dig.lua | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/changelog.txt b/changelog.txt index 326ecb147d..776aaf8b57 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ that repo. - `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters - `emigration`: reassign home site for emigrating units so they don't just come right back to the fort - `gui/liquids`: ensure tile temperature is set correctly when painting water or magma +- `gui/quickfort`: allow traffic designations to be applied over buildings ## Misc Improvements - `gui/control-panel`: add some popular startup configuration commands for `autobutcher` and `autofarm` diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index cd63c918c2..f4a2276db2 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -794,6 +794,10 @@ local function do_run_impl(zlevel, grid, ctx) digctx.occupancy.building ~= df.tile_building_occ.Dynamic then goto inner_continue end + elseif db_entry.action == do_traffic_high or db_entry.action == do_traffic_normal + or db_entry.action == do_traffic_low or db_entry.action == do_traffic_restricted + then + -- pass else -- can't dig through buildings if digctx.occupancy.building ~= 0 then From b2cced5db52b1c443e74e12d43a6635a28e527ba Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 19 Jun 2023 14:00:28 -0700 Subject: [PATCH 321/732] generate names for citizens --- changelog.txt | 1 + makeown.lua | 41 +++++++++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index 326ecb147d..ff6f1346c3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -23,6 +23,7 @@ that repo. ## Misc Improvements - `gui/control-panel`: add some popular startup configuration commands for `autobutcher` and `autofarm` - `gui/control-panel`: add option for running `fix/blood-del` on new forts (enabled by default) +- `gui/sandbox`: when creating citizens, give them names appropriate for their races - `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants - `quickfort`: now handles zones, locations, stockpile configuration, hauling routes, and more - `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased diff --git a/makeown.lua b/makeown.lua index 339743e90f..3a18060a75 100644 --- a/makeown.lua +++ b/makeown.lua @@ -1,12 +1,36 @@ ---[[ - 'tweak makeown' as a lua include - make_own(unit) -- removes foreign flags, sets civ_id to fort civ_id, and sets clothes ownership - make_citizen(unit) -- called by make_own if unit.race == fort race ---]] --@module=true -local utils = require 'utils' +local function get_translation(race_id) + local race_name = df.global.world.raws.creatures.all[race_id].creature_id + for _,translation in ipairs(df.global.world.raws.language.translations) do + if translation.name == race_name then + return translation + end + end + return df.global.world.raws.language.translations[0] +end + +local function pick_first_name(race_id) + local translation = get_translation(race_id) + return translation.words[math.random(0, #translation.words-1)].value +end + +local LANGUAGE_IDX = 0 +local word_table = df.global.world.raws.language.word_table[LANGUAGE_IDX][35] +function name_unit(unit) + if unit.name.has_name then return end + + unit.name.first_name = pick_first_name(unit.race) + unit.name.words.FrontCompound = word_table.words.FrontCompound[math.random(0, #word_table.words.FrontCompound-1)] + unit.name.words.RearCompound = word_table.words.RearCompound[math.random(0, #word_table.words.RearCompound-1)] + + unit.name.language = LANGUAGE_IDX + unit.name.parts_of_speech.FrontCompound = df.part_of_speech.Noun + unit.name.parts_of_speech.RearCompound = df.part_of_speech.Verb3rdPerson + unit.name.type = df.language_name_type.Figure + unit.name.has_name = true +end local function fix_clothing_ownership(unit) -- extracted/translated from tweak makeown plugin @@ -261,11 +285,12 @@ function make_citizen(unit) end print("makeown: migrated nemesis entry") end -- nemesis -end + -- generate a name for the unit if it doesn't already have one + name_unit(unit) +end function make_own(unit) - --tweak makeown unit.flags1.marauder = false; unit.flags1.merchant = false; unit.flags1.forest = false; From d12af63189ba1316e116f7949ca8e4822058fd0c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 19 Jun 2023 14:08:09 -0700 Subject: [PATCH 322/732] graphics being set is not a reliable metric --- changelog.txt | 1 + gui/sandbox.lua | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 326ecb147d..4e0baeb22a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes - `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters - `emigration`: reassign home site for emigrating units so they don't just come right back to the fort +- `gui/sandbox`: allow creatures that have separate caste-based graphics to be spawned (like ewes/rams) - `gui/liquids`: ensure tile temperature is set correctly when painting water or magma ## Misc Improvements diff --git a/gui/sandbox.lua b/gui/sandbox.lua index dcb91e5c85..01903da6b6 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -346,7 +346,9 @@ local function init_arena() arena_unit.castes_all:resize(0) local arena_creatures = {} for i, cre in ipairs(RAWS.creatures.all) do - if not cre.flags.VERMIN_GROUNDER and not cre.flags.VERMIN_SOIL and cre.graphics then + if not cre.flags.VERMIN_GROUNDER and not cre.flags.VERMIN_SOIL + and not cre.flags.DOES_NOT_EXIST and not cre.flags.EQUIPMENT_WAGON + then table.insert(arena_creatures, {race=i, cre=cre}) end end From 05dfbb0cef2d461fe1aa5d8a97f915ffa1303d7f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 19 Jun 2023 17:29:56 -0700 Subject: [PATCH 323/732] allow structs to be viewed again --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 480dd380aa..4de154503c 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -156,7 +156,7 @@ function GmEditorUi:verifyStack() for i, level in pairs(self.stack) do local obj=level.target - if obj._kind == "bitfield" then goto continue end + if obj._kind == "bitfield" or obj._kind == "struct" then goto continue end local keys = level.keys local selection = level.selected From 62d5e08dd4651c0de31cbb1f10ba4cd53830b2f8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 21 Jun 2023 01:02:08 -0700 Subject: [PATCH 324/732] add some more info to the lua command docs --- docs/lua.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/lua.rst b/docs/lua.rst index 1c23a871a7..bc0e1e72fd 100644 --- a/docs/lua.rst +++ b/docs/lua.rst @@ -9,7 +9,8 @@ Usage ----- ``lua`` - Start an interactive lua interpreter. + Start an interactive lua interpreter. Type ``quit`` on an empty line and hit + enter to exit the interpreter. ``lua -f ``, ``lua --file `` Load the specified file and run the lua script within. The filename is interpreted relative to the Dwarf Fortress game directory. @@ -33,4 +34,13 @@ Examples -------- ``:lua !df.global.window_z`` - Print out the current z-level. + Print out the current z-level (as distinct from the displayed elevation). + +``:lua !unit.id`` + Print out the id of the currently selected unit. + +``:lua ~item.flags`` + Print out the toggleable flags for the currently selected item. + +``:lua @df.profession`` + Print out the valid internal profession names. From 8f921d62cee58d4f5c5a9b4b38bd21650f3acaae Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 21 Jun 2023 11:40:40 -0700 Subject: [PATCH 325/732] fix flags for locations that have multiple zones attached the flags can be specified in any attached zone, not just the first --- internal/quickfort/zone.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 3066d7df55..58462507f9 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -341,13 +341,18 @@ local function set_location(zone, location, ctx) end local site = df.global.world.world_data.active_site[0] local loc_id = nil - if location.label then - loc_id = safe_index(ctx, 'zone', 'locations', location.label) + if location.label and safe_index(ctx, 'zone', 'locations', location.label) then + local cached_loc = ctx.zone.locations[location.label] + loc_id = cached_loc.id + local bld = cached_loc.bld + for flag, val in pairs(location.data.flags or {}) do + bld.flags[flag] = val + end end - local data = copyall(valid_locations[location.type]) - utils.assign(data, location.data) if not loc_id then loc_id = site.next_building_id + local data = copyall(valid_locations[location.type]) + utils.assign(data, location.data) data.name = generate_name() data.id = loc_id data.site_id = site.id @@ -370,16 +375,19 @@ local function set_location(zone, location, ctx) bld.flags[flag] = val end bld.contents.building_ids:insert('#', zone.id) + if location.label then + -- remember this location for future associations in this blueprint + local cached_loc = ensure_keys(ctx, 'zone', 'locations', location.label) + cached_loc.id = loc_id + cached_loc.flags = data.flags + cached_loc.bld = bld + end end zone.site_id = site.id zone.location_id = loc_id -- recategorize the civzone as attached to a location zone:uncategorize() zone:categorize(true) - if location.label then - -- remember this location for future associations in this blueprint - ensure_keys(ctx, 'zone', 'locations')[location.label] = loc_id - end end local function create_zone(zone, data, ctx) From d9eb954248696cd289192e9432eaec0c91eec6e1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 21 Jun 2023 11:46:55 -0700 Subject: [PATCH 326/732] reduce max stack sizes in containers to 30 --- changelog.txt | 1 + combine.lua | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index be8557474d..8ebc8c8079 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ that repo. - `gui/quickfort`: protect against meta blueprints recursing infinitely if they include themselves - `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them - `gui/quickfort`: only display post-blueprint messages once when repeating the blueprint up or down z-levels +- `combine`: reduce default max stack sizes in containers to 30 ## Removed - `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile diff --git a/combine.lua b/combine.lua index 968c228b08..fa9b299ea6 100644 --- a/combine.lua +++ b/combine.lua @@ -16,8 +16,8 @@ local opts, args = { -- default max stack size of 30 local MAX_ITEM_STACK=30 local MAX_AMMO_STACK=25 -local MAX_CONT_ITEMS=500 -local MAX_MAT_AMT=500 +local MAX_CONT_ITEMS=30 +local MAX_MAT_AMT=30 -- list of types that use race and caste local typesThatUseCreatures = utils.invert{'REMAINS', 'FISH', 'FISH_RAW', 'VERMIN', 'PET', 'EGG', 'CORPSE', 'CORPSEPIECE'} From 91c826506a3e699765900283ecadada19ef1d72b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 22 Jun 2023 07:55:02 -0700 Subject: [PATCH 327/732] align milkable check with DF logic reverse engineered by ab9rf --- changelog.txt | 1 + workorder.lua | 30 ++++++++++++------------------ 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/changelog.txt b/changelog.txt index be8557474d..f23c354797 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ that repo. - `emigration`: reassign home site for emigrating units so they don't just come right back to the fort - `gui/sandbox`: allow creatures that have separate caste-based graphics to be spawned (like ewes/rams) - `gui/liquids`: ensure tile temperature is set correctly when painting water or magma +- `workorder`: prevent autoMilkCreature from over-counting milkable animals, which was leading to cancellation spam for the MilkCreature job - `gui/quickfort`: allow traffic designations to be applied over buildings ## Misc Improvements diff --git a/workorder.lua b/workorder.lua index 487355980c..3467cde14e 100644 --- a/workorder.lua +++ b/workorder.lua @@ -554,36 +554,31 @@ default_action = function (...) create_orders(orders) end --- see https://github.com/jjyg/df-ai/blob/master/ai/population.rb --- especially `update_pets` - local uu = dfhack.units -local function isValidUnit(u) +local function isValidAnimal(u) + -- 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 uu.isOwnCiv(u) and uu.isAlive(u) and uu.isAdult(u) - and u.flags1.tame -- no idea if this is needed... - and not u.flags1.merchant - and not u.flags1.forest -- no idea what this is - and not u.flags2.for_trade - and not u.flags2.slaughter + and uu.isActive(u) + and uu.isFortControlled(u) + and uu.isTame(u) + and not uu.isMarkedForSlaughter(u) + and not uu.getMiscTrait(u, df.misc_trait_type.Migrant, false) end -local MilkCounter = df.misc_trait_type["MilkCounter"] calcAmountFor_MilkCreature = function () local cnt = 0 if debug_verbose then print "Milkable units:" end for i, u in pairs(world.units.active) do - if isValidUnit(u) - and uu.isMilkable(u) - --and uu.getMiscTrait(u, MilkCounter, false) -- aka "was milked"; but we could use its .value for something. - then - local mt_milk = uu.getMiscTrait(u, MilkCounter, false) + if isValidAnimal(u) and uu.isMilkable(u) and not uu.isPet(u) then + local mt_milk = uu.getMiscTrait(u, df.misc_trait_type.MilkCounter, false) if not mt_milk then cnt = cnt + 1 end if debug_verbose then local mt_milk_val = mt_milk and mt_milk.value or "not milked recently" - print(i, uu.getRaceName(u), mt_milk_val) + print(u.id, uu.getRaceName(u), mt_milk_val) end end end @@ -620,8 +615,7 @@ calcAmountFor_ShearCreature = function () local cnt = 0 if debug_verbose then print "Shearable units:" end for i, u in pairs(world.units.active) do - if isValidUnit(u) - then + if isValidAnimal(u) then local can, info = canShearCreature(u) if can then cnt = cnt + 1 end From f18d22c018d0baffb0b3fbe21e9c863c2f29b6cc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 22 Jun 2023 16:15:39 -0700 Subject: [PATCH 328/732] from testing, 100 goslings is just too many no further eggs are laid, reducing overall bone/leather production --- gui/control-panel.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 84a23b5fa1..199a5fca12 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -36,9 +36,9 @@ local FORT_SERVICES = { } local FORT_AUTOSTART = { - 'autobutcher target 50 50 14 2 BIRD_GOOSE', - 'autobutcher target 50 50 14 2 BIRD_TURKEY', - 'autobutcher target 50 50 14 2 BIRD_CHICKEN', + 'autobutcher target 10 10 14 2 BIRD_GOOSE', + 'autobutcher target 10 10 14 2 BIRD_TURKEY', + 'autobutcher target 10 10 14 2 BIRD_CHICKEN', 'autofarm threshold 150 grass_tail_pig', 'ban-cooking all', 'buildingplan set boulders false', From fcccedb91e48ebdbe151950c86689d52f0617ea9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 23 Jun 2023 10:57:27 -0700 Subject: [PATCH 329/732] bump to 50.08-r4 --- changelog.txt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5af6ca801c..714c66e217 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,28 +15,36 @@ that repo. ## New Scripts +## Fixes + +## Misc Improvements + +## Removed + +# 50.08-r4 + ## Fixes - `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters - `emigration`: reassign home site for emigrating units so they don't just come right back to the fort - `gui/sandbox`: allow creatures that have separate caste-based graphics to be spawned (like ewes/rams) - `gui/liquids`: ensure tile temperature is set correctly when painting water or magma -- `workorder`: prevent autoMilkCreature from over-counting milkable animals, which was leading to cancellation spam for the MilkCreature job +- `workorder`: prevent ``autoMilkCreature`` from over-counting milkable animals, which was leading to cancellation spam for the MilkCreature job - `gui/quickfort`: allow traffic designations to be applied over buildings +- `gui/quickfort`: protect against meta blueprints recursing infinitely if they include themselves ## Misc Improvements - `gui/control-panel`: add some popular startup configuration commands for `autobutcher` and `autofarm` - `gui/control-panel`: add option for running `fix/blood-del` on new forts (enabled by default) - `gui/sandbox`: when creating citizens, give them names appropriate for their races - `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants -- `quickfort`: now handles zones, locations, stockpile configuration, hauling routes, and more +- `quickfort`: significant rewrite for DF v50! now handles zones, locations, stockpile configuration, hauling routes, and more - `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased - `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types - `suspendmanager`: suspend blocking jobs when building high walls or filling corridors - `workorder`: reduce existing orders for automatic shearing and milking jobs when animals are no longer available -- `gui/quickfort`: protect against meta blueprints recursing infinitely if they include themselves - `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them - `gui/quickfort`: only display post-blueprint messages once when repeating the blueprint up or down z-levels -- `combine`: reduce default max stack sizes in containers to 30 +- `combine`: reduce max different stacks in containers to 30 to prevent contaners from getting overfull ## Removed - `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile From cd68cec6f3eee25aaca562577fcdcfb42300c75a Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 25 Jun 2023 14:34:42 -0400 Subject: [PATCH 330/732] devel/export-dt-ini: rename `game_extra` global back to `game` Changed in 50.08-r2 --- 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 9f26f5c440..1b5913ca20 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -129,7 +129,7 @@ address('historical_figures_vector',globals,'world','history','figures') address('world_site_type',df.world_site,'type') address('active_sites_vector',df.world_data,'active_site') address('gview',globals,'gview') -address('external_flag',globals,'game_extra','external_flag') +address('external_flag',globals,'game','external_flag') vtable('viewscreen_setupdwarfgame_vtable','viewscreen_setupdwarfgamest') header('offsets') From c1203f657eb28bc681e542d6a5688bdbf170110e Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 25 Jun 2023 15:03:00 -0400 Subject: [PATCH 331/732] Remove "unavailable" tag from devel/export-dt-ini The script reportedly works fine with the change in #756 --- docs/devel/export-dt-ini.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/devel/export-dt-ini.rst b/docs/devel/export-dt-ini.rst index 34f6f05e88..9b0428cd48 100644 --- a/docs/devel/export-dt-ini.rst +++ b/docs/devel/export-dt-ini.rst @@ -3,7 +3,7 @@ devel/export-dt-ini .. dfhack-tool:: :summary: Export memory addresses for Dwarf Therapist configuration. - :tags: unavailable dev + :tags: dev This tool exports an ini file containing memory addresses for Dwarf Therapist. From 0147521f4df37fbd62a91dfcb675a65432f0a415 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 25 Jun 2023 18:19:54 -0700 Subject: [PATCH 332/732] replace automelt on the control panel with logistics --- gui/control-panel.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 199a5fca12..24eba6376f 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -51,12 +51,14 @@ for _,v in ipairs(FORT_SERVICES) do end table.sort(FORT_AUTOSTART) +-- these are re-enabled by the default DFHack init scripts local SYSTEM_SERVICES = { - 'automelt', 'buildingplan', 'confirm', + 'logistics', 'overlay', } +-- these are fully controlled by the user local SYSTEM_USER_SERVICES = { 'faststart', 'work-now', From 53da4ad513cf972461785de7d5353d929dbca372 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 18 Jun 2023 00:56:24 -0700 Subject: [PATCH 333/732] use new textures for unsuspend overlay --- unsuspend.lua | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/unsuspend.lua b/unsuspend.lua index 87951d3294..9244728723 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -135,6 +135,18 @@ function SuspendOverlay:refresh_screen_buildings() self.screen_buildings = screen_buildings end +local function get_texposes() + local start = dfhack.textures.getMapUnsuspendTexposStart() + local valid = start > 0 + + local function tp(offset) + return valid and start + offset or nil + end + + return tp(0), tp(1), tp(2) +end +local PLANNED_TEXPOS, SUSPENDED_TEXPOS, REPEAT_SUSPENDED_TEXPOS = get_texposes() + function SuspendOverlay:render_marker(dc, bld, screen_pos) if not bld or #bld.jobs ~= 1 then return end local data = self.in_progress_buildings[bld.id] @@ -144,16 +156,13 @@ function SuspendOverlay:render_marker(dc, bld, screen_pos) or not job.flags.suspend then return end - local color = COLOR_YELLOW - local ch = 'x' + local color, ch, texpos = COLOR_YELLOW, 'x', SUSPENDED_TEXPOS if buildingplan and buildingplan.isPlannedBuilding(bld) then - color = COLOR_GREEN - ch = 'P' + color, ch, texpos = COLOR_GREEN, 'P', PLANNED_TEXPOS elseif data.suspend_count > 1 then - color = COLOR_RED - ch = 'X' + color, ch, texpos = COLOR_RED, 'X', REPEAT_SUSPENDED_TEXPOS end - dc:seek(screen_pos.x, screen_pos.y):tile(ch, nil, color) + dc:seek(screen_pos.x, screen_pos.y):tile(ch, texpos, color) end function SuspendOverlay:onRenderFrame(dc) From de927446833cedc05b823ce15ba363bbc5e6cd2b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 25 Jun 2023 18:55:57 -0700 Subject: [PATCH 334/732] use new icon mapping --- unsuspend.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsuspend.lua b/unsuspend.lua index 9244728723..42cf7dc22a 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -143,7 +143,7 @@ local function get_texposes() return valid and start + offset or nil end - return tp(0), tp(1), tp(2) + return tp(3), tp(1), tp(0) end local PLANNED_TEXPOS, SUSPENDED_TEXPOS, REPEAT_SUSPENDED_TEXPOS = get_texposes() From 79327c7fb0eb9f09e711cb7ddfaeee6665c27d22 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 26 Jun 2023 00:39:34 -0700 Subject: [PATCH 335/732] don't walk off the end of the stack when doing comparisons --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 4de154503c..1d0d0b34ae 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -173,7 +173,7 @@ function GmEditorUi:verifyStack() last_good_level = i - 1 break end - if not self.stack[i+1] == next_by_ref then + if self.stack[i+1] and not self.stack[i+1] == next_by_ref then failure = true break end From 7e758ba2c09e8d92b30c4a975c6fa95b80e45a62 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 26 Jun 2023 02:31:43 -0700 Subject: [PATCH 336/732] select all/none for liaison trade requests --- caravan.lua | 56 +++++++++++++++++++++++++++++++++++++++++++++++++-- changelog.txt | 1 + 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/caravan.lua b/caravan.lua index e11ad71f31..f4e54e42c9 100644 --- a/caravan.lua +++ b/caravan.lua @@ -42,7 +42,7 @@ function set_height(list_index, delta) if delta >= 0 then return end _,screen_height = dfhack.screen.getWindowSize() -- list only increments in three tiles at a time - local page_height = ((screen_height - 26) // 3) * 3 + local page_height = ((screen_height - MARGIN_HEIGHT) // 3) * 3 trade.scroll_position_item[list_index] = math.max(0, math.min(trade.scroll_position_item[list_index], trade.i_height[list_index] - page_height)) @@ -277,8 +277,60 @@ function CaravanTradeOverlay:onInput(keys) end end +DiplomacyOverlay = defclass(DiplomacyOverlay, overlay.OverlayWidget) +DiplomacyOverlay.ATTRS{ + default_pos={x=45,y=-6}, + default_enabled=true, + viewscreens='dwarfmode/Diplomacy/Requests', + frame={w=31, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +local diplomacy = df.global.game.main_interface.diplomacy +local function diplomacy_toggle_cat(idx, target_val) + local priority_idx = diplomacy.taking_requests_tablist[idx] + local priority = diplomacy.environment.meeting.sell_requests.priority[priority_idx] + if #priority == 0 then return end + target_val = target_val or (priority[0] == 0 and 4 or 0) + for i in ipairs(priority) do + priority[i] = target_val + end + return target_val +end + +local function diplomacy_toggle_cur_cat() + return diplomacy_toggle_cat(diplomacy.taking_requests_selected_tab) +end + +local function diplomacy_toggle_all_cat() + -- choose whether we're toggling on or off based on the current tab + local target_val = diplomacy_toggle_cur_cat() + for i in ipairs(diplomacy.taking_requests_tablist) do + target_val = diplomacy_toggle_cat(i, target_val) + end +end + +function DiplomacyOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='Toggle all shown', + key='CUSTOM_CTRL_A', + on_activate=diplomacy_toggle_cur_cat, + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='Toggle all categories', + key='CUSTOM_CTRL_B', + on_activate=diplomacy_toggle_all_cat, + }, + } +end + OVERLAY_WIDGETS = { - tradeScreenExtension=CaravanTradeOverlay, + trade=CaravanTradeOverlay, + diplomacy=DiplomacyOverlay, } INTERESTING_FLAGS = { diff --git a/changelog.txt b/changelog.txt index 714c66e217..8c187ab60b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes ## Misc Improvements +- `caravan`: new overlay for selecting all/none on trade request screen ## Removed From 1051a7e8fb27c69509bd0dd0b9b76a3189629fb7 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:54:18 +0200 Subject: [PATCH 337/732] [suspendmanager] Don't suspend construction on unwalkable tiles --- suspendmanager.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/suspendmanager.lua b/suspendmanager.lua index a0c627fddb..474403cce5 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -252,6 +252,9 @@ local function riskBlocking(job) --- job.pos is sometimes off by one, get the building pos local pos = {x=building.centerx,y=building.centery,z=building.z} + -- The construction is on a non walkable tile, it can't get worst + if not walkable(pos) then return false end + --- Get self risk of being blocked local risk = riskOfStuckConstructionAt(pos) From 17e75e042be10c5496a4732fc9850f3ed474d99d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 26 Jun 2023 11:23:55 -0700 Subject: [PATCH 338/732] remove toggle all categories there is not much call for the functionality, and it clutters the ui --- caravan.lua | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/caravan.lua b/caravan.lua index f4e54e42c9..f8e1fb8931 100644 --- a/caravan.lua +++ b/caravan.lua @@ -282,48 +282,29 @@ DiplomacyOverlay.ATTRS{ default_pos={x=45,y=-6}, default_enabled=true, viewscreens='dwarfmode/Diplomacy/Requests', - frame={w=31, h=4}, + frame={w=25, h=3}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } local diplomacy = df.global.game.main_interface.diplomacy -local function diplomacy_toggle_cat(idx, target_val) - local priority_idx = diplomacy.taking_requests_tablist[idx] +local function diplomacy_toggle_cat() + local priority_idx = diplomacy.taking_requests_tablist[diplomacy.taking_requests_selected_tab] local priority = diplomacy.environment.meeting.sell_requests.priority[priority_idx] if #priority == 0 then return end - target_val = target_val or (priority[0] == 0 and 4 or 0) + local target_val = priority[0] == 0 and 4 or 0 for i in ipairs(priority) do priority[i] = target_val end - return target_val -end - -local function diplomacy_toggle_cur_cat() - return diplomacy_toggle_cat(diplomacy.taking_requests_selected_tab) -end - -local function diplomacy_toggle_all_cat() - -- choose whether we're toggling on or off based on the current tab - local target_val = diplomacy_toggle_cur_cat() - for i in ipairs(diplomacy.taking_requests_tablist) do - target_val = diplomacy_toggle_cat(i, target_val) - end end function DiplomacyOverlay:init() self:addviews{ widgets.HotkeyLabel{ frame={t=0, l=0}, - label='Toggle all shown', + label='Select all/none', key='CUSTOM_CTRL_A', - on_activate=diplomacy_toggle_cur_cat, - }, - widgets.HotkeyLabel{ - frame={t=1, l=0}, - label='Toggle all categories', - key='CUSTOM_CTRL_B', - on_activate=diplomacy_toggle_all_cat, + on_activate=diplomacy_toggle_cat, }, } end From 46bca726e8044dfc2c72eddfdfcc2021a0486a13 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 26 Jun 2023 16:29:39 -0700 Subject: [PATCH 339/732] update changelog to 50.09-r1 --- changelog.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 8c187ab60b..784dd76e57 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,10 +18,15 @@ that repo. ## Fixes ## Misc Improvements -- `caravan`: new overlay for selecting all/none on trade request screen ## Removed +# 50.09-r1 + +## Misc Improvements +- `caravan`: new overlay for selecting all/none on trade request screen +- `suspendmanager`: don't suspend constructions that are built over open space + # 50.08-r4 ## Fixes From ee8217978d25bc8f9c3efa21156dd8ff99896f68 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 30 Jun 2023 15:43:25 -0700 Subject: [PATCH 340/732] update docs for ban-cooking --- docs/ban-cooking.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ban-cooking.rst b/docs/ban-cooking.rst index f7a2cbf4ed..afcf9fc86c 100644 --- a/docs/ban-cooking.rst +++ b/docs/ban-cooking.rst @@ -22,11 +22,11 @@ call: ``ban-cooking oil tallow`` will ban both oil and tallow from cooking. Examples:: - on-new-fortress ban-cooking booze; ban-cooking brew; ban-cooking fruit; - ban-cooking honey; ban-cooking milk; ban-cooking mill; ban-cooking oil; - ban-cooking seeds; ban-cooking tallow; ban-cooking thread + on-new-fortress ban-cooking all Ban cooking all otherwise useful ingredients once when starting a new fortress. +Note that this exact command can be enabled via the ``Autostart`` tab of +`gui/control-panel`. Options ------- From c85e3af91d09e412cb19721b21513d440ce1b523 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Jul 2023 16:53:11 -0700 Subject: [PATCH 341/732] properly teleport items in jobs --- changelog.txt | 1 + gui/autodump.lua | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 784dd76e57..fa50735996 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,7 @@ that repo. ## New Scripts ## Fixes +- `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported ## Misc Improvements diff --git a/gui/autodump.lua b/gui/autodump.lua index 5600d4ad76..a0ab71f686 100644 --- a/gui/autodump.lua +++ b/gui/autodump.lua @@ -338,14 +338,19 @@ function Autodump:do_dump(pos) if not pos then return end local tileattrs = df.tiletype.attrs[dfhack.maps.getTileType(pos)] local basic_shape = df.tiletype_shape.attrs[tileattrs.shape].basic_shape - print(basic_shape, df.tiletype_shape_basic[basic_shape]) local on_ground = basic_shape == df.tiletype_shape_basic.Floor or basic_shape == df.tiletype_shape_basic.Stair or basic_shape == df.tiletype_shape_basic.Ramp local items = #self.selected_items.list > 0 and self.selected_items.list or self.dump_items - print(('dumping %d items at (%d, %d, %d):'):format(#items, pos.x, pos.y, pos.z)) local mark_as_forbidden = self.subviews.mark_as_forbidden:getOptionValue() + print(('teleporting %d items'):format(#items)) for _,item in ipairs(items) do + if item.flags.in_job then + local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if job_ref then + dfhack.job.removeJob(job_ref.data.job) + end + end if dfhack.items.moveToGround(item, pos) then item.flags.dump = false item.flags.trader = false From 454361e0ba9752e01699604a84d70b2df785f33c Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 3 Jul 2023 16:35:40 +0200 Subject: [PATCH 342/732] [suspendmanager] Take in account already built buildings --- changelog.txt | 2 ++ suspendmanager.lua | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 784dd76e57..3fa88d97e6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,8 @@ that repo. ## Fixes +- `suspendmanager`: take in account already built blocking buildings + ## Misc Improvements ## Removed diff --git a/suspendmanager.lua b/suspendmanager.lua index 474403cce5..b953479526 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -206,7 +206,13 @@ local function walkable(pos) end local attrs = df.tiletype.attrs[tt] local shape_attrs = df.tiletype_shape.attrs[attrs.shape] - return shape_attrs.walkable + + if not shape_attrs.walkable then + return false + end + + local building = dfhack.buildings.findAtTile(pos) + return not building or not building.flags.exists or not isImpassable(building) end --- List neighbour coordinates of a position From 193bbb1b8d9ad0322b94b5b8b315f1794284ca85 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Jul 2023 02:43:39 +0000 Subject: [PATCH 343/732] [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.23.1 → 0.23.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.23.1...0.23.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 4b3cca5564..a2c2fccdeb 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.23.1 + rev: 0.23.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 596e68b396c4d44622f6160a6c7d4f35051c137c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 5 Jul 2023 19:31:22 -0700 Subject: [PATCH 344/732] fix typo in commandline processing --- changelog.txt | 1 + gui/gm-unit.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index d8d8dad2c2..375063963e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ that repo. ## Fixes - `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported +- `gui/gm-unit`: fix commandline processing when a unit id is specified - `suspendmanager`: take in account already built blocking buildings diff --git a/gui/gm-unit.lua b/gui/gm-unit.lua index 2e078f38b0..f37ec6f546 100644 --- a/gui/gm-unit.lua +++ b/gui/gm-unit.lua @@ -9,7 +9,7 @@ rng = rng or dfhack.random.new(nil, 10) local target --TODO: add more ways to guess what unit you want to edit if args[1] ~= nil then - target = df.units.find(args[1]) + target = df.unit.find(args[1]) else target = dfhack.gui.getSelectedUnit(true) end From 842407ac14739decb612146729d68e6057de3bd3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 5 Jul 2023 19:31:53 -0700 Subject: [PATCH 345/732] remove conflict with Ctrl-C -- Ctrl-X clears now --- gui/launcher.lua | 8 -------- 1 file changed, 8 deletions(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index cd454c8451..31f26b1865 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -518,14 +518,6 @@ end function MainPanel:onInput(keys) if MainPanel.super.onInput(self, keys) then return true - elseif keys.CUSTOM_CTRL_C then - if self.focus_group.cur == self.subviews.editfield then - self.subviews.edit:set_text('') - self.on_edit_input('') - else - self.focus_group.cur:setText('') - end - return true elseif keys.CUSTOM_CTRL_D then dev_mode = not dev_mode self.update_autocomplete(get_first_word(self.subviews.editfield.text)) From 703b5f0d482f3a3cee406620ebd3df67f0f4da3b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 5 Jul 2023 19:32:15 -0700 Subject: [PATCH 346/732] migrate to library dfhack.units.getUnitByNobleRole --- internal/quickfort/zone.lua | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 58462507f9..2118260d9b 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -226,37 +226,10 @@ local function parse_location_props(props) return location_data end -local function get_noble_position_id(positions, noble) - noble = noble:upper() - for _,position in ipairs(positions.own) do - if position.code == noble then return position.id end - end -end - -local function get_assigned_noble_unit(positions, noble_position_id) - for _,assignment in ipairs(positions.assignments) do - if assignment.position_id == noble_position_id then - local histfig = df.historical_figure.find(assignment.histfig) - if not histfig then return end - return df.unit.find(histfig.unit_id) - end - end -end - local function get_noble_unit(noble) - local site = df.global.world.world_data.active_site[0] - for _,entity_site_link in ipairs(site.entity_links) do - local gov = df.historical_entity.find(entity_site_link.entity_id) - if not gov or gov.type ~= df.historical_entity_type.SiteGovernment then goto continue end - local noble_position_id = get_noble_position_id(gov.positions, noble) - if not noble_position_id then - dfhack.printerr(('could not find a noble position for: "%s"'):format(noble)) - return - else - return get_assigned_noble_unit(gov.positions, noble_position_id) - end - ::continue:: - end + local unit = dfhack.units.getUnitByNobleRole(noble) + if not unit then log('could not find a noble position for: "%s"', noble) end + return unit end local function parse_zone_config(c, props) From 756a5b33baf5777c328c9fe9000f772d74da4bb8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 29 Jun 2023 19:40:29 -0700 Subject: [PATCH 347/732] add initial move goods to depot screen --- caravan.lua | 656 ++++++++++++++++++++++++++++++++++++++++++++++- docs/caravan.rst | 23 +- 2 files changed, 675 insertions(+), 4 deletions(-) diff --git a/caravan.lua b/caravan.lua index f8e1fb8931..cb4b456f0e 100644 --- a/caravan.lua +++ b/caravan.lua @@ -8,6 +8,7 @@ local gui = require('gui') local overlay = require('plugins.overlay') +local utils = require('utils') local widgets = require('gui.widgets') trader_selected_state = trader_selected_state or {} @@ -277,9 +278,13 @@ function CaravanTradeOverlay:onInput(keys) end end +-- ------------------- +-- DiplomacyOverlay +-- + DiplomacyOverlay = defclass(DiplomacyOverlay, overlay.OverlayWidget) DiplomacyOverlay.ATTRS{ - default_pos={x=45,y=-6}, + default_pos={x=45, y=-6}, default_enabled=true, viewscreens='dwarfmode/Diplomacy/Requests', frame={w=25, h=3}, @@ -309,9 +314,658 @@ function DiplomacyOverlay:init() } end +-- ------------------- +-- MoveGoods +-- + +MoveGoods = defclass(MoveGoods, widgets.Window) +MoveGoods.ATTRS { + frame_title='Select trade goods', + frame={w=80, h=45}, + resizable=true, + resize_min={w=50, h=20}, + pending_item_ids=DEFAULT_NIL, +} + +local VALUE_COL_WIDTH = 8 +local QTY_COL_WIDTH = 6 + +local function sort_noop(a, b) + -- this function is used as a marker and never actually gets called + error('sort_noop should not be called') +end + +local function sort_base(a, b) + return a.data.desc < b.data.desc +end + +local function sort_by_name_desc(a, b) + if a.search_key == b.search_key then + return sort_base(a, b) + end + return a.search_key < b.search_key +end + +local function sort_by_name_asc(a, b) + if a.search_key == b.search_key then + return sort_base(a, b) + end + return a.search_key > b.search_key +end + +local function sort_by_value_desc(a, b) + if a.data.real_value == b.data.real_value then + return sort_by_name_desc(a, b) + end + return a.data.real_value > b.data.real_value +end + +local function sort_by_value_asc(a, b) + if a.data.real_value == b.data.real_value then + return sort_by_name_desc(a, b) + end + return a.data.real_value < b.data.real_value +end + +local function sort_by_quantity_desc(a, b) + if a.data.quantity == b.data.quantity then + return sort_by_name_desc(a, b) + end + return a.data.quantity > b.data.quantity +end + +local function sort_by_quantity_asc(a, b) + if a.data.quantity == b.data.quantity then + return sort_by_name_desc(a, b) + end + return a.data.quantity < b.data.quantity +end + +-- takes into account trade agreements +local function get_perceived_value(item) + -- TODO: take trade agreements into account + return dfhack.items.getValue(item) +end + +local function get_value_at_depot() + local sum = 0 + -- if we're here, then the overlay has already determined that this is a depot + local depot = dfhack.gui.getSelectedBuilding(true) + for _, contained_item in ipairs(depot.contained_items) do + if contained_item.use_mode ~= 0 then goto continue end + local item = contained_item.item + sum = sum + get_perceived_value(item) + ::continue:: + end + return sum +end + +-- adapted from https://stackoverflow.com/a/50860705 +local function sig_fig(num, figures) + if num <= 0 then return 0 end + local x = figures - math.ceil(math.log(num, 10)) + return math.floor(math.floor(num * 10^x + 0.5) * 10^-x) +end + +local function obfuscate_value(value) + -- TODO: respect skill of broker + local num_sig_figs = 1 + local str = tostring(sig_fig(value, num_sig_figs)) + if #str > num_sig_figs then str = '~' .. str end + return str +end + +local CH_UP = string.char(30) +local CH_DN = string.char(31) + +function MoveGoods:init() + self.value_at_depot = get_value_at_depot() + self.value_pending = 0 + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={l=0, t=0, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_S', + options={ + {label='value'..CH_DN, value=sort_by_value_desc}, + {label='value'..CH_UP, value=sort_by_value_asc}, + {label='name'..CH_DN, value=sort_by_name_desc}, + {label='name'..CH_UP, value=sort_by_name_asc}, + {label='qty'..CH_DN, value=sort_by_quantity_desc}, + {label='qty'..CH_UP, value=sort_by_quantity_asc}, + }, + initial_option=sort_by_value_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={l=26, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, + }, + widgets.ToggleHotkeyLabel{ + view_id='show_forbidden', + frame={t=2, l=0, w=36}, + label='Include forbidden items', + key='CUSTOM_SHIFT_F', + initial_option=true, + on_change=function() self:refresh_list() end, + }, + widgets.Panel{ + frame={t=4, l=0, w=40, h=3}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_condition', + frame={l=0, t=0, w=18}, + label='Min condition:', + label_below=true, + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options={ + {label='Tattered (XX)', value=3}, + {label='Frayed (X)', value=2}, + {label='Worn (x)', value=1}, + {label='Pristine', value=0}, + }, + initial_option=3, + on_change=function(val) + if self.subviews.max_condition:getOptionValue() > val then + self.subviews.max_condition:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_condition', + frame={r=1, t=0, w=18}, + label='Max condition:', + label_below=true, + key_back='CUSTOM_SHIFT_E', + key='CUSTOM_SHIFT_R', + options={ + {label='Tattered (XX)', value=3}, + {label='Frayed (X)', value=2}, + {label='Worn (x)', value=1}, + {label='Pristine', value=0}, + }, + initial_option=0, + on_change=function(val) + if self.subviews.min_condition:getOptionValue() < val then + self.subviews.min_condition:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=2}, + num_stops=4, + get_left_idx_fn=function() + return 4 - self.subviews.min_condition:getOptionValue() + end, + get_right_idx_fn=function() + return 4 - self.subviews.max_condition:getOptionValue() + end, + on_left_change=function(idx) self.subviews.min_condition:setOption(4-idx, true) end, + on_right_change=function(idx) self.subviews.max_condition:setOption(4-idx, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=8, l=0, w=40, h=3}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_quality', + frame={l=0, t=0, w=18}, + label='Min quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_SHIFT_X', + options={ + {label='Ordinary', value=0}, + {label='Well Crafted', value=1}, + {label='Finely Crafted', value=2}, + {label='Superior', value=3}, + {label='Exceptional', value=4}, + {label='Masterful', value=5}, + {label='Artifact', value=6}, + }, + initial_option=0, + on_change=function(val) + if self.subviews.max_quality:getOptionValue() < val then + self.subviews.max_quality:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_quality', + frame={r=1, t=0, w=18}, + label='Max quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Q', + key='CUSTOM_SHIFT_W', + options={ + {label='Ordinary', value=0}, + {label='Well Crafted', value=1}, + {label='Finely Crafted', value=2}, + {label='Superior', value=3}, + {label='Exceptional', value=4}, + {label='Masterful', value=5}, + {label='Artifact', value=6}, + }, + initial_option=6, + on_change=function(val) + if self.subviews.min_quality:getOptionValue() > val then + self.subviews.min_quality:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=2}, + num_stops=7, + get_left_idx_fn=function() + return self.subviews.min_quality:getOptionValue() + 1 + end, + get_right_idx_fn=function() + return self.subviews.max_quality:getOptionValue() + 1 + end, + on_left_change=function(idx) self.subviews.min_quality:setOption(idx-1, true) end, + on_right_change=function(idx) self.subviews.max_quality:setOption(idx-1, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=12, l=0, r=0, b=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_value', + frame={l=2, t=0, w=7}, + options={ + {label='value', value=sort_noop}, + {label='value'..CH_DN, value=sort_by_value_desc}, + {label='value'..CH_UP, value=sort_by_value_asc}, + }, + initial_option=sort_by_value_desc, + on_change=self:callback('refresh_list', 'sort_value'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_quantity', + frame={l=2+VALUE_COL_WIDTH+2, t=0, w=5}, + options={ + {label='qty', value=sort_noop}, + {label='qty'..CH_DN, value=sort_by_quantity_desc}, + {label='qty'..CH_UP, value=sort_by_quantity_asc}, + }, + on_change=self:callback('refresh_list', 'sort_quantity'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_name', + frame={l=2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+2, t=0, w=6}, + options={ + {label='name', value=sort_noop}, + {label='name'..CH_DN, value=sort_by_name_desc}, + {label='name'..CH_UP, value=sort_by_name_asc}, + }, + on_change=self:callback('refresh_list', 'sort_name'), + }, + widgets.FilteredList{ + view_id='list', + frame={l=0, t=2, r=0, b=0}, + icon_width=2, + on_submit=self:callback('toggle_item'), + }, + } + }, + widgets.Label{ + frame={l=0, b=2, h=1, r=0}, + text={ + 'Value of items at trade depot/being brought to depot/total:', + {gap=1, text=obfuscate_value(self.value_at_depot)}, + '/', + {text=function() return obfuscate_value(self.value_pending) end}, + '/', + {text=function() return obfuscate_value(self.value_pending + self.value_at_depot) end} + }, + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + label='Select all/none', + key='CUSTOM_CTRL_V', + on_activate=self:callback('toggle_visible'), + auto_width=true, + }, + widgets.ToggleHotkeyLabel{ + view_id='disable_buckets', + frame={l=26, b=0}, + label='Show individual items', + key='CUSTOM_CTRL_I', + initial_option=false, + on_change=function() self:refresh_list() end, + }, + } + + -- replace the FilteredList's built-in EditField with our own + self.subviews.list.list.frame.t = 0 + self.subviews.list.edit.visible = false + self.subviews.list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + + self.subviews.list:setChoices(self:get_choices()) +end + +function MoveGoods:refresh_list(sort_widget, sort_fn) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == sort_noop then + self.subviews[sort_widget]:cycle() + return + end + for _,widget_name in ipairs{'sort', 'sort_value', 'sort_quantity', 'sort_name'} do + self.subviews[widget_name]:setOption(sort_fn) + end + local list = self.subviews.list + local saved_filter = list:getFilter() + list:setFilter('') + list:setChoices(self:get_choices(), list:getSelected()) + list:setFilter(saved_filter) +end + +local function is_tradeable_item(item) + if not item.flags.on_ground or + item.flags.hostile or + item.flags.in_inventory or + item.flags.removed or + item.flags.in_building or + item.flags.dead_dwarf or + item.flags.spider_web or + item.flags.construction or + item.flags.encased or + item.flags.unk12 or + item.flags.murder or + item.flags.trader or + item.flags.owned or + item.flags.garbage_collect or + item.flags.on_fire or + item.flags.in_chest + then + return false + end + if item.flags.in_job then + local spec_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if not spec_ref then return true end + return spec_ref.data.job.job_type == df.job_type.BringItemToDepot + end + return true +end + +local function make_search_key(str) + local out = '' + for c in str:gmatch("[%w%s]") do + out = out .. c:lower() + end + return out +end + +local to_pen = dfhack.pen.parse +local SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} +local ALL_PEN = to_pen{ch='+', fg=COLOR_LIGHTGREEN} + +local function get_entry_icon(data, item_id) + if data.selected == 0 then return nil end + if item_id then + return data.items[item_id].pending and ALL_PEN or nil + end + if data.quantity == data.selected then return ALL_PEN end + return SOME_PEN +end + +local function make_choice_text(desc, value, quantity) + return { + {width=VALUE_COL_WIDTH, rjustify=true, text=value}, + {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, + {gap=2, text=desc}, + } +end + +local function get_artifact_name(item) + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + if not gref then return end + local artifact = df.artifact_record.find(gref.artifact_id) + if not artifact then return end + return dfhack.TranslateName(artifact.name) +end + +function MoveGoods:cache_choices(disable_buckets) + if self.choices then return self.choices[disable_buckets] end + + local pending = self.pending_item_ids + local buckets = {} + for _, item in ipairs(df.global.world.items.all) do + local item_id = item.id + if not item or not is_tradeable_item(item) then goto continue end + local value = get_perceived_value(item) + if value <= 0 then goto continue end + local is_pending = not not pending[item_id] + local is_forbidden = item.flags.forbid + local wear_level = item:getWear() + local desc = item.flags.artifact and get_artifact_name(item) or + dfhack.items.getDescription(item, 0, true) + if wear_level == 1 then desc = ('x%sx'):format(desc) + elseif wear_level == 2 then desc = ('X%sX'):format(desc) + elseif wear_level == 3 then desc = ('XX%sXX'):format(desc) + end + local key = ('%s/%d'):format(desc, value) + if buckets[key] then + local bucket = buckets[key] + bucket.data.items[item_id] = {item=item, pending=is_pending} + bucket.data.quantity = bucket.data.quantity + 1 + bucket.data.selected = bucket.data.selected + (is_pending and 1 or 0) + bucket.data.has_forbidden = bucket.data.has_forbidden or is_forbidden + else + local data = { + desc=desc, + real_value=value, + display_value=obfuscate_value(value), + items={[item_id]={item=item, pending=is_pending}}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + quality=item:getQuality(), + wear=wear_level, + selected=is_pending and 1 or 0, + has_forbidden=is_forbidden, + dirty=false, + } + local entry = { + search_key=make_search_key(desc), + icon=curry(get_entry_icon, data), + data=data, + } + buckets[key] = entry + end + ::continue:: + end + + local bucket_choices, nobucket_choices = {}, {} + for _, bucket in pairs(buckets) do + local data = bucket.data + for item_id in pairs(data.items) do + local nobucket_choice = copyall(bucket) + nobucket_choice.icon = curry(get_entry_icon, data, item_id) + nobucket_choice.text = make_choice_text(data.desc, data.display_value, 1) + nobucket_choice.item_id = item_id + table.insert(nobucket_choices, nobucket_choice) + end + bucket.text = make_choice_text(data.desc, data.display_value, data.quantity) + table.insert(bucket_choices, bucket) + self.value_pending = self.value_pending + (data.real_value * data.selected) + end + + self.choices = {} + self.choices[false] = bucket_choices + self.choices[true] = nobucket_choices + return self:cache_choices(disable_buckets) +end + +function MoveGoods:get_choices() + local raw_choices = self:cache_choices(self.subviews.disable_buckets:getOptionValue()) + local choices = {} + local include_forbidden = self.subviews.show_forbidden:getOptionValue() + local min_condition = self.subviews.min_condition:getOptionValue() + local max_condition = self.subviews.max_condition:getOptionValue() + local min_quality = self.subviews.min_quality:getOptionValue() + local max_quality = self.subviews.max_quality:getOptionValue() + for _,choice in ipairs(raw_choices) do + local data = choice.data + if not include_forbidden then + if choice.item_id then + if data.items[choice.item_id].item.flags.forbid then + goto continue + end + elseif data.has_forbidden then + goto continue + end + end + if min_condition < data.wear then goto continue end + if max_condition > data.wear then goto continue end + if min_quality > data.quality then goto continue end + if max_quality < data.quality then goto continue end + table.insert(choices, choice) + ::continue:: + end + table.sort(choices, self.subviews.sort:getOptionValue()) + return choices +end + +function MoveGoods:toggle_item(_, choice, target_value) + if choice.item_id then + local item_data = choice.data.items[choice.item_id] + if item_data.pending then + self.value_pending = self.value_pending - choice.data.real_value + choice.data.selected = choice.data.selected - 1 + end + if target_value == nil then target_value = not item_data.pending end + item_data.pending = target_value + if item_data.pending then + self.value_pending = self.value_pending + choice.data.real_value + choice.data.selected = choice.data.selected + 1 + end + else + self.value_pending = self.value_pending - (choice.data.selected * choice.data.real_value) + if target_value == nil then target_value = (choice.data.selected ~= choice.data.quantity) end + for _, item_data in pairs(choice.data.items) do + item_data.pending = target_value + end + choice.data.selected = target_value and choice.data.quantity or 0 + self.value_pending = self.value_pending + (choice.data.selected * choice.data.real_value) + end + choice.data.dirty = true + return target_value +end + +function MoveGoods:toggle_visible() + local target_value + for _, choice in pairs(self.subviews.list:getVisibleChoices()) do + target_value = self:toggle_item(nil, choice, target_value) + end +end + +MoveGoodsModal = defclass(MoveGoodsModal, gui.ZScreenModal) +MoveGoodsModal.ATTRS { + focus_path='movegoods', +} + +local function get_pending_trade_item_ids() + local item_ids = {} + for _,job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.BringItemToDepot and #job.items > 0 then + item_ids[job.items[0].item.id] = true + end + end + return item_ids +end + +function MoveGoodsModal:init() + self.pending_item_ids = get_pending_trade_item_ids() + self:addviews{MoveGoods{pending_item_ids=self.pending_item_ids}} +end + +function MoveGoodsModal:onDismiss() + -- mark/unmark selected goods for trade + local depot = dfhack.gui.getSelectedBuilding(true) + if not depot then return end + local pending = self.pending_item_ids + for _, choice in ipairs(self.subviews.list:getChoices()) do + if not choice.data.dirty then goto continue end + for item_id, item_data in pairs(choice.data.items) do + if item_data.pending and not pending[item_id] then + item_data.item.flags.forbid = false + dfhack.items.markForTrade(item_data.item, depot) + elseif not item_data.pending and pending[item_id] then + local spec_ref = dfhack.items.getSpecificRef(item_data.item, df.specific_ref_type.JOB) + if spec_ref then + dfhack.job.removeJob(spec_ref.data.job) + end + end + end + ::continue:: + end +end + +-- ------------------- +-- MoveGoodsOverlay +-- + +MoveGoodsOverlay = defclass(MoveGoodsOverlay, overlay.OverlayWidget) +MoveGoodsOverlay.ATTRS{ + default_pos={x=-60, y=10}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING/TradeDepot', + frame={w=35, h=1}, + frame_background=gui.CLEAR_PEN, +} + +local function has_trade_depot_and_caravan() + local bld = dfhack.gui.getSelectedBuilding(true) + if not bld or bld:getBuildStage() < bld:getMaxBuildStage() then + return false + end + if #bld.jobs == 1 and bld.jobs[0].job_type == df.job_type.DestroyBuilding then + return false + end + + for _, caravan in ipairs(df.global.plotinfo.caravans) do + local trade_state = caravan.trade_state + local time_remaining = caravan.time_remaining + if time_remaining > 0 and + (trade_state == df.caravan_state.T_trade_state.Approaching or + trade_state == df.caravan_state.T_trade_state.AtDepot) + then + return true + end + end + return false +end + +function MoveGoodsOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='DFHack trade goods helper', + key='CUSTOM_CTRL_T', + on_activate=function() MoveGoodsModal{}:show() end, + enabled=has_trade_depot_and_caravan, + }, + } +end + OVERLAY_WIDGETS = { trade=CaravanTradeOverlay, diplomacy=DiplomacyOverlay, + movegoods=MoveGoodsOverlay, } INTERESTING_FLAGS = { diff --git a/docs/caravan.rst b/docs/caravan.rst index 4be6e00940..ae5a2f108b 100644 --- a/docs/caravan.rst +++ b/docs/caravan.rst @@ -45,10 +45,14 @@ Examples ``caravan unload`` Fix a caravan that got spooked by wildlife and refuses to fully unload. -Overlay -------- +Overlays +-------- + +Additional functionality is provided on the various trade-related screens via +`overlay` widgets. -Additional functionality is provided when the trade screen is open via an `overlay` widget: +Trade screen +```````````` - ``Shift+Click checkbox``: Select all items inside a bin without selecting the bin itself @@ -66,3 +70,16 @@ vanilla game when you hold shift while scrolling (this works everywhere). You can turn the overlay on and off in `gui/control-panel`, or you can reposition it to your liking with `gui/overlay`. The overlay is named ``caravan.tradeScreenExtension``. + +Bring item to depot +``````````````````` + +When the trade depot is selected, a button appears to bring up the DFHack +enhanced trade goods screen. You'll get a searchable, sortable list of all your +tradeable items, with options to quickly select or deselect classes of items. + +Trade agreement +``````````````` + +A small panel is shown with a hotkey (``Ctrl-A``) for selecting all/none in the +currently shown category. From 899fc9a263a1b9cbfc60e56beaa19930ecdf89e5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 30 Jun 2023 01:52:28 -0700 Subject: [PATCH 348/732] interface and functionality updates --- caravan.lua | 186 +++++++++++++++++++++++++++++++++++++---------- docs/caravan.rst | 15 +++- 2 files changed, 160 insertions(+), 41 deletions(-) diff --git a/caravan.lua b/caravan.lua index cb4b456f0e..1bc87f8a2e 100644 --- a/caravan.lua +++ b/caravan.lua @@ -321,9 +321,9 @@ end MoveGoods = defclass(MoveGoods, widgets.Window) MoveGoods.ATTRS { frame_title='Select trade goods', - frame={w=80, h=45}, + frame={w=83, h=45}, resizable=true, - resize_min={w=50, h=20}, + resize_min={h=27}, pending_item_ids=DEFAULT_NIL, } @@ -354,17 +354,19 @@ local function sort_by_name_asc(a, b) end local function sort_by_value_desc(a, b) - if a.data.real_value == b.data.real_value then + local value_field = a.item_id and 'per_item_value' or 'total_value' + if a.data[value_field] == b.data[value_field] then return sort_by_name_desc(a, b) end - return a.data.real_value > b.data.real_value + return a.data[value_field] > b.data[value_field] end local function sort_by_value_asc(a, b) - if a.data.real_value == b.data.real_value then + local value_field = a.item_id and 'per_item_value' or 'total_value' + if a.data[value_field] == b.data[value_field] then return sort_by_name_desc(a, b) end - return a.data.real_value < b.data.real_value + return a.data[value_field] < b.data[value_field] end local function sort_by_quantity_desc(a, b) @@ -381,10 +383,27 @@ local function sort_by_quantity_asc(a, b) return a.data.quantity < b.data.quantity end +local function has_export_agreement() + -- TODO: where are export agreements stored? + return false +end + +local function is_agreement_item(item_type) + -- TODO: match export agreement with civs with active caravans + return false +end + -- takes into account trade agreements local function get_perceived_value(item) -- TODO: take trade agreements into account - return dfhack.items.getValue(item) + local value = dfhack.items.getValue(item) + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + value = value + dfhack.items.getValue(contained_item) + for _,contained_contained_item in ipairs(dfhack.items.getContainedItems(contained_item)) do + value = value + dfhack.items.getValue(contained_contained_item) + end + end + return value end local function get_value_at_depot() @@ -431,10 +450,10 @@ function MoveGoods:init() options={ {label='value'..CH_DN, value=sort_by_value_desc}, {label='value'..CH_UP, value=sort_by_value_asc}, - {label='name'..CH_DN, value=sort_by_name_desc}, - {label='name'..CH_UP, value=sort_by_name_asc}, {label='qty'..CH_DN, value=sort_by_quantity_desc}, {label='qty'..CH_UP, value=sort_by_quantity_asc}, + {label='name'..CH_DN, value=sort_by_name_desc}, + {label='name'..CH_UP, value=sort_by_name_asc}, }, initial_option=sort_by_value_desc, on_change=self:callback('refresh_list', 'sort'), @@ -447,14 +466,31 @@ function MoveGoods:init() }, widgets.ToggleHotkeyLabel{ view_id='show_forbidden', - frame={t=2, l=0, w=36}, - label='Include forbidden items', + frame={t=2, l=0, w=27}, + label='Show forbidden items', key='CUSTOM_SHIFT_F', initial_option=true, on_change=function() self:refresh_list() end, }, + widgets.ToggleHotkeyLabel{ + view_id='show_banned', + frame={t=3, l=0, w=43}, + label='Show items banned by export mandates', + key='CUSTOM_SHIFT_B', + initial_option=false, + on_change=function() self:refresh_list() end, + }, + widgets.ToggleHotkeyLabel{ + view_id='only_agreement', + frame={t=4, l=0, w=52}, + label='Show only items requested by export agreement', + key='CUSTOM_SHIFT_A', + initial_option=false, + on_change=function() self:refresh_list() end, + enabled=has_export_agreement(), + }, widgets.Panel{ - frame={t=4, l=0, w=40, h=3}, + frame={t=6, l=0, w=40, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_condition', @@ -499,7 +535,7 @@ function MoveGoods:init() end, }, widgets.RangeSlider{ - frame={l=0, t=2}, + frame={l=0, t=3}, num_stops=4, get_left_idx_fn=function() return 4 - self.subviews.min_condition:getOptionValue() @@ -513,7 +549,7 @@ function MoveGoods:init() }, }, widgets.Panel{ - frame={t=8, l=0, w=40, h=3}, + frame={t=6, l=41, w=38, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_quality', @@ -564,7 +600,7 @@ function MoveGoods:init() end, }, widgets.RangeSlider{ - frame={l=0, t=2}, + frame={l=0, t=3}, num_stops=7, get_left_idx_fn=function() return self.subviews.min_quality:getOptionValue() + 1 @@ -578,7 +614,7 @@ function MoveGoods:init() }, }, widgets.Panel{ - frame={t=12, l=0, r=0, b=4}, + frame={t=11, l=0, r=0, b=6}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_value', @@ -616,11 +652,13 @@ function MoveGoods:init() frame={l=0, t=2, r=0, b=0}, icon_width=2, on_submit=self:callback('toggle_item'), + on_submit2=self:callback('toggle_range'), + on_select=self:callback('select_item'), }, } }, widgets.Label{ - frame={l=0, b=2, h=1, r=0}, + frame={l=0, b=4, h=1, r=0}, text={ 'Value of items at trade depot/being brought to depot/total:', {gap=1, text=obfuscate_value(self.value_at_depot)}, @@ -631,7 +669,7 @@ function MoveGoods:init() }, }, widgets.HotkeyLabel{ - frame={l=0, b=0}, + frame={l=0, b=2}, label='Select all/none', key='CUSTOM_CTRL_V', on_activate=self:callback('toggle_visible'), @@ -639,12 +677,16 @@ function MoveGoods:init() }, widgets.ToggleHotkeyLabel{ view_id='disable_buckets', - frame={l=26, b=0}, + frame={l=26, b=2}, label='Show individual items', key='CUSTOM_CTRL_I', initial_option=false, on_change=function() self:refresh_list() end, }, + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap='Click to mark/unmark for trade. Shift click to mark/unmark a range of items.', + }, } -- replace the FilteredList's built-in EditField with our own @@ -724,18 +766,43 @@ end local function make_choice_text(desc, value, quantity) return { - {width=VALUE_COL_WIDTH, rjustify=true, text=value}, + {width=VALUE_COL_WIDTH, rjustify=true, text=obfuscate_value(value)}, {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, {gap=2, text=desc}, } end +-- returns true if the item or any contained item is banned +local function scan_banned(item) + if not dfhack.items.checkMandates(item) then return true end + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + if not dfhack.items.checkMandates(contained_item) then return true end + end + return false +end + +local function to_title_case(str) + str = str:gsub('(%a)([%w_]*)', + function (first, rest) return first:upper()..rest:lower() end) + str = str:gsub('_', ' ') + return str +end + +local function get_item_type_str(item) + local str = to_title_case(df.item_type[item:getType()]) + if str == 'Trapparts' then + str = 'Mechanism' + end + return str +end + local function get_artifact_name(item) local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) if not gref then return end local artifact = df.artifact_record.find(gref.artifact_id) if not artifact then return end - return dfhack.TranslateName(artifact.name) + local name = dfhack.TranslateName(artifact.name) + return ('%s (%s)'):format(name, get_item_type_str(item)) end function MoveGoods:cache_choices(disable_buckets) @@ -750,6 +817,7 @@ function MoveGoods:cache_choices(disable_buckets) if value <= 0 then goto continue end local is_pending = not not pending[item_id] local is_forbidden = item.flags.forbid + local is_banned = scan_banned(item) local wear_level = item:getWear() local desc = item.flags.artifact and get_artifact_name(item) or dfhack.items.getDescription(item, 0, true) @@ -760,23 +828,24 @@ function MoveGoods:cache_choices(disable_buckets) local key = ('%s/%d'):format(desc, value) if buckets[key] then local bucket = buckets[key] - bucket.data.items[item_id] = {item=item, pending=is_pending} + bucket.data.items[item_id] = {item=item, pending=is_pending, banned=is_banned} bucket.data.quantity = bucket.data.quantity + 1 bucket.data.selected = bucket.data.selected + (is_pending and 1 or 0) bucket.data.has_forbidden = bucket.data.has_forbidden or is_forbidden + bucket.data.has_banned = bucket.data.has_banned or is_banned else local data = { desc=desc, - real_value=value, - display_value=obfuscate_value(value), - items={[item_id]={item=item, pending=is_pending}}, + per_item_value=value, + items={[item_id]={item=item, pending=is_pending, banned=is_banned}}, item_type=item:getType(), item_subtype=item:getSubtype(), quantity=1, - quality=item:getQuality(), + quality=item.flags.artifact and 6 or item:getQuality(), wear=wear_level, selected=is_pending and 1 or 0, has_forbidden=is_forbidden, + has_banned=is_banned, dirty=false, } local entry = { @@ -795,13 +864,14 @@ function MoveGoods:cache_choices(disable_buckets) for item_id in pairs(data.items) do local nobucket_choice = copyall(bucket) nobucket_choice.icon = curry(get_entry_icon, data, item_id) - nobucket_choice.text = make_choice_text(data.desc, data.display_value, 1) + nobucket_choice.text = make_choice_text(data.desc, data.per_item_value, 1) nobucket_choice.item_id = item_id table.insert(nobucket_choices, nobucket_choice) end - bucket.text = make_choice_text(data.desc, data.display_value, data.quantity) + data.total_value = data.per_item_value * data.quantity + bucket.text = make_choice_text(data.desc, data.total_value, data.quantity) table.insert(bucket_choices, bucket) - self.value_pending = self.value_pending + (data.real_value * data.selected) + self.value_pending = self.value_pending + (data.per_item_value * data.selected) end self.choices = {} @@ -814,6 +884,8 @@ function MoveGoods:get_choices() local raw_choices = self:cache_choices(self.subviews.disable_buckets:getOptionValue()) local choices = {} local include_forbidden = self.subviews.show_forbidden:getOptionValue() + local include_banned = self.subviews.show_banned:getOptionValue() + local only_agreement = self.subviews.only_agreement:getOptionValue() local min_condition = self.subviews.min_condition:getOptionValue() local max_condition = self.subviews.max_condition:getOptionValue() local min_quality = self.subviews.min_quality:getOptionValue() @@ -833,6 +905,18 @@ function MoveGoods:get_choices() if max_condition > data.wear then goto continue end if min_quality > data.quality then goto continue end if max_quality < data.quality then goto continue end + if only_agreement and not is_agreement_item(data.item_type) then + goto continue + end + if not include_banned then + if choice.item_id then + if data.items[choice.item_id].banned then + goto continue + end + elseif data.has_banned then + goto continue + end + end table.insert(choices, choice) ::continue:: end @@ -840,36 +924,60 @@ function MoveGoods:get_choices() return choices end -function MoveGoods:toggle_item(_, choice, target_value) +function MoveGoods:toggle_item_base(choice, target_value) if choice.item_id then local item_data = choice.data.items[choice.item_id] if item_data.pending then - self.value_pending = self.value_pending - choice.data.real_value + self.value_pending = self.value_pending - choice.data.per_item_value choice.data.selected = choice.data.selected - 1 end if target_value == nil then target_value = not item_data.pending end item_data.pending = target_value if item_data.pending then - self.value_pending = self.value_pending + choice.data.real_value + self.value_pending = self.value_pending + choice.data.per_item_value choice.data.selected = choice.data.selected + 1 end else - self.value_pending = self.value_pending - (choice.data.selected * choice.data.real_value) + self.value_pending = self.value_pending - (choice.data.selected * choice.data.per_item_value) if target_value == nil then target_value = (choice.data.selected ~= choice.data.quantity) end for _, item_data in pairs(choice.data.items) do item_data.pending = target_value end choice.data.selected = target_value and choice.data.quantity or 0 - self.value_pending = self.value_pending + (choice.data.selected * choice.data.real_value) + self.value_pending = self.value_pending + (choice.data.selected * choice.data.per_item_value) end choice.data.dirty = true return target_value end +function MoveGoods:select_item(idx, choice) + if not dfhack.internal.getModifiers().shift then + self.prev_list_idx = self.subviews.list.list:getSelected() + end +end + +function MoveGoods:toggle_item(idx, choice) + self:toggle_item_base(choice) +end + +function MoveGoods:toggle_range(idx, choice) + if not self.prev_list_idx then + self:toggle_item(idx, choice) + return + end + local choices = self.subviews.list:getVisibleChoices() + local list_idx = self.subviews.list.list:getSelected() + local target_value + for i = list_idx, self.prev_list_idx, list_idx < self.prev_list_idx and 1 or -1 do + target_value = self:toggle_item_base(choices[i], target_value) + end + self.prev_list_idx = list_idx +end + function MoveGoods:toggle_visible() local target_value - for _, choice in pairs(self.subviews.list:getVisibleChoices()) do - target_value = self:toggle_item(nil, choice, target_value) + for _, choice in ipairs(self.subviews.list:getVisibleChoices()) do + target_value = self:toggle_item_base(choice, target_value) end end @@ -921,10 +1029,10 @@ end MoveGoodsOverlay = defclass(MoveGoodsOverlay, overlay.OverlayWidget) MoveGoodsOverlay.ATTRS{ - default_pos={x=-60, y=10}, + default_pos={x=-64, y=10}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING/TradeDepot', - frame={w=35, h=1}, + frame={w=31, h=1}, frame_background=gui.CLEAR_PEN, } @@ -954,7 +1062,7 @@ function MoveGoodsOverlay:init() self:addviews{ widgets.HotkeyLabel{ frame={t=0, l=0}, - label='DFHack trade goods helper', + label='DFHack move trade goods', key='CUSTOM_CTRL_T', on_activate=function() MoveGoodsModal{}:show() end, enabled=has_trade_depot_and_caravan, diff --git a/docs/caravan.rst b/docs/caravan.rst index ae5a2f108b..7e0fdf15de 100644 --- a/docs/caravan.rst +++ b/docs/caravan.rst @@ -75,8 +75,19 @@ Bring item to depot ``````````````````` When the trade depot is selected, a button appears to bring up the DFHack -enhanced trade goods screen. You'll get a searchable, sortable list of all your -tradeable items, with options to quickly select or deselect classes of items. +enhanced move trade goods screen. You'll get a searchable, sortable list of all +your tradeable items, with hotkeys to quickly select or deselect all visible +items. + +There are filter sliders for selecting items of various condition levels and +quality. For example, you can quickly trade all your tattered, frayed, and worn +clothing by setting the condition slider to include from tattered to worn, then +hitting Ctrl-V to select all. + +Click on an item and shift-click on a second item to toggle all items between +the two that you clicked on. If the one that you shift-clicked on was selected, +the range of items will be deselected. If the one you shift-clicked on was not +selected, then the range of items will be selected. Trade agreement ``````````````` From 7f257e91e63440aafbf56bd24bbd2292925016c2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 30 Jun 2023 02:13:33 -0700 Subject: [PATCH 349/732] split overlays into separate files --- caravan.lua | 1078 +-------------------------- internal/caravan/movegoods.lua | 766 +++++++++++++++++++ internal/caravan/trade.lua | 268 +++++++ internal/caravan/tradeagreement.lua | 37 + 4 files changed, 1082 insertions(+), 1067 deletions(-) create mode 100644 internal/caravan/movegoods.lua create mode 100644 internal/caravan/trade.lua create mode 100644 internal/caravan/tradeagreement.lua diff --git a/caravan.lua b/caravan.lua index 1bc87f8a2e..c8b3064e05 100644 --- a/caravan.lua +++ b/caravan.lua @@ -1,1079 +1,23 @@ --- Adjusts properties of caravans and provides overlay for enhanced trading +-- Adjusts properties of caravans and provides overlays for enhanced trading --@ module = true --- TODO: the category checkbox that indicates whether all items in the category --- are selected can be incorrect after the overlay adjusts the container --- selection. the state is in trade.current_type_a_flag, but figuring out which --- index to modify is non-trivial. - -local gui = require('gui') -local overlay = require('plugins.overlay') -local utils = require('utils') -local widgets = require('gui.widgets') - -trader_selected_state = trader_selected_state or {} -broker_selected_state = broker_selected_state or {} -handle_ctrl_click_on_render = handle_ctrl_click_on_render or false -handle_shift_click_on_render = handle_shift_click_on_render or false +local trade = reqscript('internal/caravan/trade') +local tradeagreement = reqscript('internal/caravan/tradeagreement') +local movegoods = reqscript('internal/caravan/movegoods') dfhack.onStateChange.caravanTradeOverlay = function(code) if code == SC_WORLD_UNLOADED then - trader_selected_state = {} - broker_selected_state = {} - handle_ctrl_click_on_render = false - handle_shift_click_on_render = false - end -end - -local GOODFLAG = { - UNCONTAINED_UNSELECTED = 0, - UNCONTAINED_SELECTED = 1, - CONTAINED_UNSELECTED = 2, - CONTAINED_SELECTED = 3, - CONTAINER_COLLAPSED_UNSELECTED = 4, - CONTAINER_COLLAPSED_SELECTED = 5, -} - -local trade = df.global.game.main_interface.trade - -local MARGIN_HEIGHT = 26 -- screen height *other* than the list - -function set_height(list_index, delta) - trade.i_height[list_index] = trade.i_height[list_index] + delta - if delta >= 0 then return end - _,screen_height = dfhack.screen.getWindowSize() - -- list only increments in three tiles at a time - local page_height = ((screen_height - MARGIN_HEIGHT) // 3) * 3 - trade.scroll_position_item[list_index] = math.max(0, - math.min(trade.scroll_position_item[list_index], - trade.i_height[list_index] - page_height)) -end - -function select_shift_clicked_container_items(new_state, old_state, list_index) - -- if ctrl is also held, collapse the container too - local also_collapse = dfhack.internal.getModifiers().ctrl - local collapsed_item_count, collapsing_container, in_container = 0, false, false - for k, goodflag in ipairs(new_state) do - if in_container then - if goodflag <= GOODFLAG.UNCONTAINED_SELECTED - or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then - break - end - - new_state[k] = GOODFLAG.CONTAINED_SELECTED - - if collapsing_container then - collapsed_item_count = collapsed_item_count + 1 - end - goto continue - end - - if goodflag == old_state[k] then goto continue end - local is_container = df.item_binst:is_instance(trade.good[list_index][k]) - if not is_container then goto continue end - - -- deselect the container itself - if also_collapse or - old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED or - old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_SELECTED then - collapsing_container = goodflag == GOODFLAG.UNCONTAINED_SELECTED - new_state[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED - else - new_state[k] = GOODFLAG.UNCONTAINED_UNSELECTED - end - in_container = true - - ::continue:: - end - - if collapsed_item_count > 0 then - set_height(list_index, collapsed_item_count * -3) - end -end - -local CTRL_CLICK_STATE_MAP = { - [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, - [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, -} - --- collapses uncollapsed containers and restores the selection state for the container --- and contained items -function toggle_ctrl_clicked_containers(new_state, old_state, list_index) - local toggled_item_count, in_container, is_collapsing = 0, false, false - for k, goodflag in ipairs(new_state) do - if in_container then - if goodflag <= GOODFLAG.UNCONTAINED_SELECTED - or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then - break - end - toggled_item_count = toggled_item_count + 1 - new_state[k] = old_state[k] - goto continue - end - - if goodflag == old_state[k] then goto continue end - local is_contained = goodflag == GOODFLAG.CONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINED_SELECTED - if is_contained then goto continue end - local is_container = df.item_binst:is_instance(trade.good[list_index][k]) - if not is_container then goto continue end - - new_state[k] = CTRL_CLICK_STATE_MAP[old_state[k]] - in_container = true - is_collapsing = goodflag == GOODFLAG.UNCONTAINED_UNSELECTED or goodflag == GOODFLAG.UNCONTAINED_SELECTED - - ::continue:: - end - - if toggled_item_count > 0 then - set_height(list_index, toggled_item_count * 3 * (is_collapsing and -1 or 1)) - end -end - -function collapseTypes(types_list, list_index) - local type_on_count = 0 - - for k in ipairs(types_list) do - local type_on = trade.current_type_a_on[list_index][k] - if type_on then - type_on_count = type_on_count + 1 - end - types_list[k] = false - end - - trade.i_height[list_index] = type_on_count * 3 - trade.scroll_position_item[list_index] = 0 -end - -function collapseAllTypes() - collapseTypes(trade.current_type_a_expanded[0], 0) - collapseTypes(trade.current_type_a_expanded[1], 1) -end - -function collapseContainers(item_list, list_index) - local num_items_collapsed = 0 - for k, goodflag in ipairs(item_list) do - if goodflag == GOODFLAG.CONTAINED_UNSELECTED - or goodflag == GOODFLAG.CONTAINED_SELECTED then - goto continue - end - - local item = trade.good[list_index][k] - local is_container = df.item_binst:is_instance(item) - if not is_container then goto continue end - - local collapsed_this_container = false - if goodflag == GOODFLAG.UNCONTAINED_SELECTED then - item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED - collapsed_this_container = true - elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then - item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED - collapsed_this_container = true - end - - if collapsed_this_container then - num_items_collapsed = num_items_collapsed + #dfhack.items.getContainedItems(item) - end - ::continue:: - end - - if num_items_collapsed > 0 then - set_height(list_index, num_items_collapsed * -3) - end -end - -function collapseAllContainers() - collapseContainers(trade.goodflag[0], 0) - collapseContainers(trade.goodflag[1], 1) -end - -function collapseEverything() - collapseAllContainers() - collapseAllTypes() -end - -function copyGoodflagState() - trader_selected_state = copyall(trade.goodflag[0]) - broker_selected_state = copyall(trade.goodflag[1]) -end - -CaravanTradeOverlay = defclass(CaravanTradeOverlay, overlay.OverlayWidget) -CaravanTradeOverlay.ATTRS{ - default_pos={x=-3,y=-12}, - default_enabled=true, - viewscreens='dwarfmode/Trade', - frame={w=27, h=13}, - frame_style=gui.MEDIUM_FRAME, - frame_background=gui.CLEAR_PEN, -} - -function CaravanTradeOverlay:init() - self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text={ - {text='Shift+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', - NEWLINE, - ' select items inside bin', - }, - }, - widgets.Label{ - frame={t=3, l=0}, - text={ - {text='Ctrl+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', - NEWLINE, - ' collapse/expand bin', - }, - }, - widgets.HotkeyLabel{ - frame={t=6, l=0}, - label='collapse bins', - key='CUSTOM_CTRL_C', - on_activate=collapseAllContainers, - }, - widgets.HotkeyLabel{ - frame={t=7, l=0}, - label='collapse all', - key='CUSTOM_CTRL_X', - on_activate=collapseEverything, - }, - widgets.Label{ - frame={t=9, l=0}, - text = 'Shift+Scroll', - text_pen=COLOR_LIGHTGREEN, - }, - widgets.Label{ - frame={t=9, l=12}, - text = ': fast scroll', - }, - } -end - --- do our alterations *after* the vanilla response to the click has registered. otherwise --- it's very difficult to figure out which item has been clicked -function CaravanTradeOverlay:onRenderBody(dc) - if handle_shift_click_on_render then - handle_shift_click_on_render = false - select_shift_clicked_container_items(trade.goodflag[0], trader_selected_state, 0) - select_shift_clicked_container_items(trade.goodflag[1], broker_selected_state, 1) - elseif handle_ctrl_click_on_render then - handle_ctrl_click_on_render = false - toggle_ctrl_clicked_containers(trade.goodflag[0], trader_selected_state, 0) - toggle_ctrl_clicked_containers(trade.goodflag[1], broker_selected_state, 1) - end -end - -function CaravanTradeOverlay:onInput(keys) - if CaravanTradeOverlay.super.onInput(self, keys) then return true end - - if keys._MOUSE_L_DOWN then - if dfhack.internal.getModifiers().shift then - handle_shift_click_on_render = true - copyGoodflagState() - elseif dfhack.internal.getModifiers().ctrl then - handle_ctrl_click_on_render = true - copyGoodflagState() - end - end -end - --- ------------------- --- DiplomacyOverlay --- - -DiplomacyOverlay = defclass(DiplomacyOverlay, overlay.OverlayWidget) -DiplomacyOverlay.ATTRS{ - default_pos={x=45, y=-6}, - default_enabled=true, - viewscreens='dwarfmode/Diplomacy/Requests', - frame={w=25, h=3}, - frame_style=gui.MEDIUM_FRAME, - frame_background=gui.CLEAR_PEN, -} - -local diplomacy = df.global.game.main_interface.diplomacy -local function diplomacy_toggle_cat() - local priority_idx = diplomacy.taking_requests_tablist[diplomacy.taking_requests_selected_tab] - local priority = diplomacy.environment.meeting.sell_requests.priority[priority_idx] - if #priority == 0 then return end - local target_val = priority[0] == 0 and 4 or 0 - for i in ipairs(priority) do - priority[i] = target_val - end -end - -function DiplomacyOverlay:init() - self:addviews{ - widgets.HotkeyLabel{ - frame={t=0, l=0}, - label='Select all/none', - key='CUSTOM_CTRL_A', - on_activate=diplomacy_toggle_cat, - }, - } -end - --- ------------------- --- MoveGoods --- - -MoveGoods = defclass(MoveGoods, widgets.Window) -MoveGoods.ATTRS { - frame_title='Select trade goods', - frame={w=83, h=45}, - resizable=true, - resize_min={h=27}, - pending_item_ids=DEFAULT_NIL, -} - -local VALUE_COL_WIDTH = 8 -local QTY_COL_WIDTH = 6 - -local function sort_noop(a, b) - -- this function is used as a marker and never actually gets called - error('sort_noop should not be called') -end - -local function sort_base(a, b) - return a.data.desc < b.data.desc -end - -local function sort_by_name_desc(a, b) - if a.search_key == b.search_key then - return sort_base(a, b) + trade.trader_selected_state = {} + trade.broker_selected_state = {} + trade.handle_ctrl_click_on_render = false + trade.handle_shift_click_on_render = false end - return a.search_key < b.search_key -end - -local function sort_by_name_asc(a, b) - if a.search_key == b.search_key then - return sort_base(a, b) - end - return a.search_key > b.search_key -end - -local function sort_by_value_desc(a, b) - local value_field = a.item_id and 'per_item_value' or 'total_value' - if a.data[value_field] == b.data[value_field] then - return sort_by_name_desc(a, b) - end - return a.data[value_field] > b.data[value_field] -end - -local function sort_by_value_asc(a, b) - local value_field = a.item_id and 'per_item_value' or 'total_value' - if a.data[value_field] == b.data[value_field] then - return sort_by_name_desc(a, b) - end - return a.data[value_field] < b.data[value_field] -end - -local function sort_by_quantity_desc(a, b) - if a.data.quantity == b.data.quantity then - return sort_by_name_desc(a, b) - end - return a.data.quantity > b.data.quantity -end - -local function sort_by_quantity_asc(a, b) - if a.data.quantity == b.data.quantity then - return sort_by_name_desc(a, b) - end - return a.data.quantity < b.data.quantity -end - -local function has_export_agreement() - -- TODO: where are export agreements stored? - return false -end - -local function is_agreement_item(item_type) - -- TODO: match export agreement with civs with active caravans - return false -end - --- takes into account trade agreements -local function get_perceived_value(item) - -- TODO: take trade agreements into account - local value = dfhack.items.getValue(item) - for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do - value = value + dfhack.items.getValue(contained_item) - for _,contained_contained_item in ipairs(dfhack.items.getContainedItems(contained_item)) do - value = value + dfhack.items.getValue(contained_contained_item) - end - end - return value -end - -local function get_value_at_depot() - local sum = 0 - -- if we're here, then the overlay has already determined that this is a depot - local depot = dfhack.gui.getSelectedBuilding(true) - for _, contained_item in ipairs(depot.contained_items) do - if contained_item.use_mode ~= 0 then goto continue end - local item = contained_item.item - sum = sum + get_perceived_value(item) - ::continue:: - end - return sum -end - --- adapted from https://stackoverflow.com/a/50860705 -local function sig_fig(num, figures) - if num <= 0 then return 0 end - local x = figures - math.ceil(math.log(num, 10)) - return math.floor(math.floor(num * 10^x + 0.5) * 10^-x) -end - -local function obfuscate_value(value) - -- TODO: respect skill of broker - local num_sig_figs = 1 - local str = tostring(sig_fig(value, num_sig_figs)) - if #str > num_sig_figs then str = '~' .. str end - return str -end - -local CH_UP = string.char(30) -local CH_DN = string.char(31) - -function MoveGoods:init() - self.value_at_depot = get_value_at_depot() - self.value_pending = 0 - - self:addviews{ - widgets.CycleHotkeyLabel{ - view_id='sort', - frame={l=0, t=0, w=21}, - label='Sort by:', - key='CUSTOM_SHIFT_S', - options={ - {label='value'..CH_DN, value=sort_by_value_desc}, - {label='value'..CH_UP, value=sort_by_value_asc}, - {label='qty'..CH_DN, value=sort_by_quantity_desc}, - {label='qty'..CH_UP, value=sort_by_quantity_asc}, - {label='name'..CH_DN, value=sort_by_name_desc}, - {label='name'..CH_UP, value=sort_by_name_asc}, - }, - initial_option=sort_by_value_desc, - on_change=self:callback('refresh_list', 'sort'), - }, - widgets.EditField{ - view_id='search', - frame={l=26, t=0}, - label_text='Search: ', - on_char=function(ch) return ch:match('[%l -]') end, - }, - widgets.ToggleHotkeyLabel{ - view_id='show_forbidden', - frame={t=2, l=0, w=27}, - label='Show forbidden items', - key='CUSTOM_SHIFT_F', - initial_option=true, - on_change=function() self:refresh_list() end, - }, - widgets.ToggleHotkeyLabel{ - view_id='show_banned', - frame={t=3, l=0, w=43}, - label='Show items banned by export mandates', - key='CUSTOM_SHIFT_B', - initial_option=false, - on_change=function() self:refresh_list() end, - }, - widgets.ToggleHotkeyLabel{ - view_id='only_agreement', - frame={t=4, l=0, w=52}, - label='Show only items requested by export agreement', - key='CUSTOM_SHIFT_A', - initial_option=false, - on_change=function() self:refresh_list() end, - enabled=has_export_agreement(), - }, - widgets.Panel{ - frame={t=6, l=0, w=40, h=4}, - subviews={ - widgets.CycleHotkeyLabel{ - view_id='min_condition', - frame={l=0, t=0, w=18}, - label='Min condition:', - label_below=true, - key_back='CUSTOM_SHIFT_C', - key='CUSTOM_SHIFT_V', - options={ - {label='Tattered (XX)', value=3}, - {label='Frayed (X)', value=2}, - {label='Worn (x)', value=1}, - {label='Pristine', value=0}, - }, - initial_option=3, - on_change=function(val) - if self.subviews.max_condition:getOptionValue() > val then - self.subviews.max_condition:setOption(val) - end - self:refresh_list() - end, - }, - widgets.CycleHotkeyLabel{ - view_id='max_condition', - frame={r=1, t=0, w=18}, - label='Max condition:', - label_below=true, - key_back='CUSTOM_SHIFT_E', - key='CUSTOM_SHIFT_R', - options={ - {label='Tattered (XX)', value=3}, - {label='Frayed (X)', value=2}, - {label='Worn (x)', value=1}, - {label='Pristine', value=0}, - }, - initial_option=0, - on_change=function(val) - if self.subviews.min_condition:getOptionValue() < val then - self.subviews.min_condition:setOption(val) - end - self:refresh_list() - end, - }, - widgets.RangeSlider{ - frame={l=0, t=3}, - num_stops=4, - get_left_idx_fn=function() - return 4 - self.subviews.min_condition:getOptionValue() - end, - get_right_idx_fn=function() - return 4 - self.subviews.max_condition:getOptionValue() - end, - on_left_change=function(idx) self.subviews.min_condition:setOption(4-idx, true) end, - on_right_change=function(idx) self.subviews.max_condition:setOption(4-idx, true) end, - }, - }, - }, - widgets.Panel{ - frame={t=6, l=41, w=38, h=4}, - subviews={ - widgets.CycleHotkeyLabel{ - view_id='min_quality', - frame={l=0, t=0, w=18}, - label='Min quality:', - label_below=true, - key_back='CUSTOM_SHIFT_Z', - key='CUSTOM_SHIFT_X', - options={ - {label='Ordinary', value=0}, - {label='Well Crafted', value=1}, - {label='Finely Crafted', value=2}, - {label='Superior', value=3}, - {label='Exceptional', value=4}, - {label='Masterful', value=5}, - {label='Artifact', value=6}, - }, - initial_option=0, - on_change=function(val) - if self.subviews.max_quality:getOptionValue() < val then - self.subviews.max_quality:setOption(val) - end - self:refresh_list() - end, - }, - widgets.CycleHotkeyLabel{ - view_id='max_quality', - frame={r=1, t=0, w=18}, - label='Max quality:', - label_below=true, - key_back='CUSTOM_SHIFT_Q', - key='CUSTOM_SHIFT_W', - options={ - {label='Ordinary', value=0}, - {label='Well Crafted', value=1}, - {label='Finely Crafted', value=2}, - {label='Superior', value=3}, - {label='Exceptional', value=4}, - {label='Masterful', value=5}, - {label='Artifact', value=6}, - }, - initial_option=6, - on_change=function(val) - if self.subviews.min_quality:getOptionValue() > val then - self.subviews.min_quality:setOption(val) - end - self:refresh_list() - end, - }, - widgets.RangeSlider{ - frame={l=0, t=3}, - num_stops=7, - get_left_idx_fn=function() - return self.subviews.min_quality:getOptionValue() + 1 - end, - get_right_idx_fn=function() - return self.subviews.max_quality:getOptionValue() + 1 - end, - on_left_change=function(idx) self.subviews.min_quality:setOption(idx-1, true) end, - on_right_change=function(idx) self.subviews.max_quality:setOption(idx-1, true) end, - }, - }, - }, - widgets.Panel{ - frame={t=11, l=0, r=0, b=6}, - subviews={ - widgets.CycleHotkeyLabel{ - view_id='sort_value', - frame={l=2, t=0, w=7}, - options={ - {label='value', value=sort_noop}, - {label='value'..CH_DN, value=sort_by_value_desc}, - {label='value'..CH_UP, value=sort_by_value_asc}, - }, - initial_option=sort_by_value_desc, - on_change=self:callback('refresh_list', 'sort_value'), - }, - widgets.CycleHotkeyLabel{ - view_id='sort_quantity', - frame={l=2+VALUE_COL_WIDTH+2, t=0, w=5}, - options={ - {label='qty', value=sort_noop}, - {label='qty'..CH_DN, value=sort_by_quantity_desc}, - {label='qty'..CH_UP, value=sort_by_quantity_asc}, - }, - on_change=self:callback('refresh_list', 'sort_quantity'), - }, - widgets.CycleHotkeyLabel{ - view_id='sort_name', - frame={l=2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+2, t=0, w=6}, - options={ - {label='name', value=sort_noop}, - {label='name'..CH_DN, value=sort_by_name_desc}, - {label='name'..CH_UP, value=sort_by_name_asc}, - }, - on_change=self:callback('refresh_list', 'sort_name'), - }, - widgets.FilteredList{ - view_id='list', - frame={l=0, t=2, r=0, b=0}, - icon_width=2, - on_submit=self:callback('toggle_item'), - on_submit2=self:callback('toggle_range'), - on_select=self:callback('select_item'), - }, - } - }, - widgets.Label{ - frame={l=0, b=4, h=1, r=0}, - text={ - 'Value of items at trade depot/being brought to depot/total:', - {gap=1, text=obfuscate_value(self.value_at_depot)}, - '/', - {text=function() return obfuscate_value(self.value_pending) end}, - '/', - {text=function() return obfuscate_value(self.value_pending + self.value_at_depot) end} - }, - }, - widgets.HotkeyLabel{ - frame={l=0, b=2}, - label='Select all/none', - key='CUSTOM_CTRL_V', - on_activate=self:callback('toggle_visible'), - auto_width=true, - }, - widgets.ToggleHotkeyLabel{ - view_id='disable_buckets', - frame={l=26, b=2}, - label='Show individual items', - key='CUSTOM_CTRL_I', - initial_option=false, - on_change=function() self:refresh_list() end, - }, - widgets.WrappedLabel{ - frame={b=0, l=0, r=0}, - text_to_wrap='Click to mark/unmark for trade. Shift click to mark/unmark a range of items.', - }, - } - - -- replace the FilteredList's built-in EditField with our own - self.subviews.list.list.frame.t = 0 - self.subviews.list.edit.visible = false - self.subviews.list.edit = self.subviews.search - self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') - - self.subviews.list:setChoices(self:get_choices()) -end - -function MoveGoods:refresh_list(sort_widget, sort_fn) - sort_widget = sort_widget or 'sort' - sort_fn = sort_fn or self.subviews.sort:getOptionValue() - if sort_fn == sort_noop then - self.subviews[sort_widget]:cycle() - return - end - for _,widget_name in ipairs{'sort', 'sort_value', 'sort_quantity', 'sort_name'} do - self.subviews[widget_name]:setOption(sort_fn) - end - local list = self.subviews.list - local saved_filter = list:getFilter() - list:setFilter('') - list:setChoices(self:get_choices(), list:getSelected()) - list:setFilter(saved_filter) -end - -local function is_tradeable_item(item) - if not item.flags.on_ground or - item.flags.hostile or - item.flags.in_inventory or - item.flags.removed or - item.flags.in_building or - item.flags.dead_dwarf or - item.flags.spider_web or - item.flags.construction or - item.flags.encased or - item.flags.unk12 or - item.flags.murder or - item.flags.trader or - item.flags.owned or - item.flags.garbage_collect or - item.flags.on_fire or - item.flags.in_chest - then - return false - end - if item.flags.in_job then - local spec_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) - if not spec_ref then return true end - return spec_ref.data.job.job_type == df.job_type.BringItemToDepot - end - return true -end - -local function make_search_key(str) - local out = '' - for c in str:gmatch("[%w%s]") do - out = out .. c:lower() - end - return out -end - -local to_pen = dfhack.pen.parse -local SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} -local ALL_PEN = to_pen{ch='+', fg=COLOR_LIGHTGREEN} - -local function get_entry_icon(data, item_id) - if data.selected == 0 then return nil end - if item_id then - return data.items[item_id].pending and ALL_PEN or nil - end - if data.quantity == data.selected then return ALL_PEN end - return SOME_PEN -end - -local function make_choice_text(desc, value, quantity) - return { - {width=VALUE_COL_WIDTH, rjustify=true, text=obfuscate_value(value)}, - {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, - {gap=2, text=desc}, - } -end - --- returns true if the item or any contained item is banned -local function scan_banned(item) - if not dfhack.items.checkMandates(item) then return true end - for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do - if not dfhack.items.checkMandates(contained_item) then return true end - end - return false -end - -local function to_title_case(str) - str = str:gsub('(%a)([%w_]*)', - function (first, rest) return first:upper()..rest:lower() end) - str = str:gsub('_', ' ') - return str -end - -local function get_item_type_str(item) - local str = to_title_case(df.item_type[item:getType()]) - if str == 'Trapparts' then - str = 'Mechanism' - end - return str -end - -local function get_artifact_name(item) - local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) - if not gref then return end - local artifact = df.artifact_record.find(gref.artifact_id) - if not artifact then return end - local name = dfhack.TranslateName(artifact.name) - return ('%s (%s)'):format(name, get_item_type_str(item)) -end - -function MoveGoods:cache_choices(disable_buckets) - if self.choices then return self.choices[disable_buckets] end - - local pending = self.pending_item_ids - local buckets = {} - for _, item in ipairs(df.global.world.items.all) do - local item_id = item.id - if not item or not is_tradeable_item(item) then goto continue end - local value = get_perceived_value(item) - if value <= 0 then goto continue end - local is_pending = not not pending[item_id] - local is_forbidden = item.flags.forbid - local is_banned = scan_banned(item) - local wear_level = item:getWear() - local desc = item.flags.artifact and get_artifact_name(item) or - dfhack.items.getDescription(item, 0, true) - if wear_level == 1 then desc = ('x%sx'):format(desc) - elseif wear_level == 2 then desc = ('X%sX'):format(desc) - elseif wear_level == 3 then desc = ('XX%sXX'):format(desc) - end - local key = ('%s/%d'):format(desc, value) - if buckets[key] then - local bucket = buckets[key] - bucket.data.items[item_id] = {item=item, pending=is_pending, banned=is_banned} - bucket.data.quantity = bucket.data.quantity + 1 - bucket.data.selected = bucket.data.selected + (is_pending and 1 or 0) - bucket.data.has_forbidden = bucket.data.has_forbidden or is_forbidden - bucket.data.has_banned = bucket.data.has_banned or is_banned - else - local data = { - desc=desc, - per_item_value=value, - items={[item_id]={item=item, pending=is_pending, banned=is_banned}}, - item_type=item:getType(), - item_subtype=item:getSubtype(), - quantity=1, - quality=item.flags.artifact and 6 or item:getQuality(), - wear=wear_level, - selected=is_pending and 1 or 0, - has_forbidden=is_forbidden, - has_banned=is_banned, - dirty=false, - } - local entry = { - search_key=make_search_key(desc), - icon=curry(get_entry_icon, data), - data=data, - } - buckets[key] = entry - end - ::continue:: - end - - local bucket_choices, nobucket_choices = {}, {} - for _, bucket in pairs(buckets) do - local data = bucket.data - for item_id in pairs(data.items) do - local nobucket_choice = copyall(bucket) - nobucket_choice.icon = curry(get_entry_icon, data, item_id) - nobucket_choice.text = make_choice_text(data.desc, data.per_item_value, 1) - nobucket_choice.item_id = item_id - table.insert(nobucket_choices, nobucket_choice) - end - data.total_value = data.per_item_value * data.quantity - bucket.text = make_choice_text(data.desc, data.total_value, data.quantity) - table.insert(bucket_choices, bucket) - self.value_pending = self.value_pending + (data.per_item_value * data.selected) - end - - self.choices = {} - self.choices[false] = bucket_choices - self.choices[true] = nobucket_choices - return self:cache_choices(disable_buckets) -end - -function MoveGoods:get_choices() - local raw_choices = self:cache_choices(self.subviews.disable_buckets:getOptionValue()) - local choices = {} - local include_forbidden = self.subviews.show_forbidden:getOptionValue() - local include_banned = self.subviews.show_banned:getOptionValue() - local only_agreement = self.subviews.only_agreement:getOptionValue() - local min_condition = self.subviews.min_condition:getOptionValue() - local max_condition = self.subviews.max_condition:getOptionValue() - local min_quality = self.subviews.min_quality:getOptionValue() - local max_quality = self.subviews.max_quality:getOptionValue() - for _,choice in ipairs(raw_choices) do - local data = choice.data - if not include_forbidden then - if choice.item_id then - if data.items[choice.item_id].item.flags.forbid then - goto continue - end - elseif data.has_forbidden then - goto continue - end - end - if min_condition < data.wear then goto continue end - if max_condition > data.wear then goto continue end - if min_quality > data.quality then goto continue end - if max_quality < data.quality then goto continue end - if only_agreement and not is_agreement_item(data.item_type) then - goto continue - end - if not include_banned then - if choice.item_id then - if data.items[choice.item_id].banned then - goto continue - end - elseif data.has_banned then - goto continue - end - end - table.insert(choices, choice) - ::continue:: - end - table.sort(choices, self.subviews.sort:getOptionValue()) - return choices -end - -function MoveGoods:toggle_item_base(choice, target_value) - if choice.item_id then - local item_data = choice.data.items[choice.item_id] - if item_data.pending then - self.value_pending = self.value_pending - choice.data.per_item_value - choice.data.selected = choice.data.selected - 1 - end - if target_value == nil then target_value = not item_data.pending end - item_data.pending = target_value - if item_data.pending then - self.value_pending = self.value_pending + choice.data.per_item_value - choice.data.selected = choice.data.selected + 1 - end - else - self.value_pending = self.value_pending - (choice.data.selected * choice.data.per_item_value) - if target_value == nil then target_value = (choice.data.selected ~= choice.data.quantity) end - for _, item_data in pairs(choice.data.items) do - item_data.pending = target_value - end - choice.data.selected = target_value and choice.data.quantity or 0 - self.value_pending = self.value_pending + (choice.data.selected * choice.data.per_item_value) - end - choice.data.dirty = true - return target_value -end - -function MoveGoods:select_item(idx, choice) - if not dfhack.internal.getModifiers().shift then - self.prev_list_idx = self.subviews.list.list:getSelected() - end -end - -function MoveGoods:toggle_item(idx, choice) - self:toggle_item_base(choice) -end - -function MoveGoods:toggle_range(idx, choice) - if not self.prev_list_idx then - self:toggle_item(idx, choice) - return - end - local choices = self.subviews.list:getVisibleChoices() - local list_idx = self.subviews.list.list:getSelected() - local target_value - for i = list_idx, self.prev_list_idx, list_idx < self.prev_list_idx and 1 or -1 do - target_value = self:toggle_item_base(choices[i], target_value) - end - self.prev_list_idx = list_idx -end - -function MoveGoods:toggle_visible() - local target_value - for _, choice in ipairs(self.subviews.list:getVisibleChoices()) do - target_value = self:toggle_item_base(choice, target_value) - end -end - -MoveGoodsModal = defclass(MoveGoodsModal, gui.ZScreenModal) -MoveGoodsModal.ATTRS { - focus_path='movegoods', -} - -local function get_pending_trade_item_ids() - local item_ids = {} - for _,job in utils.listpairs(df.global.world.jobs.list) do - if job.job_type == df.job_type.BringItemToDepot and #job.items > 0 then - item_ids[job.items[0].item.id] = true - end - end - return item_ids -end - -function MoveGoodsModal:init() - self.pending_item_ids = get_pending_trade_item_ids() - self:addviews{MoveGoods{pending_item_ids=self.pending_item_ids}} -end - -function MoveGoodsModal:onDismiss() - -- mark/unmark selected goods for trade - local depot = dfhack.gui.getSelectedBuilding(true) - if not depot then return end - local pending = self.pending_item_ids - for _, choice in ipairs(self.subviews.list:getChoices()) do - if not choice.data.dirty then goto continue end - for item_id, item_data in pairs(choice.data.items) do - if item_data.pending and not pending[item_id] then - item_data.item.flags.forbid = false - dfhack.items.markForTrade(item_data.item, depot) - elseif not item_data.pending and pending[item_id] then - local spec_ref = dfhack.items.getSpecificRef(item_data.item, df.specific_ref_type.JOB) - if spec_ref then - dfhack.job.removeJob(spec_ref.data.job) - end - end - end - ::continue:: - end -end - --- ------------------- --- MoveGoodsOverlay --- - -MoveGoodsOverlay = defclass(MoveGoodsOverlay, overlay.OverlayWidget) -MoveGoodsOverlay.ATTRS{ - default_pos={x=-64, y=10}, - default_enabled=true, - viewscreens='dwarfmode/ViewSheets/BUILDING/TradeDepot', - frame={w=31, h=1}, - frame_background=gui.CLEAR_PEN, -} - -local function has_trade_depot_and_caravan() - local bld = dfhack.gui.getSelectedBuilding(true) - if not bld or bld:getBuildStage() < bld:getMaxBuildStage() then - return false - end - if #bld.jobs == 1 and bld.jobs[0].job_type == df.job_type.DestroyBuilding then - return false - end - - for _, caravan in ipairs(df.global.plotinfo.caravans) do - local trade_state = caravan.trade_state - local time_remaining = caravan.time_remaining - if time_remaining > 0 and - (trade_state == df.caravan_state.T_trade_state.Approaching or - trade_state == df.caravan_state.T_trade_state.AtDepot) - then - return true - end - end - return false -end - -function MoveGoodsOverlay:init() - self:addviews{ - widgets.HotkeyLabel{ - frame={t=0, l=0}, - label='DFHack move trade goods', - key='CUSTOM_CTRL_T', - on_activate=function() MoveGoodsModal{}:show() end, - enabled=has_trade_depot_and_caravan, - }, - } end OVERLAY_WIDGETS = { - trade=CaravanTradeOverlay, - diplomacy=DiplomacyOverlay, - movegoods=MoveGoodsOverlay, + trade=trade.TradeOverlay, + tradeagreement=tradeagreement.TradeAgreementOverlay, + movegoods=movegoods.MoveGoodsOverlay, } INTERESTING_FLAGS = { diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua new file mode 100644 index 0000000000..66fb3b974b --- /dev/null +++ b/internal/caravan/movegoods.lua @@ -0,0 +1,766 @@ +--@ module = true + +local gui = require('gui') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') + +-- ------------------- +-- MoveGoods +-- + +MoveGoods = defclass(MoveGoods, widgets.Window) +MoveGoods.ATTRS { + frame_title='Select trade goods', + frame={w=83, h=45}, + resizable=true, + resize_min={h=27}, + pending_item_ids=DEFAULT_NIL, +} + +local VALUE_COL_WIDTH = 8 +local QTY_COL_WIDTH = 6 + +local function sort_noop(a, b) + -- this function is used as a marker and never actually gets called + error('sort_noop should not be called') +end + +local function sort_base(a, b) + return a.data.desc < b.data.desc +end + +local function sort_by_name_desc(a, b) + if a.search_key == b.search_key then + return sort_base(a, b) + end + return a.search_key < b.search_key +end + +local function sort_by_name_asc(a, b) + if a.search_key == b.search_key then + return sort_base(a, b) + end + return a.search_key > b.search_key +end + +local function sort_by_value_desc(a, b) + local value_field = a.item_id and 'per_item_value' or 'total_value' + if a.data[value_field] == b.data[value_field] then + return sort_by_name_desc(a, b) + end + return a.data[value_field] > b.data[value_field] +end + +local function sort_by_value_asc(a, b) + local value_field = a.item_id and 'per_item_value' or 'total_value' + if a.data[value_field] == b.data[value_field] then + return sort_by_name_desc(a, b) + end + return a.data[value_field] < b.data[value_field] +end + +local function sort_by_quantity_desc(a, b) + if a.data.quantity == b.data.quantity then + return sort_by_name_desc(a, b) + end + return a.data.quantity > b.data.quantity +end + +local function sort_by_quantity_asc(a, b) + if a.data.quantity == b.data.quantity then + return sort_by_name_desc(a, b) + end + return a.data.quantity < b.data.quantity +end + +local function has_export_agreement() + -- TODO: where are export agreements stored? + return false +end + +local function is_agreement_item(item_type) + -- TODO: match export agreement with civs with active caravans + return false +end + +-- takes into account trade agreements +local function get_perceived_value(item) + -- TODO: take trade agreements into account + local value = dfhack.items.getValue(item) + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + value = value + dfhack.items.getValue(contained_item) + for _,contained_contained_item in ipairs(dfhack.items.getContainedItems(contained_item)) do + value = value + dfhack.items.getValue(contained_contained_item) + end + end + return value +end + +local function get_value_at_depot() + local sum = 0 + -- if we're here, then the overlay has already determined that this is a depot + local depot = dfhack.gui.getSelectedBuilding(true) + for _, contained_item in ipairs(depot.contained_items) do + if contained_item.use_mode ~= 0 then goto continue end + local item = contained_item.item + sum = sum + get_perceived_value(item) + ::continue:: + end + return sum +end + +-- adapted from https://stackoverflow.com/a/50860705 +local function sig_fig(num, figures) + if num <= 0 then return 0 end + local x = figures - math.ceil(math.log(num, 10)) + return math.floor(math.floor(num * 10^x + 0.5) * 10^-x) +end + +local function obfuscate_value(value) + -- TODO: respect skill of broker + local num_sig_figs = 1 + local str = tostring(sig_fig(value, num_sig_figs)) + if #str > num_sig_figs then str = '~' .. str end + return str +end + +local CH_UP = string.char(30) +local CH_DN = string.char(31) + +function MoveGoods:init() + self.value_at_depot = get_value_at_depot() + self.value_pending = 0 + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={l=0, t=0, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_S', + options={ + {label='value'..CH_DN, value=sort_by_value_desc}, + {label='value'..CH_UP, value=sort_by_value_asc}, + {label='qty'..CH_DN, value=sort_by_quantity_desc}, + {label='qty'..CH_UP, value=sort_by_quantity_asc}, + {label='name'..CH_DN, value=sort_by_name_desc}, + {label='name'..CH_UP, value=sort_by_name_asc}, + }, + initial_option=sort_by_value_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={l=26, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, + }, + widgets.ToggleHotkeyLabel{ + view_id='show_forbidden', + frame={t=2, l=0, w=27}, + label='Show forbidden items', + key='CUSTOM_SHIFT_F', + initial_option=true, + on_change=function() self:refresh_list() end, + }, + widgets.ToggleHotkeyLabel{ + view_id='show_banned', + frame={t=3, l=0, w=43}, + label='Show items banned by export mandates', + key='CUSTOM_SHIFT_B', + initial_option=false, + on_change=function() self:refresh_list() end, + }, + widgets.ToggleHotkeyLabel{ + view_id='only_agreement', + frame={t=4, l=0, w=52}, + label='Show only items requested by export agreement', + key='CUSTOM_SHIFT_A', + initial_option=false, + on_change=function() self:refresh_list() end, + enabled=has_export_agreement(), + }, + widgets.Panel{ + frame={t=6, l=0, w=40, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_condition', + frame={l=0, t=0, w=18}, + label='Min condition:', + label_below=true, + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options={ + {label='Tattered (XX)', value=3}, + {label='Frayed (X)', value=2}, + {label='Worn (x)', value=1}, + {label='Pristine', value=0}, + }, + initial_option=3, + on_change=function(val) + if self.subviews.max_condition:getOptionValue() > val then + self.subviews.max_condition:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_condition', + frame={r=1, t=0, w=18}, + label='Max condition:', + label_below=true, + key_back='CUSTOM_SHIFT_E', + key='CUSTOM_SHIFT_R', + options={ + {label='Tattered (XX)', value=3}, + {label='Frayed (X)', value=2}, + {label='Worn (x)', value=1}, + {label='Pristine', value=0}, + }, + initial_option=0, + on_change=function(val) + if self.subviews.min_condition:getOptionValue() < val then + self.subviews.min_condition:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=4, + get_left_idx_fn=function() + return 4 - self.subviews.min_condition:getOptionValue() + end, + get_right_idx_fn=function() + return 4 - self.subviews.max_condition:getOptionValue() + end, + on_left_change=function(idx) self.subviews.min_condition:setOption(4-idx, true) end, + on_right_change=function(idx) self.subviews.max_condition:setOption(4-idx, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=6, l=41, w=38, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_quality', + frame={l=0, t=0, w=18}, + label='Min quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_SHIFT_X', + options={ + {label='Ordinary', value=0}, + {label='Well Crafted', value=1}, + {label='Finely Crafted', value=2}, + {label='Superior', value=3}, + {label='Exceptional', value=4}, + {label='Masterful', value=5}, + {label='Artifact', value=6}, + }, + initial_option=0, + on_change=function(val) + if self.subviews.max_quality:getOptionValue() < val then + self.subviews.max_quality:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_quality', + frame={r=1, t=0, w=18}, + label='Max quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Q', + key='CUSTOM_SHIFT_W', + options={ + {label='Ordinary', value=0}, + {label='Well Crafted', value=1}, + {label='Finely Crafted', value=2}, + {label='Superior', value=3}, + {label='Exceptional', value=4}, + {label='Masterful', value=5}, + {label='Artifact', value=6}, + }, + initial_option=6, + on_change=function(val) + if self.subviews.min_quality:getOptionValue() > val then + self.subviews.min_quality:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=7, + get_left_idx_fn=function() + return self.subviews.min_quality:getOptionValue() + 1 + end, + get_right_idx_fn=function() + return self.subviews.max_quality:getOptionValue() + 1 + end, + on_left_change=function(idx) self.subviews.min_quality:setOption(idx-1, true) end, + on_right_change=function(idx) self.subviews.max_quality:setOption(idx-1, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=11, l=0, r=0, b=6}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_value', + frame={l=2, t=0, w=7}, + options={ + {label='value', value=sort_noop}, + {label='value'..CH_DN, value=sort_by_value_desc}, + {label='value'..CH_UP, value=sort_by_value_asc}, + }, + initial_option=sort_by_value_desc, + on_change=self:callback('refresh_list', 'sort_value'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_quantity', + frame={l=2+VALUE_COL_WIDTH+2, t=0, w=5}, + options={ + {label='qty', value=sort_noop}, + {label='qty'..CH_DN, value=sort_by_quantity_desc}, + {label='qty'..CH_UP, value=sort_by_quantity_asc}, + }, + on_change=self:callback('refresh_list', 'sort_quantity'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_name', + frame={l=2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+2, t=0, w=6}, + options={ + {label='name', value=sort_noop}, + {label='name'..CH_DN, value=sort_by_name_desc}, + {label='name'..CH_UP, value=sort_by_name_asc}, + }, + on_change=self:callback('refresh_list', 'sort_name'), + }, + widgets.FilteredList{ + view_id='list', + frame={l=0, t=2, r=0, b=0}, + icon_width=2, + on_submit=self:callback('toggle_item'), + on_submit2=self:callback('toggle_range'), + on_select=self:callback('select_item'), + }, + } + }, + widgets.Label{ + frame={l=0, b=4, h=1, r=0}, + text={ + 'Value of items at trade depot/being brought to depot/total:', + {gap=1, text=obfuscate_value(self.value_at_depot)}, + '/', + {text=function() return obfuscate_value(self.value_pending) end}, + '/', + {text=function() return obfuscate_value(self.value_pending + self.value_at_depot) end} + }, + }, + widgets.HotkeyLabel{ + frame={l=0, b=2}, + label='Select all/none', + key='CUSTOM_CTRL_V', + on_activate=self:callback('toggle_visible'), + auto_width=true, + }, + widgets.ToggleHotkeyLabel{ + view_id='disable_buckets', + frame={l=26, b=2}, + label='Show individual items', + key='CUSTOM_CTRL_I', + initial_option=false, + on_change=function() self:refresh_list() end, + }, + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap='Click to mark/unmark for trade. Shift click to mark/unmark a range of items.', + }, + } + + -- replace the FilteredList's built-in EditField with our own + self.subviews.list.list.frame.t = 0 + self.subviews.list.edit.visible = false + self.subviews.list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + + self.subviews.list:setChoices(self:get_choices()) +end + +function MoveGoods:refresh_list(sort_widget, sort_fn) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == sort_noop then + self.subviews[sort_widget]:cycle() + return + end + for _,widget_name in ipairs{'sort', 'sort_value', 'sort_quantity', 'sort_name'} do + self.subviews[widget_name]:setOption(sort_fn) + end + local list = self.subviews.list + local saved_filter = list:getFilter() + list:setFilter('') + list:setChoices(self:get_choices(), list:getSelected()) + list:setFilter(saved_filter) +end + +local function is_tradeable_item(item) + if not item.flags.on_ground or + item.flags.hostile or + item.flags.in_inventory or + item.flags.removed or + item.flags.in_building or + item.flags.dead_dwarf or + item.flags.spider_web or + item.flags.construction or + item.flags.encased or + item.flags.unk12 or + item.flags.murder or + item.flags.trader or + item.flags.owned or + item.flags.garbage_collect or + item.flags.on_fire or + item.flags.in_chest + then + return false + end + if item.flags.in_job then + local spec_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if not spec_ref then return true end + return spec_ref.data.job.job_type == df.job_type.BringItemToDepot + end + return true +end + +local function make_search_key(str) + local out = '' + for c in str:gmatch("[%w%s]") do + out = out .. c:lower() + end + return out +end + +local to_pen = dfhack.pen.parse +local SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} +local ALL_PEN = to_pen{ch='+', fg=COLOR_LIGHTGREEN} + +local function get_entry_icon(data, item_id) + if data.selected == 0 then return nil end + if item_id then + return data.items[item_id].pending and ALL_PEN or nil + end + if data.quantity == data.selected then return ALL_PEN end + return SOME_PEN +end + +local function make_choice_text(desc, value, quantity) + return { + {width=VALUE_COL_WIDTH, rjustify=true, text=obfuscate_value(value)}, + {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, + {gap=2, text=desc}, + } +end + +-- returns true if the item or any contained item is banned +local function scan_banned(item) + if not dfhack.items.checkMandates(item) then return true end + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + if not dfhack.items.checkMandates(contained_item) then return true end + end + return false +end + +local function to_title_case(str) + str = str:gsub('(%a)([%w_]*)', + function (first, rest) return first:upper()..rest:lower() end) + str = str:gsub('_', ' ') + return str +end + +local function get_item_type_str(item) + local str = to_title_case(df.item_type[item:getType()]) + if str == 'Trapparts' then + str = 'Mechanism' + end + return str +end + +local function get_artifact_name(item) + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + if not gref then return end + local artifact = df.artifact_record.find(gref.artifact_id) + if not artifact then return end + local name = dfhack.TranslateName(artifact.name) + return ('%s (%s)'):format(name, get_item_type_str(item)) +end + +function MoveGoods:cache_choices(disable_buckets) + if self.choices then return self.choices[disable_buckets] end + + local pending = self.pending_item_ids + local buckets = {} + for _, item in ipairs(df.global.world.items.all) do + local item_id = item.id + if not item or not is_tradeable_item(item) then goto continue end + local value = get_perceived_value(item) + if value <= 0 then goto continue end + local is_pending = not not pending[item_id] + local is_forbidden = item.flags.forbid + local is_banned = scan_banned(item) + local wear_level = item:getWear() + local desc = item.flags.artifact and get_artifact_name(item) or + dfhack.items.getDescription(item, 0, true) + if wear_level == 1 then desc = ('x%sx'):format(desc) + elseif wear_level == 2 then desc = ('X%sX'):format(desc) + elseif wear_level == 3 then desc = ('XX%sXX'):format(desc) + end + local key = ('%s/%d'):format(desc, value) + if buckets[key] then + local bucket = buckets[key] + bucket.data.items[item_id] = {item=item, pending=is_pending, banned=is_banned} + bucket.data.quantity = bucket.data.quantity + 1 + bucket.data.selected = bucket.data.selected + (is_pending and 1 or 0) + bucket.data.has_forbidden = bucket.data.has_forbidden or is_forbidden + bucket.data.has_banned = bucket.data.has_banned or is_banned + else + local data = { + desc=desc, + per_item_value=value, + items={[item_id]={item=item, pending=is_pending, banned=is_banned}}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + quality=item.flags.artifact and 6 or item:getQuality(), + wear=wear_level, + selected=is_pending and 1 or 0, + has_forbidden=is_forbidden, + has_banned=is_banned, + dirty=false, + } + local entry = { + search_key=make_search_key(desc), + icon=curry(get_entry_icon, data), + data=data, + } + buckets[key] = entry + end + ::continue:: + end + + local bucket_choices, nobucket_choices = {}, {} + for _, bucket in pairs(buckets) do + local data = bucket.data + for item_id in pairs(data.items) do + local nobucket_choice = copyall(bucket) + nobucket_choice.icon = curry(get_entry_icon, data, item_id) + nobucket_choice.text = make_choice_text(data.desc, data.per_item_value, 1) + nobucket_choice.item_id = item_id + table.insert(nobucket_choices, nobucket_choice) + end + data.total_value = data.per_item_value * data.quantity + bucket.text = make_choice_text(data.desc, data.total_value, data.quantity) + table.insert(bucket_choices, bucket) + self.value_pending = self.value_pending + (data.per_item_value * data.selected) + end + + self.choices = {} + self.choices[false] = bucket_choices + self.choices[true] = nobucket_choices + return self:cache_choices(disable_buckets) +end + +function MoveGoods:get_choices() + local raw_choices = self:cache_choices(self.subviews.disable_buckets:getOptionValue()) + local choices = {} + local include_forbidden = self.subviews.show_forbidden:getOptionValue() + local include_banned = self.subviews.show_banned:getOptionValue() + local only_agreement = self.subviews.only_agreement:getOptionValue() + local min_condition = self.subviews.min_condition:getOptionValue() + local max_condition = self.subviews.max_condition:getOptionValue() + local min_quality = self.subviews.min_quality:getOptionValue() + local max_quality = self.subviews.max_quality:getOptionValue() + for _,choice in ipairs(raw_choices) do + local data = choice.data + if not include_forbidden then + if choice.item_id then + if data.items[choice.item_id].item.flags.forbid then + goto continue + end + elseif data.has_forbidden then + goto continue + end + end + if min_condition < data.wear then goto continue end + if max_condition > data.wear then goto continue end + if min_quality > data.quality then goto continue end + if max_quality < data.quality then goto continue end + if only_agreement and not is_agreement_item(data.item_type) then + goto continue + end + if not include_banned then + if choice.item_id then + if data.items[choice.item_id].banned then + goto continue + end + elseif data.has_banned then + goto continue + end + end + table.insert(choices, choice) + ::continue:: + end + table.sort(choices, self.subviews.sort:getOptionValue()) + return choices +end + +function MoveGoods:toggle_item_base(choice, target_value) + if choice.item_id then + local item_data = choice.data.items[choice.item_id] + if item_data.pending then + self.value_pending = self.value_pending - choice.data.per_item_value + choice.data.selected = choice.data.selected - 1 + end + if target_value == nil then target_value = not item_data.pending end + item_data.pending = target_value + if item_data.pending then + self.value_pending = self.value_pending + choice.data.per_item_value + choice.data.selected = choice.data.selected + 1 + end + else + self.value_pending = self.value_pending - (choice.data.selected * choice.data.per_item_value) + if target_value == nil then target_value = (choice.data.selected ~= choice.data.quantity) end + for _, item_data in pairs(choice.data.items) do + item_data.pending = target_value + end + choice.data.selected = target_value and choice.data.quantity or 0 + self.value_pending = self.value_pending + (choice.data.selected * choice.data.per_item_value) + end + choice.data.dirty = true + return target_value +end + +function MoveGoods:select_item(idx, choice) + if not dfhack.internal.getModifiers().shift then + self.prev_list_idx = self.subviews.list.list:getSelected() + end +end + +function MoveGoods:toggle_item(idx, choice) + self:toggle_item_base(choice) +end + +function MoveGoods:toggle_range(idx, choice) + if not self.prev_list_idx then + self:toggle_item(idx, choice) + return + end + local choices = self.subviews.list:getVisibleChoices() + local list_idx = self.subviews.list.list:getSelected() + local target_value + for i = list_idx, self.prev_list_idx, list_idx < self.prev_list_idx and 1 or -1 do + target_value = self:toggle_item_base(choices[i], target_value) + end + self.prev_list_idx = list_idx +end + +function MoveGoods:toggle_visible() + local target_value + for _, choice in ipairs(self.subviews.list:getVisibleChoices()) do + target_value = self:toggle_item_base(choice, target_value) + end +end + +-- ------------------- +-- MoveGoodsModal +-- + +MoveGoodsModal = defclass(MoveGoodsModal, gui.ZScreenModal) +MoveGoodsModal.ATTRS { + focus_path='movegoods', +} + +local function get_pending_trade_item_ids() + local item_ids = {} + for _,job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.BringItemToDepot and #job.items > 0 then + item_ids[job.items[0].item.id] = true + end + end + return item_ids +end + +function MoveGoodsModal:init() + self.pending_item_ids = get_pending_trade_item_ids() + self:addviews{MoveGoods{pending_item_ids=self.pending_item_ids}} +end + +function MoveGoodsModal:onDismiss() + -- mark/unmark selected goods for trade + local depot = dfhack.gui.getSelectedBuilding(true) + if not depot then return end + local pending = self.pending_item_ids + for _, choice in ipairs(self.subviews.list:getChoices()) do + if not choice.data.dirty then goto continue end + for item_id, item_data in pairs(choice.data.items) do + if item_data.pending and not pending[item_id] then + item_data.item.flags.forbid = false + dfhack.items.markForTrade(item_data.item, depot) + elseif not item_data.pending and pending[item_id] then + local spec_ref = dfhack.items.getSpecificRef(item_data.item, df.specific_ref_type.JOB) + if spec_ref then + dfhack.job.removeJob(spec_ref.data.job) + end + end + end + ::continue:: + end +end + +-- ------------------- +-- MoveGoodsOverlay +-- + +MoveGoodsOverlay = defclass(MoveGoodsOverlay, overlay.OverlayWidget) +MoveGoodsOverlay.ATTRS{ + default_pos={x=-64, y=10}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING/TradeDepot', + frame={w=31, h=1}, + frame_background=gui.CLEAR_PEN, +} + +local function has_trade_depot_and_caravan() + local bld = dfhack.gui.getSelectedBuilding(true) + if not bld or bld:getBuildStage() < bld:getMaxBuildStage() then + return false + end + if #bld.jobs == 1 and bld.jobs[0].job_type == df.job_type.DestroyBuilding then + return false + end + + for _, caravan in ipairs(df.global.plotinfo.caravans) do + local trade_state = caravan.trade_state + local time_remaining = caravan.time_remaining + if time_remaining > 0 and + (trade_state == df.caravan_state.T_trade_state.Approaching or + trade_state == df.caravan_state.T_trade_state.AtDepot) + then + return true + end + end + return false +end + +function MoveGoodsOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='DFHack move trade goods', + key='CUSTOM_CTRL_T', + on_activate=function() MoveGoodsModal{}:show() end, + enabled=has_trade_depot_and_caravan, + }, + } +end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua new file mode 100644 index 0000000000..28feb10130 --- /dev/null +++ b/internal/caravan/trade.lua @@ -0,0 +1,268 @@ +--@ module = true + +-- TODO: the category checkbox that indicates whether all items in the category +-- are selected can be incorrect after the overlay adjusts the container +-- selection. the state is in trade.current_type_a_flag, but figuring out which +-- index to modify is non-trivial. + +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +trader_selected_state = trader_selected_state or {} +broker_selected_state = broker_selected_state or {} +handle_ctrl_click_on_render = handle_ctrl_click_on_render or false +handle_shift_click_on_render = handle_shift_click_on_render or false + +local GOODFLAG = { + UNCONTAINED_UNSELECTED = 0, + UNCONTAINED_SELECTED = 1, + CONTAINED_UNSELECTED = 2, + CONTAINED_SELECTED = 3, + CONTAINER_COLLAPSED_UNSELECTED = 4, + CONTAINER_COLLAPSED_SELECTED = 5, +} + +local trade = df.global.game.main_interface.trade + +local MARGIN_HEIGHT = 26 -- screen height *other* than the list + +local function set_height(list_index, delta) + trade.i_height[list_index] = trade.i_height[list_index] + delta + if delta >= 0 then return end + _,screen_height = dfhack.screen.getWindowSize() + -- list only increments in three tiles at a time + local page_height = ((screen_height - MARGIN_HEIGHT) // 3) * 3 + trade.scroll_position_item[list_index] = math.max(0, + math.min(trade.scroll_position_item[list_index], + trade.i_height[list_index] - page_height)) +end + +local function select_shift_clicked_container_items(new_state, old_state, list_index) + -- if ctrl is also held, collapse the container too + local also_collapse = dfhack.internal.getModifiers().ctrl + local collapsed_item_count, collapsing_container, in_container = 0, false, false + for k, goodflag in ipairs(new_state) do + if in_container then + if goodflag <= GOODFLAG.UNCONTAINED_SELECTED + or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then + break + end + + new_state[k] = GOODFLAG.CONTAINED_SELECTED + + if collapsing_container then + collapsed_item_count = collapsed_item_count + 1 + end + goto continue + end + + if goodflag == old_state[k] then goto continue end + local is_container = df.item_binst:is_instance(trade.good[list_index][k]) + if not is_container then goto continue end + + -- deselect the container itself + if also_collapse or + old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED or + old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_SELECTED then + collapsing_container = goodflag == GOODFLAG.UNCONTAINED_SELECTED + new_state[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED + else + new_state[k] = GOODFLAG.UNCONTAINED_UNSELECTED + end + in_container = true + + ::continue:: + end + + if collapsed_item_count > 0 then + set_height(list_index, collapsed_item_count * -3) + end +end + +local CTRL_CLICK_STATE_MAP = { + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, +} + +-- collapses uncollapsed containers and restores the selection state for the container +-- and contained items +local function toggle_ctrl_clicked_containers(new_state, old_state, list_index) + local toggled_item_count, in_container, is_collapsing = 0, false, false + for k, goodflag in ipairs(new_state) do + if in_container then + if goodflag <= GOODFLAG.UNCONTAINED_SELECTED + or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then + break + end + toggled_item_count = toggled_item_count + 1 + new_state[k] = old_state[k] + goto continue + end + + if goodflag == old_state[k] then goto continue end + local is_contained = goodflag == GOODFLAG.CONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINED_SELECTED + if is_contained then goto continue end + local is_container = df.item_binst:is_instance(trade.good[list_index][k]) + if not is_container then goto continue end + + new_state[k] = CTRL_CLICK_STATE_MAP[old_state[k]] + in_container = true + is_collapsing = goodflag == GOODFLAG.UNCONTAINED_UNSELECTED or goodflag == GOODFLAG.UNCONTAINED_SELECTED + + ::continue:: + end + + if toggled_item_count > 0 then + set_height(list_index, toggled_item_count * 3 * (is_collapsing and -1 or 1)) + end +end + +local function collapseTypes(types_list, list_index) + local type_on_count = 0 + + for k in ipairs(types_list) do + local type_on = trade.current_type_a_on[list_index][k] + if type_on then + type_on_count = type_on_count + 1 + end + types_list[k] = false + end + + trade.i_height[list_index] = type_on_count * 3 + trade.scroll_position_item[list_index] = 0 +end + +local function collapseAllTypes() + collapseTypes(trade.current_type_a_expanded[0], 0) + collapseTypes(trade.current_type_a_expanded[1], 1) +end + +local function collapseContainers(item_list, list_index) + local num_items_collapsed = 0 + for k, goodflag in ipairs(item_list) do + if goodflag == GOODFLAG.CONTAINED_UNSELECTED + or goodflag == GOODFLAG.CONTAINED_SELECTED then + goto continue + end + + local item = trade.good[list_index][k] + local is_container = df.item_binst:is_instance(item) + if not is_container then goto continue end + + local collapsed_this_container = false + if goodflag == GOODFLAG.UNCONTAINED_SELECTED then + item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED + collapsed_this_container = true + elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then + item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED + collapsed_this_container = true + end + + if collapsed_this_container then + num_items_collapsed = num_items_collapsed + #dfhack.items.getContainedItems(item) + end + ::continue:: + end + + if num_items_collapsed > 0 then + set_height(list_index, num_items_collapsed * -3) + end +end + +local function collapseAllContainers() + collapseContainers(trade.goodflag[0], 0) + collapseContainers(trade.goodflag[1], 1) +end + +local function collapseEverything() + collapseAllContainers() + collapseAllTypes() +end + +local function copyGoodflagState() + trader_selected_state = copyall(trade.goodflag[0]) + broker_selected_state = copyall(trade.goodflag[1]) +end + +TradeOverlay = defclass(TradeOverlay, overlay.OverlayWidget) +TradeOverlay.ATTRS{ + default_pos={x=-3,y=-12}, + default_enabled=true, + viewscreens='dwarfmode/Trade', + frame={w=27, h=13}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function TradeOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={ + {text='Shift+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', + NEWLINE, + ' select items inside bin', + }, + }, + widgets.Label{ + frame={t=3, l=0}, + text={ + {text='Ctrl+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', + NEWLINE, + ' collapse/expand bin', + }, + }, + widgets.HotkeyLabel{ + frame={t=6, l=0}, + label='collapse bins', + key='CUSTOM_CTRL_C', + on_activate=collapseAllContainers, + }, + widgets.HotkeyLabel{ + frame={t=7, l=0}, + label='collapse all', + key='CUSTOM_CTRL_X', + on_activate=collapseEverything, + }, + widgets.Label{ + frame={t=9, l=0}, + text = 'Shift+Scroll', + text_pen=COLOR_LIGHTGREEN, + }, + widgets.Label{ + frame={t=9, l=12}, + text = ': fast scroll', + }, + } +end + +-- do our alterations *after* the vanilla response to the click has registered. otherwise +-- it's very difficult to figure out which item has been clicked +function TradeOverlay:onRenderBody(dc) + if handle_shift_click_on_render then + handle_shift_click_on_render = false + select_shift_clicked_container_items(trade.goodflag[0], trader_selected_state, 0) + select_shift_clicked_container_items(trade.goodflag[1], broker_selected_state, 1) + elseif handle_ctrl_click_on_render then + handle_ctrl_click_on_render = false + toggle_ctrl_clicked_containers(trade.goodflag[0], trader_selected_state, 0) + toggle_ctrl_clicked_containers(trade.goodflag[1], broker_selected_state, 1) + end +end + +function TradeOverlay:onInput(keys) + if TradeOverlay.super.onInput(self, keys) then return true end + + if keys._MOUSE_L_DOWN then + if dfhack.internal.getModifiers().shift then + handle_shift_click_on_render = true + copyGoodflagState() + elseif dfhack.internal.getModifiers().ctrl then + handle_ctrl_click_on_render = true + copyGoodflagState() + end + end +end diff --git a/internal/caravan/tradeagreement.lua b/internal/caravan/tradeagreement.lua new file mode 100644 index 0000000000..52f4b5e91a --- /dev/null +++ b/internal/caravan/tradeagreement.lua @@ -0,0 +1,37 @@ +--@ module = true + +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +TradeAgreementOverlay = defclass(TradeAgreementOverlay, overlay.OverlayWidget) +TradeAgreementOverlay.ATTRS{ + default_pos={x=45, y=-6}, + default_enabled=true, + viewscreens='dwarfmode/Diplomacy/Requests', + frame={w=25, h=3}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +local diplomacy = df.global.game.main_interface.diplomacy +local function diplomacy_toggle_cat() + local priority_idx = diplomacy.taking_requests_tablist[diplomacy.taking_requests_selected_tab] + local priority = diplomacy.environment.meeting.sell_requests.priority[priority_idx] + if #priority == 0 then return end + local target_val = priority[0] == 0 and 4 or 0 + for i in ipairs(priority) do + priority[i] = target_val + end +end + +function TradeAgreementOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='Select all/none', + key='CUSTOM_CTRL_A', + on_activate=diplomacy_toggle_cat, + }, + } +end From 86083da5ce11fb3ab796150d937a7529cfc25031 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 30 Jun 2023 17:21:41 -0700 Subject: [PATCH 350/732] basic functionality for the trade screen --- internal/caravan/common.lua | 68 ++++++ internal/caravan/movegoods.lua | 116 +++------- internal/caravan/trade.lua | 390 ++++++++++++++++++++++++++++++--- 3 files changed, 458 insertions(+), 116 deletions(-) create mode 100644 internal/caravan/common.lua diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua new file mode 100644 index 0000000000..48855c4c59 --- /dev/null +++ b/internal/caravan/common.lua @@ -0,0 +1,68 @@ +--@ module = true + +CH_UP = string.char(30) +CH_DN = string.char(31) + +local to_pen = dfhack.pen.parse +SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} +ALL_PEN = to_pen{ch='+', fg=COLOR_LIGHTGREEN} + +function make_search_key(str) + local out = '' + for c in str:gmatch("[%w%s]") do + out = out .. c:lower() + end + return out +end + +-- adapted from https://stackoverflow.com/a/50860705 +local function sig_fig(num, figures) + if num <= 0 then return 0 end + local x = figures - math.ceil(math.log(num, 10)) + return math.floor(math.floor(num * 10^x + 0.5) * 10^-x) +end + +function obfuscate_value(value) + -- TODO: respect skill of broker + local num_sig_figs = 1 + local str = tostring(sig_fig(value, num_sig_figs)) + if #str > num_sig_figs then str = '~' .. str end + return str +end + +local function to_title_case(str) + str = str:gsub('(%a)([%w_]*)', + function (first, rest) return first:upper()..rest:lower() end) + str = str:gsub('_', ' ') + return str +end + +local function get_item_type_str(item) + local str = to_title_case(df.item_type[item:getType()]) + if str == 'Trapparts' then + str = 'Mechanism' + end + return str +end + +function get_artifact_name(item) + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + if not gref then return end + local artifact = df.artifact_record.find(gref.artifact_id) + if not artifact then return end + local name = dfhack.TranslateName(artifact.name) + return ('%s (%s)'):format(name, get_item_type_str(item)) +end + +-- takes into account trade agreements +function get_perceived_value(item) + -- TODO: take trade agreements into account + local value = dfhack.items.getValue(item) + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + value = value + dfhack.items.getValue(contained_item) + for _,contained_contained_item in ipairs(dfhack.items.getContainedItems(contained_item)) do + value = value + dfhack.items.getValue(contained_contained_item) + end + end + return value +end diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 66fb3b974b..895f508b41 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -1,5 +1,6 @@ --@ module = true +local common = reqscript('internal/caravan/common') local gui = require('gui') local overlay = require('plugins.overlay') local utils = require('utils') @@ -84,19 +85,6 @@ local function is_agreement_item(item_type) return false end --- takes into account trade agreements -local function get_perceived_value(item) - -- TODO: take trade agreements into account - local value = dfhack.items.getValue(item) - for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do - value = value + dfhack.items.getValue(contained_item) - for _,contained_contained_item in ipairs(dfhack.items.getContainedItems(contained_item)) do - value = value + dfhack.items.getValue(contained_contained_item) - end - end - return value -end - local function get_value_at_depot() local sum = 0 -- if we're here, then the overlay has already determined that this is a depot @@ -104,30 +92,12 @@ local function get_value_at_depot() for _, contained_item in ipairs(depot.contained_items) do if contained_item.use_mode ~= 0 then goto continue end local item = contained_item.item - sum = sum + get_perceived_value(item) + sum = sum + common.get_perceived_value(item) ::continue:: end return sum end --- adapted from https://stackoverflow.com/a/50860705 -local function sig_fig(num, figures) - if num <= 0 then return 0 end - local x = figures - math.ceil(math.log(num, 10)) - return math.floor(math.floor(num * 10^x + 0.5) * 10^-x) -end - -local function obfuscate_value(value) - -- TODO: respect skill of broker - local num_sig_figs = 1 - local str = tostring(sig_fig(value, num_sig_figs)) - if #str > num_sig_figs then str = '~' .. str end - return str -end - -local CH_UP = string.char(30) -local CH_DN = string.char(31) - function MoveGoods:init() self.value_at_depot = get_value_at_depot() self.value_pending = 0 @@ -139,12 +109,12 @@ function MoveGoods:init() label='Sort by:', key='CUSTOM_SHIFT_S', options={ - {label='value'..CH_DN, value=sort_by_value_desc}, - {label='value'..CH_UP, value=sort_by_value_asc}, - {label='qty'..CH_DN, value=sort_by_quantity_desc}, - {label='qty'..CH_UP, value=sort_by_quantity_asc}, - {label='name'..CH_DN, value=sort_by_name_desc}, - {label='name'..CH_UP, value=sort_by_name_asc}, + {label='value'..common.CH_DN, value=sort_by_value_desc}, + {label='value'..common.CH_UP, value=sort_by_value_asc}, + {label='qty'..common.CH_DN, value=sort_by_quantity_desc}, + {label='qty'..common.CH_UP, value=sort_by_quantity_asc}, + {label='name'..common.CH_DN, value=sort_by_name_desc}, + {label='name'..common.CH_UP, value=sort_by_name_asc}, }, initial_option=sort_by_value_desc, on_change=self:callback('refresh_list', 'sort'), @@ -312,8 +282,8 @@ function MoveGoods:init() frame={l=2, t=0, w=7}, options={ {label='value', value=sort_noop}, - {label='value'..CH_DN, value=sort_by_value_desc}, - {label='value'..CH_UP, value=sort_by_value_asc}, + {label='value'..common.CH_DN, value=sort_by_value_desc}, + {label='value'..common.CH_UP, value=sort_by_value_asc}, }, initial_option=sort_by_value_desc, on_change=self:callback('refresh_list', 'sort_value'), @@ -323,8 +293,8 @@ function MoveGoods:init() frame={l=2+VALUE_COL_WIDTH+2, t=0, w=5}, options={ {label='qty', value=sort_noop}, - {label='qty'..CH_DN, value=sort_by_quantity_desc}, - {label='qty'..CH_UP, value=sort_by_quantity_asc}, + {label='qty'..common.CH_DN, value=sort_by_quantity_desc}, + {label='qty'..common.CH_UP, value=sort_by_quantity_asc}, }, on_change=self:callback('refresh_list', 'sort_quantity'), }, @@ -333,8 +303,8 @@ function MoveGoods:init() frame={l=2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+2, t=0, w=6}, options={ {label='name', value=sort_noop}, - {label='name'..CH_DN, value=sort_by_name_desc}, - {label='name'..CH_UP, value=sort_by_name_asc}, + {label='name'..common.CH_DN, value=sort_by_name_desc}, + {label='name'..common.CH_UP, value=sort_by_name_asc}, }, on_change=self:callback('refresh_list', 'sort_name'), }, @@ -352,11 +322,11 @@ function MoveGoods:init() frame={l=0, b=4, h=1, r=0}, text={ 'Value of items at trade depot/being brought to depot/total:', - {gap=1, text=obfuscate_value(self.value_at_depot)}, + {gap=1, text=common.obfuscate_value(self.value_at_depot)}, '/', - {text=function() return obfuscate_value(self.value_pending) end}, + {text=function() return common.obfuscate_value(self.value_pending) end}, '/', - {text=function() return obfuscate_value(self.value_pending + self.value_at_depot) end} + {text=function() return common.obfuscate_value(self.value_pending + self.value_at_depot) end} }, }, widgets.HotkeyLabel{ @@ -434,30 +404,18 @@ local function is_tradeable_item(item) return true end -local function make_search_key(str) - local out = '' - for c in str:gmatch("[%w%s]") do - out = out .. c:lower() - end - return out -end - -local to_pen = dfhack.pen.parse -local SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} -local ALL_PEN = to_pen{ch='+', fg=COLOR_LIGHTGREEN} - local function get_entry_icon(data, item_id) if data.selected == 0 then return nil end if item_id then - return data.items[item_id].pending and ALL_PEN or nil + return data.items[item_id].pending and common.ALL_PEN or nil end - if data.quantity == data.selected then return ALL_PEN end - return SOME_PEN + if data.quantity == data.selected then return common.ALL_PEN end + return common.SOME_PEN end local function make_choice_text(desc, value, quantity) return { - {width=VALUE_COL_WIDTH, rjustify=true, text=obfuscate_value(value)}, + {width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value)}, {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, {gap=2, text=desc}, } @@ -472,30 +430,6 @@ local function scan_banned(item) return false end -local function to_title_case(str) - str = str:gsub('(%a)([%w_]*)', - function (first, rest) return first:upper()..rest:lower() end) - str = str:gsub('_', ' ') - return str -end - -local function get_item_type_str(item) - local str = to_title_case(df.item_type[item:getType()]) - if str == 'Trapparts' then - str = 'Mechanism' - end - return str -end - -local function get_artifact_name(item) - local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) - if not gref then return end - local artifact = df.artifact_record.find(gref.artifact_id) - if not artifact then return end - local name = dfhack.TranslateName(artifact.name) - return ('%s (%s)'):format(name, get_item_type_str(item)) -end - function MoveGoods:cache_choices(disable_buckets) if self.choices then return self.choices[disable_buckets] end @@ -504,13 +438,13 @@ function MoveGoods:cache_choices(disable_buckets) for _, item in ipairs(df.global.world.items.all) do local item_id = item.id if not item or not is_tradeable_item(item) then goto continue end - local value = get_perceived_value(item) + local value = common.get_perceived_value(item) if value <= 0 then goto continue end local is_pending = not not pending[item_id] local is_forbidden = item.flags.forbid local is_banned = scan_banned(item) local wear_level = item:getWear() - local desc = item.flags.artifact and get_artifact_name(item) or + local desc = item.flags.artifact and common.get_artifact_name(item) or dfhack.items.getDescription(item, 0, true) if wear_level == 1 then desc = ('x%sx'):format(desc) elseif wear_level == 2 then desc = ('X%sX'):format(desc) @@ -540,7 +474,7 @@ function MoveGoods:cache_choices(disable_buckets) dirty=false, } local entry = { - search_key=make_search_key(desc), + search_key=common.make_search_key(desc), icon=curry(get_entry_icon, data), data=data, } @@ -678,7 +612,7 @@ end MoveGoodsModal = defclass(MoveGoodsModal, gui.ZScreenModal) MoveGoodsModal.ATTRS { - focus_path='movegoods', + focus_path='caravan/movegoods', } local function get_pending_trade_item_ids() diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 28feb10130..0a28c6425f 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -5,6 +5,7 @@ -- selection. the state is in trade.current_type_a_flag, but figuring out which -- index to modify is non-trivial. +local common = reqscript('internal/caravan/common') local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') @@ -25,20 +26,353 @@ local GOODFLAG = { local trade = df.global.game.main_interface.trade +-- ------------------- +-- Trade +-- + +Trade = defclass(Trade, widgets.Window) +Trade.ATTRS { + frame_title='Select trade goods', + frame={w=54, h=45}, + resizable=true, + resize_min={h=27}, +} + +local VALUE_COL_WIDTH = 8 + +local function sort_noop(a, b) + -- this function is used as a marker and never actually gets called + error('sort_noop should not be called') +end + +local function sort_base(a, b) + return a.data.desc < b.data.desc +end + +local function sort_by_name_desc(a, b) + if a.search_key == b.search_key then + return sort_base(a, b) + end + return a.search_key < b.search_key +end + +local function sort_by_name_asc(a, b) + if a.search_key == b.search_key then + return sort_base(a, b) + end + return a.search_key > b.search_key +end + +local function sort_by_value_desc(a, b) + if a.data.value == b.data.value then + return sort_by_name_desc(a, b) + end + return a.data.value > b.data.value +end + +local function sort_by_value_asc(a, b) + if a.data.value == b.data.value then + return sort_by_name_desc(a, b) + end + return a.data.value < b.data.value +end + +function Trade:init() + self.choices = {[0]={}, [1]={}} + self.cur_page = 1 + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={l=0, t=0, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_S', + options={ + {label='value'..common.CH_DN, value=sort_by_value_desc}, + {label='value'..common.CH_UP, value=sort_by_value_asc}, + {label='name'..common.CH_DN, value=sort_by_name_desc}, + {label='name'..common.CH_UP, value=sort_by_name_asc}, + }, + initial_option=sort_by_value_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={l=26, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, + }, + widgets.ToggleHotkeyLabel{ + view_id='trade_bins', + frame={t=2, l=0, w=27}, + label='Bins', + key='CUSTOM_SHIFT_B', + options={ + {label='trade bin with contents', value=true}, + {label='trade contents only', value=false}, + }, + initial_option=false, + on_change=function() self:refresh_list() end, + }, + widgets.TabBar{ + frame={t=4, l=0}, + labels={ + 'Caravan goods', + 'Fort goods', + }, + on_select=function(idx) + self.cur_page = idx + self:refresh_list() + end, + get_cur_page=function() return self.cur_page end, + }, + widgets.Panel{ + frame={t=6, l=0, r=0, b=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_value', + frame={l=2, t=0, w=7}, + options={ + {label='value', value=sort_noop}, + {label='value'..common.CH_DN, value=sort_by_value_desc}, + {label='value'..common.CH_UP, value=sort_by_value_asc}, + }, + initial_option=sort_by_value_desc, + on_change=self:callback('refresh_list', 'sort_value'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_name', + frame={l=2+VALUE_COL_WIDTH+2, t=0, w=6}, + options={ + {label='name', value=sort_noop}, + {label='name'..common.CH_DN, value=sort_by_name_desc}, + {label='name'..common.CH_UP, value=sort_by_name_asc}, + }, + on_change=self:callback('refresh_list', 'sort_name'), + }, + widgets.FilteredList{ + view_id='list', + frame={l=0, t=2, r=0, b=0}, + icon_width=2, + on_submit=self:callback('toggle_item'), + on_submit2=self:callback('toggle_range'), + on_select=self:callback('select_item'), + }, + } + }, + widgets.HotkeyLabel{ + frame={l=0, b=2}, + label='Select all/none', + key='CUSTOM_CTRL_V', + on_activate=self:callback('toggle_visible'), + auto_width=true, + }, + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap='Click to mark/unmark for trade. Shift click to mark/unmark a range of items.', + }, + } + + -- replace the FilteredList's built-in EditField with our own + self.subviews.list.list.frame.t = 0 + self.subviews.list.edit.visible = false + self.subviews.list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + + self.subviews.list:setChoices(self:get_choices()) +end + +function Trade:refresh_list(sort_widget, sort_fn) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == sort_noop then + self.subviews[sort_widget]:cycle() + return + end + for _,widget_name in ipairs{'sort', 'sort_value', 'sort_name'} do + self.subviews[widget_name]:setOption(sort_fn) + end + local list = self.subviews.list + local saved_filter = list:getFilter() + list:setFilter('') + list:setChoices(self:get_choices(), list:getSelected()) + list:setFilter(saved_filter) +end + +local TOGGLE_MAP = { + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_SELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_SELECTED, + [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, +} + +local TARGET_MAP = { + [true]={ + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_SELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, + [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_SELECTED, + [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + }, + [false]={ + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_UNSELECTED, + [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, + }, +} + +local TARGET_REVMAP = { + [GOODFLAG.UNCONTAINED_UNSELECTED] = false, + [GOODFLAG.UNCONTAINED_SELECTED] = true, + [GOODFLAG.CONTAINED_UNSELECTED] = false, + [GOODFLAG.CONTAINED_SELECTED] = true, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = false, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = true, +} + +local function get_entry_icon(data) + if TARGET_REVMAP[trade.goodflag[data.list_idx][data.item_idx]] then + return common.ALL_PEN + end +end + +local function make_choice_text(desc, value) + return { + {width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value)}, + {gap=2, text=desc}, + } +end + +function Trade:cache_choices(list_idx, trade_bins) + if self.choices[list_idx][trade_bins] then return self.choices[list_idx][trade_bins] end + + local goodflags = trade.goodflag[list_idx] + local trade_bins_choices, notrade_bins_choices = {}, {} + local parent_idx + for item_idx, item in ipairs(trade.good[list_idx]) do + local goodflag = goodflags[item_idx] + if goodflag ~= GOODFLAG.CONTAINED_UNSELECTED and goodflag ~= GOODFLAG.CONTAINED_SELECTED then + parent_idx = nil + end + local desc = item.flags.artifact and common.get_artifact_name(item) or + dfhack.items.getDescription(item, 0, true) + local data = { + desc=desc, + value=common.get_perceived_value(item), + list_idx=list_idx, + item_idx=item_idx, + } + if parent_idx then + data.update_container_fn = function(from, to) + -- TODO + end + end + local choice = { + search_key=common.make_search_key(desc), + icon=curry(get_entry_icon, data), + data=data, + text=make_choice_text(desc, data.value), + } + local is_container = df.item_binst:is_instance(item) + if not data.update_container_fn then + table.insert(trade_bins_choices, choice) + end + if data.update_container_fn or not is_container then + table.insert(notrade_bins_choices, choice) + end + if is_container then parent_idx = item_idx end + end + + self.choices[list_idx][true] = trade_bins_choices + self.choices[list_idx][false] = notrade_bins_choices + return self:cache_choices(list_idx, trade_bins) +end + +function Trade:get_choices() + local choices = self:cache_choices(self.cur_page-1, self.subviews.trade_bins:getOptionValue()) + table.sort(choices, self.subviews.sort:getOptionValue()) + return choices +end + +local function toggle_item_base(choice, target_value) + local goodflag = trade.goodflag[choice.data.list_idx][choice.data.item_idx] + local goodflag_map = target_value == nil and TOGGLE_MAP or TARGET_MAP[target_value] + trade.goodflag[choice.data.list_idx][choice.data.item_idx] = goodflag_map[goodflag] + target_value = TARGET_REVMAP[trade.goodflag[choice.data.list_idx][choice.data.item_idx]] + if choice.data.update_container_fn then + choice.data.update_container_fn(TARGET_REVMAP[goodflag], target_value) + end + return target_value +end + +function Trade:select_item(idx, choice) + if not dfhack.internal.getModifiers().shift then + self.prev_list_idx = self.subviews.list.list:getSelected() + end +end + +function Trade:toggle_item(idx, choice) + toggle_item_base(choice) +end + +function Trade:toggle_range(idx, choice) + if not self.prev_list_idx then + self:toggle_item(idx, choice) + return + end + local choices = self.subviews.list:getVisibleChoices() + local list_idx = self.subviews.list.list:getSelected() + local target_value + for i = list_idx, self.prev_list_idx, list_idx < self.prev_list_idx and 1 or -1 do + target_value = toggle_item_base(choices[i], target_value) + end + self.prev_list_idx = list_idx +end + +function Trade:toggle_visible() + local target_value + for _, choice in ipairs(self.subviews.list:getVisibleChoices()) do + target_value = toggle_item_base(choice, target_value) + end +end + +-- ------------------- +-- TradeModal +-- + +TradeModal = defclass(TradeModal, gui.ZScreenModal) +TradeModal.ATTRS { + focus_path='caravan/trade', +} + +function TradeModal:init() + self:addviews{Trade{}} +end + +-- ------------------- +-- TradeOverlay +-- + local MARGIN_HEIGHT = 26 -- screen height *other* than the list -local function set_height(list_index, delta) - trade.i_height[list_index] = trade.i_height[list_index] + delta +local function set_height(list_idx, delta) + trade.i_height[list_idx] = trade.i_height[list_idx] + delta if delta >= 0 then return end _,screen_height = dfhack.screen.getWindowSize() -- list only increments in three tiles at a time local page_height = ((screen_height - MARGIN_HEIGHT) // 3) * 3 - trade.scroll_position_item[list_index] = math.max(0, - math.min(trade.scroll_position_item[list_index], - trade.i_height[list_index] - page_height)) + trade.scroll_position_item[list_idx] = math.max(0, + math.min(trade.scroll_position_item[list_idx], + trade.i_height[list_idx] - page_height)) end -local function select_shift_clicked_container_items(new_state, old_state, list_index) +local function select_shift_clicked_container_items(new_state, old_state, list_idx) -- if ctrl is also held, collapse the container too local also_collapse = dfhack.internal.getModifiers().ctrl local collapsed_item_count, collapsing_container, in_container = 0, false, false @@ -58,7 +392,7 @@ local function select_shift_clicked_container_items(new_state, old_state, list_i end if goodflag == old_state[k] then goto continue end - local is_container = df.item_binst:is_instance(trade.good[list_index][k]) + local is_container = df.item_binst:is_instance(trade.good[list_idx][k]) if not is_container then goto continue end -- deselect the container itself @@ -76,7 +410,7 @@ local function select_shift_clicked_container_items(new_state, old_state, list_i end if collapsed_item_count > 0 then - set_height(list_index, collapsed_item_count * -3) + set_height(list_idx, collapsed_item_count * -3) end end @@ -89,7 +423,7 @@ local CTRL_CLICK_STATE_MAP = { -- collapses uncollapsed containers and restores the selection state for the container -- and contained items -local function toggle_ctrl_clicked_containers(new_state, old_state, list_index) +local function toggle_ctrl_clicked_containers(new_state, old_state, list_idx) local toggled_item_count, in_container, is_collapsing = 0, false, false for k, goodflag in ipairs(new_state) do if in_container then @@ -105,7 +439,7 @@ local function toggle_ctrl_clicked_containers(new_state, old_state, list_index) if goodflag == old_state[k] then goto continue end local is_contained = goodflag == GOODFLAG.CONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINED_SELECTED if is_contained then goto continue end - local is_container = df.item_binst:is_instance(trade.good[list_index][k]) + local is_container = df.item_binst:is_instance(trade.good[list_idx][k]) if not is_container then goto continue end new_state[k] = CTRL_CLICK_STATE_MAP[old_state[k]] @@ -116,23 +450,23 @@ local function toggle_ctrl_clicked_containers(new_state, old_state, list_index) end if toggled_item_count > 0 then - set_height(list_index, toggled_item_count * 3 * (is_collapsing and -1 or 1)) + set_height(list_idx, toggled_item_count * 3 * (is_collapsing and -1 or 1)) end end -local function collapseTypes(types_list, list_index) +local function collapseTypes(types_list, list_idx) local type_on_count = 0 for k in ipairs(types_list) do - local type_on = trade.current_type_a_on[list_index][k] + local type_on = trade.current_type_a_on[list_idx][k] if type_on then type_on_count = type_on_count + 1 end types_list[k] = false end - trade.i_height[list_index] = type_on_count * 3 - trade.scroll_position_item[list_index] = 0 + trade.i_height[list_idx] = type_on_count * 3 + trade.scroll_position_item[list_idx] = 0 end local function collapseAllTypes() @@ -140,7 +474,7 @@ local function collapseAllTypes() collapseTypes(trade.current_type_a_expanded[1], 1) end -local function collapseContainers(item_list, list_index) +local function collapseContainers(item_list, list_idx) local num_items_collapsed = 0 for k, goodflag in ipairs(item_list) do if goodflag == GOODFLAG.CONTAINED_UNSELECTED @@ -148,7 +482,7 @@ local function collapseContainers(item_list, list_index) goto continue end - local item = trade.good[list_index][k] + local item = trade.good[list_idx][k] local is_container = df.item_binst:is_instance(item) if not is_container then goto continue end @@ -168,7 +502,7 @@ local function collapseContainers(item_list, list_index) end if num_items_collapsed > 0 then - set_height(list_index, num_items_collapsed * -3) + set_height(list_idx, num_items_collapsed * -3) end end @@ -192,15 +526,21 @@ TradeOverlay.ATTRS{ default_pos={x=-3,y=-12}, default_enabled=true, viewscreens='dwarfmode/Trade', - frame={w=27, h=13}, + frame={w=27, h=15}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } function TradeOverlay:init() self:addviews{ - widgets.Label{ + widgets.HotkeyLabel{ frame={t=0, l=0}, + label='DFHack trade UI', + key='CUSTOM_CTRL_T', + on_activate=function() TradeModal{}:show() end, + }, + widgets.Label{ + frame={t=2, l=0}, text={ {text='Shift+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', NEWLINE, @@ -208,7 +548,7 @@ function TradeOverlay:init() }, }, widgets.Label{ - frame={t=3, l=0}, + frame={t=5, l=0}, text={ {text='Ctrl+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', NEWLINE, @@ -216,24 +556,24 @@ function TradeOverlay:init() }, }, widgets.HotkeyLabel{ - frame={t=6, l=0}, + frame={t=8, l=0}, label='collapse bins', key='CUSTOM_CTRL_C', on_activate=collapseAllContainers, }, widgets.HotkeyLabel{ - frame={t=7, l=0}, + frame={t=9, l=0}, label='collapse all', key='CUSTOM_CTRL_X', on_activate=collapseEverything, }, widgets.Label{ - frame={t=9, l=0}, + frame={t=11, l=0}, text = 'Shift+Scroll', text_pen=COLOR_LIGHTGREEN, }, widgets.Label{ - frame={t=9, l=12}, + frame={t=11, l=12}, text = ': fast scroll', }, } From 2e0dc7e0a83cf243981b7fe1dd3a80cb063019b2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 4 Jul 2023 04:00:57 -0700 Subject: [PATCH 351/732] use new item module APIs add sort by status, allow simulataneous use of vanilla trade screen, add items already in depot so they can be deselected --- caravan.lua | 2 +- internal/caravan/common.lua | 59 +++++++++++---- internal/caravan/movegoods.lua | 127 ++++++++++++++++++++++++--------- internal/caravan/trade.lua | 32 ++++++--- 4 files changed, 161 insertions(+), 59 deletions(-) diff --git a/caravan.lua b/caravan.lua index c8b3064e05..43168ded2b 100644 --- a/caravan.lua +++ b/caravan.lua @@ -1,9 +1,9 @@ -- Adjusts properties of caravans and provides overlays for enhanced trading --@ module = true +local movegoods = reqscript('internal/caravan/movegoods') local trade = reqscript('internal/caravan/trade') local tradeagreement = reqscript('internal/caravan/tradeagreement') -local movegoods = reqscript('internal/caravan/movegoods') dfhack.onStateChange.caravanTradeOverlay = function(code) if code == SC_WORLD_UNLOADED then diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 48855c4c59..2d6b00bf43 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -15,19 +15,49 @@ function make_search_key(str) return out end --- adapted from https://stackoverflow.com/a/50860705 -local function sig_fig(num, figures) - if num <= 0 then return 0 end - local x = figures - math.ceil(math.log(num, 10)) - return math.floor(math.floor(num * 10^x + 0.5) * 10^-x) +local 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 + if skill.id == df.job_skill.APPRAISAL then + return skill.rating + end + end + return 0 end +local 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 + if broker_skill <= df.skill_rating.Competent then return 50 end + if broker_skill <= df.skill_rating.Skilled then return 100 end + if broker_skill <= df.skill_rating.Proficient then return 200 end + if broker_skill <= df.skill_rating.Talented then return 500 end + if broker_skill <= df.skill_rating.Adept then return 1000 end + if broker_skill <= df.skill_rating.Expert then return 1500 end + if broker_skill <= df.skill_rating.Professional then return 2000 end + if broker_skill <= df.skill_rating.Accomplished then return 2500 end + if broker_skill <= df.skill_rating.Great then return 3000 end + 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 +end + +-- If the item's value is below the threshold, it gets shown exactly as-is. +-- Otherwise, if it's less than or equal to [threshold + 50], it will round to the nearest multiple of 10 as an Estimate +-- 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) - -- TODO: respect skill of broker - local num_sig_figs = 1 - local str = tostring(sig_fig(value, num_sig_figs)) - if #str > num_sig_figs then str = '~' .. str end - return str + local threshold = get_threshold(get_broker_skill()) + if value < threshold then return tostring(value) end + threshold = threshold + 50 + if value <= threshold then return ('~%d'):format(((value+5)//10)*10) end + if value <= threshold*3 then return ('~%d'):format(((value+50)//100)*100) end + if value <= threshold*30 then return ('~%d'):format(((value+500)//1000)*1000) end + return ('%d?'):format(((threshold*30 + 999)//1000)*1000) end local function to_title_case(str) @@ -55,13 +85,12 @@ function get_artifact_name(item) end -- takes into account trade agreements -function get_perceived_value(item) - -- TODO: take trade agreements into account - local value = dfhack.items.getValue(item) +function get_perceived_value(item, caravan_state, caravan_buying) + local value = dfhack.items.getValue(item, caravan_state, caravan_buying) for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do - value = value + dfhack.items.getValue(contained_item) + value = value + dfhack.items.getValue(contained_item, caravan_state, caravan_buying) for _,contained_contained_item in ipairs(dfhack.items.getContainedItems(contained_item)) do - value = value + dfhack.items.getValue(contained_contained_item) + value = value + dfhack.items.getValue(contained_contained_item, caravan_state, caravan_buying) end end return value diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 895f508b41..19c0bd9538 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -19,8 +19,9 @@ MoveGoods.ATTRS { pending_item_ids=DEFAULT_NIL, } +local STATUS_COL_WIDTH = 7 local VALUE_COL_WIDTH = 8 -local QTY_COL_WIDTH = 6 +local QTY_COL_WIDTH = 5 local function sort_noop(a, b) -- this function is used as a marker and never actually gets called @@ -61,27 +62,49 @@ local function sort_by_value_asc(a, b) return a.data[value_field] < b.data[value_field] end +local function sort_by_status_desc(a, b) + local a_unselected = a.data.selected == 0 or (a.item_id and not a.items[a.item_id].pending) + local b_unselected = b.data.selected == 0 or (b.item_id and not b.items[b.item_id].pending) + if a_unselected == b_unselected then + return sort_by_value_desc(a, b) + end + return not a_unselected +end + +local function sort_by_status_asc(a, b) + local a_unselected = a.data.selected == 0 or (a.item_id and not a.items[a.item_id].pending) + local b_unselected = b.data.selected == 0 or (b.item_id and not b.items[b.item_id].pending) + if a_unselected == b_unselected then + return sort_by_value_desc(a, b) + end + return not b_unselected +end + local function sort_by_quantity_desc(a, b) if a.data.quantity == b.data.quantity then - return sort_by_name_desc(a, b) + return sort_by_value_desc(a, b) end return a.data.quantity > b.data.quantity end local function sort_by_quantity_asc(a, b) if a.data.quantity == b.data.quantity then - return sort_by_name_desc(a, b) + return sort_by_value_desc(a, b) end return a.data.quantity < b.data.quantity end local function has_export_agreement() - -- TODO: where are export agreements stored? - return false -end - -local function is_agreement_item(item_type) - -- TODO: match export agreement with civs with active caravans + for _,caravan in ipairs(df.global.plotinfo.caravans) do + local trade_state = caravan.trade_state + if caravan.time_remaining > 0 and + (trade_state == df.caravan_state.T_trade_state.Approaching or + trade_state == df.caravan_state.T_trade_state.AtDepot) and + caravan.sell_prices + then + return true + end + end return false end @@ -92,6 +115,7 @@ local function get_value_at_depot() for _, contained_item in ipairs(depot.contained_items) do if contained_item.use_mode ~= 0 then goto continue end local item = contained_item.item + if item.flags.trader or not item.flags.in_building then goto continue end sum = sum + common.get_perceived_value(item) ::continue:: end @@ -109,6 +133,8 @@ function MoveGoods:init() label='Sort by:', key='CUSTOM_SHIFT_S', options={ + {label='status'..common.CH_DN, value=sort_by_status_desc}, + {label='status'..common.CH_UP, value=sort_by_status_asc}, {label='value'..common.CH_DN, value=sort_by_value_desc}, {label='value'..common.CH_UP, value=sort_by_value_asc}, {label='qty'..common.CH_DN, value=sort_by_quantity_desc}, @@ -116,7 +142,7 @@ function MoveGoods:init() {label='name'..common.CH_DN, value=sort_by_name_desc}, {label='name'..common.CH_UP, value=sort_by_name_asc}, }, - initial_option=sort_by_value_desc, + initial_option=sort_by_status_desc, on_change=self:callback('refresh_list', 'sort'), }, widgets.EditField{ @@ -277,35 +303,49 @@ function MoveGoods:init() widgets.Panel{ frame={t=11, l=0, r=0, b=6}, subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_status', + frame={l=0, t=0, w=7}, + options={ + {label='status', value=sort_noop}, + {label='status'..common.CH_DN, value=sort_by_status_desc}, + {label='status'..common.CH_UP, value=sort_by_status_asc}, + }, + initial_option=sort_by_status_desc, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_status'), + }, widgets.CycleHotkeyLabel{ view_id='sort_value', - frame={l=2, t=0, w=7}, + frame={l=STATUS_COL_WIDTH+2, t=0, w=6}, options={ {label='value', value=sort_noop}, {label='value'..common.CH_DN, value=sort_by_value_desc}, {label='value'..common.CH_UP, value=sort_by_value_asc}, }, - initial_option=sort_by_value_desc, + option_gap=0, on_change=self:callback('refresh_list', 'sort_value'), }, widgets.CycleHotkeyLabel{ view_id='sort_quantity', - frame={l=2+VALUE_COL_WIDTH+2, t=0, w=5}, + frame={l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+2, t=0, w=4}, options={ {label='qty', value=sort_noop}, {label='qty'..common.CH_DN, value=sort_by_quantity_desc}, {label='qty'..common.CH_UP, value=sort_by_quantity_asc}, }, + option_gap=0, on_change=self:callback('refresh_list', 'sort_quantity'), }, widgets.CycleHotkeyLabel{ view_id='sort_name', - frame={l=2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+2, t=0, w=6}, + frame={l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+2, t=0, w=5}, options={ {label='name', value=sort_noop}, {label='name'..common.CH_DN, value=sort_by_name_desc}, {label='name'..common.CH_UP, value=sort_by_name_asc}, }, + option_gap=0, on_change=self:callback('refresh_list', 'sort_name'), }, widgets.FilteredList{ @@ -332,7 +372,7 @@ function MoveGoods:init() widgets.HotkeyLabel{ frame={l=0, b=2}, label='Select all/none', - key='CUSTOM_CTRL_V', + key='CUSTOM_CTRL_A', on_activate=self:callback('toggle_visible'), auto_width=true, }, @@ -366,7 +406,7 @@ function MoveGoods:refresh_list(sort_widget, sort_fn) self.subviews[sort_widget]:cycle() return end - for _,widget_name in ipairs{'sort', 'sort_value', 'sort_quantity', 'sort_name'} do + for _,widget_name in ipairs{'sort', 'sort_status', 'sort_value', 'sort_quantity', 'sort_name'} do self.subviews[widget_name]:setOption(sort_fn) end local list = self.subviews.list @@ -376,12 +416,10 @@ function MoveGoods:refresh_list(sort_widget, sort_fn) list:setFilter(saved_filter) end -local function is_tradeable_item(item) - if not item.flags.on_ground or - item.flags.hostile or +local function is_tradeable_item(item, depot) + if item.flags.hostile or item.flags.in_inventory or item.flags.removed or - item.flags.in_building or item.flags.dead_dwarf or item.flags.spider_web or item.flags.construction or @@ -401,6 +439,14 @@ local function is_tradeable_item(item) if not spec_ref then return true end return spec_ref.data.job.job_type == df.job_type.BringItemToDepot end + if item.flags.in_building then + if dfhack.items.getHolderBuilding(item) ~= depot then return false end + for _, contained_item in ipairs(depot.contained_items) do + if contained_item.use_mode == 0 then return true end + -- building construction materials + if item == contained_item.item then return false end + end + end return true end @@ -415,9 +461,9 @@ end local function make_choice_text(desc, value, quantity) return { - {width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value)}, - {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, - {gap=2, text=desc}, + {width=STATUS_COL_WIDTH+VALUE_COL_WIDTH-3, rjustify=true, text=common.obfuscate_value(value)}, + {gap=3, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, + {gap=4, text=desc}, } end @@ -433,16 +479,18 @@ end function MoveGoods:cache_choices(disable_buckets) if self.choices then return self.choices[disable_buckets] end + local depot = dfhack.gui.getSelectedBuilding(true) local pending = self.pending_item_ids local buckets = {} for _, item in ipairs(df.global.world.items.all) do local item_id = item.id - if not item or not is_tradeable_item(item) then goto continue end + if not item or not is_tradeable_item(item, depot) then goto continue end local value = common.get_perceived_value(item) if value <= 0 then goto continue end - local is_pending = not not pending[item_id] + local is_pending = not not pending[item_id] or item.flags.in_building local is_forbidden = item.flags.forbid local is_banned = scan_banned(item) + local is_requested = dfhack.items.isRequestedTradeGood(item) local wear_level = item:getWear() local desc = item.flags.artifact and common.get_artifact_name(item) or dfhack.items.getDescription(item, 0, true) @@ -453,16 +501,17 @@ function MoveGoods:cache_choices(disable_buckets) local key = ('%s/%d'):format(desc, value) if buckets[key] then local bucket = buckets[key] - bucket.data.items[item_id] = {item=item, pending=is_pending, banned=is_banned} + bucket.data.items[item_id] = {item=item, pending=is_pending, banned=is_banned, requested=is_requested} bucket.data.quantity = bucket.data.quantity + 1 bucket.data.selected = bucket.data.selected + (is_pending and 1 or 0) bucket.data.has_forbidden = bucket.data.has_forbidden or is_forbidden bucket.data.has_banned = bucket.data.has_banned or is_banned + bucket.data.has_requested = bucket.data.has_requested or is_requested else local data = { desc=desc, per_item_value=value, - items={[item_id]={item=item, pending=is_pending, banned=is_banned}}, + items={[item_id]={item=item, pending=is_pending, banned=is_banned, requested=is_requested}}, item_type=item:getType(), item_subtype=item:getSubtype(), quantity=1, @@ -471,6 +520,7 @@ function MoveGoods:cache_choices(disable_buckets) selected=is_pending and 1 or 0, has_forbidden=is_forbidden, has_banned=is_banned, + has_requested=is_requested, dirty=false, } local entry = { @@ -530,8 +580,14 @@ function MoveGoods:get_choices() if max_condition > data.wear then goto continue end if min_quality > data.quality then goto continue end if max_quality < data.quality then goto continue end - if only_agreement and not is_agreement_item(data.item_type) then - goto continue + if only_agreement then + if choice.item_id then + if not data.items[choice.item_id].requested then + goto continue + end + elseif not data.has_requested then + goto continue + end end if not include_banned then if choice.item_id then @@ -638,14 +694,21 @@ function MoveGoodsModal:onDismiss() for _, choice in ipairs(self.subviews.list:getChoices()) do if not choice.data.dirty then goto continue end for item_id, item_data in pairs(choice.data.items) do + local item = item_data.item if item_data.pending and not pending[item_id] then - item_data.item.flags.forbid = false - dfhack.items.markForTrade(item_data.item, depot) + item.flags.forbid = false + if dfhack.items.getHolderBuilding(item) then + item.flags.in_building = true + else + dfhack.items.markForTrade(item, depot) + end elseif not item_data.pending and pending[item_id] then - local spec_ref = dfhack.items.getSpecificRef(item_data.item, df.specific_ref_type.JOB) + local spec_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) if spec_ref then dfhack.job.removeJob(spec_ref.data.job) end + elseif not item_data.pending and item.flags.in_building then + item.flags.in_building = false end end ::continue:: diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 0a28c6425f..9d62ffe7cb 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -33,7 +33,7 @@ local trade = df.global.game.main_interface.trade Trade = defclass(Trade, widgets.Window) Trade.ATTRS { frame_title='Select trade goods', - frame={w=54, h=45}, + frame={w=78, h=45}, resizable=true, resize_min={h=27}, } @@ -104,8 +104,8 @@ function Trade:init() }, widgets.ToggleHotkeyLabel{ view_id='trade_bins', - frame={t=2, l=0, w=27}, - label='Bins', + frame={t=2, l=0, w=36}, + label='Bins:', key='CUSTOM_SHIFT_B', options={ {label='trade bin with contents', value=true}, @@ -163,7 +163,7 @@ function Trade:init() widgets.HotkeyLabel{ frame={l=0, b=2}, label='Select all/none', - key='CUSTOM_CTRL_V', + key='CUSTOM_CTRL_A', on_activate=self:callback('toggle_visible'), auto_width=true, }, @@ -264,7 +264,7 @@ function Trade:cache_choices(list_idx, trade_bins) dfhack.items.getDescription(item, 0, true) local data = { desc=desc, - value=common.get_perceived_value(item), + value=common.get_perceived_value(item, trade.mer, list_idx == 1), list_idx=list_idx, item_idx=item_idx, } @@ -343,18 +343,24 @@ function Trade:toggle_visible() end -- ------------------- --- TradeModal +-- TradeScreen -- -TradeModal = defclass(TradeModal, gui.ZScreenModal) -TradeModal.ATTRS { +view = view or nil + +TradeScreen = defclass(TradeScreen, gui.ZScreen) +TradeScreen.ATTRS { focus_path='caravan/trade', } -function TradeModal:init() +function TradeScreen:init() self:addviews{Trade{}} end +function TradeScreen:onDismiss() + view = nil +end + -- ------------------- -- TradeOverlay -- @@ -525,7 +531,7 @@ TradeOverlay = defclass(TradeOverlay, overlay.OverlayWidget) TradeOverlay.ATTRS{ default_pos={x=-3,y=-12}, default_enabled=true, - viewscreens='dwarfmode/Trade', + viewscreens='dwarfmode/Trade/Default', frame={w=27, h=15}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, @@ -537,7 +543,7 @@ function TradeOverlay:init() frame={t=0, l=0}, label='DFHack trade UI', key='CUSTOM_CTRL_T', - on_activate=function() TradeModal{}:show() end, + on_activate=function() view = view and view:raise() or TradeScreen{}:show() end, }, widgets.Label{ frame={t=2, l=0}, @@ -604,5 +610,9 @@ function TradeOverlay:onInput(keys) handle_ctrl_click_on_render = true copyGoodflagState() end + elseif keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + if view then + view:dismiss() + end end end From 971f8c5665f655c9fd41d156756777cf424e2a17 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 4 Jul 2023 15:30:25 -0700 Subject: [PATCH 352/732] implement elf safety filter --- internal/caravan/common.lua | 2 + internal/caravan/movegoods.lua | 237 +++++++++++++++++++++++++++------ 2 files changed, 196 insertions(+), 43 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 2d6b00bf43..2f012f4f27 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -2,6 +2,8 @@ CH_UP = string.char(30) 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} diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 19c0bd9538..00360f0e6a 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -94,36 +94,46 @@ local function sort_by_quantity_asc(a, b) return a.data.quantity < b.data.quantity end -local function has_export_agreement() +local function is_active_caravan(caravan) + local trade_state = caravan.trade_state + return caravan.time_remaining > 0 and + (trade_state == df.caravan_state.T_trade_state.Approaching or + trade_state == df.caravan_state.T_trade_state.AtDepot) +end + +local function is_tree_lover_caravan(caravan) + local caravan_he = df.historical_entity.find(caravan.entity); + if not caravan_he then return false end + local wood_ethic = caravan_he.entity_raw.ethic[df.ethic_type.KILL_PLANT] + return wood_ethic == df.ethic_response.MISGUIDED or + wood_ethic == df.ethic_response.SHUN or + wood_ethic == df.ethic_response.APPALLING or + wood_ethic == df.ethic_response.PUNISH_REPRIMAND or + wood_ethic == df.ethic_response.PUNISH_SERIOUS or + wood_ethic == df.ethic_response.PUNISH_EXILE or + wood_ethic == df.ethic_response.PUNISH_CAPITAL or + wood_ethic == df.ethic_response.UNTHINKABLE +end + +local function is_tree_lover_at_depot() for _,caravan in ipairs(df.global.plotinfo.caravans) do - local trade_state = caravan.trade_state - if caravan.time_remaining > 0 and - (trade_state == df.caravan_state.T_trade_state.Approaching or - trade_state == df.caravan_state.T_trade_state.AtDepot) and - caravan.sell_prices - then + if is_active_caravan(caravan) and is_tree_lover_caravan(caravan) then return true end end return false end -local function get_value_at_depot() - local sum = 0 - -- if we're here, then the overlay has already determined that this is a depot - local depot = dfhack.gui.getSelectedBuilding(true) - for _, contained_item in ipairs(depot.contained_items) do - if contained_item.use_mode ~= 0 then goto continue end - local item = contained_item.item - if item.flags.trader or not item.flags.in_building then goto continue end - sum = sum + common.get_perceived_value(item) - ::continue:: +local function has_export_agreement() + for _,caravan in ipairs(df.global.plotinfo.caravans) do + if caravan.sell_prices and is_active_caravan(caravan) then + return true + end end - return sum + return false end function MoveGoods:init() - self.value_at_depot = get_value_at_depot() self.value_pending = 0 self:addviews{ @@ -159,11 +169,24 @@ function MoveGoods:init() initial_option=true, on_change=function() self:refresh_list() end, }, + widgets.CycleHotkeyLabel{ + view_id='elf_safe', + frame={t=2, l=32, w=27}, + label='Elf-safe items:', + key='CUSTOM_SHIFT_G', + options={ + {label='Only', value='only'}, + {label='Show', value='show'}, + {label='Hide', value='hide'}, + }, + initial_option=is_tree_lover_at_depot() and 'only' or 'show', + on_change=function() self:refresh_list() end, + }, widgets.ToggleHotkeyLabel{ view_id='show_banned', frame={t=3, l=0, w=43}, label='Show items banned by export mandates', - key='CUSTOM_SHIFT_B', + key='CUSTOM_SHIFT_D', initial_option=false, on_change=function() self:refresh_list() end, }, @@ -187,9 +210,9 @@ function MoveGoods:init() key_back='CUSTOM_SHIFT_C', key='CUSTOM_SHIFT_V', options={ - {label='Tattered (XX)', value=3}, - {label='Frayed (X)', value=2}, - {label='Worn (x)', value=1}, + {label='XXTatteredXX', value=3}, + {label='XFrayedX', value=2}, + {label='xWornx', value=1}, {label='Pristine', value=0}, }, initial_option=3, @@ -208,9 +231,9 @@ function MoveGoods:init() key_back='CUSTOM_SHIFT_E', key='CUSTOM_SHIFT_R', options={ - {label='Tattered (XX)', value=3}, - {label='Frayed (X)', value=2}, - {label='Worn (x)', value=1}, + {label='XXTatteredXX', value=3}, + {label='XFrayedX', value=2}, + {label='xWornx', value=1}, {label='Pristine', value=0}, }, initial_option=0, @@ -247,11 +270,11 @@ function MoveGoods:init() key='CUSTOM_SHIFT_X', options={ {label='Ordinary', value=0}, - {label='Well Crafted', value=1}, - {label='Finely Crafted', value=2}, - {label='Superior', value=3}, - {label='Exceptional', value=4}, - {label='Masterful', value=5}, + {label='-Well Crafted-', value=1}, + {label='+Finely Crafted+', value=2}, + {label='*Superior*', value=3}, + {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, + {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, {label='Artifact', value=6}, }, initial_option=0, @@ -271,11 +294,11 @@ function MoveGoods:init() key='CUSTOM_SHIFT_W', options={ {label='Ordinary', value=0}, - {label='Well Crafted', value=1}, - {label='Finely Crafted', value=2}, - {label='Superior', value=3}, - {label='Exceptional', value=4}, - {label='Masterful', value=5}, + {label='-Well Crafted-', value=1}, + {label='+Finely Crafted+', value=2}, + {label='*Superior*', value=3}, + {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, + {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, {label='Artifact', value=6}, }, initial_option=6, @@ -301,7 +324,73 @@ function MoveGoods:init() }, }, widgets.Panel{ - frame={t=11, l=0, r=0, b=6}, + frame={t=11, l=0, w=40, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_value', + frame={l=0, t=0, w=18}, + label='Min value:', + label_below=true, + key_back='CUSTOM_SHIFT_B', + key='CUSTOM_SHIFT_N', + options={ + {label='1'..common.CH_MONEY, value={index=1, value=1}}, + {label='20'..common.CH_MONEY, value={index=2, value=20}}, + {label='50'..common.CH_MONEY, value={index=3, value=50}}, + {label='100'..common.CH_MONEY, value={index=4, value=100}}, + {label='500'..common.CH_MONEY, value={index=5, value=500}}, + {label='1000'..common.CH_MONEY, value={index=6, value=1000}}, + -- max "min" value is less than max "max" value since the range of inf - inf is not useful + {label='5000'..common.CH_MONEY, value={index=7, value=5000}}, + }, + initial_option=1, + on_change=function(val) + if self.subviews.max_value:getOptionValue().value < val.value then + self.subviews.max_value:setOption(val.index) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_value', + frame={r=1, t=0, w=18}, + label='Max value:', + label_below=true, + key_back='CUSTOM_SHIFT_T', + key='CUSTOM_SHIFT_Y', + options={ + {label='1'..common.CH_MONEY, value={index=1, value=1}}, + {label='20'..common.CH_MONEY, value={index=2, value=20}}, + {label='50'..common.CH_MONEY, value={index=3, value=50}}, + {label='100'..common.CH_MONEY, value={index=4, value=100}}, + {label='500'..common.CH_MONEY, value={index=5, value=500}}, + {label='1000'..common.CH_MONEY, value={index=6, value=1000}}, + {label='Max', value={index=7, value=math.huge}}, + }, + initial_option=7, + on_change=function(val) + if self.subviews.min_value:getOptionValue().value > val.value then + self.subviews.min_value:setOption(val.index) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=7, + get_left_idx_fn=function() + return self.subviews.min_value:getOptionValue().index + end, + get_right_idx_fn=function() + return self.subviews.max_value:getOptionValue().index + end, + on_left_change=function(idx) self.subviews.min_value:setOption(idx, true) end, + on_right_change=function(idx) self.subviews.max_value:setOption(idx, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=16, l=0, r=0, b=6}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', @@ -361,12 +450,9 @@ function MoveGoods:init() widgets.Label{ frame={l=0, b=4, h=1, r=0}, text={ - 'Value of items at trade depot/being brought to depot/total:', - {gap=1, text=common.obfuscate_value(self.value_at_depot)}, - '/', - {text=function() return common.obfuscate_value(self.value_pending) end}, - '/', - {text=function() return common.obfuscate_value(self.value_pending + self.value_at_depot) end} + 'Total value of trade items:', + {gap=1, + text=function() return common.obfuscate_value(self.value_pending) end}, }, }, widgets.HotkeyLabel{ @@ -476,6 +562,61 @@ local function scan_banned(item) return false end +local function is_wood_based(mat_type, mat_index) + if mat_type == df.builtin_mats.LYE or + mat_type == df.builtin_mats.GLASS_CLEAR or + mat_type == df.builtin_mats.GLASS_CRYSTAL or + (mat_type == df.builtin_mats.COAL and mat_index ~= 0) or + mat_type == df.builtin_mats.POTASH or + mat_type == df.builtin_mats.ASH or + mat_type == df.builtin_mats.PEARLASH + then + return true + end + + local mi = dfhack.matinfo.decode(mat_type, mat_index) + return mi and mi.material and (mi.material.flags.WOOD or mi.material.flags.SOAP) +end + +local function has_wood(item) + if item.flags2.grown then return false end + + if is_wood_based(item:getMaterial(), item:getMaterialIndex()) then + return true + end + + if item:hasImprovements() then + for _, imp in ipairs(item.improvements) do + if is_wood_based(imp.mat_type, imp.mat_index) then + return true + end + end + end + + return false +end + +local function can_trade_to_elves(item) + if item.flags.container then + -- ignore the safety of the container itself (unless the container is empty) + -- so items inside can still be traded + local has_items = false + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + has_items = true + if not contained_item:isAnimalProduct() and not has_wood(contained_item) then + return true + end + end + + if has_items then + -- no contained items are safe + return false + end + end + + return not item:isAnimalProduct() and not has_wood(item) +end + function MoveGoods:cache_choices(disable_buckets) if self.choices then return self.choices[disable_buckets] end @@ -521,6 +662,7 @@ function MoveGoods:cache_choices(disable_buckets) has_forbidden=is_forbidden, has_banned=is_banned, has_requested=is_requested, + elf_safe=can_trade_to_elves(item), dirty=false, } local entry = { @@ -561,12 +703,19 @@ function MoveGoods:get_choices() local include_forbidden = self.subviews.show_forbidden:getOptionValue() local include_banned = self.subviews.show_banned:getOptionValue() local only_agreement = self.subviews.only_agreement:getOptionValue() + local elf_safe = self.subviews.elf_safe:getOptionValue() local min_condition = self.subviews.min_condition:getOptionValue() local max_condition = self.subviews.max_condition:getOptionValue() local min_quality = self.subviews.min_quality:getOptionValue() local max_quality = self.subviews.max_quality:getOptionValue() + local min_value = self.subviews.min_value:getOptionValue().value + local max_value = self.subviews.max_value:getOptionValue().value for _,choice in ipairs(raw_choices) do local data = choice.data + if elf_safe ~= 'show' then + if elf_safe == 'hide' and data.elf_safe then goto continue end + if elf_safe == 'only' and not data.elf_safe then goto continue end + end if not include_forbidden then if choice.item_id then if data.items[choice.item_id].item.flags.forbid then @@ -580,6 +729,8 @@ function MoveGoods:get_choices() if max_condition > data.wear then goto continue end if min_quality > data.quality then goto continue end if max_quality < data.quality then goto continue end + if min_value > data.per_item_value then goto continue end + if max_value < data.per_item_value then goto continue end if only_agreement then if choice.item_id then if not data.items[choice.item_id].requested then From f9cb86bec18908339a315ea1db6813a10d975cb5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 4 Jul 2023 15:40:33 -0700 Subject: [PATCH 353/732] classify non-bin containers properly for elf safety --- internal/caravan/movegoods.lua | 37 +++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 00360f0e6a..73b56b7145 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -175,9 +175,9 @@ function MoveGoods:init() label='Elf-safe items:', key='CUSTOM_SHIFT_G', options={ - {label='Only', value='only'}, + {label='Only', value='only', pen=COLOR_GREEN}, {label='Show', value='show'}, - {label='Hide', value='hide'}, + {label='Hide', value='hide', pen=COLOR_RED}, }, initial_option=is_tree_lover_at_depot() and 'only' or 'show', on_change=function() self:refresh_list() end, @@ -598,19 +598,28 @@ end local function can_trade_to_elves(item) if item.flags.container then - -- ignore the safety of the container itself (unless the container is empty) - -- so items inside can still be traded - local has_items = false - for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do - has_items = true - if not contained_item:isAnimalProduct() and not has_wood(contained_item) then - return true + local contained_items = dfhack.items.getContainedItems(item) + if df.item_binst:is_instance(item) then + -- ignore the safety of the bin itself (unless the bin is empty) + -- so items inside can still be traded + local has_items = false + for _, contained_item in ipairs(contained_items) do + has_items = true + if not contained_item:isAnimalProduct() and not has_wood(contained_item) then + return true + end + end + if has_items then + -- no contained items are safe + return false + end + else + -- for other types of containers, any contamination makes it untradeable + for _, contained_item in ipairs(contained_items) do + if contained_item:isAnimalProduct() or has_wood(contained_item) then + return false + end end - end - - if has_items then - -- no contained items are safe - return false end end From 45d926d9ecdf298d0e15dc2aa7923763472b2167 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 5 Jul 2023 19:07:44 -0700 Subject: [PATCH 354/732] implement better mandate, risk, and ethics checks and reorganize ui --- internal/caravan/movegoods.lua | 372 +++++++++++++++++++++++++++------ 1 file changed, 303 insertions(+), 69 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 73b56b7145..6e5b9c5b7c 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -1,6 +1,7 @@ --@ module = true local common = reqscript('internal/caravan/common') +local dialogs = require('gui.dialogs') local gui = require('gui') local overlay = require('plugins.overlay') local utils = require('utils') @@ -13,9 +14,9 @@ local widgets = require('gui.widgets') MoveGoods = defclass(MoveGoods, widgets.Window) MoveGoods.ATTRS { frame_title='Select trade goods', - frame={w=83, h=45}, + frame={w=84, h=45}, resizable=true, - resize_min={h=27}, + resize_min={h=35}, pending_item_ids=DEFAULT_NIL, } @@ -63,8 +64,8 @@ local function sort_by_value_asc(a, b) end local function sort_by_status_desc(a, b) - local a_unselected = a.data.selected == 0 or (a.item_id and not a.items[a.item_id].pending) - local b_unselected = b.data.selected == 0 or (b.item_id and not b.items[b.item_id].pending) + local a_unselected = a.data.selected == 0 or (a.item_id and not a.data.items[a.item_id].pending) + local b_unselected = b.data.selected == 0 or (b.item_id and not b.data.items[b.item_id].pending) if a_unselected == b_unselected then return sort_by_value_desc(a, b) end @@ -72,8 +73,8 @@ local function sort_by_status_desc(a, b) end local function sort_by_status_asc(a, b) - local a_unselected = a.data.selected == 0 or (a.item_id and not a.items[a.item_id].pending) - local b_unselected = b.data.selected == 0 or (b.item_id and not b.items[b.item_id].pending) + local a_unselected = a.data.selected == 0 or (a.item_id and not a.data.items[a.item_id].pending) + local b_unselected = b.data.selected == 0 or (b.item_id and not b.data.items[b.item_id].pending) if a_unselected == b_unselected then return sort_by_value_desc(a, b) end @@ -115,27 +116,164 @@ local function is_tree_lover_caravan(caravan) wood_ethic == df.ethic_response.UNTHINKABLE end -local function is_tree_lover_at_depot() +local function is_animal_lover_caravan(caravan) + local caravan_he = df.historical_entity.find(caravan.entity); + if not caravan_he then return false end + local animal_ethic = caravan_he.entity_raw.ethic[df.ethic_type.KILL_ANIMAL] + return animal_ethic == df.ethic_response.JUSTIFIED_IF_SELF_DEFENSE or + animal_ethic == df.ethic_response.JUSTIFIED_IF_EXTREME_REASON or + animal_ethic == df.ethic_response.MISGUIDED or + animal_ethic == df.ethic_response.SHUN or + animal_ethic == df.ethic_response.APPALLING or + animal_ethic == df.ethic_response.PUNISH_REPRIMAND or + animal_ethic == df.ethic_response.PUNISH_SERIOUS or + animal_ethic == df.ethic_response.PUNISH_EXILE or + animal_ethic == df.ethic_response.PUNISH_CAPITAL or + animal_ethic == df.ethic_response.UNTHINKABLE +end + +local function get_ethics_restrictions() + local animal_ethics, wood_ethics = false, false for _,caravan in ipairs(df.global.plotinfo.caravans) do - if is_active_caravan(caravan) and is_tree_lover_caravan(caravan) then - return true + if is_active_caravan(caravan) then + animal_ethics = animal_ethics or is_animal_lover_caravan(caravan) + wood_ethics = wood_ethics or is_tree_lover_caravan(caravan) end end - return false + return animal_ethics, wood_ethics +end + +local function get_ethics_token(animal_ethics, wood_ethics) + local restrictions = {} + if animal_ethics or wood_ethics then + if animal_ethics then table.insert(restrictions, "Animals") end + if wood_ethics then table.insert(restrictions, "Trees") end + end + return { + gap=2, + text=#restrictions == 0 and 'None' or table.concat(restrictions, ', '), + pen=#restrictions ~= 0 and COLOR_LIGHTRED or COLOR_GREY, + } +end + +-- works for both mandates and unit preferences +-- adds spec to registry, but only if not in filter +local function register_item_type(registry, spec, filter) + if not safe_index(filter, spec.item_type, spec.item_subtype) then + ensure_keys(registry, spec.item_type)[spec.item_subtype] = true + end end -local function has_export_agreement() +local function get_banned_items() + local banned_items = {} + for _, mandate in ipairs(df.global.world.mandates) do + if mandate.mode == df.mandate.T_mode.Export then + register_item_type(banned_items, mandate) + end + end + return banned_items +end + +local function analyze_noble(unit, risky_items, banned_items) + for _, preference in ipairs(unit.status.current_soul.preferences) do + if preference.type == df.unit_preference.T_type.LikeItem and + preference.active + then + register_item_type(risky_items, preference, banned_items) + end + end +end + +local function get_mandate_noble_roles() + local roles = {} + for _, link in ipairs(df.global.world.world_data.active_site[0].entity_links) do + local he = df.historical_entity.find(link.entity_id); + if not he or + (he.type ~= df.historical_entity_type.SiteGovernment and + he.type ~= df.historical_entity_type.Civilization) + then + goto continue + end + for _, position in ipairs(he.positions.own) do + if position.mandate_max > 0 then + table.insert(roles, position.code) + end + end + ::continue:: + end + return roles +end + +local function get_risky_items(banned_items) + local risky_items = {} + for _, role in ipairs(get_mandate_noble_roles()) do + for _, unit in ipairs(dfhack.units.getUnitsByNobleRole(role)) do + analyze_noble(unit, risky_items, banned_items) + end + end + return risky_items +end + +local function make_item_description(item_type, subtype) + -- TODO: get a subtype-specific string + local str = string.lower(df.item_type[item_type]) + -- if subtype ~= -1 then + -- str = str .. (' (%d)'):format(subtype) + -- end + return str +end + +local function get_banned_token(banned_items) + if not next(banned_items) then + return { + gap=2, + text='None', + pen=COLOR_GREY, + } + end + local strs = {} + for item_type, subtypes in pairs(banned_items) do + for subtype in pairs(subtypes) do + table.insert(strs, make_item_description(item_type, subtype)) + end + end + return { + gap=2, + text=table.concat(strs, ', '), + pen=COLOR_LIGHTRED, + } +end + +local function get_export_agreements() + local export_agreements = {} for _,caravan in ipairs(df.global.plotinfo.caravans) do - if caravan.sell_prices and is_active_caravan(caravan) then - return true + if caravan.buy_prices and is_active_caravan(caravan) then + table.insert(export_agreements, caravan.buy_prices) end end - return false + return export_agreements +end + +local function show_export_agreements(export_agreements) + local strs = {} + for _, agreement in ipairs(export_agreements) do + for idx, price in ipairs(agreement.price) do + local desc = make_item_description(agreement.items.item_type[idx], agreement.items.item_subtype[idx]) + local percent = (price * 100) // 256 + table.insert(strs, ('%20s %d%%'):format(desc..':', percent)) + end + end + dialogs.showMessage('Price agreement for exported items', table.concat(strs, '\n')) end function MoveGoods:init() self.value_pending = 0 + local export_agreements = get_export_agreements() + local animal_ethics, wood_ethics = get_ethics_restrictions() + local banned_items = get_banned_items() + self.risky_items = get_risky_items(banned_items) + self:addviews{ widgets.CycleHotkeyLabel{ view_id='sort', @@ -162,45 +300,106 @@ function MoveGoods:init() on_char=function(ch) return ch:match('[%l -]') end, }, widgets.ToggleHotkeyLabel{ - view_id='show_forbidden', + view_id='hide_forbidden', frame={t=2, l=0, w=27}, - label='Show forbidden items', + label='Hide forbidden items:', key='CUSTOM_SHIFT_F', - initial_option=true, - on_change=function() self:refresh_list() end, - }, - widgets.CycleHotkeyLabel{ - view_id='elf_safe', - frame={t=2, l=32, w=27}, - label='Elf-safe items:', - key='CUSTOM_SHIFT_G', options={ - {label='Only', value='only', pen=COLOR_GREEN}, - {label='Show', value='show'}, - {label='Hide', value='hide', pen=COLOR_RED}, + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} }, - initial_option=is_tree_lover_at_depot() and 'only' or 'show', + initial_option=false, on_change=function() self:refresh_list() end, }, - widgets.ToggleHotkeyLabel{ - view_id='show_banned', - frame={t=3, l=0, w=43}, - label='Show items banned by export mandates', - key='CUSTOM_SHIFT_D', - initial_option=false, - on_change=function() self:refresh_list() end, + widgets.Panel{ + frame={t=4, l=0, w=41, h=2}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Merchant export agreements:', + {gap=1, text='None', pen=COLOR_GREY}, + }, + }, + widgets.HotkeyLabel{ + frame={t=0, l=28}, + key='CUSTOM_SHIFT_H', + label='[details]', + text_pen=COLOR_LIGHTRED, + on_activate=function() show_export_agreements(export_agreements) end, + visible=#export_agreements > 0, + }, + widgets.ToggleHotkeyLabel{ + view_id='only_agreement', + frame={t=1, l=0}, + label='Show only requested items:', + key='CUSTOM_SHIFT_A', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=false, + on_change=function() self:refresh_list() end, + visible=#export_agreements > 0, + }, + }, }, - widgets.ToggleHotkeyLabel{ - view_id='only_agreement', - frame={t=4, l=0, w=52}, - label='Show only items requested by export agreement', - key='CUSTOM_SHIFT_A', - initial_option=false, - on_change=function() self:refresh_list() end, - enabled=has_export_agreement(), + widgets.Panel{ + frame={t=7, l=0, r=40, h=3}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Merchant ethical restrictions:', NEWLINE, + get_ethics_token(animal_ethics, wood_ethics), + }, + }, + widgets.CycleHotkeyLabel{ + view_id='ethical', + frame={t=2, l=0}, + key='CUSTOM_SHIFT_G', + options={ + {label='Show only ethically acceptable items', value='only', pen=COLOR_GREEN}, + {label='Ignore ethical restrictions', value='show'}, + {label='Show only ethically unacceptable items', value='hide', pen=COLOR_RED}, + }, + initial_option='only', + option_gap=0, + visible=animal_ethics or wood_ethics, + on_change=function() self:refresh_list() end, + }, + }, }, widgets.Panel{ - frame={t=6, l=0, w=40, h=4}, + frame={t=11, l=0, r=40, h=5}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Items banned by export mandates:', NEWLINE, + get_banned_token(banned_items), NEWLINE, + 'Additional items at risk of mandates:', NEWLINE, + get_banned_token(self.risky_items), + }, + }, + widgets.CycleHotkeyLabel{ + view_id='banned', + frame={t=4, l=0}, + key='CUSTOM_SHIFT_D', + options={ + {label='Hide banned and risky items', value='both', pen=COLOR_GREEN}, + {label='Hide banned items', value='banned_only', pen=COLOR_YELLOW}, + {label='Ignore mandate restrictions', value='ignore', pen=COLOR_RED}, + }, + initial_option='both', + option_gap=0, + visible=next(banned_items) or next(self.risky_items), + on_change=function() self:refresh_list() end, + }, + }, + }, + widgets.Panel{ + frame={t=2, r=0, w=38, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_condition', @@ -259,7 +458,7 @@ function MoveGoods:init() }, }, widgets.Panel{ - frame={t=6, l=41, w=38, h=4}, + frame={t=7, r=0, w=38, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_quality', @@ -324,7 +523,7 @@ function MoveGoods:init() }, }, widgets.Panel{ - frame={t=11, l=0, w=40, h=4}, + frame={t=12, r=0, w=38, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_value', @@ -390,7 +589,7 @@ function MoveGoods:init() }, }, widgets.Panel{ - frame={t=16, l=0, r=0, b=6}, + frame={t=17, l=0, r=0, b=6}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', @@ -465,8 +664,12 @@ function MoveGoods:init() widgets.ToggleHotkeyLabel{ view_id='disable_buckets', frame={l=26, b=2}, - label='Show individual items', + label='Show individual items:', key='CUSTOM_CTRL_I', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, initial_option=false, on_change=function() self:refresh_list() end, }, @@ -533,7 +736,8 @@ local function is_tradeable_item(item, depot) if item == contained_item.item then return false end end end - return true + return dfhack.maps.canWalkBetween(xyz2pos(dfhack.items.getPosition(item)), + xyz2pos(depot.centerx, depot.centery, depot.z)) end local function get_entry_icon(data, item_id) @@ -553,20 +757,33 @@ local function make_choice_text(desc, value, quantity) } end --- returns true if the item or any contained item is banned -local function scan_banned(item) - if not dfhack.items.checkMandates(item) then return true end - for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do - if not dfhack.items.checkMandates(contained_item) then return true end +local function match_risky(item, risky_items) + for item_type, subtypes in pairs(risky_items) do + for subtype in pairs(subtypes) do + if item_type == item:getType() and (subtype == -1 or subtype == item:getSubtype()) then + return true + end + end end return false end +-- returns is_banned, is_risky +local function scan_banned(item, risky_items) + if not dfhack.items.checkMandates(item) then return true, true end + if match_risky(item, risky_items) then return false, true end + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + if not dfhack.items.checkMandates(contained_item) then return true, true end + if match_risky(contained_item, risky_items) then return false, true end + end + return false, false +end + local function is_wood_based(mat_type, mat_index) if mat_type == df.builtin_mats.LYE or mat_type == df.builtin_mats.GLASS_CLEAR or mat_type == df.builtin_mats.GLASS_CRYSTAL or - (mat_type == df.builtin_mats.COAL and mat_index ~= 0) or + (mat_type == df.builtin_mats.COAL and mat_index == 1) or mat_type == df.builtin_mats.POTASH or mat_type == df.builtin_mats.ASH or mat_type == df.builtin_mats.PEARLASH @@ -575,7 +792,10 @@ local function is_wood_based(mat_type, mat_index) end local mi = dfhack.matinfo.decode(mat_type, mat_index) - return mi and mi.material and (mi.material.flags.WOOD or mi.material.flags.SOAP) + return mi and mi.material and + (mi.material.flags.WOOD or + mi.material.flags.STRUCTURAL_PLANT_MAT or + mi.material.flags.SOAP) end local function has_wood(item) @@ -596,7 +816,7 @@ local function has_wood(item) return false end -local function can_trade_to_elves(item) +local function is_ethical_product(item, fn) if item.flags.container then local contained_items = dfhack.items.getContainedItems(item) if df.item_binst:is_instance(item) then @@ -626,9 +846,19 @@ local function can_trade_to_elves(item) return not item:isAnimalProduct() and not has_wood(item) end +local function is_ethical_animal_product(item) + return is_ethical_product(item, function(it) return it:isAnimalProduct() end) +end + +local function is_ethical_wood_product(item) + return is_ethical_product(item, function(it) return has_wood(it) end) +end + function MoveGoods:cache_choices(disable_buckets) if self.choices then return self.choices[disable_buckets] end + local animal_ethics, wood_ethics = get_ethics_restrictions() + local depot = dfhack.gui.getSelectedBuilding(true) local pending = self.pending_item_ids local buckets = {} @@ -639,7 +869,7 @@ function MoveGoods:cache_choices(disable_buckets) if value <= 0 then goto continue end local is_pending = not not pending[item_id] or item.flags.in_building local is_forbidden = item.flags.forbid - local is_banned = scan_banned(item) + local is_banned, is_risky = scan_banned(item, self.risky_items) local is_requested = dfhack.items.isRequestedTradeGood(item) local wear_level = item:getWear() local desc = item.flags.artifact and common.get_artifact_name(item) or @@ -656,12 +886,15 @@ function MoveGoods:cache_choices(disable_buckets) bucket.data.selected = bucket.data.selected + (is_pending and 1 or 0) bucket.data.has_forbidden = bucket.data.has_forbidden or is_forbidden bucket.data.has_banned = bucket.data.has_banned or is_banned + bucket.data.has_risky = bucket.data.has_risky or is_risky bucket.data.has_requested = bucket.data.has_requested or is_requested else + local is_ethical = (not animal_ethics or is_ethical_animal_product(item)) and + (not wood_ethics or is_ethical_wood_product(item)) local data = { desc=desc, per_item_value=value, - items={[item_id]={item=item, pending=is_pending, banned=is_banned, requested=is_requested}}, + items={[item_id]={item=item, pending=is_pending, banned=is_banned, risky=is_risky, requested=is_requested}}, item_type=item:getType(), item_subtype=item:getSubtype(), quantity=1, @@ -670,8 +903,9 @@ function MoveGoods:cache_choices(disable_buckets) selected=is_pending and 1 or 0, has_forbidden=is_forbidden, has_banned=is_banned, + has_risky=is_risky, has_requested=is_requested, - elf_safe=can_trade_to_elves(item), + ethical=is_ethical, dirty=false, } local entry = { @@ -709,10 +943,10 @@ end function MoveGoods:get_choices() local raw_choices = self:cache_choices(self.subviews.disable_buckets:getOptionValue()) local choices = {} - local include_forbidden = self.subviews.show_forbidden:getOptionValue() - local include_banned = self.subviews.show_banned:getOptionValue() + local include_forbidden = not self.subviews.hide_forbidden:getOptionValue() + local banned = self.subviews.banned:getOptionValue() local only_agreement = self.subviews.only_agreement:getOptionValue() - local elf_safe = self.subviews.elf_safe:getOptionValue() + local ethical = self.subviews.ethical:getOptionValue() local min_condition = self.subviews.min_condition:getOptionValue() local max_condition = self.subviews.max_condition:getOptionValue() local min_quality = self.subviews.min_quality:getOptionValue() @@ -721,9 +955,9 @@ function MoveGoods:get_choices() local max_value = self.subviews.max_value:getOptionValue().value for _,choice in ipairs(raw_choices) do local data = choice.data - if elf_safe ~= 'show' then - if elf_safe == 'hide' and data.elf_safe then goto continue end - if elf_safe == 'only' and not data.elf_safe then goto continue end + if ethical ~= 'show' then + if ethical == 'hide' and data.ethical then goto continue end + if ethical == 'only' and not data.ethical then goto continue end end if not include_forbidden then if choice.item_id then @@ -749,12 +983,12 @@ function MoveGoods:get_choices() goto continue end end - if not include_banned then + if banned ~= 'ignore' then if choice.item_id then - if data.items[choice.item_id].banned then + if data.items[choice.item_id].banned or (banned ~= 'banned_only' and data.items[choice.item_id].risky) then goto continue end - elseif data.has_banned then + elseif data.has_banned or (banned ~= 'banned_only' and data.has_risky) then goto continue end end From 7ae8f0958d917a69dde69a1e8c7b55c294a81e92 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 5 Jul 2023 19:29:37 -0700 Subject: [PATCH 355/732] use itemdef-based item descriptions --- internal/caravan/movegoods.lua | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 6e5b9c5b7c..1bc9e55d35 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -16,7 +16,7 @@ MoveGoods.ATTRS { frame_title='Select trade goods', frame={w=84, h=45}, resizable=true, - resize_min={h=35}, + resize_min={w=81,h=35}, pending_item_ids=DEFAULT_NIL, } @@ -215,12 +215,9 @@ local function get_risky_items(banned_items) end local function make_item_description(item_type, subtype) - -- TODO: get a subtype-specific string - local str = string.lower(df.item_type[item_type]) - -- if subtype ~= -1 then - -- str = str .. (' (%d)'):format(subtype) - -- end - return str + local itemdef = dfhack.items.getSubtypeDef(item_type, subtype) + return itemdef and string.lower(itemdef.name_plural) or + string.lower(df.item_type[item_type]):gsub('_', ' ') end local function get_banned_token(banned_items) From 43afc5c2ce2a2c7ff6d0dc652312ae12e446aedb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 5 Jul 2023 23:32:43 -0700 Subject: [PATCH 356/732] rearrange ui for better resize behavior --- internal/caravan/movegoods.lua | 204 ++++++++++++++++----------------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 1bc9e55d35..954cbab7ce 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -296,107 +296,8 @@ function MoveGoods:init() label_text='Search: ', on_char=function(ch) return ch:match('[%l -]') end, }, - widgets.ToggleHotkeyLabel{ - view_id='hide_forbidden', - frame={t=2, l=0, w=27}, - label='Hide forbidden items:', - key='CUSTOM_SHIFT_F', - options={ - {label='Yes', value=true, pen=COLOR_GREEN}, - {label='No', value=false} - }, - initial_option=false, - on_change=function() self:refresh_list() end, - }, - widgets.Panel{ - frame={t=4, l=0, w=41, h=2}, - subviews={ - widgets.Label{ - frame={t=0, l=0}, - text={ - 'Merchant export agreements:', - {gap=1, text='None', pen=COLOR_GREY}, - }, - }, - widgets.HotkeyLabel{ - frame={t=0, l=28}, - key='CUSTOM_SHIFT_H', - label='[details]', - text_pen=COLOR_LIGHTRED, - on_activate=function() show_export_agreements(export_agreements) end, - visible=#export_agreements > 0, - }, - widgets.ToggleHotkeyLabel{ - view_id='only_agreement', - frame={t=1, l=0}, - label='Show only requested items:', - key='CUSTOM_SHIFT_A', - options={ - {label='Yes', value=true, pen=COLOR_GREEN}, - {label='No', value=false} - }, - initial_option=false, - on_change=function() self:refresh_list() end, - visible=#export_agreements > 0, - }, - }, - }, - widgets.Panel{ - frame={t=7, l=0, r=40, h=3}, - subviews={ - widgets.Label{ - frame={t=0, l=0}, - text={ - 'Merchant ethical restrictions:', NEWLINE, - get_ethics_token(animal_ethics, wood_ethics), - }, - }, - widgets.CycleHotkeyLabel{ - view_id='ethical', - frame={t=2, l=0}, - key='CUSTOM_SHIFT_G', - options={ - {label='Show only ethically acceptable items', value='only', pen=COLOR_GREEN}, - {label='Ignore ethical restrictions', value='show'}, - {label='Show only ethically unacceptable items', value='hide', pen=COLOR_RED}, - }, - initial_option='only', - option_gap=0, - visible=animal_ethics or wood_ethics, - on_change=function() self:refresh_list() end, - }, - }, - }, - widgets.Panel{ - frame={t=11, l=0, r=40, h=5}, - subviews={ - widgets.Label{ - frame={t=0, l=0}, - text={ - 'Items banned by export mandates:', NEWLINE, - get_banned_token(banned_items), NEWLINE, - 'Additional items at risk of mandates:', NEWLINE, - get_banned_token(self.risky_items), - }, - }, - widgets.CycleHotkeyLabel{ - view_id='banned', - frame={t=4, l=0}, - key='CUSTOM_SHIFT_D', - options={ - {label='Hide banned and risky items', value='both', pen=COLOR_GREEN}, - {label='Hide banned items', value='banned_only', pen=COLOR_YELLOW}, - {label='Ignore mandate restrictions', value='ignore', pen=COLOR_RED}, - }, - initial_option='both', - option_gap=0, - visible=next(banned_items) or next(self.risky_items), - on_change=function() self:refresh_list() end, - }, - }, - }, widgets.Panel{ - frame={t=2, r=0, w=38, h=4}, + frame={t=2, l=0, w=38, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_condition', @@ -455,7 +356,7 @@ function MoveGoods:init() }, }, widgets.Panel{ - frame={t=7, r=0, w=38, h=4}, + frame={t=7, l=0, w=38, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_quality', @@ -520,7 +421,7 @@ function MoveGoods:init() }, }, widgets.Panel{ - frame={t=12, r=0, w=38, h=4}, + frame={t=12, l=0, w=38, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_value', @@ -585,6 +486,105 @@ function MoveGoods:init() }, }, }, + widgets.ToggleHotkeyLabel{ + view_id='hide_forbidden', + frame={t=2, l=40, w=27}, + label='Hide forbidden items:', + key='CUSTOM_SHIFT_F', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=false, + on_change=function() self:refresh_list() end, + }, + widgets.Panel{ + frame={t=4, l=40, r=0, h=2}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Merchant export agreements:', + {gap=1, text='None', pen=COLOR_GREY}, + }, + }, + widgets.HotkeyLabel{ + frame={t=0, l=28}, + key='CUSTOM_SHIFT_H', + label='[details]', + text_pen=COLOR_LIGHTRED, + on_activate=function() show_export_agreements(export_agreements) end, + visible=#export_agreements > 0, + }, + widgets.ToggleHotkeyLabel{ + view_id='only_agreement', + frame={t=1, l=0}, + label='Show only requested items:', + key='CUSTOM_SHIFT_A', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=false, + on_change=function() self:refresh_list() end, + visible=#export_agreements > 0, + }, + }, + }, + widgets.Panel{ + frame={t=7, l=40, r=0, h=3}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Merchant ethical restrictions:', NEWLINE, + get_ethics_token(animal_ethics, wood_ethics), + }, + }, + widgets.CycleHotkeyLabel{ + view_id='ethical', + frame={t=2, l=0}, + key='CUSTOM_SHIFT_G', + options={ + {label='Show only ethically acceptable items', value='only', pen=COLOR_GREEN}, + {label='Ignore ethical restrictions', value='show'}, + {label='Show only ethically unacceptable items', value='hide', pen=COLOR_RED}, + }, + initial_option='only', + option_gap=0, + visible=animal_ethics or wood_ethics, + on_change=function() self:refresh_list() end, + }, + }, + }, + widgets.Panel{ + frame={t=11, l=40, r=0, h=5}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Items banned by export mandates:', NEWLINE, + get_banned_token(banned_items), NEWLINE, + 'Additional items at risk of mandates:', NEWLINE, + get_banned_token(self.risky_items), + }, + }, + widgets.CycleHotkeyLabel{ + view_id='banned', + frame={t=4, l=0}, + key='CUSTOM_SHIFT_D', + options={ + {label='Hide banned and risky items', value='both', pen=COLOR_GREEN}, + {label='Hide banned items', value='banned_only', pen=COLOR_YELLOW}, + {label='Ignore mandate restrictions', value='ignore', pen=COLOR_RED}, + }, + initial_option='both', + option_gap=0, + visible=next(banned_items) or next(self.risky_items), + on_change=function() self:refresh_list() end, + }, + }, + }, widgets.Panel{ frame={t=17, l=0, r=0, b=6}, subviews={ From ded755d0a72eb73561cc028b5fa362ab4f78733f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 6 Jul 2023 00:04:24 -0700 Subject: [PATCH 357/732] indicate which items are already at the depot --- internal/caravan/movegoods.lua | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 954cbab7ce..2fd29ac8bd 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -21,7 +21,7 @@ MoveGoods.ATTRS { } local STATUS_COL_WIDTH = 7 -local VALUE_COL_WIDTH = 8 +local VALUE_COL_WIDTH = 6 local QTY_COL_WIDTH = 5 local function sort_noop(a, b) @@ -216,7 +216,7 @@ end local function make_item_description(item_type, subtype) local itemdef = dfhack.items.getSubtypeDef(item_type, subtype) - return itemdef and string.lower(itemdef.name_plural) or + return itemdef and string.lower(itemdef.name) or string.lower(df.item_type[item_type]):gsub('_', ' ') end @@ -590,7 +590,7 @@ function MoveGoods:init() subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', - frame={l=0, t=0, w=7}, + frame={t=0, l=STATUS_COL_WIDTH+1-7, w=7}, options={ {label='status', value=sort_noop}, {label='status'..common.CH_DN, value=sort_by_status_desc}, @@ -602,7 +602,7 @@ function MoveGoods:init() }, widgets.CycleHotkeyLabel{ view_id='sort_value', - frame={l=STATUS_COL_WIDTH+2, t=0, w=6}, + frame={t=0, l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+1-6, w=6}, options={ {label='value', value=sort_noop}, {label='value'..common.CH_DN, value=sort_by_value_desc}, @@ -613,7 +613,7 @@ function MoveGoods:init() }, widgets.CycleHotkeyLabel{ view_id='sort_quantity', - frame={l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+2, t=0, w=4}, + frame={t=0, l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+1-4, w=4}, options={ {label='qty', value=sort_noop}, {label='qty'..common.CH_DN, value=sort_by_quantity_desc}, @@ -624,7 +624,7 @@ function MoveGoods:init() }, widgets.CycleHotkeyLabel{ view_id='sort_name', - frame={l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+2, t=0, w=5}, + frame={t=0, l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+2+QTY_COL_WIDTH+2, w=5}, options={ {label='name', value=sort_noop}, {label='name'..common.CH_DN, value=sort_by_name_desc}, @@ -746,11 +746,12 @@ local function get_entry_icon(data, item_id) return common.SOME_PEN end -local function make_choice_text(desc, value, quantity) +local function make_choice_text(at_depot, value, quantity, desc) return { - {width=STATUS_COL_WIDTH+VALUE_COL_WIDTH-3, rjustify=true, text=common.obfuscate_value(value)}, - {gap=3, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, - {gap=4, text=desc}, + {width=STATUS_COL_WIDTH-2, text=at_depot and 'depot' or ''}, + {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value)}, + {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, + {gap=2, text=desc}, } end @@ -881,6 +882,7 @@ function MoveGoods:cache_choices(disable_buckets) bucket.data.items[item_id] = {item=item, pending=is_pending, banned=is_banned, requested=is_requested} bucket.data.quantity = bucket.data.quantity + 1 bucket.data.selected = bucket.data.selected + (is_pending and 1 or 0) + bucket.data.num_at_depot = bucket.data.num_at_depot + (item.flags.in_building and 1 or 0) bucket.data.has_forbidden = bucket.data.has_forbidden or is_forbidden bucket.data.has_banned = bucket.data.has_banned or is_banned bucket.data.has_risky = bucket.data.has_risky or is_risky @@ -898,6 +900,7 @@ function MoveGoods:cache_choices(disable_buckets) quality=item.flags.artifact and 6 or item:getQuality(), wear=wear_level, selected=is_pending and 1 or 0, + num_at_depot=item.flags.in_building and 1 or 0, has_forbidden=is_forbidden, has_banned=is_banned, has_risky=is_risky, @@ -918,15 +921,15 @@ function MoveGoods:cache_choices(disable_buckets) local bucket_choices, nobucket_choices = {}, {} for _, bucket in pairs(buckets) do local data = bucket.data - for item_id in pairs(data.items) do + for item_id, item_data in pairs(data.items) do local nobucket_choice = copyall(bucket) nobucket_choice.icon = curry(get_entry_icon, data, item_id) - nobucket_choice.text = make_choice_text(data.desc, data.per_item_value, 1) + nobucket_choice.text = make_choice_text(item_data.item.flags.in_building, data.per_item_value, 1, data.desc) nobucket_choice.item_id = item_id table.insert(nobucket_choices, nobucket_choice) end data.total_value = data.per_item_value * data.quantity - bucket.text = make_choice_text(data.desc, data.total_value, data.quantity) + bucket.text = make_choice_text(data.num_at_depot == data.quantity, data.total_value, data.quantity, data.desc) table.insert(bucket_choices, bucket) self.value_pending = self.value_pending + (data.per_item_value * data.selected) end From 4e21bccc6cc0992a65f098ce29b6c4147c6e9116 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 6 Jul 2023 03:29:23 -0700 Subject: [PATCH 358/732] refactor movegoods for sharing with trade screen --- internal/caravan/common.lua | 503 ++++++++++++++++++++++++++++++- internal/caravan/movegoods.lua | 527 ++------------------------------- internal/caravan/trade.lua | 268 +++++++++++++---- 3 files changed, 734 insertions(+), 564 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 2f012f4f27..d26d0b0039 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -1,5 +1,8 @@ --@ module = true +local dialogs = require('gui.dialogs') +local widgets = require('gui.widgets') + CH_UP = string.char(30) CH_DN = string.char(31) CH_MONEY = string.char(15) @@ -77,7 +80,7 @@ local function get_item_type_str(item) return str end -function get_artifact_name(item) +local function get_artifact_name(item) local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) if not gref then return end local artifact = df.artifact_record.find(gref.artifact_id) @@ -86,6 +89,17 @@ function get_artifact_name(item) return ('%s (%s)'):format(name, get_item_type_str(item)) end +function get_item_description(item) + local desc = item.flags.artifact and get_artifact_name(item) or + dfhack.items.getDescription(item, 0, true) + local wear_level = item:getWear() + if wear_level == 1 then desc = ('x%sx'):format(desc) + elseif wear_level == 2 then desc = ('X%sX'):format(desc) + elseif wear_level == 3 then desc = ('XX%sXX'):format(desc) + end + return desc +end + -- takes into account trade agreements function get_perceived_value(item, caravan_state, caravan_buying) local value = dfhack.items.getValue(item, caravan_state, caravan_buying) @@ -97,3 +111,490 @@ function get_perceived_value(item, caravan_state, caravan_buying) end return value end + +function get_slider_widgets(self, suffix) + suffix = suffix or '' + return { + widgets.Panel{ + frame={t=0, l=0, r=0, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_condition'..suffix, + frame={l=0, t=0, w=18}, + label='Min condition:', + label_below=true, + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options={ + {label='XXTatteredXX', value=3}, + {label='XFrayedX', value=2}, + {label='xWornx', value=1}, + {label='Pristine', value=0}, + }, + initial_option=3, + on_change=function(val) + if self.subviews['max_condition'..suffix]:getOptionValue() > val then + self.subviews['max_condition'..suffix]:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_condition'..suffix, + frame={r=1, t=0, w=18}, + label='Max condition:', + label_below=true, + key_back='CUSTOM_SHIFT_E', + key='CUSTOM_SHIFT_R', + options={ + {label='XXTatteredXX', value=3}, + {label='XFrayedX', value=2}, + {label='xWornx', value=1}, + {label='Pristine', value=0}, + }, + initial_option=0, + on_change=function(val) + if self.subviews['min_condition'..suffix]:getOptionValue() < val then + self.subviews['min_condition'..suffix]:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=4, + get_left_idx_fn=function() + return 4 - self.subviews['min_condition'..suffix]:getOptionValue() + end, + get_right_idx_fn=function() + return 4 - self.subviews['max_condition'..suffix]:getOptionValue() + end, + on_left_change=function(idx) self.subviews['min_condition'..suffix]:setOption(4-idx, true) end, + on_right_change=function(idx) self.subviews['max_condition'..suffix]:setOption(4-idx, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=5, l=0, r=0, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_quality'..suffix, + frame={l=0, t=0, w=18}, + label='Min quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_SHIFT_X', + options={ + {label='Ordinary', value=0}, + {label='-Well Crafted-', value=1}, + {label='+Finely Crafted+', value=2}, + {label='*Superior*', value=3}, + {label=CH_EXCEPTIONAL..'Exceptional'..CH_EXCEPTIONAL, value=4}, + {label=CH_MONEY..'Masterful'..CH_MONEY, value=5}, + {label='Artifact', value=6}, + }, + initial_option=0, + on_change=function(val) + if self.subviews['max_quality'..suffix]:getOptionValue() < val then + self.subviews['max_quality'..suffix]:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_quality'..suffix, + frame={r=1, t=0, w=18}, + label='Max quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Q', + key='CUSTOM_SHIFT_W', + options={ + {label='Ordinary', value=0}, + {label='-Well Crafted-', value=1}, + {label='+Finely Crafted+', value=2}, + {label='*Superior*', value=3}, + {label=CH_EXCEPTIONAL..'Exceptional'..CH_EXCEPTIONAL, value=4}, + {label=CH_MONEY..'Masterful'..CH_MONEY, value=5}, + {label='Artifact', value=6}, + }, + initial_option=6, + on_change=function(val) + if self.subviews['min_quality'..suffix]:getOptionValue() > val then + self.subviews['min_quality'..suffix]:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=7, + get_left_idx_fn=function() + return self.subviews['min_quality'..suffix]:getOptionValue() + 1 + end, + get_right_idx_fn=function() + return self.subviews['max_quality'..suffix]:getOptionValue() + 1 + end, + on_left_change=function(idx) self.subviews['min_quality'..suffix]:setOption(idx-1, true) end, + on_right_change=function(idx) self.subviews['max_quality'..suffix]:setOption(idx-1, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=10, l=0, r=0, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_value'..suffix, + frame={l=0, t=0, w=18}, + label='Min value:', + label_below=true, + key_back='CUSTOM_SHIFT_B', + key='CUSTOM_SHIFT_N', + options={ + {label='1'..CH_MONEY, value={index=1, value=1}}, + {label='20'..CH_MONEY, value={index=2, value=20}}, + {label='50'..CH_MONEY, value={index=3, value=50}}, + {label='100'..CH_MONEY, value={index=4, value=100}}, + {label='500'..CH_MONEY, value={index=5, value=500}}, + {label='1000'..CH_MONEY, value={index=6, value=1000}}, + -- max "min" value is less than max "max" value since the range of inf - inf is not useful + {label='5000'..CH_MONEY, value={index=7, value=5000}}, + }, + initial_option=1, + on_change=function(val) + if self.subviews['max_value'..suffix]:getOptionValue().value < val.value then + self.subviews['max_value'..suffix]:setOption(val.index) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_value'..suffix, + frame={r=1, t=0, w=18}, + label='Max value:', + label_below=true, + key_back='CUSTOM_SHIFT_T', + key='CUSTOM_SHIFT_Y', + options={ + {label='1'..CH_MONEY, value={index=1, value=1}}, + {label='20'..CH_MONEY, value={index=2, value=20}}, + {label='50'..CH_MONEY, value={index=3, value=50}}, + {label='100'..CH_MONEY, value={index=4, value=100}}, + {label='500'..CH_MONEY, value={index=5, value=500}}, + {label='1000'..CH_MONEY, value={index=6, value=1000}}, + {label='Max', value={index=7, value=math.huge}}, + }, + initial_option=7, + on_change=function(val) + if self.subviews['min_value'..suffix]:getOptionValue().value > val.value then + self.subviews['min_value'..suffix]:setOption(val.index) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=7, + get_left_idx_fn=function() + return self.subviews['min_value'..suffix]:getOptionValue().index + end, + get_right_idx_fn=function() + return self.subviews['max_value'..suffix]:getOptionValue().index + end, + on_left_change=function(idx) self.subviews['min_value'..suffix]:setOption(idx, true) end, + on_right_change=function(idx) self.subviews['max_value'..suffix]:setOption(idx, true) end, + }, + }, + }, + } +end + +function is_tree_lover_caravan(caravan) + local caravan_he = df.historical_entity.find(caravan.entity); + if not caravan_he then return false end + local wood_ethic = caravan_he.entity_raw.ethic[df.ethic_type.KILL_PLANT] + return wood_ethic == df.ethic_response.MISGUIDED or + wood_ethic == df.ethic_response.SHUN or + wood_ethic == df.ethic_response.APPALLING or + wood_ethic == df.ethic_response.PUNISH_REPRIMAND or + wood_ethic == df.ethic_response.PUNISH_SERIOUS or + wood_ethic == df.ethic_response.PUNISH_EXILE or + wood_ethic == df.ethic_response.PUNISH_CAPITAL or + wood_ethic == df.ethic_response.UNTHINKABLE +end + +function is_animal_lover_caravan(caravan) + local caravan_he = df.historical_entity.find(caravan.entity); + if not caravan_he then return false end + local animal_ethic = caravan_he.entity_raw.ethic[df.ethic_type.KILL_ANIMAL] + return animal_ethic == df.ethic_response.JUSTIFIED_IF_SELF_DEFENSE or + animal_ethic == df.ethic_response.JUSTIFIED_IF_EXTREME_REASON or + animal_ethic == df.ethic_response.MISGUIDED or + animal_ethic == df.ethic_response.SHUN or + animal_ethic == df.ethic_response.APPALLING or + animal_ethic == df.ethic_response.PUNISH_REPRIMAND or + animal_ethic == df.ethic_response.PUNISH_SERIOUS or + animal_ethic == df.ethic_response.PUNISH_EXILE or + animal_ethic == df.ethic_response.PUNISH_CAPITAL or + animal_ethic == df.ethic_response.UNTHINKABLE +end + +-- works for both mandates and unit preferences +-- adds spec to registry, but only if not in filter +local function register_item_type(registry, spec, filter) + if not safe_index(filter, spec.item_type, spec.item_subtype) then + ensure_keys(registry, spec.item_type)[spec.item_subtype] = true + end +end + +function get_banned_items() + local banned_items = {} + for _, mandate in ipairs(df.global.world.mandates) do + if mandate.mode == df.mandate.T_mode.Export then + register_item_type(banned_items, mandate) + end + end + return banned_items +end + +local function analyze_noble(unit, risky_items, banned_items) + for _, preference in ipairs(unit.status.current_soul.preferences) do + if preference.type == df.unit_preference.T_type.LikeItem and + preference.active + then + register_item_type(risky_items, preference, banned_items) + end + end +end + +local function get_mandate_noble_roles() + local roles = {} + for _, link in ipairs(df.global.world.world_data.active_site[0].entity_links) do + local he = df.historical_entity.find(link.entity_id); + if not he or + (he.type ~= df.historical_entity_type.SiteGovernment and + he.type ~= df.historical_entity_type.Civilization) + then + goto continue + end + for _, position in ipairs(he.positions.own) do + if position.mandate_max > 0 then + table.insert(roles, position.code) + end + end + ::continue:: + end + return roles +end + +function get_risky_items(banned_items) + local risky_items = {} + for _, role in ipairs(get_mandate_noble_roles()) do + for _, unit in ipairs(dfhack.units.getUnitsByNobleRole(role)) do + analyze_noble(unit, risky_items, banned_items) + end + end + return risky_items +end + +local function make_item_description(item_type, subtype) + local itemdef = dfhack.items.getSubtypeDef(item_type, subtype) + return itemdef and string.lower(itemdef.name) or + string.lower(df.item_type[item_type]):gsub('_', ' ') +end + +local function get_banned_token(banned_items) + if not next(banned_items) then + return { + gap=2, + text='None', + pen=COLOR_GREY, + } + end + local strs = {} + for item_type, subtypes in pairs(banned_items) do + for subtype in pairs(subtypes) do + table.insert(strs, make_item_description(item_type, subtype)) + end + end + return { + gap=2, + text=table.concat(strs, ', '), + pen=COLOR_LIGHTRED, + } +end + +local function show_export_agreements(export_agreements) + local strs = {} + for _, agreement in ipairs(export_agreements) do + for idx, price in ipairs(agreement.price) do + local desc = make_item_description(agreement.items.item_type[idx], agreement.items.item_subtype[idx]) + local percent = (price * 100) // 256 + table.insert(strs, ('%20s %d%%'):format(desc..':', percent)) + end + end + dialogs.showMessage('Price agreement for exported items', table.concat(strs, '\n')) +end + +local function get_ethics_token(animal_ethics, wood_ethics) + local restrictions = {} + if animal_ethics or wood_ethics then + if animal_ethics then table.insert(restrictions, "Animals") end + if wood_ethics then table.insert(restrictions, "Trees") end + end + return { + gap=2, + text=#restrictions == 0 and 'None' or table.concat(restrictions, ', '), + pen=#restrictions ~= 0 and COLOR_LIGHTRED or COLOR_GREY, + } +end + +function get_info_widgets(self, export_agreements) + return { + widgets.Panel{ + frame={t=0, l=0, r=0, h=2}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Merchant export agreements:', + {gap=1, text='None', pen=COLOR_GREY}, + }, + }, + widgets.HotkeyLabel{ + frame={t=0, l=28}, + key='CUSTOM_SHIFT_H', + label='[details]', + text_pen=COLOR_LIGHTRED, + on_activate=function() show_export_agreements(export_agreements) end, + visible=#export_agreements > 0, + }, + widgets.ToggleHotkeyLabel{ + view_id='only_agreement', + frame={t=1, l=0}, + label='Show only requested items:', + key='CUSTOM_SHIFT_A', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=false, + on_change=function() self:refresh_list() end, + visible=#export_agreements > 0, + }, + }, + }, + widgets.Panel{ + frame={t=3, l=0, r=0, h=3}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Merchant ethical restrictions:', NEWLINE, + get_ethics_token(self.animal_ethics, self.wood_ethics), + }, + }, + widgets.CycleHotkeyLabel{ + view_id='ethical', + frame={t=2, l=0}, + key='CUSTOM_SHIFT_G', + options={ + {label='Show only ethically acceptable items', value='only', pen=COLOR_GREEN}, + {label='Ignore ethical restrictions', value='show'}, + {label='Show only ethically unacceptable items', value='hide', pen=COLOR_RED}, + }, + initial_option='only', + option_gap=0, + visible=self.animal_ethics or self.wood_ethics, + on_change=function() self:refresh_list() end, + }, + }, + }, + widgets.Panel{ + frame={t=7, l=0, r=0, h=5}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Items banned by export mandates:', NEWLINE, + get_banned_token(self.banned_items), NEWLINE, + 'Additional items at risk of mandates:', NEWLINE, + get_banned_token(self.risky_items), + }, + }, + widgets.CycleHotkeyLabel{ + view_id='banned', + frame={t=4, l=0}, + key='CUSTOM_SHIFT_D', + options={ + {label='Hide banned and risky items', value='both', pen=COLOR_GREEN}, + {label='Hide banned items', value='banned_only', pen=COLOR_YELLOW}, + {label='Ignore mandate restrictions', value='ignore', pen=COLOR_RED}, + }, + initial_option='both', + option_gap=0, + visible=next(self.banned_items) or next(self.risky_items), + on_change=function() self:refresh_list() end, + }, + }, + }, + } +end + +local function match_risky(item, risky_items) + for item_type, subtypes in pairs(risky_items) do + for subtype in pairs(subtypes) do + if item_type == item:getType() and (subtype == -1 or subtype == item:getSubtype()) then + return true + end + end + end + return false +end + +-- returns is_banned, is_risky +function scan_banned(item, risky_items) + if not dfhack.items.checkMandates(item) then return true, true end + if match_risky(item, risky_items) then return false, true end + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + if not dfhack.items.checkMandates(contained_item) then return true, true end + if match_risky(contained_item, risky_items) then return false, true end + end + return false, false +end + +local function is_wood_based(mat_type, mat_index) + if mat_type == df.builtin_mats.LYE or + mat_type == df.builtin_mats.GLASS_CLEAR or + mat_type == df.builtin_mats.GLASS_CRYSTAL or + (mat_type == df.builtin_mats.COAL and mat_index == 1) or + mat_type == df.builtin_mats.POTASH or + mat_type == df.builtin_mats.ASH or + mat_type == df.builtin_mats.PEARLASH + then + return true + end + + local mi = dfhack.matinfo.decode(mat_type, mat_index) + return mi and mi.material and + (mi.material.flags.WOOD or + mi.material.flags.STRUCTURAL_PLANT_MAT or + mi.material.flags.SOAP) +end + +function has_wood(item) + if item.flags2.grown then return false end + + if is_wood_based(item:getMaterial(), item:getMaterialIndex()) then + return true + end + + if item:hasImprovements() then + for _, imp in ipairs(item.improvements) do + if is_wood_based(imp.mat_type, imp.mat_index) then + return true + end + end + end + + return false +end diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 2fd29ac8bd..18a6b54541 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -1,7 +1,6 @@ --@ module = true local common = reqscript('internal/caravan/common') -local dialogs = require('gui.dialogs') local gui = require('gui') local overlay = require('plugins.overlay') local utils = require('utils') @@ -13,7 +12,7 @@ local widgets = require('gui.widgets') MoveGoods = defclass(MoveGoods, widgets.Window) MoveGoods.ATTRS { - frame_title='Select trade goods', + frame_title='Move goods to/from depot', frame={w=84, h=45}, resizable=true, resize_min={w=81,h=35}, @@ -102,145 +101,17 @@ local function is_active_caravan(caravan) trade_state == df.caravan_state.T_trade_state.AtDepot) end -local function is_tree_lover_caravan(caravan) - local caravan_he = df.historical_entity.find(caravan.entity); - if not caravan_he then return false end - local wood_ethic = caravan_he.entity_raw.ethic[df.ethic_type.KILL_PLANT] - return wood_ethic == df.ethic_response.MISGUIDED or - wood_ethic == df.ethic_response.SHUN or - wood_ethic == df.ethic_response.APPALLING or - wood_ethic == df.ethic_response.PUNISH_REPRIMAND or - wood_ethic == df.ethic_response.PUNISH_SERIOUS or - wood_ethic == df.ethic_response.PUNISH_EXILE or - wood_ethic == df.ethic_response.PUNISH_CAPITAL or - wood_ethic == df.ethic_response.UNTHINKABLE -end - -local function is_animal_lover_caravan(caravan) - local caravan_he = df.historical_entity.find(caravan.entity); - if not caravan_he then return false end - local animal_ethic = caravan_he.entity_raw.ethic[df.ethic_type.KILL_ANIMAL] - return animal_ethic == df.ethic_response.JUSTIFIED_IF_SELF_DEFENSE or - animal_ethic == df.ethic_response.JUSTIFIED_IF_EXTREME_REASON or - animal_ethic == df.ethic_response.MISGUIDED or - animal_ethic == df.ethic_response.SHUN or - animal_ethic == df.ethic_response.APPALLING or - animal_ethic == df.ethic_response.PUNISH_REPRIMAND or - animal_ethic == df.ethic_response.PUNISH_SERIOUS or - animal_ethic == df.ethic_response.PUNISH_EXILE or - animal_ethic == df.ethic_response.PUNISH_CAPITAL or - animal_ethic == df.ethic_response.UNTHINKABLE -end - local function get_ethics_restrictions() local animal_ethics, wood_ethics = false, false for _,caravan in ipairs(df.global.plotinfo.caravans) do if is_active_caravan(caravan) then - animal_ethics = animal_ethics or is_animal_lover_caravan(caravan) - wood_ethics = wood_ethics or is_tree_lover_caravan(caravan) + animal_ethics = animal_ethics or common.is_animal_lover_caravan(caravan) + wood_ethics = wood_ethics or common.is_tree_lover_caravan(caravan) end end return animal_ethics, wood_ethics end -local function get_ethics_token(animal_ethics, wood_ethics) - local restrictions = {} - if animal_ethics or wood_ethics then - if animal_ethics then table.insert(restrictions, "Animals") end - if wood_ethics then table.insert(restrictions, "Trees") end - end - return { - gap=2, - text=#restrictions == 0 and 'None' or table.concat(restrictions, ', '), - pen=#restrictions ~= 0 and COLOR_LIGHTRED or COLOR_GREY, - } -end - --- works for both mandates and unit preferences --- adds spec to registry, but only if not in filter -local function register_item_type(registry, spec, filter) - if not safe_index(filter, spec.item_type, spec.item_subtype) then - ensure_keys(registry, spec.item_type)[spec.item_subtype] = true - end -end - -local function get_banned_items() - local banned_items = {} - for _, mandate in ipairs(df.global.world.mandates) do - if mandate.mode == df.mandate.T_mode.Export then - register_item_type(banned_items, mandate) - end - end - return banned_items -end - -local function analyze_noble(unit, risky_items, banned_items) - for _, preference in ipairs(unit.status.current_soul.preferences) do - if preference.type == df.unit_preference.T_type.LikeItem and - preference.active - then - register_item_type(risky_items, preference, banned_items) - end - end -end - -local function get_mandate_noble_roles() - local roles = {} - for _, link in ipairs(df.global.world.world_data.active_site[0].entity_links) do - local he = df.historical_entity.find(link.entity_id); - if not he or - (he.type ~= df.historical_entity_type.SiteGovernment and - he.type ~= df.historical_entity_type.Civilization) - then - goto continue - end - for _, position in ipairs(he.positions.own) do - if position.mandate_max > 0 then - table.insert(roles, position.code) - end - end - ::continue:: - end - return roles -end - -local function get_risky_items(banned_items) - local risky_items = {} - for _, role in ipairs(get_mandate_noble_roles()) do - for _, unit in ipairs(dfhack.units.getUnitsByNobleRole(role)) do - analyze_noble(unit, risky_items, banned_items) - end - end - return risky_items -end - -local function make_item_description(item_type, subtype) - local itemdef = dfhack.items.getSubtypeDef(item_type, subtype) - return itemdef and string.lower(itemdef.name) or - string.lower(df.item_type[item_type]):gsub('_', ' ') -end - -local function get_banned_token(banned_items) - if not next(banned_items) then - return { - gap=2, - text='None', - pen=COLOR_GREY, - } - end - local strs = {} - for item_type, subtypes in pairs(banned_items) do - for subtype in pairs(subtypes) do - table.insert(strs, make_item_description(item_type, subtype)) - end - end - return { - gap=2, - text=table.concat(strs, ', '), - pen=COLOR_LIGHTRED, - } -end - local function get_export_agreements() local export_agreements = {} for _,caravan in ipairs(df.global.plotinfo.caravans) do @@ -251,25 +122,12 @@ local function get_export_agreements() return export_agreements end -local function show_export_agreements(export_agreements) - local strs = {} - for _, agreement in ipairs(export_agreements) do - for idx, price in ipairs(agreement.price) do - local desc = make_item_description(agreement.items.item_type[idx], agreement.items.item_subtype[idx]) - local percent = (price * 100) // 256 - table.insert(strs, ('%20s %d%%'):format(desc..':', percent)) - end - end - dialogs.showMessage('Price agreement for exported items', table.concat(strs, '\n')) -end - function MoveGoods:init() self.value_pending = 0 - local export_agreements = get_export_agreements() - local animal_ethics, wood_ethics = get_ethics_restrictions() - local banned_items = get_banned_items() - self.risky_items = get_risky_items(banned_items) + self.animal_ethics, self.wood_ethics = get_ethics_restrictions() + self.banned_items = common.get_banned_items() + self.risky_items = common.get_risky_items(self.banned_items) self:addviews{ widgets.CycleHotkeyLabel{ @@ -297,194 +155,8 @@ function MoveGoods:init() on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ - frame={t=2, l=0, w=38, h=4}, - subviews={ - widgets.CycleHotkeyLabel{ - view_id='min_condition', - frame={l=0, t=0, w=18}, - label='Min condition:', - label_below=true, - key_back='CUSTOM_SHIFT_C', - key='CUSTOM_SHIFT_V', - options={ - {label='XXTatteredXX', value=3}, - {label='XFrayedX', value=2}, - {label='xWornx', value=1}, - {label='Pristine', value=0}, - }, - initial_option=3, - on_change=function(val) - if self.subviews.max_condition:getOptionValue() > val then - self.subviews.max_condition:setOption(val) - end - self:refresh_list() - end, - }, - widgets.CycleHotkeyLabel{ - view_id='max_condition', - frame={r=1, t=0, w=18}, - label='Max condition:', - label_below=true, - key_back='CUSTOM_SHIFT_E', - key='CUSTOM_SHIFT_R', - options={ - {label='XXTatteredXX', value=3}, - {label='XFrayedX', value=2}, - {label='xWornx', value=1}, - {label='Pristine', value=0}, - }, - initial_option=0, - on_change=function(val) - if self.subviews.min_condition:getOptionValue() < val then - self.subviews.min_condition:setOption(val) - end - self:refresh_list() - end, - }, - widgets.RangeSlider{ - frame={l=0, t=3}, - num_stops=4, - get_left_idx_fn=function() - return 4 - self.subviews.min_condition:getOptionValue() - end, - get_right_idx_fn=function() - return 4 - self.subviews.max_condition:getOptionValue() - end, - on_left_change=function(idx) self.subviews.min_condition:setOption(4-idx, true) end, - on_right_change=function(idx) self.subviews.max_condition:setOption(4-idx, true) end, - }, - }, - }, - widgets.Panel{ - frame={t=7, l=0, w=38, h=4}, - subviews={ - widgets.CycleHotkeyLabel{ - view_id='min_quality', - frame={l=0, t=0, w=18}, - label='Min quality:', - label_below=true, - key_back='CUSTOM_SHIFT_Z', - key='CUSTOM_SHIFT_X', - options={ - {label='Ordinary', value=0}, - {label='-Well Crafted-', value=1}, - {label='+Finely Crafted+', value=2}, - {label='*Superior*', value=3}, - {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, - {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, - {label='Artifact', value=6}, - }, - initial_option=0, - on_change=function(val) - if self.subviews.max_quality:getOptionValue() < val then - self.subviews.max_quality:setOption(val) - end - self:refresh_list() - end, - }, - widgets.CycleHotkeyLabel{ - view_id='max_quality', - frame={r=1, t=0, w=18}, - label='Max quality:', - label_below=true, - key_back='CUSTOM_SHIFT_Q', - key='CUSTOM_SHIFT_W', - options={ - {label='Ordinary', value=0}, - {label='-Well Crafted-', value=1}, - {label='+Finely Crafted+', value=2}, - {label='*Superior*', value=3}, - {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, - {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, - {label='Artifact', value=6}, - }, - initial_option=6, - on_change=function(val) - if self.subviews.min_quality:getOptionValue() > val then - self.subviews.min_quality:setOption(val) - end - self:refresh_list() - end, - }, - widgets.RangeSlider{ - frame={l=0, t=3}, - num_stops=7, - get_left_idx_fn=function() - return self.subviews.min_quality:getOptionValue() + 1 - end, - get_right_idx_fn=function() - return self.subviews.max_quality:getOptionValue() + 1 - end, - on_left_change=function(idx) self.subviews.min_quality:setOption(idx-1, true) end, - on_right_change=function(idx) self.subviews.max_quality:setOption(idx-1, true) end, - }, - }, - }, - widgets.Panel{ - frame={t=12, l=0, w=38, h=4}, - subviews={ - widgets.CycleHotkeyLabel{ - view_id='min_value', - frame={l=0, t=0, w=18}, - label='Min value:', - label_below=true, - key_back='CUSTOM_SHIFT_B', - key='CUSTOM_SHIFT_N', - options={ - {label='1'..common.CH_MONEY, value={index=1, value=1}}, - {label='20'..common.CH_MONEY, value={index=2, value=20}}, - {label='50'..common.CH_MONEY, value={index=3, value=50}}, - {label='100'..common.CH_MONEY, value={index=4, value=100}}, - {label='500'..common.CH_MONEY, value={index=5, value=500}}, - {label='1000'..common.CH_MONEY, value={index=6, value=1000}}, - -- max "min" value is less than max "max" value since the range of inf - inf is not useful - {label='5000'..common.CH_MONEY, value={index=7, value=5000}}, - }, - initial_option=1, - on_change=function(val) - if self.subviews.max_value:getOptionValue().value < val.value then - self.subviews.max_value:setOption(val.index) - end - self:refresh_list() - end, - }, - widgets.CycleHotkeyLabel{ - view_id='max_value', - frame={r=1, t=0, w=18}, - label='Max value:', - label_below=true, - key_back='CUSTOM_SHIFT_T', - key='CUSTOM_SHIFT_Y', - options={ - {label='1'..common.CH_MONEY, value={index=1, value=1}}, - {label='20'..common.CH_MONEY, value={index=2, value=20}}, - {label='50'..common.CH_MONEY, value={index=3, value=50}}, - {label='100'..common.CH_MONEY, value={index=4, value=100}}, - {label='500'..common.CH_MONEY, value={index=5, value=500}}, - {label='1000'..common.CH_MONEY, value={index=6, value=1000}}, - {label='Max', value={index=7, value=math.huge}}, - }, - initial_option=7, - on_change=function(val) - if self.subviews.min_value:getOptionValue().value > val.value then - self.subviews.min_value:setOption(val.index) - end - self:refresh_list() - end, - }, - widgets.RangeSlider{ - frame={l=0, t=3}, - num_stops=7, - get_left_idx_fn=function() - return self.subviews.min_value:getOptionValue().index - end, - get_right_idx_fn=function() - return self.subviews.max_value:getOptionValue().index - end, - on_left_change=function(idx) self.subviews.min_value:setOption(idx, true) end, - on_right_change=function(idx) self.subviews.max_value:setOption(idx, true) end, - }, - }, + frame={t=2, l=0, w=38, h=14}, + subviews=common.get_slider_widgets(self), }, widgets.ToggleHotkeyLabel{ view_id='hide_forbidden', @@ -499,91 +171,8 @@ function MoveGoods:init() on_change=function() self:refresh_list() end, }, widgets.Panel{ - frame={t=4, l=40, r=0, h=2}, - subviews={ - widgets.Label{ - frame={t=0, l=0}, - text={ - 'Merchant export agreements:', - {gap=1, text='None', pen=COLOR_GREY}, - }, - }, - widgets.HotkeyLabel{ - frame={t=0, l=28}, - key='CUSTOM_SHIFT_H', - label='[details]', - text_pen=COLOR_LIGHTRED, - on_activate=function() show_export_agreements(export_agreements) end, - visible=#export_agreements > 0, - }, - widgets.ToggleHotkeyLabel{ - view_id='only_agreement', - frame={t=1, l=0}, - label='Show only requested items:', - key='CUSTOM_SHIFT_A', - options={ - {label='Yes', value=true, pen=COLOR_GREEN}, - {label='No', value=false} - }, - initial_option=false, - on_change=function() self:refresh_list() end, - visible=#export_agreements > 0, - }, - }, - }, - widgets.Panel{ - frame={t=7, l=40, r=0, h=3}, - subviews={ - widgets.Label{ - frame={t=0, l=0}, - text={ - 'Merchant ethical restrictions:', NEWLINE, - get_ethics_token(animal_ethics, wood_ethics), - }, - }, - widgets.CycleHotkeyLabel{ - view_id='ethical', - frame={t=2, l=0}, - key='CUSTOM_SHIFT_G', - options={ - {label='Show only ethically acceptable items', value='only', pen=COLOR_GREEN}, - {label='Ignore ethical restrictions', value='show'}, - {label='Show only ethically unacceptable items', value='hide', pen=COLOR_RED}, - }, - initial_option='only', - option_gap=0, - visible=animal_ethics or wood_ethics, - on_change=function() self:refresh_list() end, - }, - }, - }, - widgets.Panel{ - frame={t=11, l=40, r=0, h=5}, - subviews={ - widgets.Label{ - frame={t=0, l=0}, - text={ - 'Items banned by export mandates:', NEWLINE, - get_banned_token(banned_items), NEWLINE, - 'Additional items at risk of mandates:', NEWLINE, - get_banned_token(self.risky_items), - }, - }, - widgets.CycleHotkeyLabel{ - view_id='banned', - frame={t=4, l=0}, - key='CUSTOM_SHIFT_D', - options={ - {label='Hide banned and risky items', value='both', pen=COLOR_GREEN}, - {label='Hide banned items', value='banned_only', pen=COLOR_YELLOW}, - {label='Ignore mandate restrictions', value='ignore', pen=COLOR_RED}, - }, - initial_option='both', - option_gap=0, - visible=next(banned_items) or next(self.risky_items), - on_change=function() self:refresh_list() end, - }, - }, + frame={t=4, l=40, r=0, h=12}, + subviews=common.get_info_widgets(self, get_export_agreements()), }, widgets.Panel{ frame={t=17, l=0, r=0, b=6}, @@ -755,66 +344,8 @@ local function make_choice_text(at_depot, value, quantity, desc) } end -local function match_risky(item, risky_items) - for item_type, subtypes in pairs(risky_items) do - for subtype in pairs(subtypes) do - if item_type == item:getType() and (subtype == -1 or subtype == item:getSubtype()) then - return true - end - end - end - return false -end - --- returns is_banned, is_risky -local function scan_banned(item, risky_items) - if not dfhack.items.checkMandates(item) then return true, true end - if match_risky(item, risky_items) then return false, true end - for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do - if not dfhack.items.checkMandates(contained_item) then return true, true end - if match_risky(contained_item, risky_items) then return false, true end - end - return false, false -end - -local function is_wood_based(mat_type, mat_index) - if mat_type == df.builtin_mats.LYE or - mat_type == df.builtin_mats.GLASS_CLEAR or - mat_type == df.builtin_mats.GLASS_CRYSTAL or - (mat_type == df.builtin_mats.COAL and mat_index == 1) or - mat_type == df.builtin_mats.POTASH or - mat_type == df.builtin_mats.ASH or - mat_type == df.builtin_mats.PEARLASH - then - return true - end - - local mi = dfhack.matinfo.decode(mat_type, mat_index) - return mi and mi.material and - (mi.material.flags.WOOD or - mi.material.flags.STRUCTURAL_PLANT_MAT or - mi.material.flags.SOAP) -end - -local function has_wood(item) - if item.flags2.grown then return false end - - if is_wood_based(item:getMaterial(), item:getMaterialIndex()) then - return true - end - - if item:hasImprovements() then - for _, imp in ipairs(item.improvements) do - if is_wood_based(imp.mat_type, imp.mat_index) then - return true - end - end - end - - return false -end - -local function is_ethical_product(item, fn) +local function is_ethical_product(item, animal_ethics, wood_ethics) + if not animal_ethics and not wood_ethics then return true end if item.flags.container then local contained_items = dfhack.items.getContainedItems(item) if df.item_binst:is_instance(item) then @@ -823,7 +354,10 @@ local function is_ethical_product(item, fn) local has_items = false for _, contained_item in ipairs(contained_items) do has_items = true - if not contained_item:isAnimalProduct() and not has_wood(contained_item) then + if (not animal_ethics or not contained_item:isAnimalProduct()) and + (not wood_ethics or not common.has_wood(contained_item)) + then + -- bin passes if at least one contained item is safe return true end end @@ -834,29 +368,22 @@ local function is_ethical_product(item, fn) else -- for other types of containers, any contamination makes it untradeable for _, contained_item in ipairs(contained_items) do - if contained_item:isAnimalProduct() or has_wood(contained_item) then + if (animal_ethics and contained_item:isAnimalProduct()) or + (wood_ethics and common.has_wood(contained_item)) + then return false end end end end - return not item:isAnimalProduct() and not has_wood(item) -end - -local function is_ethical_animal_product(item) - return is_ethical_product(item, function(it) return it:isAnimalProduct() end) -end - -local function is_ethical_wood_product(item) - return is_ethical_product(item, function(it) return has_wood(it) end) + return (not animal_ethics or not item:isAnimalProduct()) and + (not wood_ethics or not common.has_wood(item)) end function MoveGoods:cache_choices(disable_buckets) if self.choices then return self.choices[disable_buckets] end - local animal_ethics, wood_ethics = get_ethics_restrictions() - local depot = dfhack.gui.getSelectedBuilding(true) local pending = self.pending_item_ids local buckets = {} @@ -867,15 +394,10 @@ function MoveGoods:cache_choices(disable_buckets) if value <= 0 then goto continue end local is_pending = not not pending[item_id] or item.flags.in_building local is_forbidden = item.flags.forbid - local is_banned, is_risky = scan_banned(item, self.risky_items) + local is_banned, is_risky = common.scan_banned(item, self.risky_items) local is_requested = dfhack.items.isRequestedTradeGood(item) local wear_level = item:getWear() - local desc = item.flags.artifact and common.get_artifact_name(item) or - dfhack.items.getDescription(item, 0, true) - if wear_level == 1 then desc = ('x%sx'):format(desc) - elseif wear_level == 2 then desc = ('X%sX'):format(desc) - elseif wear_level == 3 then desc = ('XX%sXX'):format(desc) - end + local desc = common.get_item_description(item) local key = ('%s/%d'):format(desc, value) if buckets[key] then local bucket = buckets[key] @@ -888,8 +410,7 @@ function MoveGoods:cache_choices(disable_buckets) bucket.data.has_risky = bucket.data.has_risky or is_risky bucket.data.has_requested = bucket.data.has_requested or is_requested else - local is_ethical = (not animal_ethics or is_ethical_animal_product(item)) and - (not wood_ethics or is_ethical_wood_product(item)) + local is_ethical = is_ethical_product(item, self.animal_ethics, self.wood_ethics) local data = { desc=desc, per_item_value=value, diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 9d62ffe7cb..1e2da6b8ed 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -29,16 +29,56 @@ local trade = df.global.game.main_interface.trade -- ------------------- -- Trade -- - Trade = defclass(Trade, widgets.Window) Trade.ATTRS { frame_title='Select trade goods', - frame={w=78, h=45}, + frame={w=84, h=45}, resizable=true, - resize_min={h=27}, + resize_min={w=48, h=27}, +} + +local TOGGLE_MAP = { + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_SELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_SELECTED, + [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, +} + +local TARGET_MAP = { + [true]={ + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_SELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, + [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_SELECTED, + [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, + }, + [false]={ + [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, + [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_UNSELECTED, + [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, + }, +} + +local TARGET_REVMAP = { + [GOODFLAG.UNCONTAINED_UNSELECTED] = false, + [GOODFLAG.UNCONTAINED_SELECTED] = true, + [GOODFLAG.CONTAINED_UNSELECTED] = false, + [GOODFLAG.CONTAINED_SELECTED] = true, + [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = false, + [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = true, } -local VALUE_COL_WIDTH = 8 +local function get_entry_icon(data) + if TARGET_REVMAP[trade.goodflag[data.list_idx][data.item_idx]] then + return common.ALL_PEN + end +end local function sort_noop(a, b) -- this function is used as a marker and never actually gets called @@ -77,10 +117,37 @@ local function sort_by_value_asc(a, b) return a.data.value < b.data.value end +local function sort_by_status_desc(a, b) + local a_selected = get_entry_icon(a.data) + local b_selected = get_entry_icon(b.data) + if a_selected == b_selected then + return sort_by_value_desc(a, b) + end + return a_selected +end + +local function sort_by_status_asc(a, b) + local a_selected = get_entry_icon(a.data) + local b_selected = get_entry_icon(b.data) + if a_selected == b_selected then + return sort_by_value_desc(a, b) + end + return b_selected +end + +local STATUS_COL_WIDTH = 7 +local VALUE_COL_WIDTH = 6 +local FILTER_HEIGHT = 15 + function Trade:init() self.choices = {[0]={}, [1]={}} self.cur_page = 1 + self.animal_ethics = common.is_animal_lover_caravan(trade.mer) + self.wood_ethics = common.is_tree_lover_caravan(trade.mer) + self.banned_items = common.get_banned_items() + self.risky_items = common.get_risky_items(self.banned_items) + self:addviews{ widgets.CycleHotkeyLabel{ view_id='sort', @@ -88,12 +155,14 @@ function Trade:init() label='Sort by:', key='CUSTOM_SHIFT_S', options={ + {label='status'..common.CH_DN, value=sort_by_status_desc}, + {label='status'..common.CH_UP, value=sort_by_status_asc}, {label='value'..common.CH_DN, value=sort_by_value_desc}, {label='value'..common.CH_UP, value=sort_by_value_asc}, {label='name'..common.CH_DN, value=sort_by_name_desc}, {label='name'..common.CH_UP, value=sort_by_name_asc}, }, - initial_option=sort_by_value_desc, + initial_option=sort_by_status_desc, on_change=self:callback('refresh_list', 'sort'), }, widgets.EditField{ @@ -114,6 +183,18 @@ function Trade:init() initial_option=false, on_change=function() self:refresh_list() end, }, + widgets.ToggleHotkeyLabel{ + view_id='filters', + frame={t=2, l=40, w=36}, + label='Show filters:', + key='CUSTOM_SHIFT_F', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=false, + on_change=function() self:updateLayout() end, + }, widgets.TabBar{ frame={t=4, l=0}, labels={ @@ -127,27 +208,70 @@ function Trade:init() get_cur_page=function() return self.cur_page end, }, widgets.Panel{ - frame={t=6, l=0, r=0, b=4}, + frame={t=7, l=0, r=0, h=FILTER_HEIGHT}, + visible=function() return self.subviews.filters:getOptionValue() end, + on_layout=function() + local panel_frame = self.subviews.list_panel.frame + if self.subviews.filters:getOptionValue() then + panel_frame.t = 7 + FILTER_HEIGHT + else + panel_frame.t = 7 + end + end, subviews={ + widgets.Panel{ + frame={t=0, l=0, w=38, h=FILTER_HEIGHT}, + visible=function() return self.cur_page == 1 end, + subviews=common.get_slider_widgets(self, '1'), + }, + widgets.Panel{ + frame={t=0, l=0, w=38, h=FILTER_HEIGHT}, + visible=function() return self.cur_page == 2 end, + subviews=common.get_slider_widgets(self, '2'), + }, + widgets.Panel{ + frame={t=2, l=40, r=0, h=FILTER_HEIGHT-2}, + visible=function() return self.cur_page == 2 end, + subviews=common.get_info_widgets(self, {trade.mer.buy_prices}), + }, + }, + }, + widgets.Panel{ + view_id='list_panel', + frame={t=7, l=0, r=0, b=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_status', + frame={t=0, l=0, w=7}, + options={ + {label='status', value=sort_noop}, + {label='status'..common.CH_DN, value=sort_by_status_desc}, + {label='status'..common.CH_UP, value=sort_by_status_asc}, + }, + initial_option=sort_by_status_desc, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_status'), + }, widgets.CycleHotkeyLabel{ view_id='sort_value', - frame={l=2, t=0, w=7}, + frame={t=0, l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+1-6, w=6}, options={ {label='value', value=sort_noop}, {label='value'..common.CH_DN, value=sort_by_value_desc}, {label='value'..common.CH_UP, value=sort_by_value_asc}, }, - initial_option=sort_by_value_desc, + option_gap=0, on_change=self:callback('refresh_list', 'sort_value'), }, widgets.CycleHotkeyLabel{ view_id='sort_name', - frame={l=2+VALUE_COL_WIDTH+2, t=0, w=6}, + frame={t=0, l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+2, w=5}, options={ {label='name', value=sort_noop}, {label='name'..common.CH_DN, value=sort_by_name_desc}, {label='name'..common.CH_UP, value=sort_by_name_asc}, }, + option_gap=0, on_change=self:callback('refresh_list', 'sort_name'), }, widgets.FilteredList{ @@ -189,7 +313,7 @@ function Trade:refresh_list(sort_widget, sort_fn) self.subviews[sort_widget]:cycle() return end - for _,widget_name in ipairs{'sort', 'sort_value', 'sort_name'} do + for _,widget_name in ipairs{'sort', 'sort_status', 'sort_value', 'sort_name'} do self.subviews[widget_name]:setOption(sort_fn) end local list = self.subviews.list @@ -199,52 +323,26 @@ function Trade:refresh_list(sort_widget, sort_fn) list:setFilter(saved_filter) end -local TOGGLE_MAP = { - [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_SELECTED, - [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, - [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_SELECTED, - [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_UNSELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, -} - -local TARGET_MAP = { - [true]={ - [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_SELECTED, - [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, - [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_SELECTED, - [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_SELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, - }, - [false]={ - [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, - [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, - [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_UNSELECTED, - [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_UNSELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, - }, -} - -local TARGET_REVMAP = { - [GOODFLAG.UNCONTAINED_UNSELECTED] = false, - [GOODFLAG.UNCONTAINED_SELECTED] = true, - [GOODFLAG.CONTAINED_UNSELECTED] = false, - [GOODFLAG.CONTAINED_SELECTED] = true, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = false, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = true, -} - -local function get_entry_icon(data) - if TARGET_REVMAP[trade.goodflag[data.list_idx][data.item_idx]] then - return common.ALL_PEN +local function is_ethical_product(item, animal_ethics, wood_ethics) + if not animal_ethics and not wood_ethics then return true end + -- bin contents are already split out; no need to double-check them + if item.flags.container and not df.item_binst:is_instance(item) then + for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do + if (animal_ethics and contained_item:isAnimalProduct()) or + (wood_ethics and common.has_wood(contained_item)) + then + return false + end + end end + + return (not animal_ethics or not item:isAnimalProduct()) and + (not wood_ethics or not common.has_wood(item)) end -local function make_choice_text(desc, value) +local function make_choice_text(value, desc) return { - {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)}, {gap=2, text=desc}, } end @@ -254,30 +352,43 @@ function Trade:cache_choices(list_idx, trade_bins) local goodflags = trade.goodflag[list_idx] local trade_bins_choices, notrade_bins_choices = {}, {} - local parent_idx + local parent_data for item_idx, item in ipairs(trade.good[list_idx]) do local goodflag = goodflags[item_idx] if goodflag ~= GOODFLAG.CONTAINED_UNSELECTED and goodflag ~= GOODFLAG.CONTAINED_SELECTED then - parent_idx = nil + parent_data = nil end - local desc = item.flags.artifact and common.get_artifact_name(item) or - dfhack.items.getDescription(item, 0, true) + local is_banned, is_risky = common.scan_banned(item, self.risky_items) + local is_requested = dfhack.items.isRequestedTradeGood(item, trade.mer) + local wear_level = item:getWear() + local desc = common.get_item_description(item) + local is_ethical = is_ethical_product(item, self.animal_ethics, self.wood_ethics) local data = { desc=desc, value=common.get_perceived_value(item, trade.mer, list_idx == 1), list_idx=list_idx, item_idx=item_idx, + quality=item.flags.artifact and 6 or item:getQuality(), + wear=wear_level, + has_banned=is_banned, + has_risky=is_risky, + has_requested=is_requested, + ethical=is_ethical, } - if parent_idx then + if parent_data then data.update_container_fn = function(from, to) -- TODO end + parent_data.has_banned = parent_data.has_banned or is_banned + parent_data.has_risky = parent_data.has_risky or is_risky + parent_data.has_requested = parent_data.has_requested or is_requested + parent_data.ethical = parent_data.ethical and is_ethical end local choice = { search_key=common.make_search_key(desc), icon=curry(get_entry_icon, data), data=data, - text=make_choice_text(desc, data.value), + text=make_choice_text(data.value, desc), } local is_container = df.item_binst:is_instance(item) if not data.update_container_fn then @@ -286,7 +397,7 @@ function Trade:cache_choices(list_idx, trade_bins) if data.update_container_fn or not is_container then table.insert(notrade_bins_choices, choice) end - if is_container then parent_idx = item_idx end + if is_container then parent_data = data end end self.choices[list_idx][true] = trade_bins_choices @@ -295,7 +406,38 @@ function Trade:cache_choices(list_idx, trade_bins) end function Trade:get_choices() - local choices = self:cache_choices(self.cur_page-1, self.subviews.trade_bins:getOptionValue()) + local raw_choices = self:cache_choices(self.cur_page-1, self.subviews.trade_bins:getOptionValue()) + local banned = self.cur_page == 1 and 'ignore' or self.subviews.banned:getOptionValue() + local only_agreement = self.cur_page == 2 and self.subviews.only_agreement:getOptionValue() or false + local ethical = self.cur_page == 1 and 'show' or self.subviews.ethical:getOptionValue() + local min_condition = self.subviews['min_condition'..self.cur_page]:getOptionValue() + local max_condition = self.subviews['max_condition'..self.cur_page]:getOptionValue() + local min_quality = self.subviews['min_quality'..self.cur_page]:getOptionValue() + local max_quality = self.subviews['max_quality'..self.cur_page]:getOptionValue() + local min_value = self.subviews['min_value'..self.cur_page]:getOptionValue().value + local max_value = self.subviews['max_value'..self.cur_page]:getOptionValue().value + local choices = {} + for _,choice in ipairs(raw_choices) do + local data = choice.data + if ethical ~= 'show' then + if ethical == 'hide' and data.ethical then goto continue end + if ethical == 'only' and not data.ethical then goto continue end + end + if min_condition < data.wear then goto continue end + if max_condition > data.wear then goto continue end + if min_quality > data.quality then goto continue end + if max_quality < data.quality then goto continue end + if min_value > data.value then goto continue end + if max_value < data.value then goto continue end + if only_agreement and not data.has_requested then goto continue end + if banned ~= 'ignore' then + if data.has_banned or (banned ~= 'banned_only' and data.has_risky) then + goto continue + end + end + table.insert(choices, choice) + ::continue:: + end table.sort(choices, self.subviews.sort:getOptionValue()) return choices end @@ -357,6 +499,12 @@ function TradeScreen:init() self:addviews{Trade{}} end +function TradeScreen:onRenderFrame() + if not df.global.game.main_interface.trade.open then + view:dismiss() + end +end + function TradeScreen:onDismiss() view = nil end From 36b1dd308920fc404cf2a67d5ad697b375ec984c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 6 Jul 2023 03:35:47 -0700 Subject: [PATCH 359/732] adjust window size --- internal/caravan/trade.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 1e2da6b8ed..c5df840a1d 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -32,7 +32,7 @@ local trade = df.global.game.main_interface.trade Trade = defclass(Trade, widgets.Window) Trade.ATTRS { frame_title='Select trade goods', - frame={w=84, h=45}, + frame={w=84, h=47}, resizable=true, resize_min={w=48, h=27}, } From e7a9aab2cd0065a7b90b03c27919b692fba44a0b Mon Sep 17 00:00:00 2001 From: Myk Date: Thu, 6 Jul 2023 17:03:26 -0700 Subject: [PATCH 360/732] Fix off by one error --- internal/caravan/movegoods.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 18a6b54541..0514006483 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -160,7 +160,7 @@ function MoveGoods:init() }, widgets.ToggleHotkeyLabel{ view_id='hide_forbidden', - frame={t=2, l=40, w=27}, + frame={t=2, l=40, w=28}, label='Hide forbidden items:', key='CUSTOM_SHIFT_F', options={ From 33ecf539b0dc993818d6830ce26a234a321b9ae3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Jul 2023 13:12:34 -0700 Subject: [PATCH 361/732] add missing changelog for new trade screens --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 375063963e..6ad851bad1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -14,11 +14,11 @@ that repo. # Future ## New Scripts +- `caravan`: new trade screen UI replacements for bringing goods to trade depot and trading ## Fixes - `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported - `gui/gm-unit`: fix commandline processing when a unit id is specified - - `suspendmanager`: take in account already built blocking buildings ## Misc Improvements From 37701795f5705f5498df9543afdfb1255595976e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Jul 2023 15:27:22 -0700 Subject: [PATCH 362/732] add trade good ui launch point from vanilla trade good screen --- caravan.lua | 1 + internal/caravan/movegoods.lua | 50 +++++++++++++++++++++++++++++++--- internal/caravan/trade.lua | 17 +++++++++++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/caravan.lua b/caravan.lua index 43168ded2b..70b949a3aa 100644 --- a/caravan.lua +++ b/caravan.lua @@ -18,6 +18,7 @@ OVERLAY_WIDGETS = { trade=trade.TradeOverlay, tradeagreement=tradeagreement.TradeAgreementOverlay, movegoods=movegoods.MoveGoodsOverlay, + assigntrade=movegoods.AssignTradeOverlay, } INTERESTING_FLAGS = { diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 0514006483..e2ae19e2d3 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -17,6 +17,7 @@ MoveGoods.ATTRS { resizable=true, resize_min={w=81,h=35}, pending_item_ids=DEFAULT_NIL, + depot=DEFAULT_NIL, } local STATUS_COL_WIDTH = 7 @@ -384,12 +385,11 @@ end function MoveGoods:cache_choices(disable_buckets) if self.choices then return self.choices[disable_buckets] end - local depot = dfhack.gui.getSelectedBuilding(true) local pending = self.pending_item_ids local buckets = {} for _, item in ipairs(df.global.world.items.all) do local item_id = item.id - if not item or not is_tradeable_item(item, depot) then goto continue end + if not item or not is_tradeable_item(item, self.depot) then goto continue end local value = common.get_perceived_value(item) if value <= 0 then goto continue end local is_pending = not not pending[item_id] or item.flags.in_building @@ -584,6 +584,8 @@ end MoveGoodsModal = defclass(MoveGoodsModal, gui.ZScreenModal) MoveGoodsModal.ATTRS { focus_path='caravan/movegoods', + depot=DEFAULT_NIL, + on_dismiss=DEFAULT_NIL, } local function get_pending_trade_item_ids() @@ -598,12 +600,18 @@ end function MoveGoodsModal:init() self.pending_item_ids = get_pending_trade_item_ids() - self:addviews{MoveGoods{pending_item_ids=self.pending_item_ids}} + self.depot = self.depot or dfhack.gui.getSelectedBuilding(true) + self:addviews{ + MoveGoods{ + pending_item_ids=self.pending_item_ids, + depot=self.depot, + }, + } end function MoveGoodsModal:onDismiss() -- mark/unmark selected goods for trade - local depot = dfhack.gui.getSelectedBuilding(true) + local depot = self.depot if not depot then return end local pending = self.pending_item_ids for _, choice in ipairs(self.subviews.list:getChoices()) do @@ -628,6 +636,9 @@ function MoveGoodsModal:onDismiss() end ::continue:: end + if self.on_dismiss then + self.on_dismiss(self) + end end -- ------------------- @@ -676,3 +687,34 @@ function MoveGoodsOverlay:init() }, } end + +-- ------------------- +-- AssignTradeOverlay +-- + +AssignTradeOverlay = defclass(AssignTradeOverlay, overlay.OverlayWidget) +AssignTradeOverlay.ATTRS{ + default_pos={x=-3,y=-25}, + default_enabled=true, + viewscreens='dwarfmode/AssignTrade', + frame={w=27, h=3}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function AssignTradeOverlay:init() + local on_dismiss = function(scr) + scr:sendInputToParent('LEAVESCREEN') + end + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='DFHack goods UI', + key='CUSTOM_CTRL_T', + on_activate=function() + local depot = df.global.game.main_interface.assign_trade.trade_depot_bld + MoveGoodsModal{depot=depot, on_dismiss=on_dismiss}:show() + end, + }, + } +end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index c5df840a1d..437865ad7f 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -140,8 +140,9 @@ local VALUE_COL_WIDTH = 6 local FILTER_HEIGHT = 15 function Trade:init() - self.choices = {[0]={}, [1]={}} self.cur_page = 1 + self.saved_talkline = trade.talkline + self:check_cache() self.animal_ethics = common.is_animal_lover_caravan(trade.mer) self.wood_ethics = common.is_tree_lover_caravan(trade.mer) @@ -484,6 +485,20 @@ function Trade:toggle_visible() end end +function Trade:check_cache() + if self.saved_talkline ~= trade.talkline then + self.saved_talkline = trade.talkline + -- react to trade button being clicked + self.choices = {[0]={}, [1]={}} + self:refresh_list() + end +end + +function Trade:onRenderFrame(dc, rect) + Trade.super.onRenderFrame(self, dc, rect) + self:check_cache() +end + -- ------------------- -- TradeScreen -- From 7a3d257433abf5d7994d0aa097707818b389594b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Jul 2023 16:34:12 -0700 Subject: [PATCH 363/732] make syndrome lists searchable and fix input processing snafus --- changelog.txt | 1 + gui/unit-syndromes.lua | 37 +++++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6ad851bad1..5f30f4c738 100644 --- a/changelog.txt +++ b/changelog.txt @@ -22,6 +22,7 @@ that repo. - `suspendmanager`: take in account already built blocking buildings ## Misc Improvements +- `gui/unit-syndromes`: make lists searchable ## Removed diff --git a/gui/unit-syndromes.lua b/gui/unit-syndromes.lua index b57a237e6a..a4cc4538fa 100644 --- a/gui/unit-syndromes.lua +++ b/gui/unit-syndromes.lua @@ -371,14 +371,6 @@ UnitSyndromes.ATTRS { } function UnitSyndromes:init() - local is_not_main_page = function() - return self.subviews.pages:getSelected() > 1 - end - - local previous_page = function() - self.subviews.pages:setSelected(self.subviews.pages:getSelected() - 1) - end - self:addviews{ widgets.Pages{ view_id = 'pages', @@ -397,15 +389,15 @@ function UnitSyndromes:init() }, on_submit = self:callback('showUnits'), }, - widgets.List{ + widgets.FilteredList{ view_id = 'units', frame = {t = 0, l = 0}, row_height = 3, on_submit = self:callback('showUnitSyndromes'), }, - widgets.List{ + widgets.FilteredList{ view_id = 'unit_syndromes', - frame = {t = 0, l = 0}, + frame = {t = 0, l = 0, r = 0}, on_submit = self:callback('showSyndromeEffects'), }, widgets.WrappedLabel{ @@ -422,19 +414,30 @@ function UnitSyndromes:init() label='Back', auto_width=true, key='LEAVESCREEN', - on_activate=previous_page, - active=is_not_main_page, - enabled=is_not_main_page, + on_activate=self:callback('previous_page'), }, } end +function UnitSyndromes:previous_page() + local pages = self.subviews.pages + if pages:getSelected() == 1 then + view:dismiss() + return + end + if pages:getSelectedPage().view_id == 'syndrome_effects' then + pages:setSelected('unit_syndromes') + else + pages:setSelected(1) + end +end + function UnitSyndromes:onInput(keys) - UnitSyndromes.super.onInput(self, keys) - if keys._R_MOUSE_DOWN then + if keys._MOUSE_R_DOWN then self:previous_page() return true end + return UnitSyndromes.super.onInput(self, keys) end function UnitSyndromes:showUnits(index, choice) @@ -456,6 +459,7 @@ function UnitSyndromes:showUnits(index, choice) self.subviews.pages:setSelected('unit_syndromes') self.subviews.unit_syndromes:setChoices(choices) + self.subviews.unit_syndromes.edit:setFocus(true) return end @@ -475,6 +479,7 @@ function UnitSyndromes:showUnits(index, choice) self.subviews.pages:setSelected('units') self.subviews.units:setChoices(choices) + self.subviews.units.edit:setFocus(true) end function UnitSyndromes:showUnitSyndromes(index, choice) From f4537f4da199a4ddba65c25b710d2f2fb5039dfc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Jul 2023 17:37:08 -0700 Subject: [PATCH 364/732] track ui state properly, add syndrome id to details --- gui/unit-syndromes.lua | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/gui/unit-syndromes.lua b/gui/unit-syndromes.lua index a4cc4538fa..93a3476a7c 100644 --- a/gui/unit-syndromes.lua +++ b/gui/unit-syndromes.lua @@ -371,6 +371,8 @@ UnitSyndromes.ATTRS { } function UnitSyndromes:init() + self.stack = {} + self:addviews{ widgets.Pages{ view_id = 'pages', @@ -421,17 +423,23 @@ end function UnitSyndromes:previous_page() local pages = self.subviews.pages - if pages:getSelected() == 1 then + local cur_page = pages:getSelected() + if cur_page == 1 then view:dismiss() return end - if pages:getSelectedPage().view_id == 'syndrome_effects' then - pages:setSelected('unit_syndromes') - else - pages:setSelected(1) + + local state = table.remove(self.stack, #self.stack) + pages:setSelected(state.page) + if state.edit then + state.edit:setFocus(true) end end +function UnitSyndromes:push_state() + table.insert(self.stack, {page=self.subviews.pages:getSelected(), edit=self.focus_group.cur}) +end + function UnitSyndromes:onInput(keys) if keys._MOUSE_R_DOWN then self:previous_page() @@ -457,6 +465,7 @@ function UnitSyndromes:showUnits(index, choice) :: skipsyndrome :: end + self:push_state() self.subviews.pages:setSelected('unit_syndromes') self.subviews.unit_syndromes:setChoices(choices) self.subviews.unit_syndromes.edit:setFocus(true) @@ -477,6 +486,7 @@ function UnitSyndromes:showUnits(index, choice) }) end + self:push_state() self.subviews.pages:setSelected('units') self.subviews.units:setChoices(choices) self.subviews.units.edit:setFocus(true) @@ -506,12 +516,14 @@ function UnitSyndromes:showUnitSyndromes(index, choice) :: skipsyndrome :: end + self:push_state() self.subviews.pages:setSelected('unit_syndromes') self.subviews.unit_syndromes:setChoices(choices) + self.subviews.unit_syndromes.edit:setFocus(true) end function UnitSyndromes:showSyndromeEffects(index, choice) - local choices = {} + local choices = {'ID: '..tostring(choice.syndrome_type)} for _, effect in pairs(getSyndromeEffects(choice.syndrome_type)) do local effect_name = df.creature_interaction_effect_type[effect:getType()] @@ -533,6 +545,7 @@ function UnitSyndromes:showSyndromeEffects(index, choice) )) end + self:push_state() self.subviews.pages:setSelected('syndrome_effects') self.subviews.syndrome_effects.text_to_wrap = table.concat(choices, "\n\n") self.subviews.syndrome_effects:updateLayout() From 08e88d9835b441261037afadb0ff2bab462dc482 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Jul 2023 13:10:58 -0700 Subject: [PATCH 365/732] ensure in-building items are at depot before we mark them as such --- internal/caravan/movegoods.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index e2ae19e2d3..d1b27f3f5e 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -620,7 +620,7 @@ function MoveGoodsModal:onDismiss() local item = item_data.item if item_data.pending and not pending[item_id] then item.flags.forbid = false - if dfhack.items.getHolderBuilding(item) then + if dfhack.items.getHolderBuilding(item) == depot then item.flags.in_building = true else dfhack.items.markForTrade(item, depot) From f288780464352648b1829afb3a81128ee3cb0aeb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Jul 2023 13:55:24 -0700 Subject: [PATCH 366/732] ensure cache is initialized before first use --- internal/caravan/trade.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 437865ad7f..562efa6657 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -141,8 +141,6 @@ local FILTER_HEIGHT = 15 function Trade:init() self.cur_page = 1 - self.saved_talkline = trade.talkline - self:check_cache() self.animal_ethics = common.is_animal_lover_caravan(trade.mer) self.wood_ethics = common.is_tree_lover_caravan(trade.mer) @@ -304,6 +302,7 @@ function Trade:init() self.subviews.list.edit = self.subviews.search self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + self:check_cache() self.subviews.list:setChoices(self:get_choices()) end From 14552dde1fca00bec8fbcab4b361f36727b5f75c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Jul 2023 14:12:19 -0700 Subject: [PATCH 367/732] don't allow trade screen if traders aren't ready --- internal/caravan/trade.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 562efa6657..cc6e3e556c 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -705,6 +705,7 @@ function TradeOverlay:init() frame={t=0, l=0}, label='DFHack trade UI', key='CUSTOM_CTRL_T', + enabled=function() return trade.stillunloading == 0 and trade.havetalker == 1 end, on_activate=function() view = view and view:raise() or TradeScreen{}:show() end, }, widgets.Label{ From 181ac9e06b8a251cce5ff23819848efb7ea788da Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Jul 2023 16:23:57 -0700 Subject: [PATCH 368/732] expose key suppression preference in control panel --- gui/control-panel.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 24eba6376f..a7bf536b3f 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -74,6 +74,9 @@ local PREFERENCES = { desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, HIDE_ARMOK_TOOLS={label='Hide "armok" tools in command lists', type='bool', default=false, desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.'}, + SUPPRESS_DUPLICATE_KEYBOARD_EVENTS={label='Prevent duplicate key events', + type='bool', default=true, + desc='Whether to pass key events through to DF when DFHack keybindings are triggered.'}, }, ['gui']={ DEFAULT_INITIAL_PAUSE={label='DFHack tools autopause game', type='bool', default=true, From 87f5e68829e2c36bedcf47b1f0db2b94a3cc61f2 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:08:59 +0200 Subject: [PATCH 369/732] [suspendmanager] Don't consider branches suitable to access a build --- changelog.txt | 1 + suspendmanager.lua | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/changelog.txt b/changelog.txt index 6ad851bad1..cc1b67e186 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ that repo. - `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported - `gui/gm-unit`: fix commandline processing when a unit id is specified - `suspendmanager`: take in account already built blocking buildings +- `suspendmanager`: don't consider branches as a suitable access to a build ## Misc Improvements diff --git a/suspendmanager.lua b/suspendmanager.lua index b953479526..347bb9040d 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -205,6 +205,12 @@ local function walkable(pos) return false end local attrs = df.tiletype.attrs[tt] + + if attrs.shape == df.tiletype_shape.BRANCH or attrs.shape == df.tiletype_shape.TRUNK_BRANCH then + -- Branches can be walked on, but most of the time we can assume that it's not a suitable access. + return false + end + local shape_attrs = df.tiletype_shape.attrs[attrs.shape] if not shape_attrs.walkable then From 009e9f8e4e8e18ef1b2d9aebf7132a17278ddbbc Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 10 Jul 2023 18:15:12 +0200 Subject: [PATCH 370/732] Cleanup: Remove redundant checks for opentiles --- build-now.lua | 5 +---- deep-embark.lua | 6 +----- modtools/create-unit.lua | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/build-now.lua b/build-now.lua index 0008bab42e..f14e0d1bdc 100644 --- a/build-now.lua +++ b/build-now.lua @@ -176,11 +176,8 @@ local function is_good_dump_pos(pos) local shape_attrs = df.tiletype_shape.attrs[attrs.shape] -- reject hidden tiles if flags.hidden then return false, false end - -- reject unwalkable or open tiles + -- reject unwalkable tiles if not shape_attrs.walkable then return false, false end - if shape_attrs.basic_shape == df.tiletype_shape_basic.Open then - return false, false - end -- reject footprints within other buildings. this could potentially be -- relaxed a bit since we can technically dump items on passable tiles -- within other buildings, but that would look messy. diff --git a/deep-embark.lua b/deep-embark.lua index c72447a301..5953ae0dac 100644 --- a/deep-embark.lua +++ b/deep-embark.lua @@ -65,11 +65,7 @@ function isValidTiletype(tiletype) end end local shapeAttrs = df.tiletype_shape.attrs[tiletypeAttrs.shape] - if shapeAttrs.walkable and shapeAttrs.basic_shape ~= df.tiletype_shape_basic.Open then -- downward ramps are walkable but open; units placed here would fall - return true - else - return false - end + return shapeAttrs.walkable end function getValidEmbarkTiles(block) diff --git a/modtools/create-unit.lua b/modtools/create-unit.lua index 531497b6d5..687be8ae13 100644 --- a/modtools/create-unit.lua +++ b/modtools/create-unit.lua @@ -396,7 +396,7 @@ function isValidSpawnLocation(pos, locationType) end return false elseif locationType == 'Walkable' then - if tileShapeAttrs.walkable and tileShapeAttrs.basic_shape ~= df.tiletype_shape_basic.Open then + if tileShapeAttrs.walkable then return true end return false From e33f76240aa9972e4056132036b615ef07e9e89c Mon Sep 17 00:00:00 2001 From: master-spike Date: Tue, 11 Jul 2023 20:15:09 +0100 Subject: [PATCH 371/732] added script to empty rocks from all clogged wheelbarrows on the map --- fix/empty-wheelbarrows.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 fix/empty-wheelbarrows.lua diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua new file mode 100644 index 0000000000..9ca1a88c4b --- /dev/null +++ b/fix/empty-wheelbarrows.lua @@ -0,0 +1,16 @@ + +for _,e in ipairs(df.global.world.items.other.TOOL) do + if ((not e.flags.in_job) and e.flags.on_ground) then + if e.subtype.id == "ITEM_TOOL_WHEELBARROW" then + local items = dfhack.items.getContainedItems(e) + print('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) + if #items > 0 then + print('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) + for _,i in ipairs(items) do + print(' ' .. dfhack.items.getDescription(i, 0)) + dfhack.items.moveToGround(i, e.pos) + end + end + end + end +end \ No newline at end of file From 5e1827c84751347ad8f91f3b11771e10b9ae089f Mon Sep 17 00:00:00 2001 From: master-spike Date: Tue, 11 Jul 2023 20:24:34 +0100 Subject: [PATCH 372/732] added script description in comments --- fix/empty-wheelbarrows.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua index 9ca1a88c4b..77a46899bb 100644 --- a/fix/empty-wheelbarrows.lua +++ b/fix/empty-wheelbarrows.lua @@ -1,5 +1,10 @@ +--checks all wheelbarrows on map for rocks stuck in them. If a wheelbarrow isn't in use for a job (hauling) then there should be no rocks in them +--rocks will occasionally get stuck in wheelbarrows, and accumulate if the wheelbarrow gets used. +--this script empties all wheelbarrows which have rocks stuck in them. + for _,e in ipairs(df.global.world.items.other.TOOL) do + -- wheelbarrow must be on ground and not in a job if ((not e.flags.in_job) and e.flags.on_ground) then if e.subtype.id == "ITEM_TOOL_WHEELBARROW" then local items = dfhack.items.getContainedItems(e) From 65421f30288dbbb2c7a612fc2356be8096716b6e Mon Sep 17 00:00:00 2001 From: master-spike Date: Tue, 11 Jul 2023 20:38:31 +0100 Subject: [PATCH 373/732] fixed end of file --- fix/empty-wheelbarrows.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua index 77a46899bb..1e14e6e9ac 100644 --- a/fix/empty-wheelbarrows.lua +++ b/fix/empty-wheelbarrows.lua @@ -18,4 +18,4 @@ for _,e in ipairs(df.global.world.items.other.TOOL) do end end end -end \ No newline at end of file +end From 9b737efb40dc1e364dcbe494001acf569cea60ab Mon Sep 17 00:00:00 2001 From: master-spike Date: Tue, 11 Jul 2023 22:08:57 +0100 Subject: [PATCH 374/732] added quiet/dryrun opts and using isWheelbarrow() vmethod --- fix/empty-wheelbarrows.lua | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua index 1e14e6e9ac..2102140009 100644 --- a/fix/empty-wheelbarrows.lua +++ b/fix/empty-wheelbarrows.lua @@ -2,20 +2,36 @@ --rocks will occasionally get stuck in wheelbarrows, and accumulate if the wheelbarrow gets used. --this script empties all wheelbarrows which have rocks stuck in them. +local args = {...} + +local quiet = false +local dryrun = false +for i,arg in ipairs(args) do + if (arg == "--quiet") then + quiet = true + end + if (arg == "--dryrun") then + dryrun = true + end +end + +local i_count = 0 +local e_count = 0 for _,e in ipairs(df.global.world.items.other.TOOL) do -- wheelbarrow must be on ground and not in a job - if ((not e.flags.in_job) and e.flags.on_ground) then - if e.subtype.id == "ITEM_TOOL_WHEELBARROW" then - local items = dfhack.items.getContainedItems(e) - print('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) - if #items > 0 then - print('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) - for _,i in ipairs(items) do - print(' ' .. dfhack.items.getDescription(i, 0)) - dfhack.items.moveToGround(i, e.pos) - end + if ((not e.flags.in_job) and e.flags.on_ground and e:isWheelbarrow()) then + local items = dfhack.items.getContainedItems(e) + if #items > 0 then + if (not quiet) then print('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) end + e_count = e_count + 1 + for _,i in ipairs(items) do + if (not quiet) then print(' ' .. dfhack.items.getDescription(i, 0)) end + if (not dryrun) then dfhack.items.moveToGround(i, e.pos) end + i_count = i_count + 1 end end end end + +print("fix/empty-wheelbarrows - removed " .. i_count .. " items from " .. e_count .. " wheelbarrows.") From f8ef42b1420f05caaaa87eaa054d8109fae226d1 Mon Sep 17 00:00:00 2001 From: master-spike Date: Tue, 11 Jul 2023 22:09:39 +0100 Subject: [PATCH 375/732] fix/empty-wheelbarrows documentation --- docs/fix/empty-wheelbarrows.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/fix/empty-wheelbarrows.rst diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst new file mode 100644 index 0000000000..99b728a61d --- /dev/null +++ b/docs/fix/empty-wheelbarrows.rst @@ -0,0 +1,23 @@ +fix/empty-wheelbarrows +===================== + +.. dfhack-tool:: + :summary: Empties stuck items from wheelbarrows + :tags: fort bugfix items + +Empties all wheelbarrows which contain rocks that have become 'stuck' in them. + +This works around the issue encountered with :bug:`6074`, and should be run +if you notice wheelbarrows lying around with rocks in them that aren't +being used in a task. + + +Usage +----- + +``fix/empty-wheelbarrows`` + Empties all items, listing all wheelbarrows emptied and their contents +``fix/empty-wheelbarrows --dryrun`` + Lists all wheelbarrows that would be emptied and their contents without performing the action. +``fix/empty-wheelbarrows --quiet`` + Does the action while surpressing output to console \ No newline at end of file From 8863b9297ce45efe0bf1d9dc2bfe51277e12cd8b Mon Sep 17 00:00:00 2001 From: master-spike Date: Tue, 11 Jul 2023 22:12:26 +0100 Subject: [PATCH 376/732] fixed eof --- docs/fix/empty-wheelbarrows.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index 99b728a61d..75f37b9cf8 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -20,4 +20,4 @@ Usage ``fix/empty-wheelbarrows --dryrun`` Lists all wheelbarrows that would be emptied and their contents without performing the action. ``fix/empty-wheelbarrows --quiet`` - Does the action while surpressing output to console \ No newline at end of file + Does the action while surpressing output to console From f30ff5dbe259b264f7b25a5a0e91f4a1abbf4f2f Mon Sep 17 00:00:00 2001 From: master-spike Date: Tue, 11 Jul 2023 22:39:52 +0100 Subject: [PATCH 377/732] gated final print statement by quiet option --- fix/empty-wheelbarrows.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua index 2102140009..e693022a5b 100644 --- a/fix/empty-wheelbarrows.lua +++ b/fix/empty-wheelbarrows.lua @@ -18,15 +18,19 @@ end local i_count = 0 local e_count = 0 +local function printNotQuiet(str) + if (not quiet) then print(str) end +end + for _,e in ipairs(df.global.world.items.other.TOOL) do -- wheelbarrow must be on ground and not in a job if ((not e.flags.in_job) and e.flags.on_ground and e:isWheelbarrow()) then local items = dfhack.items.getContainedItems(e) if #items > 0 then - if (not quiet) then print('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) end + printNotQuiet('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) e_count = e_count + 1 for _,i in ipairs(items) do - if (not quiet) then print(' ' .. dfhack.items.getDescription(i, 0)) end + printNotQuiet(' ' .. dfhack.items.getDescription(i, 0)) if (not dryrun) then dfhack.items.moveToGround(i, e.pos) end i_count = i_count + 1 end @@ -34,4 +38,4 @@ for _,e in ipairs(df.global.world.items.other.TOOL) do end end -print("fix/empty-wheelbarrows - removed " .. i_count .. " items from " .. e_count .. " wheelbarrows.") +printNotQuiet("fix/empty-wheelbarrows - removed " .. i_count .. " items from " .. e_count .. " wheelbarrows.") From 3dc24beef2c5cf7e751e0b13e82c932e33542e39 Mon Sep 17 00:00:00 2001 From: master-spike Date: Tue, 11 Jul 2023 22:45:32 +0100 Subject: [PATCH 378/732] fixed title/underline mismatch --- docs/fix/empty-wheelbarrows.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index 75f37b9cf8..c0c1efdcb7 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -1,5 +1,5 @@ fix/empty-wheelbarrows -===================== +====================== .. dfhack-tool:: :summary: Empties stuck items from wheelbarrows From fa20810e58a6cbdcd175488e1a93aff4335f6a95 Mon Sep 17 00:00:00 2001 From: master-spike Date: Wed, 12 Jul 2023 17:52:06 +0100 Subject: [PATCH 379/732] now parsing args using processArgsGetopts() --- fix/empty-wheelbarrows.lua | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua index e693022a5b..b776f852b6 100644 --- a/fix/empty-wheelbarrows.lua +++ b/fix/empty-wheelbarrows.lua @@ -2,18 +2,17 @@ --rocks will occasionally get stuck in wheelbarrows, and accumulate if the wheelbarrow gets used. --this script empties all wheelbarrows which have rocks stuck in them. +local argparse = require("argparse") + local args = {...} local quiet = false local dryrun = false -for i,arg in ipairs(args) do - if (arg == "--quiet") then - quiet = true - end - if (arg == "--dryrun") then - dryrun = true - end -end + +local cmds = argparse.processArgsGetopt(args, { + {'q', 'quiet', handler=function() quiet = true end}, + {'n', 'dryrun', handler=function() dryrun = true end}, +}) local i_count = 0 local e_count = 0 @@ -38,4 +37,4 @@ for _,e in ipairs(df.global.world.items.other.TOOL) do end end -printNotQuiet("fix/empty-wheelbarrows - removed " .. i_count .. " items from " .. e_count .. " wheelbarrows.") +printNotQuiet(("fix/empty-wheelbarrows - removed %d items from %d wheelbarrows."):format(i_count, e_count)) From be20696392b8c94119896123499dbb0cccb4d81e Mon Sep 17 00:00:00 2001 From: master-spike Date: Wed, 12 Jul 2023 19:05:10 +0100 Subject: [PATCH 380/732] adjusted dry-run option to match precedent --- docs/fix/empty-wheelbarrows.rst | 12 +++++++++++- fix/empty-wheelbarrows.lua | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index c0c1efdcb7..f1505a399f 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -15,9 +15,19 @@ being used in a task. Usage ----- +``fix/empty-wheelbarrows [options]`` + +-q, --quiet surpress console output +-d, --dry-run dry run, don't commit changes + +Examples +-------- + ``fix/empty-wheelbarrows`` Empties all items, listing all wheelbarrows emptied and their contents -``fix/empty-wheelbarrows --dryrun`` +``fix/empty-wheelbarrows --dry-run`` Lists all wheelbarrows that would be emptied and their contents without performing the action. ``fix/empty-wheelbarrows --quiet`` Does the action while surpressing output to console +``repeat --name empty-wheelbarrows --time 1200 command [ fix/empty-wheelbarrows --quiet ]`` + Runs empty-wheelbarrows quietly every 1200 game ticks, which is once per in-game day. diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua index b776f852b6..828c1c7e4a 100644 --- a/fix/empty-wheelbarrows.lua +++ b/fix/empty-wheelbarrows.lua @@ -11,7 +11,7 @@ local dryrun = false local cmds = argparse.processArgsGetopt(args, { {'q', 'quiet', handler=function() quiet = true end}, - {'n', 'dryrun', handler=function() dryrun = true end}, + {'d', 'dry-run', handler=function() dryrun = true end}, }) local i_count = 0 From 33829ed1ce6344ce648b84b4be4f15aac43c71e5 Mon Sep 17 00:00:00 2001 From: master-spike Date: Thu, 13 Jul 2023 08:54:35 +0100 Subject: [PATCH 381/732] prints in quiet mode --- fix/empty-wheelbarrows.lua | 39 +++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua index 828c1c7e4a..b3c0234832 100644 --- a/fix/empty-wheelbarrows.lua +++ b/fix/empty-wheelbarrows.lua @@ -17,24 +17,33 @@ local cmds = argparse.processArgsGetopt(args, { local i_count = 0 local e_count = 0 -local function printNotQuiet(str) - if (not quiet) then print(str) end +local function emptyContainedItems(e, outputCallback) + local items = dfhack.items.getContainedItems(e) + if #items > 0 then + outputCallback('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) + e_count = e_count + 1 + for _,i in ipairs(items) do + outputCallback(' ' .. dfhack.items.getDescription(i, 0)) + if (not dryrun) then dfhack.items.moveToGround(i, e.pos) end + i_count = i_count + 1 + end + end end -for _,e in ipairs(df.global.world.items.other.TOOL) do - -- wheelbarrow must be on ground and not in a job - if ((not e.flags.in_job) and e.flags.on_ground and e:isWheelbarrow()) then - local items = dfhack.items.getContainedItems(e) - if #items > 0 then - printNotQuiet('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) - e_count = e_count + 1 - for _,i in ipairs(items) do - printNotQuiet(' ' .. dfhack.items.getDescription(i, 0)) - if (not dryrun) then dfhack.items.moveToGround(i, e.pos) end - i_count = i_count + 1 - end +local function emptyWheelbarrows(outputCallback) + for _,e in ipairs(df.global.world.items.other.TOOL) do + -- wheelbarrow must be on ground and not in a job + if ((not e.flags.in_job) and e.flags.on_ground and e:isWheelbarrow()) then + emptyContainedItems(e, outputCallback) end end end -printNotQuiet(("fix/empty-wheelbarrows - removed %d items from %d wheelbarrows."):format(i_count, e_count)) +local output +if (quiet) then output = (function(...) end) else output = print end + +emptyWheelbarrows(output) + +if (i_count > 0 or (not quiet)) then + print(("fix/empty-wheelbarrows - removed %d items from %d wheelbarrows."):format(i_count, e_count)) +end From d8e9a5d5294f1f31670f78440d64abd79f25577d Mon Sep 17 00:00:00 2001 From: master-spike Date: Thu, 13 Jul 2023 08:55:39 +0100 Subject: [PATCH 382/732] doc and changelog fix/empty-wheelbarrows updated --- changelog.txt | 1 + docs/fix/empty-wheelbarrows.rst | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index ce248c0511..48fd3fe8fa 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,7 @@ that repo. ## New Scripts - `caravan`: new trade screen UI replacements for bringing goods to trade depot and trading +- `fix/empty-wheelbarrows`: new script to empty stuck rocks from all wheelbarrows on the map ## Fixes - `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index f1505a399f..91a0205935 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -11,23 +11,28 @@ This works around the issue encountered with :bug:`6074`, and should be run if you notice wheelbarrows lying around with rocks in them that aren't being used in a task. - Usage ----- +:: -``fix/empty-wheelbarrows [options]`` - --q, --quiet surpress console output --d, --dry-run dry run, don't commit changes + fix/empty-wheelbarrows [options] Examples -------- ``fix/empty-wheelbarrows`` - Empties all items, listing all wheelbarrows emptied and their contents + Empties all items, listing all wheelbarrows emptied and their contents. ``fix/empty-wheelbarrows --dry-run`` Lists all wheelbarrows that would be emptied and their contents without performing the action. ``fix/empty-wheelbarrows --quiet`` Does the action while surpressing output to console ``repeat --name empty-wheelbarrows --time 1200 command [ fix/empty-wheelbarrows --quiet ]`` Runs empty-wheelbarrows quietly every 1200 game ticks, which is once per in-game day. + +Options +------- + +``-q``, ``--quiet`` + Surpress console output (final status update is still printed if at least one item was affected). +``-d``, ``--dry-run`` + Dry run, don't commit changes. From b255d828ae2abf49f865d8f5c1da35c5d31a2e95 Mon Sep 17 00:00:00 2001 From: master-spike Date: Thu, 13 Jul 2023 09:07:11 +0100 Subject: [PATCH 383/732] periods added --- docs/fix/empty-wheelbarrows.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index 91a0205935..30203f663f 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -2,7 +2,7 @@ fix/empty-wheelbarrows ====================== .. dfhack-tool:: - :summary: Empties stuck items from wheelbarrows + :summary: Empties stuck items from wheelbarrows. :tags: fort bugfix items Empties all wheelbarrows which contain rocks that have become 'stuck' in them. @@ -25,7 +25,7 @@ Examples ``fix/empty-wheelbarrows --dry-run`` Lists all wheelbarrows that would be emptied and their contents without performing the action. ``fix/empty-wheelbarrows --quiet`` - Does the action while surpressing output to console + Does the action while surpressing output to console. ``repeat --name empty-wheelbarrows --time 1200 command [ fix/empty-wheelbarrows --quiet ]`` Runs empty-wheelbarrows quietly every 1200 game ticks, which is once per in-game day. From 6f5d21ca852b02ff006e1d4701c96af9a1de91ba Mon Sep 17 00:00:00 2001 From: master-spike Date: Thu, 13 Jul 2023 23:22:58 +0100 Subject: [PATCH 384/732] added angle braces in doc [options] -> [] for clarity --- docs/fix/empty-wheelbarrows.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index 30203f663f..8f2cb24470 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -15,7 +15,7 @@ Usage ----- :: - fix/empty-wheelbarrows [options] + fix/empty-wheelbarrows [] Examples -------- From e4b4ae848c2d3052c711fa3af57778c3a9a840bb Mon Sep 17 00:00:00 2001 From: master-spike Date: Thu, 13 Jul 2023 23:31:32 +0100 Subject: [PATCH 385/732] added fix/empty-wheelbarrows to maintenance tab of gui/control-panel --- docs/fix/empty-wheelbarrows.rst | 5 ++--- gui/control-panel.lua | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index 8f2cb24470..a6070d3091 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -9,7 +9,8 @@ Empties all wheelbarrows which contain rocks that have become 'stuck' in them. This works around the issue encountered with :bug:`6074`, and should be run if you notice wheelbarrows lying around with rocks in them that aren't -being used in a task. +being used in a task. This script can also be set to run periodically in +the background by toggling the Maintenance task in `gui/control-panel`. Usage ----- @@ -26,8 +27,6 @@ Examples Lists all wheelbarrows that would be emptied and their contents without performing the action. ``fix/empty-wheelbarrows --quiet`` Does the action while surpressing output to console. -``repeat --name empty-wheelbarrows --time 1200 command [ fix/empty-wheelbarrows --quiet ]`` - Runs empty-wheelbarrows quietly every 1200 game ticks, which is once per in-game day. Options ------- diff --git a/gui/control-panel.lua b/gui/control-panel.lua index a7bf536b3f..f7c0910b18 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -119,6 +119,8 @@ local REPEATS = { ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, + ['empty-wheelbarrows']={ + command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-wheelbarrows', '-q', ']'}}, } local REPEATS_LIST = {} for k in pairs(REPEATS) do From de97384f07f05bb50714ddb737b01e8e3dcd62ee Mon Sep 17 00:00:00 2001 From: master-spike Date: Thu, 13 Jul 2023 23:44:14 +0100 Subject: [PATCH 386/732] fixed typo (stuck -> empty) --- gui/control-panel.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index f7c0910b18..4989a60efc 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -120,7 +120,8 @@ local REPEATS = { desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, ['empty-wheelbarrows']={ - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-wheelbarrows', '-q', ']'}}, + desc='Empties wheelbarrows which have rocks stuck in them.', + command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, } local REPEATS_LIST = {} for k in pairs(REPEATS) do From 6f2390c71e9d4fb396a363ef91a778fbe9341609 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 15 Jul 2023 14:32:56 -0700 Subject: [PATCH 387/732] protect against missing units --- autofish.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/autofish.lua b/autofish.lua index 13470d7c62..cdbf872e2c 100644 --- a/autofish.lua +++ b/autofish.lua @@ -85,7 +85,9 @@ function toggle_fishing_labour(state) for _,v2 in ipairs(v.assigned_units) do -- find unit by ID and toggle fishing local unit = df.unit.find(v2) - unit.status.labors.FISH = state + if unit then + unit.status.labors.FISH = state + end end end end From 38e016389e5595eec2b2eb2ce6234dcbe112a337 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:20:29 +0200 Subject: [PATCH 388/732] SuspendManager: job overlay --- suspendmanager.lua | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/suspendmanager.lua b/suspendmanager.lua index 347bb9040d..b6513ec208 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -8,6 +8,9 @@ local argparse = require('argparse') local eventful = require('plugins.eventful') local utils = require('utils') local repeatUtil = require('repeat-util') +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') local ok, buildingplan = pcall(require, 'plugins.buildingplan') if not ok then buildingplan = nil @@ -43,6 +46,15 @@ REASON_TEXT = { [REASON.DEADEND] = 'dead end', } +--- Description of suspension +--- This should likely not include description for any of the external +--- reasons +REASON_DESCRIPTION = { + [REASON.RISK_BLOCKING] = 'Risk blocking another', + [REASON.ERASE_DESIGNATION] = 'On a tile designation', + [REASON.DEADEND] = 'Blocking a dead-end' +} + --- Suspension reasons from an external source --- SuspendManager does not actively suspend such jobs, but --- will not unsuspend them @@ -378,6 +390,18 @@ function SuspendManager:shouldStaySuspended(job) return self.suspensions[job.id] end +function SuspendManager:suspensionDescription(job) + if not job then + return nil + end + local reason = self.suspensions[job.id] + if not reason then + return nil + end + + return REASON_DESCRIPTION[reason] +end + --- Recompute the list of suspended jobs function SuspendManager:refresh() self.suspensions = {} @@ -531,3 +555,42 @@ end if not dfhack_flags.module then main({...}) end + +-- Overlay Widget +JobOverlay = defclass(JobOverlay, overlay.OverlayWidget) +JobOverlay.ATTRS{ + default_pos={x=-41,y=14}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING', + frame={w=30, h=5}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function JobOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Staying suspended:' + }, + widgets.Label{ + frame={t=2, l=0}, + text={'', {text=self:callback('get_reason_string')}} + } + } +end + +function JobOverlay:get_reason_string() + return Instance:suspensionDescription(dfhack.gui.getSelectedJob()) +end + +function JobOverlay:render(dc) + if not isEnabled() or not self:get_reason_string() then + return + end + JobOverlay.super.render(self, dc) +end + +OVERLAY_WIDGETS = { + inspector=JobOverlay +} From 1731ceb90fb22657d0f14a76f6fa009be8966bae Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sun, 16 Jul 2023 19:28:16 +0200 Subject: [PATCH 389/732] changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 48fd3fe8fa..7236fc99f8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ that repo. ## Misc Improvements - `gui/unit-syndromes`: make lists searchable +- `suspendmanager`: display the suspension reason ## Removed From a14482f17903ab3e0e4bf9815b183edb27efd9f7 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sun, 16 Jul 2023 19:31:13 +0200 Subject: [PATCH 390/732] reword comment --- suspendmanager.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index b6513ec208..613be3d51e 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -47,8 +47,8 @@ REASON_TEXT = { } --- Description of suspension ---- This should likely not include description for any of the external ---- reasons +--- This only cover the reason where suspendmanager actively +--- suspend jobs REASON_DESCRIPTION = { [REASON.RISK_BLOCKING] = 'Risk blocking another', [REASON.ERASE_DESIGNATION] = 'On a tile designation', @@ -390,6 +390,8 @@ function SuspendManager:shouldStaySuspended(job) return self.suspensions[job.id] end +--- Return a human readable description of why suspendmanager keeps a job suspended +--- or nil if the job is not kept suspended function SuspendManager:suspensionDescription(job) if not job then return nil From 5fca9408015efbb5aa29dc6cab40aec90eca5a75 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 17 Jul 2023 14:43:01 +0200 Subject: [PATCH 391/732] Address PR comments, move the overlay on top of the "suspend" button --- changelog.txt | 2 +- suspendmanager.lua | 101 ++++++++++++++++++++++++++++++++------------- 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7236fc99f8..76a5f3ae58 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,7 +25,7 @@ that repo. ## Misc Improvements - `gui/unit-syndromes`: make lists searchable -- `suspendmanager`: display the suspension reason +- `suspendmanager`: display the suspension reason when viewing a suspended building ## Removed diff --git a/suspendmanager.lua b/suspendmanager.lua index 613be3d51e..b00204508e 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -50,9 +50,9 @@ REASON_TEXT = { --- This only cover the reason where suspendmanager actively --- suspend jobs REASON_DESCRIPTION = { - [REASON.RISK_BLOCKING] = 'Risk blocking another', - [REASON.ERASE_DESIGNATION] = 'On a tile designation', - [REASON.DEADEND] = 'Blocking a dead-end' + [REASON.RISK_BLOCKING] = 'May block another build job', + [REASON.ERASE_DESIGNATION] = 'Waiting for carve/smooth/engrave on same tile', + [REASON.DEADEND] = 'Blocks another build job' } --- Suspension reasons from an external source @@ -184,6 +184,12 @@ local FILTER_JOB_TYPES = utils.invert{ df.job_type.RemoveStairs, } +--- Returns true if the job is a planned job from buildingplan +local function isBuildingPlanJob(job) + local bld = dfhack.job.getHolder(job) + return bld and buildingplan and buildingplan.isPlannedBuilding(bld) +end + --- Check if a building is blocking once constructed ---@param building building_constructionst|building ---@return boolean @@ -394,14 +400,15 @@ end --- or nil if the job is not kept suspended function SuspendManager:suspensionDescription(job) if not job then - return nil + return '' end local reason = self.suspensions[job.id] - if not reason then - return nil - end - - return REASON_DESCRIPTION[reason] + return reason and REASON_DESCRIPTION[reason] or "External interruption" +-- if not reason then +-- return "External Interruption" +-- end +-- +-- return REASON_DESCRIPTION[reason] or "External Interruption" end --- Recompute the list of suspended jobs @@ -415,8 +422,7 @@ function SuspendManager:refresh() self.suspensions[job.id]=REASON.UNDER_WATER end - local bld = dfhack.job.getHolder(job) - if bld and buildingplan and buildingplan.isPlannedBuilding(bld) then + if isBuildingPlanJob(job) then self.suspensions[job.id]=REASON.BUILDINGPLAN end end @@ -558,41 +564,80 @@ if not dfhack_flags.module then main({...}) end --- Overlay Widget -JobOverlay = defclass(JobOverlay, overlay.OverlayWidget) -JobOverlay.ATTRS{ - default_pos={x=-41,y=14}, +-- Overlay Widgets +StatusOverlay = defclass(StatusOverlay, overlay.OverlayWidget) +StatusOverlay.ATTRS{ + default_pos={x=-39,y=16}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', - frame={w=30, h=5}, + frame={w=59, h=3}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } -function JobOverlay:init() +function StatusOverlay:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text='Staying suspended:' + text={ + {text=self:callback('get_status_string')} + } }, - widgets.Label{ - frame={t=2, l=0}, - text={'', {text=self:callback('get_reason_string')}} - } } end -function JobOverlay:get_reason_string() - return Instance:suspensionDescription(dfhack.gui.getSelectedJob()) +function StatusOverlay:get_status_string() + local job = dfhack.gui.getSelectedJob() + if job and job.flags.suspend then + return "Suspended because: " .. Instance:suspensionDescription(job) .. "." + end + return "Not suspended." +end + +function StatusOverlay:render(dc) + local job = dfhack.gui.getSelectedJob() + if not job or not isEnabled() or isBuildingPlanJob(job) then + return + end + StatusOverlay.super.render(self, dc) +end + +ToggleOverlay = defclass(ToggleOverlay, overlay.OverlayWidget) +ToggleOverlay.ATTRS{ + default_pos={x=-57,y=23}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING', + frame={w=40, h=1}, + frame_background=gui.CLEAR_PEN, +} + +function ToggleOverlay:init() + self:addviews{ + widgets.ToggleHotkeyLabel{ + view_id="enable_toggle", + frame={t=0, l=0, w=34}, + label="Suspendmanager is", + key="CUSTOM_CTRL_M", + options={{value=true, label="Enabled"}, + {value=false, label="Disabled"}}, + initial_option = isEnabled(), + on_change=function(val) dfhack.run_command{val and "enable" or "disable", "suspendmanager"} end + }, + } end -function JobOverlay:render(dc) - if not isEnabled() or not self:get_reason_string() then +function ToggleOverlay:render(dc) + local job = dfhack.gui.getSelectedJob() + if not job or isBuildingPlanJob(job) then return end - JobOverlay.super.render(self, dc) + -- Update the option: the "initial_option" value is not up to date since the widget + -- is not reinitialized for overlays + self.subviews.enable_toggle:setOption(isEnabled(), false) + ToggleOverlay.super.render(self, dc) end OVERLAY_WIDGETS = { - inspector=JobOverlay + status=StatusOverlay, + toggle=ToggleOverlay, } From 09e75faebf6244a287187eaf51d37332c539aa8a Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 17 Jul 2023 14:45:40 +0200 Subject: [PATCH 392/732] shorten reason --- suspendmanager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index b00204508e..139445a8e9 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -51,7 +51,7 @@ REASON_TEXT = { --- suspend jobs REASON_DESCRIPTION = { [REASON.RISK_BLOCKING] = 'May block another build job', - [REASON.ERASE_DESIGNATION] = 'Waiting for carve/smooth/engrave on same tile', + [REASON.ERASE_DESIGNATION] = 'Waiting for carve/smooth/engrave', [REASON.DEADEND] = 'Blocks another build job' } From 9904b61c755ce06be30cb9bb51b1222a4ea81e8f Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 17 Jul 2023 15:00:52 +0200 Subject: [PATCH 393/732] remove commented code --- suspendmanager.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 139445a8e9..0e735c6c97 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -404,11 +404,6 @@ function SuspendManager:suspensionDescription(job) end local reason = self.suspensions[job.id] return reason and REASON_DESCRIPTION[reason] or "External interruption" --- if not reason then --- return "External Interruption" --- end --- --- return REASON_DESCRIPTION[reason] or "External Interruption" end --- Recompute the list of suspended jobs From d4abe819e04176b3d22aac3600312fe410634e54 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 17 Jul 2023 15:01:51 +0200 Subject: [PATCH 394/732] remove redundant check --- suspendmanager.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 0e735c6c97..356e0d3209 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -399,9 +399,6 @@ end --- Return a human readable description of why suspendmanager keeps a job suspended --- or nil if the job is not kept suspended function SuspendManager:suspensionDescription(job) - if not job then - return '' - end local reason = self.suspensions[job.id] return reason and REASON_DESCRIPTION[reason] or "External interruption" end From 22bab162b0d168ed23c8c0897e70356069add839 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 17 Jul 2023 15:03:31 +0200 Subject: [PATCH 395/732] Update comment --- suspendmanager.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 356e0d3209..17c9e89055 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -397,7 +397,7 @@ function SuspendManager:shouldStaySuspended(job) end --- Return a human readable description of why suspendmanager keeps a job suspended ---- or nil if the job is not kept suspended +--- or "External interruption" if the job is not kept suspended by suspendmanager function SuspendManager:suspensionDescription(job) local reason = self.suspensions[job.id] return reason and REASON_DESCRIPTION[reason] or "External interruption" @@ -581,7 +581,7 @@ end function StatusOverlay:get_status_string() local job = dfhack.gui.getSelectedJob() if job and job.flags.suspend then - return "Suspended because: " .. Instance:suspensionDescription(job) .. "." + return "Suspended because: " .. Instance:suspensionDescription(job) or "External interruption" .. "." end return "Not suspended." end From df5e02b732432f91fd840783cf3269d9c57e12a6 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:05:16 +0200 Subject: [PATCH 396/732] only render for building construction --- suspendmanager.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 17c9e89055..0523a2b57e 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -588,7 +588,7 @@ end function StatusOverlay:render(dc) local job = dfhack.gui.getSelectedJob() - if not job or not isEnabled() or isBuildingPlanJob(job) then + if not job or job.job_type ~= df.job_type.ConstructBuilding or not isEnabled() or isBuildingPlanJob(job) then return end StatusOverlay.super.render(self, dc) @@ -620,7 +620,7 @@ end function ToggleOverlay:render(dc) local job = dfhack.gui.getSelectedJob() - if not job or isBuildingPlanJob(job) then + if not job or job.job_type ~= df.job_type.ConstructBuilding or isBuildingPlanJob(job) then return end -- Update the option: the "initial_option" value is not up to date since the widget From 2a177f1de01a30de1c5870c745df724528e96466 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 19 Jul 2023 00:34:42 -0700 Subject: [PATCH 397/732] convert keystroke pref to a cpp pref --- gui/control-panel.lua | 64 ++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 4989a60efc..480c9e58f4 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -74,9 +74,6 @@ local PREFERENCES = { desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, HIDE_ARMOK_TOOLS={label='Hide "armok" tools in command lists', type='bool', default=false, desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.'}, - SUPPRESS_DUPLICATE_KEYBOARD_EVENTS={label='Prevent duplicate key events', - type='bool', default=true, - desc='Whether to pass key events through to DF when DFHack keybindings are triggered.'}, }, ['gui']={ DEFAULT_INITIAL_PAUSE={label='DFHack tools autopause game', type='bool', default=true, @@ -93,6 +90,17 @@ local PREFERENCES = { desc='Whether to search for a match in the full text (true) or just at the start of words (false).'}, }, } +local CPP_PREFERENCES = { + { + label='Prevent duplicate key events', + type='bool', + default=true, + desc='Whether to pass key events through to DF when DFHack keybindings are triggered.', + init_fmt=':lua dfhack.internal.setSuppressDuplicateKeyboardEvents(%s)', + get_fn=dfhack.internal.getSuppressDuplicateKeyboardEvents, + set_fn=dfhack.internal.setSuppressDuplicateKeyboardEvents, + }, +} local REPEATS = { ['autoMilkCreature']={ @@ -648,25 +656,33 @@ function Preferences:onInput(keys) return handled end +local function make_preference_text(label, value) + return { + {tile=BUTTON_PEN_LEFT}, + {tile=CONFIGURE_PEN_CENTER}, + {tile=BUTTON_PEN_RIGHT}, + ' ', + ('%s (%s)'):format(label, value), + } +end + function Preferences:refresh() if self.subviews.input_dlg.visible then return end local choices = {} for ctx_name,settings in pairs(PREFERENCES) do local ctx_env = require(ctx_name) for id,spec in pairs(settings) do - local label = ('%s (%s)'):format(spec.label, ctx_env[id]) - local text = { - {tile=BUTTON_PEN_LEFT}, - {tile=CONFIGURE_PEN_CENTER}, - {tile=BUTTON_PEN_RIGHT}, - ' ', - label, - } + local text = make_preference_text(spec.label, ctx_env[id]) table.insert(choices, - {text=text, desc=spec.desc, search_key=label, + {text=text, desc=spec.desc, search_key=text[#text], ctx_env=ctx_env, id=id, spec=spec}) end end + for _,spec in ipairs(CPP_PREFERENCES) do + local text = make_preference_text(spec.label, spec.get_fn()) + table.insert(choices, + {text=text, desc=spec.desc, search_key=text[#text], spec=spec}) + end table.sort(choices, function(a, b) return a.spec.label < b.spec.label end) local list = self.subviews.list local filter = list:getFilter() @@ -677,7 +693,11 @@ function Preferences:refresh() end local function preferences_set_and_save(self, choice, val) - choice.ctx_env[choice.id] = val + if choice.spec.set_fn then + choice.spec.set_fn(val) + else + choice.ctx_env[choice.id] = val + end self:do_save() self:refresh() end @@ -685,11 +705,16 @@ end function Preferences:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end + local cur_val + if choice.spec.get_fn then + cur_val = choice.spec.get_fn() + else + cur_val = choice.ctx_env[choice.id] + end if choice.spec.type == 'bool' then - preferences_set_and_save(self, choice, not choice.ctx_env[choice.id]) + preferences_set_and_save(self, choice, not cur_val) elseif choice.spec.type == 'int' then - self.subviews.input_dlg:show(choice.id, choice.spec, - choice.ctx_env[choice.id]) + self.subviews.input_dlg:show(choice.id or choice.spec.label, choice.spec, cur_val) end end @@ -708,6 +733,10 @@ function Preferences:do_save() ctx_name, id, tostring(ctx_env[id]))) end end + for _,spec in ipairs(CPP_PREFERENCES) do + local line = spec.init_fmt:format(spec.get_fn()) + f:write(('%s\n'):format(line)) + end end save_file(PREFERENCES_INIT_FILE, save_fn) end @@ -719,6 +748,9 @@ function Preferences:restore_defaults() ctx_env[id] = spec.default end end + for _,spec in ipairs(CPP_PREFERENCES) do + spec.set_fn(spec.default) + end os.remove(PREFERENCES_INIT_FILE) self:refresh() dialogs.showMessage('Success', 'Default preferences restored.') From 2be31633455e13d7a9b79b5c3f450927d4499230 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Wed, 19 Jul 2023 21:54:33 +0200 Subject: [PATCH 398/732] remove redundant suspension description --- suspendmanager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 0523a2b57e..bbb0b79eb1 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -581,7 +581,7 @@ end function StatusOverlay:get_status_string() local job = dfhack.gui.getSelectedJob() if job and job.flags.suspend then - return "Suspended because: " .. Instance:suspensionDescription(job) or "External interruption" .. "." + return "Suspended because: " .. Instance:suspensionDescription(job) .. "." end return "Not suspended." end From ae7c3a01cf908ffc2caf61e3119d786ae39a499b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Jul 2023 17:48:54 -0700 Subject: [PATCH 399/732] add modded blueprint libraries to the blueprint list --- changelog.txt | 1 + docs/quickfort.rst | 3 ++- internal/quickfort/list.lua | 53 +++++++++++++++++++++++++++++-------- quickfort.lua | 15 ++++++++++- 4 files changed, 59 insertions(+), 13 deletions(-) diff --git a/changelog.txt b/changelog.txt index 76a5f3ae58..f156b5d757 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,7 @@ that repo. ## Misc Improvements - `gui/unit-syndromes`: make lists searchable - `suspendmanager`: display the suspension reason when viewing a suspended building +- `quickfort`: blueprint libraries are now moddable -- add a ``blueprints/`` directory to your mod and they'll show up in `quickfort` and `gui/quickfort`! ## Removed diff --git a/docs/quickfort.rst b/docs/quickfort.rst index bf9dec67e4..7f5d6d1107 100644 --- a/docs/quickfort.rst +++ b/docs/quickfort.rst @@ -22,7 +22,8 @@ paste sections of your fort if you need to. There are many ready-to-use blueprints in the `blueprint library ` that is distributed with DFHack, so you can use this tool productively even if you haven't created any blueprints -yourself. +yourself. Additional library blueprints can be +`added with mods ` as well. Usage ----- diff --git a/internal/quickfort/list.lua b/internal/quickfort/list.lua index 0d483f837b..9ede350a97 100644 --- a/internal/quickfort/list.lua +++ b/internal/quickfort/list.lua @@ -5,20 +5,45 @@ if not dfhack_flags.module then qerror('this script cannot be called directly') end +local scriptmanager = require('script-manager') local utils = require('utils') local xlsxreader = require('plugins.xlsxreader') local quickfort_parse = reqscript('internal/quickfort/parse') local quickfort_set = reqscript('internal/quickfort/set') --- blueprint_name is relative to the blueprints dir +blueprint_dirs = blueprint_dirs or nil + +local function get_blueprint_dirs() + if blueprint_dirs then return blueprint_dirs end + blueprint_dirs = {} + for _,v in ipairs(scriptmanager.get_mod_paths('blueprints')) do + blueprint_dirs[v.id] = v.path + end + return blueprint_dirs +end + function get_blueprint_filepath(blueprint_name) - local is_library = blueprint_name:startswith('library/') - if is_library then blueprint_name = blueprint_name:sub(9) end - return ('%s/%s'):format( - is_library and - quickfort_set.get_setting('blueprints_library_dir') or - quickfort_set.get_setting('blueprints_user_dir'), + local fullpath = ('%s/%s'):format( + quickfort_set.get_setting('blueprints_user_dir'), + blueprint_name) + if dfhack.filesystem.exists(fullpath) then + return fullpath + end + local dirmap = get_blueprint_dirs() + local _, _, prefix = blueprint_name:find('^([^/]+)/') + if not prefix then + return fullpath + end + blueprint_name = blueprint_name:sub(#prefix + 2) + if prefix == 'library' then + return ('%s/%s'):format( + quickfort_set.get_setting('blueprints_library_dir'), blueprint_name) + end + if not dirmap[prefix] then + return fullpath + end + return ('%s/%s'):format(dirmap[prefix], blueprint_name) end local blueprint_cache = {} @@ -92,15 +117,16 @@ end local blueprints, blueprint_modes, file_scope_aliases = {}, {}, {} local num_library_blueprints = 0 -local function scan_blueprint_dir(bp_dir, is_library) +local function scan_blueprint_dir(bp_dir, library_prefix) local paths = dfhack.filesystem.listdir_recursive(bp_dir, nil, false) if not paths then dfhack.printerr(('Cannot find blueprints directory: "%s"'):format(bp_dir)) return end + local is_library = library_prefix and #library_prefix > 0 for _, v in ipairs(paths) do local file_aliases = {} - local path = (is_library and 'library/' or '') .. v.path + local path = (library_prefix or '') .. v.path if not v.isdir and v.path:lower():endswith('.csv') then local modelines, aliases = scan_csv_blueprint(path) file_aliases = aliases @@ -163,8 +189,13 @@ end local function scan_blueprints() blueprints, blueprint_modes, file_scope_aliases = {}, {}, {} num_library_blueprints = 0 - scan_blueprint_dir(quickfort_set.get_setting('blueprints_user_dir'), false) - scan_blueprint_dir(quickfort_set.get_setting('blueprints_library_dir'), true) + scan_blueprint_dir(quickfort_set.get_setting('blueprints_user_dir')) + scan_blueprint_dir(quickfort_set.get_setting('blueprints_library_dir'), 'library/') + for id,path in pairs(get_blueprint_dirs()) do + if id ~= 'library' then + scan_blueprint_dir(path, id..'/') + end + end end function get_blueprint_by_number(list_num) diff --git a/quickfort.lua b/quickfort.lua index 70e991f6e1..0835689318 100644 --- a/quickfort.lua +++ b/quickfort.lua @@ -9,6 +9,8 @@ local quickfort_common = reqscript('internal/quickfort/common') local quickfort_list = reqscript('internal/quickfort/list') local quickfort_set = reqscript('internal/quickfort/set') +local GLOBAL_KEY = 'quickfort' + function refresh_scripts() -- reqscript all internal files here, even if they're not directly used by this -- top-level file. this ensures modified transitive dependencies are properly @@ -65,9 +67,20 @@ local function do_gui(params) dfhack.run_script('gui/quickfort', table.unpack(params)) end +local function do_reset() + quickfort_list.blueprint_dirs = nil + quickfort_set.do_reset() +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_WORLD_LOADED or sc == SC_WORLD_UNLOADED then + do_reset() + end +end + local action_switch = { set=quickfort_set.do_set, - reset=quickfort_set.do_reset, + reset=do_reset, list=quickfort_list.do_list, gui=do_gui, run=quickfort_command.do_command, From 3d31d0ed9f116ea6de9afc37b6e7f1c69ebede77 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Jul 2023 19:18:33 -0700 Subject: [PATCH 400/732] adapt to changes in pathable plugin --- gui/pathable.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gui/pathable.lua b/gui/pathable.lua index b59bf0d1c4..863e625ece 100644 --- a/gui/pathable.lua +++ b/gui/pathable.lua @@ -34,11 +34,11 @@ function Pathable:init() initial_option=true, }, widgets.ToggleHotkeyLabel{ - view_id='skip', + view_id='show', frame={t=2, l=0}, key='CUSTOM_CTRL_U', - label='Skip unrevealed', - initial_option=true, + label='Show hidden', + initial_option=false, }, widgets.EditField{ view_id='group', @@ -63,22 +63,22 @@ function Pathable:onRenderBody() self.saved_target = target local group = self.subviews.group - local skip = self.subviews.skip:getOptionValue() + local show = self.subviews.show:getOptionValue() if not target then group:setText('') return - elseif skip and not dfhack.maps.isTileVisible(target) then + elseif not show and not dfhack.maps.isTileVisible(target) then group:setText('Hidden') return end local block = dfhack.maps.getTileBlock(target) - local walk_group = block.walkable[target.x % 16][target.y % 16] + local walk_group = block and block.walkable[target.x % 16][target.y % 16] or 0 group:setText(walk_group == 0 and 'None' or tostring(walk_group)) if self.subviews.draw:getOptionValue() then - plugin.paintScreen(target, skip) + plugin.paintScreenPathable(target, show) end end From 67d4e63daeb30986e8f318c96c58d105a00c468d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Jul 2023 23:25:54 -0700 Subject: [PATCH 401/732] align unsuspend docs with implementation --- docs/unsuspend.rst | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/unsuspend.rst b/docs/unsuspend.rst index b82811a734..f6c2e9bb72 100644 --- a/docs/unsuspend.rst +++ b/docs/unsuspend.rst @@ -33,17 +33,21 @@ Overlay ------- This script also provides an overlay that is managed by the `overlay` framework. -When the overlay is enabled, a letter will appear over suspended buildings: - -- ``P`` (green in ASCII mode) indicates that the building still in planning mode - and is waiting on materials. The `buildingplan` plugin will unsuspend it for - you when those materials become available. -- ``x`` (yellow in ASCII mode) means that the building is suspended and that you - can unsuspend it manually or with the `unsuspend` command. -- ``X`` (red in ASCII mode) means that the building has been re-suspended - multiple times, and that you might need to look into whatever is preventing - the building from being built. +When the overlay is enabled, an icon or letter will appear over suspended +buildings: + +- A clock icon (green ``P`` in ASCII mode) indicates that the building is still + in planning mode and is waiting on materials. The `buildingplan` plugin will + unsuspend it for you when those materials become available. +- A yellow ``x`` means that the building is suspended. If you don't have + `suspendmanager` managing suspensions for you, you can unsuspend it + manually or with the `unsuspend` command. +- A red ``X`` means that the building has been re-suspended multiple times. + You might need to look into whatever is preventing the building from being + built (e.g. the building material for the building is inaccessible or there + is an in-use item blocking the building site). Note that in ASCII mode the letter will only appear when the game is paused -since it takes up the whole tile. In graphics mode, the letter can appear even -when the game is unpaused since you can still see the building underneath. +since it takes up the whole tile and makes the underlying building invisible. +In graphics mode, the icon only covers part of the building and so can always +be visible. From dd03f13e6c88309b732b298f84b72269144bf26d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Jul 2023 14:25:22 -0700 Subject: [PATCH 402/732] add dwarfvet to control panel list --- gui/control-panel.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 480c9e58f4..4ee57ea6e5 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -22,6 +22,7 @@ local FORT_SERVICES = { 'autolabor', 'autonestbox', 'autoslab', + 'dwarfvet', 'emigration', 'fastdwarf', 'fix/protect-nicks', From 43bec58960c24cfe0e318699921ffa1675e16d47 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Jul 2023 15:28:26 -0700 Subject: [PATCH 403/732] adjust to EditField behavior changes --- gui/launcher.lua | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index 31f26b1865..ca0597f4b8 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -264,8 +264,13 @@ function EditPanel:reset_history_idx() self.history_idx = #history + 1 end -function EditPanel:set_text(text) - self.subviews.editfield:setText(text) +function EditPanel:set_text(text, inhibit_change_callback) + local edit = self.subviews.editfield + if inhibit_change_callback then + edit.on_change = nil + end + edit:setText(text) + edit.on_change = self.on_change self:reset_history_idx() end @@ -629,7 +634,7 @@ function LauncherUI:init(args) end function LauncherUI:update_help(text, firstword, show_help) - local firstword = firstword or get_first_word(text) + firstword = firstword or get_first_word(text) if firstword == self.firstword then return end @@ -732,7 +737,7 @@ end function LauncherUI:on_autocomplete(_, option) if option then - self.subviews.edit:set_text(option.text) + self.subviews.edit:set_text(option.text..' ', true) self:update_help(option.text) end end @@ -773,7 +778,6 @@ function LauncherUI:run_command(reappear, command) end -- reappear and show the command output self.subviews.edit:set_text('') - self:on_edit_input('') if #output == 0 then output = 'Command finished successfully' else From ee7cf731749351e28de1f8fb676ec833774bc642 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Jul 2023 18:11:41 -0700 Subject: [PATCH 404/732] refine trade uis --- caravan.lua | 1 + internal/caravan/common.lua | 166 +++++++++++++++++++++++++++++++-- internal/caravan/movegoods.lua | 21 +++-- internal/caravan/trade.lua | 59 +++++++++--- 4 files changed, 216 insertions(+), 31 deletions(-) diff --git a/caravan.lua b/caravan.lua index 70b949a3aa..2d8d29f241 100644 --- a/caravan.lua +++ b/caravan.lua @@ -16,6 +16,7 @@ end OVERLAY_WIDGETS = { trade=trade.TradeOverlay, + tradebanner=trade.TradeBannerOverlay, tradeagreement=tradeagreement.TradeAgreementOverlay, movegoods=movegoods.MoveGoodsOverlay, assigntrade=movegoods.AssignTradeOverlay, diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index d26d0b0039..af8768085d 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -1,6 +1,7 @@ --@ module = true local dialogs = require('gui.dialogs') +local scriptmanager = require('script-manager') local widgets = require('gui.widgets') CH_UP = string.char(30) @@ -50,6 +51,12 @@ local function get_threshold(broker_skill) return math.huge end +local function estimate(value, round_base, granularity) + local rounded = ((value+round_base)//granularity)*granularity + local clamped = math.max(rounded, granularity) + return clamped +end + -- If the item's value is below the threshold, it gets shown exactly as-is. -- Otherwise, if it's less than or equal to [threshold + 50], it will round to the nearest multiple of 10 as an Estimate -- Otherwise, if it's less than or equal to [threshold + 50] * 3, it will round to the nearest multiple of 100 @@ -59,10 +66,10 @@ function obfuscate_value(value) local threshold = get_threshold(get_broker_skill()) if value < threshold then return tostring(value) end threshold = threshold + 50 - if value <= threshold then return ('~%d'):format(((value+5)//10)*10) end - if value <= threshold*3 then return ('~%d'):format(((value+50)//100)*100) end - if value <= threshold*30 then return ('~%d'):format(((value+500)//1000)*1000) end - return ('%d?'):format(((threshold*30 + 999)//1000)*1000) + if value <= threshold then return ('~%d'):format(estimate(value, 5, 10)) end + if value <= threshold*3 then return ('~%d'):format(estimate(value, 50, 100)) end + if value <= threshold*30 then return ('~%d'):format(estimate(value, 500, 1000)) end + return ('%d?'):format(estimate(threshold*30, 999, 1000)) end local function to_title_case(str) @@ -175,7 +182,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=5, l=0, r=0, h=4}, + frame={t=6, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_quality'..suffix, @@ -240,7 +247,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=10, l=0, r=0, h=4}, + frame={t=12, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_value'..suffix, @@ -396,10 +403,13 @@ function get_risky_items(banned_items) return risky_items end +local function to_item_type_str(item_type) + return string.lower(df.item_type[item_type]):gsub('_', ' ') +end + local function make_item_description(item_type, subtype) local itemdef = dfhack.items.getSubtypeDef(item_type, subtype) - return itemdef and string.lower(itemdef.name) or - string.lower(df.item_type[item_type]):gsub('_', ' ') + return itemdef and string.lower(itemdef.name) or to_item_type_str(item_type) end local function get_banned_token(banned_items) @@ -448,7 +458,96 @@ local function get_ethics_token(animal_ethics, wood_ethics) } end -function get_info_widgets(self, export_agreements) +local PREDICATE_LIBRARY = { + {name='weapons-grade metal', match=function(item) + if item.mat_type ~= 0 then return false end + local flags = df.global.world.raws.inorganics[item.mat_index].material.flags + return flags.IS_METAL and + (flags.ITEMS_METAL or flags.ITEMS_WEAPON or flags.ITEMS_WEAPON_RANGED or flags.ITEMS_AMMO or flags.ITEMS_ARMOR) + end}, +} +for _,item_type in ipairs(df.item_type) do + table.insert(PREDICATE_LIBRARY, { + name=to_item_type_str(item_type), + group='item type', + match=function(item) return item_type == item:getType() end, + }) +end + +local PREDICATES_VAR = 'PREDICATES' + +local function get_user_predicates() + local user_predicates = {} + local load_user_predicates = function(env_name, env) + local predicates = env[PREDICATES_VAR] + if not predicates then return end + if type(predicates) ~= 'table' then + dfhack.printerr( + ('error loading predicates from "%s": %s map is malformed') + :format(env_name, PREDICATES_VAR)) + return + end + for i,predicate in ipairs(predicates) do + if type(predicate) ~= 'table' then + dfhack.printerr(('error loading predicate %s:%d (must be a table)'):format(env_name, i)) + goto continue + end + if type(predicate.name) ~= 'string' or #predicate.name == 0 then + dfhack.printerr(('error loading predicate %s:%d (must have a string "name" field)'):format(env_name, i)) + goto continue + end + if type(predicate.match) ~= 'function' then + dfhack.printerr(('error loading predicate %s:%d (must have a function "match" field)'):format(env_name, i)) + goto continue + end + table.insert(user_predicates, {id=('%s:%s'):format(env_name, predicate.name), name=predicate.name, match=predicate.match}) + ::continue:: + end + end + scriptmanager.foreach_module_script(load_user_predicates) + return user_predicates +end + +local function customize_predicates(predicates, on_close) + local user_predicates = get_user_predicates() + local predicate = nil + if #user_predicates > 0 then + predicate = user_predicates[1] + else + predicate = PREDICATE_LIBRARY[1] + end + predicates[predicate.name] = {match=predicate.match, show=true} + on_close() +end + +local function make_predicate_str(predicates) + local preset, names = nil, {} + for name, predicate in pairs(predicates) do + if not preset then + preset = predicate.preset or '' + end + if #preset > 0 and preset ~= predicate.preset then + preset = '' + end + table.insert(names, name) + end + if preset and #preset > 0 then + return preset + end + if #names > 0 then + return table.concat(names, ', ') + end + return 'All' +end + +local function get_context_predicates(context) + return {} +end + +function get_info_widgets(self, export_agreements, context) + self.predicates = get_context_predicates(context) + local predicate_str = make_predicate_str(self.predicates) + return { widgets.Panel{ frame={t=0, l=0, r=0, h=2}, @@ -537,9 +636,58 @@ function get_info_widgets(self, export_agreements) }, }, }, + widgets.Panel{ + frame={t=13, l=0, r=0, h=2}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Advanced filter:', + }, + widgets.HotkeyLabel{ + frame={t=0, l=18, w=9}, + key='CUSTOM_SHIFT_J', + label='[edit]', + on_activate=function() + customize_predicates(self.predicates, + function() + predicate_str = make_predicate_str(self.predicates) + self:refresh_list() + end) + end, + }, + widgets.HotkeyLabel{ + frame={t=0, l=34, w=10}, + key='CUSTOM_SHIFT_K', + label='[clear]', + text_pen=COLOR_LIGHTRED, + on_activate=function() + self.predicates = {} + predicate_str = make_predicate_str(self.predicates) + self:refresh_list() + end, + enabled=function() return next(self.predicates) end, + }, + widgets.Label{ + frame={t=1, l=2}, + text={{text=function() return predicate_str end}}, + text_pen=COLOR_GREEN, + }, + }, + }, } end +function pass_predicates(item, predicates) + local has_show = false + for _,predicate in pairs(predicates) do + local matches = predicate.match(item) + has_show = has_show or predicate.show + if matches and predicate.show then return true end + if not matches and predicate.hide then return false end + end + return not has_show +end + local function match_risky(item, risky_items) for item_type, subtypes in pairs(risky_items) do for subtype in pairs(subtypes) do diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index d1b27f3f5e..b4e0692f58 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -13,9 +13,9 @@ local widgets = require('gui.widgets') MoveGoods = defclass(MoveGoods, widgets.Window) MoveGoods.ATTRS { frame_title='Move goods to/from depot', - frame={w=84, h=45}, + frame={w=84, h=46}, resizable=true, - resize_min={w=81,h=35}, + resize_min={h=35}, pending_item_ids=DEFAULT_NIL, depot=DEFAULT_NIL, } @@ -96,6 +96,7 @@ local function sort_by_quantity_asc(a, b) end local function is_active_caravan(caravan) + if caravan.flags.tribute then return false end local trade_state = caravan.trade_state return caravan.time_remaining > 0 and (trade_state == df.caravan_state.T_trade_state.Approaching or @@ -156,7 +157,7 @@ function MoveGoods:init() on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ - frame={t=2, l=0, w=38, h=14}, + frame={t=2, l=0, w=38, h=16}, subviews=common.get_slider_widgets(self), }, widgets.ToggleHotkeyLabel{ @@ -172,11 +173,11 @@ function MoveGoods:init() on_change=function() self:refresh_list() end, }, widgets.Panel{ - frame={t=4, l=40, r=0, h=12}, + frame={t=4, l=40, r=0, h=15}, subviews=common.get_info_widgets(self, get_export_agreements()), }, widgets.Panel{ - frame={t=17, l=0, r=0, b=6}, + frame={t=19, l=0, r=0, b=6}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', @@ -414,6 +415,7 @@ function MoveGoods:cache_choices(disable_buckets) local data = { desc=desc, per_item_value=value, + item=item, -- a representative item that we can use for filtering later items={[item_id]={item=item, pending=is_pending, banned=is_banned, risky=is_risky, requested=is_requested}}, item_type=item:getType(), item_subtype=item:getSubtype(), @@ -513,6 +515,9 @@ function MoveGoods:get_choices() goto continue end end + if not common.pass_predicates(data.item, self.predicates) then + goto continue + end table.insert(choices, choice) ::continue:: end @@ -650,7 +655,7 @@ MoveGoodsOverlay.ATTRS{ default_pos={x=-64, y=10}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING/TradeDepot', - frame={w=31, h=1}, + frame={w=33, h=1}, frame_background=gui.CLEAR_PEN, } @@ -664,6 +669,7 @@ local function has_trade_depot_and_caravan() end for _, caravan in ipairs(df.global.plotinfo.caravans) do + if caravan.flags.tribute then goto continue end local trade_state = caravan.trade_state local time_remaining = caravan.time_remaining if time_remaining > 0 and @@ -672,13 +678,14 @@ local function has_trade_depot_and_caravan() then return true end + ::continue:: end return false end function MoveGoodsOverlay:init() self:addviews{ - widgets.HotkeyLabel{ + widgets.TextButton{ frame={t=0, l=0}, label='DFHack move trade goods', key='CUSTOM_CTRL_T', diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index cc6e3e556c..9991cd32a1 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -29,6 +29,7 @@ local trade = df.global.game.main_interface.trade -- ------------------- -- Trade -- + Trade = defclass(Trade, widgets.Window) Trade.ATTRS { frame_title='Select trade goods', @@ -367,6 +368,7 @@ function Trade:cache_choices(list_idx, trade_bins) desc=desc, value=common.get_perceived_value(item, trade.mer, list_idx == 1), list_idx=list_idx, + item=item, item_idx=item_idx, quality=item.flags.artifact and 6 or item:getQuality(), wear=wear_level, @@ -435,6 +437,9 @@ function Trade:get_choices() goto continue end end + if not common.pass_predicates(data.item, self.predicates) then + goto continue + end table.insert(choices, choice) ::continue:: end @@ -694,22 +699,15 @@ TradeOverlay.ATTRS{ default_pos={x=-3,y=-12}, default_enabled=true, viewscreens='dwarfmode/Trade/Default', - frame={w=27, h=15}, + frame={w=27, h=13}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } function TradeOverlay:init() self:addviews{ - widgets.HotkeyLabel{ - frame={t=0, l=0}, - label='DFHack trade UI', - key='CUSTOM_CTRL_T', - enabled=function() return trade.stillunloading == 0 and trade.havetalker == 1 end, - on_activate=function() view = view and view:raise() or TradeScreen{}:show() end, - }, widgets.Label{ - frame={t=2, l=0}, + frame={t=0, l=0}, text={ {text='Shift+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', NEWLINE, @@ -717,7 +715,7 @@ function TradeOverlay:init() }, }, widgets.Label{ - frame={t=5, l=0}, + frame={t=3, l=0}, text={ {text='Ctrl+Click checkbox', pen=COLOR_LIGHTGREEN}, ':', NEWLINE, @@ -725,24 +723,24 @@ function TradeOverlay:init() }, }, widgets.HotkeyLabel{ - frame={t=8, l=0}, + frame={t=6, l=0}, label='collapse bins', key='CUSTOM_CTRL_C', on_activate=collapseAllContainers, }, widgets.HotkeyLabel{ - frame={t=9, l=0}, + frame={t=7, l=0}, label='collapse all', key='CUSTOM_CTRL_X', on_activate=collapseEverything, }, widgets.Label{ - frame={t=11, l=0}, + frame={t=9, l=0}, text = 'Shift+Scroll', text_pen=COLOR_LIGHTGREEN, }, widgets.Label{ - frame={t=11, l=12}, + frame={t=9, l=12}, text = ': fast scroll', }, } @@ -773,7 +771,38 @@ function TradeOverlay:onInput(keys) handle_ctrl_click_on_render = true copyGoodflagState() end - elseif keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + end +end + +-- ------------------- +-- TradeBannerOverlay +-- + +TradeBannerOverlay = defclass(TradeBannerOverlay, overlay.OverlayWidget) +TradeBannerOverlay.ATTRS{ + default_pos={x=-31,y=-7}, + default_enabled=true, + viewscreens='dwarfmode/Trade/Default', + frame={w=25, h=1}, + frame_background=gui.CLEAR_PEN, +} + +function TradeBannerOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='DFHack trade UI', + key='CUSTOM_CTRL_T', + enabled=function() return trade.stillunloading == 0 and trade.havetalker == 1 end, + on_activate=function() view = view and view:raise() or TradeScreen{}:show() end, + }, + } +end + +function TradeBannerOverlay:onInput(keys) + if TradeBannerOverlay.super.onInput(self, keys) then return true end + + if keys._MOUSE_R_DOWN or keys.LEAVESCREEN then if view then view:dismiss() end From 261a22b50e1ca0a19e4a2c0d901fffa437f60c42 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Jul 2023 18:19:11 -0700 Subject: [PATCH 405/732] temporarily revert work on predicates --- internal/caravan/common.lua | 145 +-------------------------------- internal/caravan/movegoods.lua | 9 +- internal/caravan/trade.lua | 3 - 3 files changed, 6 insertions(+), 151 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index af8768085d..cdf22ca90f 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -1,7 +1,6 @@ --@ module = true local dialogs = require('gui.dialogs') -local scriptmanager = require('script-manager') local widgets = require('gui.widgets') CH_UP = string.char(30) @@ -182,7 +181,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=6, l=0, r=0, h=4}, + frame={t=5, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_quality'..suffix, @@ -247,7 +246,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=12, l=0, r=0, h=4}, + frame={t=10, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_value'..suffix, @@ -458,96 +457,7 @@ local function get_ethics_token(animal_ethics, wood_ethics) } end -local PREDICATE_LIBRARY = { - {name='weapons-grade metal', match=function(item) - if item.mat_type ~= 0 then return false end - local flags = df.global.world.raws.inorganics[item.mat_index].material.flags - return flags.IS_METAL and - (flags.ITEMS_METAL or flags.ITEMS_WEAPON or flags.ITEMS_WEAPON_RANGED or flags.ITEMS_AMMO or flags.ITEMS_ARMOR) - end}, -} -for _,item_type in ipairs(df.item_type) do - table.insert(PREDICATE_LIBRARY, { - name=to_item_type_str(item_type), - group='item type', - match=function(item) return item_type == item:getType() end, - }) -end - -local PREDICATES_VAR = 'PREDICATES' - -local function get_user_predicates() - local user_predicates = {} - local load_user_predicates = function(env_name, env) - local predicates = env[PREDICATES_VAR] - if not predicates then return end - if type(predicates) ~= 'table' then - dfhack.printerr( - ('error loading predicates from "%s": %s map is malformed') - :format(env_name, PREDICATES_VAR)) - return - end - for i,predicate in ipairs(predicates) do - if type(predicate) ~= 'table' then - dfhack.printerr(('error loading predicate %s:%d (must be a table)'):format(env_name, i)) - goto continue - end - if type(predicate.name) ~= 'string' or #predicate.name == 0 then - dfhack.printerr(('error loading predicate %s:%d (must have a string "name" field)'):format(env_name, i)) - goto continue - end - if type(predicate.match) ~= 'function' then - dfhack.printerr(('error loading predicate %s:%d (must have a function "match" field)'):format(env_name, i)) - goto continue - end - table.insert(user_predicates, {id=('%s:%s'):format(env_name, predicate.name), name=predicate.name, match=predicate.match}) - ::continue:: - end - end - scriptmanager.foreach_module_script(load_user_predicates) - return user_predicates -end - -local function customize_predicates(predicates, on_close) - local user_predicates = get_user_predicates() - local predicate = nil - if #user_predicates > 0 then - predicate = user_predicates[1] - else - predicate = PREDICATE_LIBRARY[1] - end - predicates[predicate.name] = {match=predicate.match, show=true} - on_close() -end - -local function make_predicate_str(predicates) - local preset, names = nil, {} - for name, predicate in pairs(predicates) do - if not preset then - preset = predicate.preset or '' - end - if #preset > 0 and preset ~= predicate.preset then - preset = '' - end - table.insert(names, name) - end - if preset and #preset > 0 then - return preset - end - if #names > 0 then - return table.concat(names, ', ') - end - return 'All' -end - -local function get_context_predicates(context) - return {} -end - -function get_info_widgets(self, export_agreements, context) - self.predicates = get_context_predicates(context) - local predicate_str = make_predicate_str(self.predicates) - +function get_info_widgets(self, export_agreements) return { widgets.Panel{ frame={t=0, l=0, r=0, h=2}, @@ -636,58 +546,9 @@ function get_info_widgets(self, export_agreements, context) }, }, }, - widgets.Panel{ - frame={t=13, l=0, r=0, h=2}, - subviews={ - widgets.Label{ - frame={t=0, l=0}, - text='Advanced filter:', - }, - widgets.HotkeyLabel{ - frame={t=0, l=18, w=9}, - key='CUSTOM_SHIFT_J', - label='[edit]', - on_activate=function() - customize_predicates(self.predicates, - function() - predicate_str = make_predicate_str(self.predicates) - self:refresh_list() - end) - end, - }, - widgets.HotkeyLabel{ - frame={t=0, l=34, w=10}, - key='CUSTOM_SHIFT_K', - label='[clear]', - text_pen=COLOR_LIGHTRED, - on_activate=function() - self.predicates = {} - predicate_str = make_predicate_str(self.predicates) - self:refresh_list() - end, - enabled=function() return next(self.predicates) end, - }, - widgets.Label{ - frame={t=1, l=2}, - text={{text=function() return predicate_str end}}, - text_pen=COLOR_GREEN, - }, - }, - }, } end -function pass_predicates(item, predicates) - local has_show = false - for _,predicate in pairs(predicates) do - local matches = predicate.match(item) - has_show = has_show or predicate.show - if matches and predicate.show then return true end - if not matches and predicate.hide then return false end - end - return not has_show -end - local function match_risky(item, risky_items) for item_type, subtypes in pairs(risky_items) do for subtype in pairs(subtypes) do diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index b4e0692f58..8b4e9c3d25 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -157,7 +157,7 @@ function MoveGoods:init() on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ - frame={t=2, l=0, w=38, h=16}, + frame={t=2, l=0, w=38, h=14}, subviews=common.get_slider_widgets(self), }, widgets.ToggleHotkeyLabel{ @@ -173,11 +173,11 @@ function MoveGoods:init() on_change=function() self:refresh_list() end, }, widgets.Panel{ - frame={t=4, l=40, r=0, h=15}, + frame={t=4, l=40, r=0, h=12}, subviews=common.get_info_widgets(self, get_export_agreements()), }, widgets.Panel{ - frame={t=19, l=0, r=0, b=6}, + frame={t=17, l=0, r=0, b=6}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', @@ -515,9 +515,6 @@ function MoveGoods:get_choices() goto continue end end - if not common.pass_predicates(data.item, self.predicates) then - goto continue - end table.insert(choices, choice) ::continue:: end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 9991cd32a1..d6951763d3 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -437,9 +437,6 @@ function Trade:get_choices() goto continue end end - if not common.pass_predicates(data.item, self.predicates) then - goto continue - end table.insert(choices, choice) ::continue:: end From 8f1e40e004a13a078d216670ae9afb00d59ee98a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Jul 2023 18:26:52 -0700 Subject: [PATCH 406/732] move assign goods overlay to bottom of screen --- internal/caravan/movegoods.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 8b4e9c3d25..c5bac09907 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -698,11 +698,10 @@ end AssignTradeOverlay = defclass(AssignTradeOverlay, overlay.OverlayWidget) AssignTradeOverlay.ATTRS{ - default_pos={x=-3,y=-25}, + default_pos={x=-41,y=-5}, default_enabled=true, viewscreens='dwarfmode/AssignTrade', - frame={w=27, h=3}, - frame_style=gui.MEDIUM_FRAME, + frame={w=33, h=1}, frame_background=gui.CLEAR_PEN, } @@ -711,9 +710,9 @@ function AssignTradeOverlay:init() scr:sendInputToParent('LEAVESCREEN') end self:addviews{ - widgets.HotkeyLabel{ + widgets.TextButton{ frame={t=0, l=0}, - label='DFHack goods UI', + label='DFHack move trade goods', key='CUSTOM_CTRL_T', on_activate=function() local depot = df.global.game.main_interface.assign_trade.trade_depot_bld From 03bf65057d3a04badee4dfbffd99f85dd33b8e73 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Jul 2023 18:48:30 -0700 Subject: [PATCH 407/732] update changelog for 50.09-r2 and sync docs --- changelog.txt | 14 +++++++++++--- docs/points.rst | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index f156b5d757..2497728fd4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -13,6 +13,16 @@ that repo. # Future +## New Scripts + +## Fixes + +## Misc Improvements + +## Removed + +# 50.09-r2 + ## New Scripts - `caravan`: new trade screen UI replacements for bringing goods to trade depot and trading - `fix/empty-wheelbarrows`: new script to empty stuck rocks from all wheelbarrows on the map @@ -21,15 +31,13 @@ that repo. - `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported - `gui/gm-unit`: fix commandline processing when a unit id is specified - `suspendmanager`: take in account already built blocking buildings -- `suspendmanager`: don't consider branches as a suitable access to a build +- `suspendmanager`: don't consider tree branches as a suitable access path to a building ## Misc Improvements - `gui/unit-syndromes`: make lists searchable - `suspendmanager`: display the suspension reason when viewing a suspended building - `quickfort`: blueprint libraries are now moddable -- add a ``blueprints/`` directory to your mod and they'll show up in `quickfort` and `gui/quickfort`! -## Removed - # 50.09-r1 ## Misc Improvements diff --git a/docs/points.rst b/docs/points.rst index 0016f779ef..6188f7908d 100644 --- a/docs/points.rst +++ b/docs/points.rst @@ -3,7 +3,7 @@ points .. dfhack-tool:: :summary: Sets available points at the embark screen. - :tags: unavailable embark fort armok + :tags: embark fort armok Run at the embark screen when you are choosing items to bring with you and skills to assign to your dwarves. You can set the available points to any From 954d5a5a0b05a0d4589f8102260717bf063b74ca Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Jul 2023 13:56:32 -0700 Subject: [PATCH 408/732] fix detection of goods list changes --- internal/caravan/trade.lua | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index d6951763d3..f1afc0ed47 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -303,8 +303,7 @@ function Trade:init() self.subviews.list.edit = self.subviews.search self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') - self:check_cache() - self.subviews.list:setChoices(self:get_choices()) + self:reset_cache() end function Trade:refresh_list(sort_widget, sort_fn) @@ -319,9 +318,11 @@ function Trade:refresh_list(sort_widget, sort_fn) end local list = self.subviews.list local saved_filter = list:getFilter() + local saved_top = list.list.page_top list:setFilter('') list:setChoices(self:get_choices(), list:getSelected()) list:setFilter(saved_filter) + list.list:on_scrollbar(math.max(0, saved_top - list.list.page_top)) end local function is_ethical_product(item, animal_ethics, wood_ethics) @@ -486,18 +487,9 @@ function Trade:toggle_visible() end end -function Trade:check_cache() - if self.saved_talkline ~= trade.talkline then - self.saved_talkline = trade.talkline - -- react to trade button being clicked - self.choices = {[0]={}, [1]={}} - self:refresh_list() - end -end - -function Trade:onRenderFrame(dc, rect) - Trade.super.onRenderFrame(self, dc, rect) - self:check_cache() +function Trade:reset_cache() + self.choices = {[0]={}, [1]={}} + self:refresh_list() end -- ------------------- @@ -512,12 +504,26 @@ TradeScreen.ATTRS { } function TradeScreen:init() - self:addviews{Trade{}} + self.trade_window = Trade{} + self:addviews{self.trade_window} +end + +function TradeScreen:onInput(keys) + if self.reset_pending then return false end + local handled = TradeScreen.super.onInput(self, keys) + if keys._MOUSE_L_DOWN and not self.trade_window:getMouseFramePos() then + -- "trade" or "offer" buttons may have been clicked and we need to reset the cache + self.reset_pending = true + end + return handled end function TradeScreen:onRenderFrame() if not df.global.game.main_interface.trade.open then view:dismiss() + elseif self.reset_pending then + self.reset_pending = nil + self.trade_window:reset_cache() end end From 806ba5ca540081cdd8446ee34949399f904490b1 Mon Sep 17 00:00:00 2001 From: Moonwarden64 <140351121+Moonwarden64@users.noreply.github.com> Date: Sun, 23 Jul 2023 18:21:57 -0300 Subject: [PATCH 409/732] Update assign-preferences.rst Added an example for how to assign a food preference for a creature's meat. --- docs/assign-preferences.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assign-preferences.rst b/docs/assign-preferences.rst index 0308f3c0ae..1345b1d262 100644 --- a/docs/assign-preferences.rst +++ b/docs/assign-preferences.rst @@ -32,9 +32,9 @@ Examples assign-preferences --reset --likecreature SPARROW -* "prefers to consume dwarven wine and olives":: +* "prefers to consume dwarven wine, olives and yak":: - assign-preferences --reset --likefood [ PLANT:MUSHROOM_HELMET_PLUMP:DRINK PLANT:OLIVE:FRUIT ] + assign-preferences --reset --likefood [ PLANT:MUSHROOM_HELMET_PLUMP:DRINK PLANT:OLIVE:FRUIT CREATURE_MAT:YAK:MUSCLE ] * "absolutely detests jumping spiders:: From ce9876126f22ec4bb43ec4ce2f4dbd110a5a0773 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Tue, 25 Jul 2023 21:20:33 +0200 Subject: [PATCH 410/732] Use a different icon for jobs suspended by suspendmanager --- changelog.txt | 1 + suspendmanager.lua | 11 +++++++++++ unsuspend.lua | 6 ++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 2497728fd4..1af6ccd2d0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ that repo. ## Fixes ## Misc Improvements +- `suspendmanager`: display a different color for jobs suspended by suspendmanager ## Removed diff --git a/suspendmanager.lua b/suspendmanager.lua index bbb0b79eb1..ed63e5d4a7 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -92,6 +92,17 @@ function preventBlockingEnabled() return Instance.preventBlocking end +--- Returns true if the job is maintained suspended by suspendmanager +---@param job job +function isKeptSuspended(job) + if not isEnabled() or not preventBlockingEnabled() then + return false + end + + local reason = Instance.suspensions[job.id] + return reason and not EXTERNAL_REASONS[reason] +end + local function persist_state() persist.GlobalTable[GLOBAL_KEY] = json.encode({ enabled=enabled, diff --git a/unsuspend.lua b/unsuspend.lua index 42cf7dc22a..2d8eb8e310 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -143,9 +143,9 @@ local function get_texposes() return valid and start + offset or nil end - return tp(3), tp(1), tp(0) + return tp(3), tp(2), tp(1), tp(0) end -local PLANNED_TEXPOS, SUSPENDED_TEXPOS, REPEAT_SUSPENDED_TEXPOS = get_texposes() +local PLANNED_TEXPOS, KEPT_SUSPENDED_TEXTPOS, SUSPENDED_TEXPOS, REPEAT_SUSPENDED_TEXPOS = get_texposes() function SuspendOverlay:render_marker(dc, bld, screen_pos) if not bld or #bld.jobs ~= 1 then return end @@ -159,6 +159,8 @@ function SuspendOverlay:render_marker(dc, bld, screen_pos) local color, ch, texpos = COLOR_YELLOW, 'x', SUSPENDED_TEXPOS if buildingplan and buildingplan.isPlannedBuilding(bld) then color, ch, texpos = COLOR_GREEN, 'P', PLANNED_TEXPOS + elseif suspendmanager and suspendmanager.isKeptSuspended(job) then + color, ch, texpos = COLOR_WHITE, 'x', KEPT_SUSPENDED_TEXTPOS elseif data.suspend_count > 1 then color, ch, texpos = COLOR_RED, 'X', REPEAT_SUSPENDED_TEXPOS end From 7264a15359ed4f603a445a44a1364982b5d02910 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Tue, 25 Jul 2023 21:23:52 +0200 Subject: [PATCH 411/732] Update the overlay doc --- docs/unsuspend.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/unsuspend.rst b/docs/unsuspend.rst index f6c2e9bb72..697792e585 100644 --- a/docs/unsuspend.rst +++ b/docs/unsuspend.rst @@ -39,6 +39,8 @@ buildings: - A clock icon (green ``P`` in ASCII mode) indicates that the building is still in planning mode and is waiting on materials. The `buildingplan` plugin will unsuspend it for you when those materials become available. +- A white ``x`` means that the building is maintained suspended by + `suspendmanager`, selecting it will provide a reason for the suspension - A yellow ``x`` means that the building is suspended. If you don't have `suspendmanager` managing suspensions for you, you can unsuspend it manually or with the `unsuspend` command. From 0ad6d86fea186621c412af4d22f48ff78a8f2111 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sat, 29 Jul 2023 17:37:03 +0200 Subject: [PATCH 412/732] Fix suspendmanager enabling/disabling itself unexpectedly --- suspendmanager.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index bbb0b79eb1..a19fd274f8 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -618,9 +618,13 @@ function ToggleOverlay:init() } end -function ToggleOverlay:render(dc) +function ToggleOverlay:shouldRender() local job = dfhack.gui.getSelectedJob() - if not job or job.job_type ~= df.job_type.ConstructBuilding or isBuildingPlanJob(job) then + return job and job.job_type == df.job_type.ConstructBuilding and not isBuildingPlanJob(job) +end + +function ToggleOverlay:render(dc) + if not self:shouldRender() then return end -- Update the option: the "initial_option" value is not up to date since the widget @@ -629,6 +633,13 @@ function ToggleOverlay:render(dc) ToggleOverlay.super.render(self, dc) end +function ToggleOverlay:onInput(keys) + if not self:shouldRender() then + return + end + ToggleOverlay.super.onInput(self, keys) +end + OVERLAY_WIDGETS = { status=StatusOverlay, toggle=ToggleOverlay, From fb9e420c2381565c068d1e1aa373d2f1bd2ac79c Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Sat, 29 Jul 2023 17:39:44 +0200 Subject: [PATCH 413/732] changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 2497728fd4..2a721dff39 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,7 @@ that repo. ## New Scripts ## Fixes +- `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly ## Misc Improvements From 43506f64638f98be1a5edf725568d23b1b4c8732 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Jul 2023 09:48:27 -0700 Subject: [PATCH 414/732] don't error if the map block is unallocated --- internal/quickfort/dig.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index f4a2276db2..a307986739 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -680,6 +680,7 @@ end local function init_dig_ctx(ctx, pos, direction) local flags, occupancy = dfhack.maps.getTileFlags(pos) + if not flags then return end local tileattrs = df.tiletype.attrs[dfhack.maps.getTileType(pos)] local engraving = nil if is_smooth(tileattrs) then @@ -787,6 +788,7 @@ local function do_run_impl(zlevel, grid, ctx) get_track_direction(extent_x, extent_y, extent.width, extent.height)) local digctx = init_dig_ctx(ctx, extent_pos, direction) + if not digctx then goto inner_continue end if db_entry.action == do_smooth or db_entry.action == do_engrave or db_entry.action == do_track then -- can only smooth passable tiles From c59b8e813d6912c80d6e698a5df62917484112bf Mon Sep 17 00:00:00 2001 From: lethosor Date: Mon, 31 Jul 2023 23:14:59 -0400 Subject: [PATCH 415/732] devel/dump-offsets: support extended table --- devel/dump-offsets.lua | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/devel/dump-offsets.lua b/devel/dump-offsets.lua index 3137bfc270..f7acee7e92 100644 --- a/devel/dump-offsets.lua +++ b/devel/dump-offsets.lua @@ -188,9 +188,9 @@ local data = ms.get_data_segment() or qerror('Could not find data segment') local search if dfhack.getArchitecture() == 64 then - search = {0x1234567812345678, 0x8765432187654321} + search = {0x1234567812345678, 0x8765432187654321, 0x89abcdef89abcdef} else - search = {0x12345678, 0x87654321} + search = {0x12345678, 0x87654321, 0x89abcdef} end local addrs = {} @@ -202,15 +202,27 @@ function save_addr(name, addr) addrs[name] = addr end +local extended = false local start = data.intptr_t:find_one(search) +if start then + extended = true +else + -- try searching for a non-extended table + table.remove(search, #search) + start = data.intptr_t:find_one(search) +end +if not start then + qerror('Could not find global table header') +end local index = 1 +local entry_size = (extended and 3 or 2) while true do - local p_name = data.intptr_t[start + (index * 2)] + local p_name = data.intptr_t[start + (index * entry_size)] if p_name == 0 then break end - local g_addr = data.intptr_t[start + (index * 2) + 1] + local g_addr = data.intptr_t[start + (index * entry_size) + 1] local df_name = read_cstr(p_name) local g_name = GLOBALS[df_name] if df_name:find('^index[12]_') then From d1dc9926885483859accf84eb8e7f9b28c6d4ff6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 04:22:09 +0000 Subject: [PATCH 416/732] [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.23.2 → 0.23.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.23.2...0.23.3) --- .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 a2c2fccdeb..3b9b1e7b84 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.23.2 + rev: 0.23.3 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From c1148f8b60291096c2cb25ef933a1dcbe9c3ad5b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Aug 2023 01:02:09 -0700 Subject: [PATCH 417/732] use reusable workflows for docs and lint --- .github/workflows/build.yml | 66 +++++++++---------------------------- 1 file changed, 15 insertions(+), 51 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 919b3242e8..f941325cdf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,55 +4,19 @@ on: [push, pull_request] jobs: docs: - runs-on: ubuntu-22.04 - steps: - - name: Set up Python 3 - uses: actions/setup-python@v4 - with: - python-version: 3 - - name: Install dependencies - run: | - pip install 'sphinx<4.4.0' - - name: Clone scripts - uses: actions/checkout@v1 - - name: Set up DFHack - run: | - git clone https://github.com/DFHack/dfhack.git $HOME/dfhack --depth 1 --branch develop - git -C $HOME/dfhack submodule update --init --depth 1 --remote plugins/stonesense library/xml - rmdir $HOME/dfhack/scripts - ln -sv $(pwd) $HOME/dfhack/scripts - - name: Build docs - run: | - sphinx-build -W --keep-going -j3 --color $HOME/dfhack html - - name: Check for missing docs - if: success() || failure() - run: python $HOME/dfhack/ci/script-docs.py . - - name: Upload docs - if: success() || failure() - uses: actions/upload-artifact@master - with: - name: docs - path: html + uses: DFHack/dfhack/.github/workflows/build-linux.yml + with: + dfhack_ref: develop + scripts_ref: ${{ github.ref }} + artifact-name: docs + platform-files: false + common-files: false + docs: true + secrets: inherit + lint: - runs-on: ubuntu-22.04 - steps: - - name: Set up Python 3 - uses: actions/setup-python@v4 - with: - python-version: 3 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install lua5.3 - - name: Clone scripts - uses: actions/checkout@v1 - - name: Set up DFHack - run: | - git clone https://github.com/DFHack/dfhack.git $HOME/dfhack --depth 1 --branch develop - rmdir $HOME/dfhack/scripts - ln -sv $(pwd) $HOME/dfhack/scripts - - name: Check whitespace - run: python $HOME/dfhack/ci/lint.py --git-only --github-actions - - name: Check Lua syntax - if: success() || failure() - run: python $HOME/dfhack/ci/script-syntax.py --ext=lua --cmd="luac5.3 -p" --github-actions + uses: DFHack/dfhack/.github/workflows/lint.yml@develop + with: + dfhack_ref: develop + scripts_ref: ${{ github.ref }} + secrets: inherit From aa23ab7d56e037b5af6d9edf935ca619cb26c2f1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Aug 2023 01:32:00 -0700 Subject: [PATCH 418/732] use reusable test workflow --- .github/workflows/build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f941325cdf..fd2292b068 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,13 @@ name: Build on: [push, pull_request] jobs: + test: + uses: DFHack/dfhack/.github/workflows/test.yml + with: + dfhack_ref: develop + scripts_ref: ${{ github.ref }} + secrets: inherit + docs: uses: DFHack/dfhack/.github/workflows/build-linux.yml with: From c0b17a57f36fd37eaf76eff2162bd6379f8f7e42 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Aug 2023 01:43:16 -0700 Subject: [PATCH 419/732] fix workflow refs --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd2292b068..117ea287bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,14 +4,14 @@ on: [push, pull_request] jobs: test: - uses: DFHack/dfhack/.github/workflows/test.yml + uses: DFHack/dfhack/.github/workflows/test.yml@develop with: dfhack_ref: develop scripts_ref: ${{ github.ref }} secrets: inherit docs: - uses: DFHack/dfhack/.github/workflows/build-linux.yml + uses: DFHack/dfhack/.github/workflows/build-linux.yml@develop with: dfhack_ref: develop scripts_ref: ${{ github.ref }} From 28bf411e7f2ab3b14ef6396c0ac4e23651de15d2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Aug 2023 12:59:39 -0700 Subject: [PATCH 420/732] add clean cache workflow --- .github/workflows/clean-cache.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/clean-cache.yml diff --git a/.github/workflows/clean-cache.yml b/.github/workflows/clean-cache.yml new file mode 100644 index 0000000000..3439019d01 --- /dev/null +++ b/.github/workflows/clean-cache.yml @@ -0,0 +1,11 @@ +name: Clean up PR caches + +on: + pull_request_target: + types: + - closed + +jobs: + cleanup: + uses: DFHack/dfhack/.github/workflows/clean-cache.yml@develop + secrets: inherit From fd6a323a8ce3a28ed8848daf764fea93dd8e2391 Mon Sep 17 00:00:00 2001 From: lethosor Date: Sat, 5 Aug 2023 01:00:06 -0400 Subject: [PATCH 421/732] devel/lsmem: add filtering capabilities --- changelog.txt | 1 + devel/lsmem.lua | 39 ++++++++++++++++++++++++++++++++++++++- docs/devel/lsmem.rst | 18 ++++++++++++++++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index be03c25230..5e8c9d385c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ that repo. - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly ## Misc Improvements +- `devel/lsmem`: added support for filtering by memory addresses and filenames - `suspendmanager`: display a different color for jobs suspended by suspendmanager ## Removed diff --git a/devel/lsmem.lua b/devel/lsmem.lua index dc9100e2f4..286a5b3f2b 100644 --- a/devel/lsmem.lua +++ b/devel/lsmem.lua @@ -8,6 +8,39 @@ valid, whether a certain library/plugin is loaded, etc. ]====] +function range_contains_any(range, addrs) + for _, a in ipairs(addrs) do + if a >= range.start_addr and a < range.end_addr then + return true + end + end + return false +end + +function range_name_match_any(range, names) + for _, n in ipairs(names) do + if range.name:lower():find(n, 1, true) or range.name:lower():find(n) then + return true + end + end + return false +end + +local args = {...} +local filter_addrs = {} +local filter_names = {} +for _, arg in ipairs(args) do + if arg:lower():startswith('0x') then + arg = arg:sub(3) + end + local addr = tonumber(arg, 16) + if addr then + table.insert(filter_addrs, addr) + else + table.insert(filter_names, arg:lower()) + end +end + for _,v in ipairs(dfhack.internal.getMemRanges()) do local access = { '-', '-', '-', 'p' } if v.read then access[1] = 'r' end @@ -18,5 +51,9 @@ for _,v in ipairs(dfhack.internal.getMemRanges()) do elseif v.shared then access[4] = 's' end - print(string.format('%08x-%08x %s %s', v.start_addr, v.end_addr, table.concat(access), v.name)) + if (#filter_addrs == 0 or range_contains_any(v, filter_addrs)) and + (#filter_names == 0 or range_name_match_any(v, filter_names)) + then + print(string.format('%08x-%08x %s %s', v.start_addr, v.end_addr, table.concat(access), v.name)) + end end diff --git a/docs/devel/lsmem.rst b/docs/devel/lsmem.rst index f046e4f41d..8bc3b68afb 100644 --- a/docs/devel/lsmem.rst +++ b/docs/devel/lsmem.rst @@ -3,7 +3,7 @@ devel/lsmem .. dfhack-tool:: :summary: Print memory ranges of the DF process. - :tags: unavailable dev + :tags: dev Useful for checking whether a pointer is valid, whether a certain library/plugin is loaded, etc. @@ -13,4 +13,18 @@ Usage :: - devel/lsmem + devel/lsmem [
...] [ ...] + +Examples +-------- + +``devel/lsmem 0x1234 5678 90ab`` + List any ranges containing the addresses ``0x1234``, ``0x5678``, or ``0x90ab``. + Addresses are interpreted as hex; the ``0x`` prefix is optional. + +``devel/lsmem dwarf g_src`` + List any ranges corresponding to files matching ``dwarf`` or ``g_src`` + (case-insensitive). + +``devel/lsmem .+`` + List any ranges with non-empty filenames. Any Lua patterns are allowed. From 51c1ad09992089181719fae039e49f6f95cad476 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 5 Aug 2023 23:25:43 -0700 Subject: [PATCH 422/732] align with the new repository-agnostic workflows --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 117ea287bd..ea87b32ec6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,14 +6,12 @@ jobs: test: uses: DFHack/dfhack/.github/workflows/test.yml@develop with: - dfhack_ref: develop scripts_ref: ${{ github.ref }} secrets: inherit docs: uses: DFHack/dfhack/.github/workflows/build-linux.yml@develop with: - dfhack_ref: develop scripts_ref: ${{ github.ref }} artifact-name: docs platform-files: false @@ -24,6 +22,5 @@ jobs: lint: uses: DFHack/dfhack/.github/workflows/lint.yml@develop with: - dfhack_ref: develop scripts_ref: ${{ github.ref }} secrets: inherit From 802842d06775484d1b1b1201ce4ed1aa6e052f34 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Aug 2023 17:36:02 -0700 Subject: [PATCH 423/732] fix price calculations for items affected by trade agreements --- changelog.txt | 2 ++ internal/caravan/common.lua | 10 +++++----- internal/caravan/trade.lua | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5e8c9d385c..032cbdb5ef 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,8 @@ that repo. ## Fixes - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly +- `caravan`: Correct price adjustment values in trade agreement details screen +- `caravan`: Apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index cdf22ca90f..189fdc645d 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -107,12 +107,12 @@ function get_item_description(item) end -- takes into account trade agreements -function get_perceived_value(item, caravan_state, caravan_buying) - local value = dfhack.items.getValue(item, caravan_state, caravan_buying) +function get_perceived_value(item, caravan_state) + local value = dfhack.items.getValue(item, caravan_state) for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do - value = value + dfhack.items.getValue(contained_item, caravan_state, caravan_buying) + value = value + dfhack.items.getValue(contained_item, caravan_state) for _,contained_contained_item in ipairs(dfhack.items.getContainedItems(contained_item)) do - value = value + dfhack.items.getValue(contained_contained_item, caravan_state, caravan_buying) + value = value + dfhack.items.getValue(contained_contained_item, caravan_state) end end return value @@ -437,7 +437,7 @@ local function show_export_agreements(export_agreements) for _, agreement in ipairs(export_agreements) do for idx, price in ipairs(agreement.price) do local desc = make_item_description(agreement.items.item_type[idx], agreement.items.item_subtype[idx]) - local percent = (price * 100) // 256 + local percent = (price * 100) // 128 table.insert(strs, ('%20s %d%%'):format(desc..':', percent)) end end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index f1afc0ed47..ec9d27be82 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -367,7 +367,7 @@ function Trade:cache_choices(list_idx, trade_bins) local is_ethical = is_ethical_product(item, self.animal_ethics, self.wood_ethics) local data = { desc=desc, - value=common.get_perceived_value(item, trade.mer, list_idx == 1), + value=common.get_perceived_value(item, trade.mer), list_idx=list_idx, item=item, item_idx=item_idx, From c8155a4ea84038b81509a0e0b1c8e26ff931ae10 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Aug 2023 23:02:16 -0700 Subject: [PATCH 424/732] add changelog template for new verions --- changelog.txt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 5e8c9d385c..5d2471588c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -9,11 +9,24 @@ top of this file (even if no changes are listed under it), or you will get a changelogs when making a new release, docs/changelog.txt in the dfhack repo must have the new release listed in the right place, even if no changes were made in that repo. + +Template for new versions: + +# Future + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + ]]] # Future -## New Scripts +## New Features ## Fixes - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly From 3626611e687c7be2f00507d917666c9c6d9a9f2f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 7 Aug 2023 03:18:09 -0700 Subject: [PATCH 425/732] add overlays to integrate into legends UI --- changelog.txt | 1 + docs/exportlegends.rst | 41 +++++-- exportlegends.lua | 267 ++++++++++++++++++++++++++++++++--------- 3 files changed, 244 insertions(+), 65 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4dfa0db3b1..a33fc46be2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Features +- `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! ## Fixes - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly diff --git a/docs/exportlegends.rst b/docs/exportlegends.rst index 6f0088178b..2d7c4702d6 100644 --- a/docs/exportlegends.rst +++ b/docs/exportlegends.rst @@ -5,17 +5,25 @@ exportlegends :summary: Exports extended legends data for external viewing. :tags: legends inspection -When run from the legends mode screen, you can export detailed data about your -world so that it can be browsed with external programs like -:forums:`Legends Browser <179848>` and other similar utilities. The data -exported with this tool is more detailed than what you can get with vanilla -export functionality, and some external tools depend on this extra information. +When run from the legends mode screen, this tool will export detailed data +about your world so that it can be browsed with external programs like +:forums:`Legends Browser <179848>`. The data is more detailed than what you can +get with vanilla export functionality, and many external tools depend on this +extra information. + +By default, ``exportlegends`` hooks into the standard vanilla ``Export XML`` button and runs in the background when you click it, allowing both the vanilla export and the extended data export to execute simultaneously. You can continue to browse legends mode via the vanilla UI while the export is running. To use: -- enter legends mode -- click the vanilla "Export XML" button to get the standard export -- run this command (``exportlegends``) to get the extended export +- Enter legends by "Starting a new game" in an existing world and selecting + Legends mode +- Ensure the toggle for "Also export extended legends data" is on (which is the + default) +- Click the "Export XML" button to generate both the standard export and the + extended data export + +You can also generate just the extended data export by manually running the +``exportlegends`` command while legends mode is open. Usage ----- @@ -23,3 +31,20 @@ Usage :: exportlegends + +Overlay +------- + +This script also provides an overlay that is managed by the `overlay` framework. +When the overlay is enabled, a toggle for exporting extended legends data will +appear below the vanilla "Export XML" button. If the toggle is enabled when the +"Export XML" button is clicked, then ``exportlegends`` will run alongside the +vanilla data export. + +While the extended data is being exported, a status line will appear in place +of the toggle, reporting the current export target and the overall percent +complete. + +There is an additional overlay that masks out the "Done" button while the +extended export is running. This prevents the player from exiting legends mode +before the export is complete. diff --git a/exportlegends.lua b/exportlegends.lua index 97e3281de9..82a85f3409 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -1,7 +1,12 @@ -- Export everything from legends mode --luacheck-flags: strictsubtype +--@ module=true local gui = require('gui') +local overlay = require('plugins.overlay') +local script = require('gui.script') +local widgets = require('gui.widgets') + local args = {...} -- Get the date of the world as a string @@ -39,15 +44,34 @@ local function table_containskey(self, key) return false end +progress_item = progress_item or '' +step_size = step_size or 1 +step_percent = -1 +progress_percent = progress_percent or -1 +last_update_ms = 0 + +local function yield_if_timeout() + local now_ms = dfhack.getTickCount() + if now_ms - last_update_ms > 10 then + script.sleep(1, 'frames') + last_update_ms = dfhack.getTickCount() + end +end + --luacheck: skip local function progress_ipairs(vector, desc, interval) desc = desc or 'item' interval = interval or 10000 local cb = ipairs(vector) return function(vector, k, ...) - if k and #vector >= interval and (k % interval == 0 or k == #vector - 1) then - print((' %s %i/%i (%0.f%%)'):format(desc, k, #vector, k * 100 / #vector)) + if k then + local prev_progress_percent = progress_percent + progress_percent = math.max(progress_percent, step_percent + ((k * step_size) // #vector)) + if #vector >= interval and (k % interval == 0 or k == #vector - 1) then + print((' %s %i/%i (%0.f%%)'):format(desc, k, #vector, (k * 100) / #vector)) + end end + yield_if_timeout() return cb(vector, k) end, vector, nil end @@ -96,7 +120,17 @@ local function export_more_legends_xml() file:write(""..escape_xml(dfhack.df2utf(dfhack.TranslateName(df.global.world.world_data.name))).."\n") file:write(""..escape_xml(dfhack.df2utf(dfhack.TranslateName(df.global.world.world_data.name,1))).."\n") - file:write("\n") + local function write_chunk(name, fn) + progress_item = name + yield_if_timeout() + file:write("<" .. name .. ">\n") + fn() + file:write("\n") + end + + local chunks = {} + + table.insert(chunks, {name='landmasses', fn=function() for landmassK, landmassV in progress_ipairs(df.global.world.world_data.landmasses, 'landmass') do file:write("\t\n") file:write("\t\t"..landmassV.index.."\n") @@ -105,9 +139,9 @@ local function export_more_legends_xml() file:write("\t\t"..landmassV.max_x..","..landmassV.max_y.."\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='mountain_peaks', fn=function() for mountainK, mountainV in progress_ipairs(df.global.world.world_data.mountain_peaks, 'mountain') do file:write("\t\n") file:write("\t\t"..mountainK.."\n") @@ -119,9 +153,9 @@ local function export_more_legends_xml() end file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='regions', fn=function() for regionK, regionV in progress_ipairs(df.global.world.world_data.regions, 'region') do file:write("\t\n") file:write("\t\t"..regionV.index.."\n") @@ -142,9 +176,9 @@ local function export_more_legends_xml() end file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='underground_regions', fn=function() for regionK, regionV in progress_ipairs(df.global.world.world_data.underground_regions, 'underground region') do file:write("\t\n") file:write("\t\t"..regionV.index.."\n") @@ -155,9 +189,9 @@ local function export_more_legends_xml() file:write("\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='rivers', fn=function() for riverK, riverV in progress_ipairs(df.global.world.world_data.rivers, 'river') do file:write("\t\n") file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(riverV.name, 1))).."\n") @@ -172,9 +206,9 @@ local function export_more_legends_xml() file:write("\t\t"..riverV.end_pos.x..","..riverV.end_pos.y.."\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='creature_raw', fn=function() for creatureK, creatureV in ipairs (df.global.world.raws.creatures.all) do file:write("\t\n") file:write("\t\t"..creatureV.creature_id.."\n") @@ -187,15 +221,15 @@ local function export_more_legends_xml() end file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='sites', fn=function() for siteK, siteV in progress_ipairs(df.global.world.world_data.sites, 'site') do file:write("\t\n") for k,v in pairs(siteV) do if (k == "id" or k == "civ_id" or k == "cur_owner_id") then printifvalue(file, 2, k, v) --- file:write("\t\t<"..k..">"..tostring(v).."\n") + -- file:write("\t\t<"..k..">"..tostring(v).."\n") elseif (k == "buildings") then if (#siteV.buildings > 0) then file:write("\t\t\n") @@ -229,9 +263,9 @@ local function export_more_legends_xml() end file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='world_constructions', fn=function() for wcK, wcV in progress_ipairs(df.global.world.world_data.constructions.list, 'construction') do file:write("\t\n") file:write("\t\t"..wcV.id.."\n") @@ -244,9 +278,9 @@ local function export_more_legends_xml() file:write("\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='artifacts', fn=function() for artifactK, artifactV in progress_ipairs(df.global.world.artifacts.all, 'artifact') do file:write("\t\n") file:write("\t\t"..artifactV.id.."\n") @@ -277,9 +311,9 @@ local function export_more_legends_xml() end file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='historical_figures', fn=function() for hfK, hfV in progress_ipairs(df.global.world.history.figures, 'historical figure') do file:write("\t\n") file:write("\t\t"..hfV.id.."\n") @@ -287,9 +321,9 @@ local function export_more_legends_xml() if hfV.race >= 0 then file:write("\t\t"..escape_xml(dfhack.df2utf(df.creature_raw.find(hfV.race).name[0])).."\n") end file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='identities', fn=function() for idK, idV in progress_ipairs(df.global.world.identities.all, 'identity') do file:write("\t\n") file:write("\t\t"..idV.id.."\n") @@ -308,9 +342,9 @@ local function export_more_legends_xml() file:write("\t\t"..idV.entity_id.."\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='entity_populations', fn=function() for entityPopK, entityPopV in progress_ipairs(df.global.world.entity_populations, 'entity population') do file:write("\t\n") file:write("\t\t"..entityPopV.id.."\n") @@ -321,9 +355,9 @@ local function export_more_legends_xml() file:write("\t\t"..entityPopV.civ_id.."\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='entities', fn=function() for entityK, entityV in progress_ipairs(df.global.world.entities.all, 'entity') do file:write("\t\n") file:write("\t\t"..entityV.id.."\n") @@ -431,9 +465,9 @@ local function export_more_legends_xml() end file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='historical_events', fn=function() for ID, event in progress_ipairs(df.global.world.history.events, 'event') do if df.history_event_add_hf_entity_linkst:is_instance(event) or df.history_event_add_hf_site_linkst:is_instance(event) @@ -847,8 +881,9 @@ local function export_more_legends_xml() file:write("\t\n") end end - file:write("\n") - file:write("\n") + end}) + + table.insert(chunks, {name='historical_event_relationships', fn=function() for ID, set in progress_ipairs(df.global.world.history.relationship_events, 'relationship_event') do for k = 0, set.next_element - 1 do file:write("\t\n") @@ -860,8 +895,9 @@ local function export_more_legends_xml() file:write("\t\n") end end - file:write("\n") - file:write("\n") + end}) + + table.insert(chunks, {name='historical_event_relationship_supplements', fn=function() for ID, event in progress_ipairs(df.global.world.history.relationship_event_supplements, 'relationship_event_supplement') do file:write("\t\n") file:write("\t\t"..event.event.."\n") @@ -870,13 +906,13 @@ local function export_more_legends_xml() file:write("\t\t"..event.unk_1.."\n") file:write("\t\n") end - file:write("\n") - file:write("\n") - file:write("\n") - file:write("\n") - file:write("\n") + end}) + + table.insert(chunks, {name='historical_event_collections', fn=function() end}) + + table.insert(chunks, {name='historical_eras', fn=function() end}) - file:write("\n") + table.insert(chunks, {name='written_contents', fn=function() for wcK, wcV in progress_ipairs(df.global.world.written_contents.all) do file:write("\t\n") file:write("\t\t"..wcV.id.."\n") @@ -914,57 +950,174 @@ local function export_more_legends_xml() file:write("\t\t"..wcV.author.."\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='poetic_forms', fn=function() for formK, formV in progress_ipairs(df.global.world.poetic_forms.all, 'poetic form') do file:write("\t\n") file:write("\t\t"..formV.id.."\n") file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(formV.name,1))).."\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='musical_forms', fn=function() for formK, formV in progress_ipairs(df.global.world.musical_forms.all, 'musical form') do file:write("\t\n") file:write("\t\t"..formV.id.."\n") file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(formV.name,1))).."\n") file:write("\t\n") end - file:write("\n") + end}) - file:write("\n") + table.insert(chunks, {name='dance_forms', fn=function() for formK, formV in progress_ipairs(df.global.world.dance_forms.all, 'dance form') do file:write("\t\n") file:write("\t\t"..formV.id.."\n") file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(formV.name,1))).."\n") file:write("\t\n") end - file:write("\n") + end}) + + step_size = math.max(1, 100 // #chunks) + for k, chunk in ipairs(chunks) do + progress_percent = math.max(progress_percent, (100 * k) // #chunks) + step_percent = progress_percent + write_chunk(chunk.name, chunk.fn) + end file:write("\n") file:close() local problem_elements_exist = false - for i, element in pairs (problem_elements) do - for k, field in pairs (element) do - dfhack.printerr (i.." element '"..k.."' attempted to be processed as simple type.") + for i, element in pairs(problem_elements) do + for k, field in pairs(element) do + dfhack.printerr(i.." element '"..k.."' attempted to be processed as simple type.") end problem_elements_exist = true end if problem_elements_exist then - dfhack.printerr ("Some elements could not be interpreted correctly because they were not simple elements.") - dfhack.printerr ("These elements are reported above. Please notify the DFHack community of these value pairs.") - dfhack.printerr ("Note that these issues have not invalidated the XML file: it ought to still be usable.") + dfhack.printerr("Some elements could not be interpreted correctly because they were not simple elements.") + dfhack.printerr("These elements are reported above. Please notify the DFHack community of these value pairs.") + dfhack.printerr("Note that these issues have not invalidated the XML file: it ought to still be usable.") + end + + print("Done exporting extended legends data to: " .. filename) +end + +local function wrap_export() + if progress_percent >= 0 then + qerror('exportlegends already in progress') + end + progress_percent = 0 + step_size = 1 + progress_item = 'basic info' + yield_if_timeout() + local ok, err = pcall(export_more_legends_xml) + if not ok then + dfhack.printerr(err) end + progress_percent = -1 + step_size = 1 + progress_item = '' +end + +-- ------------------- +-- LegendsOverlay +-- + +LegendsOverlay = defclass(LegendsOverlay, overlay.OverlayWidget) +LegendsOverlay.ATTRS{ + default_pos={x=2, y=2}, + default_enabled=true, + viewscreens='legends/Default', + frame={w=70, h=5}, +} + +function LegendsOverlay:init() + self:addviews{ + widgets.Panel{ + view_id='button_mask', + frame={t=0, l=0, w=15, h=3}, + }, + widgets.Panel{ + frame={b=0, l=0, r=0, h=1}, + subviews={ + widgets.ToggleHotkeyLabel{ + view_id='do_export', + frame={t=0, l=0, w=48}, + label='Also export extended legends data:', + key='CUSTOM_CTRL_D', + visible=function() return progress_percent < 0 end, + }, + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Exporting ', + {text=function() return progress_item end}, + ' (', + {text=function() return progress_percent end, pen=COLOR_YELLOW}, + '% complete)' + }, + visible=function() return progress_percent >= 0 end, + }, + }, + }, + } +end + +function LegendsOverlay:onInput(keys) + if keys._MOUSE_L_DOWN and progress_percent < 0 and + self.subviews.button_mask:getMousePos() and + self.subviews.do_export:getOptionValue() + then + script.start(wrap_export) + end + return LegendsOverlay.super.onInput(self, keys) +end + +-- ------------------- +-- DoneMaskOverlay +-- + +DoneMaskOverlay = defclass(DoneMaskOverlay, overlay.OverlayWidget) +DoneMaskOverlay.ATTRS{ + default_pos={x=-2, y=2}, + default_enabled=true, + viewscreens='legends', + frame={w=9, h=3}, +} + +function DoneMaskOverlay:init() + self:addviews{ + widgets.Panel{ + frame_background=gui.CLEAR_PEN, + visible=function() return progress_percent >= 0 end, + } + } +end + +function DoneMaskOverlay:onInput(keys) + if progress_percent >= 0 then + if keys.LEAVESCREEN or (keys._MOUSE_L_DOWN and self:getMousePos()) then + return true + end + end + return DoneMaskOverlay.super.onInput(self, keys) +end + +OVERLAY_WIDGETS = { + export=LegendsOverlay, + mask=DoneMaskOverlay, +} + +if dfhack_flags.module then + return end -- Check if on legends screen and trigger the export if so -if dfhack.gui.matchFocusString('legends') then - export_more_legends_xml() -else +if not dfhack.gui.matchFocusString('legends') then qerror('exportlegends must be run from the main legends view') end -print("Exported files can be found in the top-level DF game folder.") +script.start(wrap_export) From 8ed047aa299c95ee23e118f46dafab8b8f336834 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 7 Aug 2023 03:55:14 -0700 Subject: [PATCH 426/732] document yield latency --- exportlegends.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/exportlegends.lua b/exportlegends.lua index 82a85f3409..ab8b585b67 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -50,9 +50,13 @@ step_percent = -1 progress_percent = progress_percent or -1 last_update_ms = 0 +-- should be frequent enough so that user can still effectively use +-- the vanilla legends UI to browse while export is in progress +local YIELD_TIMEOUT_MS = 10 + local function yield_if_timeout() local now_ms = dfhack.getTickCount() - if now_ms - last_update_ms > 10 then + if now_ms - last_update_ms > YIELD_TIMEOUT_MS then script.sleep(1, 'frames') last_update_ms = dfhack.getTickCount() end @@ -65,7 +69,6 @@ local function progress_ipairs(vector, desc, interval) local cb = ipairs(vector) return function(vector, k, ...) if k then - local prev_progress_percent = progress_percent progress_percent = math.max(progress_percent, step_percent + ((k * step_size) // #vector)) if #vector >= interval and (k % interval == 0 or k == #vector - 1) then print((' %s %i/%i (%0.f%%)'):format(desc, k, #vector, (k * 100) / #vector)) From 6891dbbe2486739c201d21e050a5fee66e2ae72e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 7 Aug 2023 12:42:23 -0700 Subject: [PATCH 427/732] make exportlegends widget more distictively dfhack --- exportlegends.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/exportlegends.lua b/exportlegends.lua index ab8b585b67..69633a6320 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -1034,7 +1034,7 @@ LegendsOverlay.ATTRS{ default_pos={x=2, y=2}, default_enabled=true, viewscreens='legends/Default', - frame={w=70, h=5}, + frame={w=55, h=5}, } function LegendsOverlay:init() @@ -1043,18 +1043,18 @@ function LegendsOverlay:init() view_id='button_mask', frame={t=0, l=0, w=15, h=3}, }, - widgets.Panel{ + widgets.BannerPanel{ frame={b=0, l=0, r=0, h=1}, subviews={ widgets.ToggleHotkeyLabel{ view_id='do_export', - frame={t=0, l=0, w=48}, - label='Also export extended legends data:', + frame={t=0, l=1, w=53}, + label='Also export DFHack extended legends data:', key='CUSTOM_CTRL_D', visible=function() return progress_percent < 0 end, }, widgets.Label{ - frame={t=0, l=0}, + frame={t=0, l=1}, text={ 'Exporting ', {text=function() return progress_item end}, From 3cc28ae5ba74e4ad738ee4b21ecc8117a117b56c Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Wed, 9 Aug 2023 22:33:11 +0200 Subject: [PATCH 428/732] suspendmanager: improve detection on "T" and "+" shapes --- changelog.txt | 1 + suspendmanager.lua | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index a33fc46be2..6c8eeed715 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly - `caravan`: Correct price adjustment values in trade agreement details screen - `caravan`: Apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices +- `suspendmanager`: Improve the detection on "T" and "+" shaped high walls ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames diff --git a/suspendmanager.lua b/suspendmanager.lua index baa65174d2..ccd4cd4486 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -66,6 +66,7 @@ EXTERNAL_REASONS = { ---@class SuspendManager ---@field preventBlocking boolean ---@field suspensions table +---@field leadsToDeadend table ---@field lastAutoRunTick integer SuspendManager = defclass(SuspendManager) SuspendManager.ATTRS { @@ -76,6 +77,9 @@ SuspendManager.ATTRS { --- Current job suspensions with their reasons suspensions = {}, + --- Current job that are part of a dead-end, not worth considering as an exit + leadsToDeadend = {}, + --- Last tick where it was run automatically lastAutoRunTick = -1, } @@ -316,11 +320,6 @@ function SuspendManager:suspendDeadend(start_job) if not building then return end local pos = {x=building.centerx,y=building.centery,z=building.z} - -- visited building ids of this potential dead end - local visited = { - [building.id] = true - } - --- Support dead ends of a maximum length of 1000 for _=0,1000 do -- building plan on the way to the exit @@ -338,7 +337,7 @@ function SuspendManager:suspendDeadend(start_job) return end - if visited[impassablePlan.id] then + if self.leadsToDeadend[impassablePlan.id] then -- already visited, not an exit goto continue end @@ -356,14 +355,19 @@ function SuspendManager:suspendDeadend(start_job) if not exit then return end - -- exit is the single exit point of this corridor, suspend its construction job + -- exit is the single exit point of this corridor, suspend its construction job, + -- mark the current tile of the corridor as leading to a dead-end -- and continue the exploration from its position for _,job in ipairs(exit.jobs) do if job.job_type == df.job_type.ConstructBuilding then self.suspensions[job.id] = REASON.DEADEND end end - visited[exit.id] = true + local building = dfhack.buildings.findAtTile(pos) + if building then + self.leadsToDeadend[building.id] = true + end + pos = {x=exit.centerx,y=exit.centery,z=exit.z} end end @@ -417,6 +421,7 @@ end --- Recompute the list of suspended jobs function SuspendManager:refresh() self.suspensions = {} + self.leadsToDeadend = {} for _,job in utils.listpairs(df.global.world.jobs.list) do -- External reasons to suspend a job From 8e9d0405da11b8ef55feebcc26d6e76d189f15c1 Mon Sep 17 00:00:00 2001 From: lethosor Date: Wed, 9 Aug 2023 23:34:42 -0400 Subject: [PATCH 429/732] devel/dump-offsets: add global_table, print table length --- devel/dump-offsets.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/devel/dump-offsets.lua b/devel/dump-offsets.lua index f7acee7e92..38d3bf8858 100644 --- a/devel/dump-offsets.lua +++ b/devel/dump-offsets.lua @@ -203,7 +203,7 @@ function save_addr(name, addr) end local extended = false -local start = data.intptr_t:find_one(search) +local start, start_addr = data.intptr_t:find_one(search) if start then extended = true else @@ -215,6 +215,11 @@ if not start then qerror('Could not find global table header') end +if extended then + -- structures only has types for an extended global table + save_addr('global_table', start_addr + (#search * data.intptr_t.esize)) +end + local index = 1 local entry_size = (extended and 3 or 2) while true do @@ -236,3 +241,5 @@ while true do end index = index + 1 end + +print('global table length:', index) From 3f562df173833e9134d33c14451ede7b9deedc43 Mon Sep 17 00:00:00 2001 From: lethosor Date: Thu, 10 Aug 2023 15:19:11 -0400 Subject: [PATCH 430/732] Add devel/scan-vtables (Linux-only, squashed) --- devel/scan-vtables.lua | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 devel/scan-vtables.lua diff --git a/devel/scan-vtables.lua b/devel/scan-vtables.lua new file mode 100644 index 0000000000..058eb8a89d --- /dev/null +++ b/devel/scan-vtables.lua @@ -0,0 +1,60 @@ +memscan = require('memscan') + +local df_ranges = {} +for i,mem in ipairs(dfhack.internal.getMemRanges()) do + if mem.read and ( + string.match(mem.name,'/dwarfort%.exe$') + or string.match(mem.name,'/dwarfort$') + or string.match(mem.name,'/Dwarf_Fortress$') + or string.match(mem.name,'Dwarf Fortress%.exe') + or string.match(mem.name,'/libg_src_lib.so$') + ) + then + table.insert(df_ranges, mem) + end +end + +function is_df_addr(a) + for _, mem in ipairs(df_ranges) do + if a >= mem.start_addr and a < mem.end_addr then + return true + end + end + return false +end + +for _, range in ipairs(df_ranges) do + if (not range.read) or range.write or range.execute or range.name:match('g_src') then + goto next_range + end + + local area = memscan.MemoryArea.new(range.start_addr, range.end_addr) + for i = 1, area.uintptr_t.count - 1 do + local vtable = area.uintptr_t:idx2addr(i) + local typeinfo = area.uintptr_t[i - 1] + if is_df_addr(typeinfo + 8) then + local typestring = df.reinterpret_cast('uintptr_t', typeinfo + 8)[0] + if is_df_addr(typestring) then + local vlen = 0 + while is_df_addr(vtable + (8*vlen)) and is_df_addr(df.reinterpret_cast('uintptr_t', vtable + (8*vlen))[0]) do + vlen = vlen + 1 + break -- for now, any vtable with one valid function pointer is valid enough + end + if vlen > 0 then + local ok, name = pcall(function() + return memscan.read_c_string(df.reinterpret_cast('char', typestring))--:gsub('^%d+', '') + end) + if not ok then + else + local demangled_name = dfhack.internal.cxxDemangle('_Z' .. name) + if demangled_name and not demangled_name:match('[<>]') and not demangled_name:match('^std::') then + print((""):format(demangled_name, vtable)) + end + end + end + end + end + + end + ::next_range:: +end From 3624f0dcda933030b771160e5fef8f0161f78821 Mon Sep 17 00:00:00 2001 From: lethosor Date: Thu, 10 Aug 2023 15:35:00 -0400 Subject: [PATCH 431/732] Docs, check for unsupported platforms --- devel/scan-vtables.lua | 19 +++++++++++++++---- docs/devel/scan-vtables.rst | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 docs/devel/scan-vtables.rst diff --git a/devel/scan-vtables.lua b/devel/scan-vtables.lua index 058eb8a89d..c64d549d16 100644 --- a/devel/scan-vtables.lua +++ b/devel/scan-vtables.lua @@ -1,5 +1,11 @@ +-- Scan and dump likely vtable addresses memscan = require('memscan') +local osType = dfhack.getOSType() +if osType ~= 'linux' then + qerror('unsupported OS: ' .. osType) +end + local df_ranges = {} for i,mem in ipairs(dfhack.internal.getMemRanges()) do if mem.read and ( @@ -30,22 +36,27 @@ for _, range in ipairs(df_ranges) do local area = memscan.MemoryArea.new(range.start_addr, range.end_addr) for i = 1, area.uintptr_t.count - 1 do + -- take every pointer-aligned value in memory mapped to the DF executable, and see if it is a valid vtable + -- start by following the logic in Process::doReadClassName() and ensure it doesn't crash local vtable = area.uintptr_t:idx2addr(i) local typeinfo = area.uintptr_t[i - 1] if is_df_addr(typeinfo + 8) then local typestring = df.reinterpret_cast('uintptr_t', typeinfo + 8)[0] if is_df_addr(typestring) then + -- rule out false positives by checking that the vtable points to a table of valid pointers + -- TODO: check that the pointers are actually function pointers local vlen = 0 while is_df_addr(vtable + (8*vlen)) and is_df_addr(df.reinterpret_cast('uintptr_t', vtable + (8*vlen))[0]) do vlen = vlen + 1 - break -- for now, any vtable with one valid function pointer is valid enough + break -- for now, any vtable with one valid pointer is valid enough end if vlen > 0 then + -- some false positives can be ruled out if the string.char() call in read_c_string() throws an error for invalid characters local ok, name = pcall(function() - return memscan.read_c_string(df.reinterpret_cast('char', typestring))--:gsub('^%d+', '') + return memscan.read_c_string(df.reinterpret_cast('char', typestring)) end) - if not ok then - else + if ok then + -- GCC strips the "_Z" prefix from typeinfo names, so add it back local demangled_name = dfhack.internal.cxxDemangle('_Z' .. name) if demangled_name and not demangled_name:match('[<>]') and not demangled_name:match('^std::') then print((""):format(demangled_name, vtable)) diff --git a/docs/devel/scan-vtables.rst b/docs/devel/scan-vtables.rst new file mode 100644 index 0000000000..e17957c13d --- /dev/null +++ b/docs/devel/scan-vtables.rst @@ -0,0 +1,24 @@ +devel/scan-vtables +================== + +.. dfhack-tool:: + :summary: Scan for and print likely vtable addresses. + :tags: dev + +.. warning:: + + THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS. + + Running this script on a new DF version will NOT MAKE IT RUN CORRECTLY if + any data structures changed, thus possibly leading to CRASHES AND/OR + PERMANENT SAVE CORRUPTION. + +This script scans for likely vtables in memory pages mapped to the DF +executable, and prints them in a format ready for inclusion in ``symbols.xml`` + +Usage +----- + +:: + + devel/scan-vtables From b062072d684065ab58228c7bd4627a264e4bfb00 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Thu, 10 Aug 2023 21:46:31 +0200 Subject: [PATCH 432/732] suspendmanager: internal cleanup --- suspendmanager.lua | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index ccd4cd4486..4f650f8b27 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -58,9 +58,9 @@ REASON_DESCRIPTION = { --- Suspension reasons from an external source --- SuspendManager does not actively suspend such jobs, but --- will not unsuspend them -EXTERNAL_REASONS = { - [REASON.UNDER_WATER]=true, - [REASON.BUILDINGPLAN]=true, +EXTERNAL_REASONS = utils.invert{ + REASON.UNDER_WATER, + REASON.BUILDINGPLAN, } ---@class SuspendManager @@ -156,25 +156,25 @@ function foreach_construction_job(fn) end end -local CONSTRUCTION_IMPASSABLE = { - [df.construction_type.Wall]=true, - [df.construction_type.Fortification]=true, +local CONSTRUCTION_IMPASSABLE = utils.invert{ + df.construction_type.Wall, + df.construction_type.Fortification, } -local BUILDING_IMPASSABLE = { - [df.building_type.Floodgate]=true, - [df.building_type.Statue]=true, - [df.building_type.WindowGlass]=true, - [df.building_type.WindowGem]=true, - [df.building_type.GrateWall]=true, - [df.building_type.BarsVertical]=true, +local BUILDING_IMPASSABLE = utils.invert{ + df.building_type.Floodgate, + df.building_type.Statue, + df.building_type.WindowGlass, + df.building_type.WindowGem, + df.building_type.GrateWall, + df.building_type.BarsVertical, } --- Designation job type that are erased if a building is built on top of it -local ERASABLE_DESIGNATION = { - [df.job_type.CarveTrack]=true, - [df.job_type.SmoothFloor]=true, - [df.job_type.DetailFloor]=true, +local ERASABLE_DESIGNATION = utils.invert{ + df.job_type.CarveTrack, + df.job_type.SmoothFloor, + df.job_type.DetailFloor, } --- Job types that impact suspendmanager @@ -363,11 +363,9 @@ function SuspendManager:suspendDeadend(start_job) self.suspensions[job.id] = REASON.DEADEND end end - local building = dfhack.buildings.findAtTile(pos) - if building then - self.leadsToDeadend[building.id] = true - end + self.leadsToDeadend[building.id] = true + building = exit pos = {x=exit.centerx,y=exit.centery,z=exit.z} end end From 7ae1ed75aae9d5e1adbe621a5718854eb7a629be Mon Sep 17 00:00:00 2001 From: lethosor Date: Fri, 11 Aug 2023 01:50:06 -0400 Subject: [PATCH 433/732] Mark several dev tools as available and update the spreadsheet --- docs/devel/dump-offsets.rst | 2 +- docs/devel/find-primitive.rst | 2 +- docs/devel/print-args.rst | 2 +- docs/devel/print-args2.rst | 2 +- docs/devel/save-version.rst | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/devel/dump-offsets.rst b/docs/devel/dump-offsets.rst index 80748e0d32..53b75d0751 100644 --- a/docs/devel/dump-offsets.rst +++ b/docs/devel/dump-offsets.rst @@ -3,7 +3,7 @@ devel/dump-offsets .. dfhack-tool:: :summary: Dump the contents of the table of global addresses. - :tags: unavailable dev + :tags: dev .. warning:: diff --git a/docs/devel/find-primitive.rst b/docs/devel/find-primitive.rst index d162560e07..773fbc5911 100644 --- a/docs/devel/find-primitive.rst +++ b/docs/devel/find-primitive.rst @@ -3,7 +3,7 @@ devel/find-primitive .. dfhack-tool:: :summary: Discover memory offsets for new variables. - :tags: unavailable dev + :tags: dev This tool helps find a primitive variable in DF's data section, relying on the user to change its value and then scanning for memory that has changed to that diff --git a/docs/devel/print-args.rst b/docs/devel/print-args.rst index f5ad8af85b..2cdf1f81a3 100644 --- a/docs/devel/print-args.rst +++ b/docs/devel/print-args.rst @@ -3,7 +3,7 @@ devel/print-args .. dfhack-tool:: :summary: Echo parameters to the output. - :tags: unavailable dev + :tags: dev Prints all the arguments you supply to the script, one per line. diff --git a/docs/devel/print-args2.rst b/docs/devel/print-args2.rst index aac9b27f94..1561227758 100644 --- a/docs/devel/print-args2.rst +++ b/docs/devel/print-args2.rst @@ -3,7 +3,7 @@ devel/print-args2 .. dfhack-tool:: :summary: Echo parameters to the output. - :tags: unavailable dev + :tags: dev Prints all the arguments you supply to the script, one per line, with quotes around them. diff --git a/docs/devel/save-version.rst b/docs/devel/save-version.rst index 5b33d6680f..aa654488c0 100644 --- a/docs/devel/save-version.rst +++ b/docs/devel/save-version.rst @@ -3,7 +3,7 @@ devel/save-version .. dfhack-tool:: :summary: Display what DF version has handled the current save. - :tags: unavailable dev + :tags: dev This tool displays the DF version that created the game, the most recent DF version that has loaded and saved the game, and the current DF version. From e5b30497c384cc72b91484bfc94ba2321690459c Mon Sep 17 00:00:00 2001 From: lethosor Date: Fri, 11 Aug 2023 01:53:22 -0400 Subject: [PATCH 434/732] devel/save-version: Add 50.05-50.09 --- devel/save-version.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/devel/save-version.lua b/devel/save-version.lua index 866e56eb0d..8ac449eac5 100644 --- a/devel/save-version.lua +++ b/devel/save-version.lua @@ -119,6 +119,7 @@ versions = { [2078] = "50.01", [2079] = "50.02", [2080] = "50.03", -- and 50.04 + [2081] = "50.05", -- through 50.09 } --luacheck: global From 855f0f6b1293c3ae26e27567a2e6730a0cc81792 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 11 Aug 2023 10:25:58 -0700 Subject: [PATCH 435/732] fix Backspace label --- gui/cp437-table.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index e38ac7a401..19d486f804 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -62,7 +62,8 @@ function CPDialog:init(info) widgets.HotkeyLabel{ frame={b=0}, key='STRING_A000', - label='Backspace', + key_sep='', + label='Click here to Backspace', auto_width=true, on_activate=function() self.subviews.edit:onInput{_STRING=0} end, }, From d1ed5b7401340286faa626ef3a26f16a7b80a6bb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 11 Aug 2023 13:17:48 -0700 Subject: [PATCH 436/732] vanish the undead instead of marking them inactive so undead sieges can end --- changelog.txt | 1 + starvingdead.lua | 13 ++++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6c8eeed715..15bee9cdea 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: - `caravan`: Correct price adjustment values in trade agreement details screen - `caravan`: Apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices - `suspendmanager`: Improve the detection on "T" and "+" shaped high walls +- `starvingdead`: ensure sieges end properly when undead siegers starve ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames diff --git a/starvingdead.lua b/starvingdead.lua index 72728bbff8..8bf523fa38 100644 --- a/starvingdead.lua +++ b/starvingdead.lua @@ -45,26 +45,22 @@ end StarvingDead = defclass(StarvingDead) StarvingDead.ATTRS{ decay_rate = 1, - death_threshold = 6 + death_threshold = 6, } function StarvingDead:init() self.timeout_id = nil -- Percentage goal each attribute should reach before death. - self.attribute_goal = 10 - self.attribute_decay = (self.attribute_goal ^ (1 / ((self.death_threshold * 28 / self.decay_rate)))) / 100 - self.undead_count = 0 + local attribute_goal = 10 + self.attribute_decay = (attribute_goal ^ (1 / ((self.death_threshold * 28 / self.decay_rate)))) / 100 self:checkDecay() print(([[StarvingDead started, checking every %s days and killing off at %s months]]):format(self.decay_rate, self.death_threshold)) end function StarvingDead:checkDecay() - self.undead_count = 0 for _, unit in pairs(df.global.world.units.active) do if (unit.enemy.undead and not unit.flags1.inactive) then - self.undead_count = self.undead_count + 1 - -- time_on_site is measured in ticks, a month is 33600 ticks. -- @see https://dwarffortresswiki.org/index.php/Time for _, attribute in pairs(unit.body.physical_attrs) do @@ -72,8 +68,7 @@ function StarvingDead:checkDecay() end if unit.curse.time_on_site > (self.death_threshold * 33600) then - unit.flags1.inactive = true - unit.curse.rem_tags2.FIT_FOR_ANIMATION = true + unit.animal.vanish_countdown = 1 end end end From c893121dfe5f38a4dc167cf64844a2210f41a483 Mon Sep 17 00:00:00 2001 From: lethosor Date: Fri, 11 Aug 2023 17:24:29 -0400 Subject: [PATCH 437/732] Changelog for #793 --- changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog.txt b/changelog.txt index 6c8eeed715..f0be855fc6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,9 @@ Template for new versions: ## New Features - `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! +## New Scripts +- `devel/scan-vtables`: Scan and dump likely vtable addresses (for memory research) + ## Fixes - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly - `caravan`: Correct price adjustment values in trade agreement details screen From 9e0460df0951cbbc7ac3afa21bee35aad739d68b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 11 Aug 2023 15:33:36 -0700 Subject: [PATCH 438/732] use New Tools section and add it to the template --- changelog.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index f0be855fc6..2aeb15a6dc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -12,7 +12,7 @@ that repo. Template for new versions: -# Future +## New Tools ## New Features @@ -26,12 +26,12 @@ Template for new versions: # Future +## New Tools +- `devel/scan-vtables`: Scan and dump likely vtable addresses (for memory research) + ## New Features - `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! -## New Scripts -- `devel/scan-vtables`: Scan and dump likely vtable addresses (for memory research) - ## Fixes - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly - `caravan`: Correct price adjustment values in trade agreement details screen From 5b59f60c30557ab24d6bc869c4976fdb70c322a5 Mon Sep 17 00:00:00 2001 From: lethosor Date: Sat, 12 Aug 2023 00:46:54 -0400 Subject: [PATCH 439/732] devel/dump-offsets: add missing next_id globals dump_df_globals.rb was retrieving these because it uses an additional regex-based match for next_id globals --- devel/dump-offsets.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/devel/dump-offsets.lua b/devel/dump-offsets.lua index 38d3bf8858..9109a00daa 100644 --- a/devel/dump-offsets.lua +++ b/devel/dump-offsets.lua @@ -167,6 +167,8 @@ GLOBALS = { next_unit_global_id = "unit_next_id", next_vehicle_global_id = "vehicle_next_id", next_written_content_global_id = "written_content_next_id", + next_divination_set_global_id = "divination_set_next_id", + next_image_set_global_id = "image_set_next_id", } function read_cstr(addr) From 8a4daf37169f6712f3b61c5108f335f5229ed8aa Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 12 Aug 2023 11:53:47 +0200 Subject: [PATCH 440/732] Update workorder-recheck.lua make `workorder-recheck` work again, add a widget --- workorder-recheck.lua | 68 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/workorder-recheck.lua b/workorder-recheck.lua index d16c9755f7..cd160b3001 100644 --- a/workorder-recheck.lua +++ b/workorder-recheck.lua @@ -3,18 +3,66 @@ workorder-recheck ================= -Sets the status to ``Checking`` (from ``Active``) of the selected work order (in the ``j-m`` or ``u-m`` screens). +Sets the status to ``Checking`` (from ``Active``) of the selected work order. This makes the manager reevaluate its conditions. ]====] -local scr = dfhack.gui.getCurViewscreen() -if df.viewscreen_jobmanagementst:is_instance(scr) then - local orders = df.global.world.manager_orders - local idx = scr.sel_idx - if idx < #orders then - orders[idx].status.active = false + +--@ module = true + +local gui = require('gui') +local widgets = require('gui.widgets') +local overlay = require('plugins.overlay') + +local function set_current_inactive() + local scrConditions = df.global.game.main_interface.info.work_orders.conditions + if scrConditions.open then + local order = scrConditions.wq + order.status.active = false else - qerror("Invalid order selected") + qerror("Order conditions is not open") end -else - qerror('Must be called on the manager screen (j-m or u-m)') end + +-- ------------------- +-- RecheckOverlay +-- + +local focusString = 'dwarfmode/Info/WORK_ORDERS/Conditions' + +RecheckOverlay = defclass(RecheckOverlay, overlay.OverlayWidget) +RecheckOverlay.ATTRS{ + default_pos={x=6,y=2}, + default_enabled=true, + viewscreens=focusString, + frame={w=17, h=3}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function RecheckOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='recheck', + key='CUSTOM_CTRL_A', + on_activate=set_current_inactive, + }, + } +end + +-- ------------------- + +OVERLAY_WIDGETS = { + recheck=RecheckOverlay, +} + +if dfhack_flags.module then + return +end + +-- Check if on correct screen and perform the action if so +if not dfhack.gui.matchFocusString(focusString) then + qerror('workorder-recheck must be run from the manager order conditions view') +end + +set_current_inactive() From 84db52e6b65d3acac491faf160994607470ae938 Mon Sep 17 00:00:00 2001 From: shevernitskiy Date: Sat, 12 Aug 2023 16:18:48 +0300 Subject: [PATCH 441/732] use dynamic texture mechanism --- gui/civ-alert.lua | 7 ++++--- gui/control-panel.lua | 10 ++++------ gui/cp437-table.lua | 6 +++--- gui/design.lua | 28 +++++++++++++++------------- gui/launcher.lua | 31 +++++++++++++++++-------------- unsuspend.lua | 22 ++++++++-------------- 6 files changed, 51 insertions(+), 53 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 75b58bb4c2..049eafd1a3 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -118,13 +118,14 @@ end last_tp_start = last_tp_start or 0 CONFIG_BUTTON_PENS = CONFIG_BUTTON_PENS or {} local function get_button_pen(idx) - local start = dfhack.textures.getControlPanelTexposStart() + local start = dfhack.textures.getAsset("hack/data/art/control-panel.png", 0) if last_tp_start == start then return CONFIG_BUTTON_PENS[idx] end last_tp_start = start local tp = function(offset) - if start == -1 then return nil end - return start + offset + local texpos = dfhack.textures.getAsset("hack/data/art/control-panel.png", offset) + if texpos == -1 then return nil end + return texpos end CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=tp(6), ch=string.byte('[')} diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 4ee57ea6e5..c1be2b6638 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -153,12 +153,10 @@ local function save_file(path, save_fn) end local function get_icon_pens() - local start = dfhack.textures.getControlPanelTexposStart() - local valid = start > 0 - start = start + 10 - - local function tp(offset) - return valid and start + offset or nil + local tp = function(offset) + local texpos = dfhack.textures.getAsset("hack/data/art/control-panel.png", offset + 10) + if texpos == -1 then return nil end + return texpos end local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index e38ac7a401..731ca5ae8a 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -6,10 +6,10 @@ local widgets = require('gui.widgets') local to_pen = dfhack.pen.parse -local tb_texpos = dfhack.textures.getThinBordersTexposStart() local tp = function(offset) - if tb_texpos == -1 then return nil end - return tb_texpos + offset + local texpos = dfhack.textures.getAsset("hack/data/art/border-thin.png", offset) + if texpos == -1 then return nil end + return texpos end local function get_key_pens(ch) diff --git a/gui/design.lua b/gui/design.lua index 696dbb46b6..6c20521abf 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -115,31 +115,33 @@ end -- Utilities local function get_icon_pens() - local start = dfhack.textures.getControlPanelTexposStart() - local valid = start > 0 - start = start + 10 + local tp = function(offset) + local texpos = dfhack.textures.getAsset("hack/data/art/control-panel.png", offset + 10) + if texpos == -1 then return nil end + return texpos + end local enabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = valid and (start + 0) or nil, ch = string.byte('[') } + tile = tp(0) or nil, ch = string.byte('[') } local enabled_pen_center = dfhack.pen.parse { fg = COLOR_LIGHTGREEN, - tile = valid and (start + 1) or nil, ch = 251 } -- check + tile = tp(1) or nil, ch = 251 } -- check local enabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = valid and (start + 2) or nil, ch = string.byte(']') } + tile = tp(2) or nil, ch = string.byte(']') } local disabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = valid and (start + 3) or nil, ch = string.byte('[') } + tile = tp(3) or nil, ch = string.byte('[') } local disabled_pen_center = dfhack.pen.parse { fg = COLOR_RED, - tile = valid and (start + 4) or nil, ch = string.byte('x') } + tile = tp(4) or nil, ch = string.byte('x') } local disabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = valid and (start + 5) or nil, ch = string.byte(']') } + tile = tp(5) or nil, ch = string.byte(']') } local button_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = valid and (start + 6) or nil, ch = string.byte('[') } + tile = tp(6) or nil, ch = string.byte('[') } local button_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = valid and (start + 7) or nil, ch = string.byte(']') } + tile = tp(7) or nil, ch = string.byte(']') } local help_pen_center = dfhack.pen.parse { - tile = valid and (start + 8) or nil, ch = string.byte('?') + tile = tp(8) or nil, ch = string.byte('?') } local configure_pen_center = dfhack.pen.parse { - tile = valid and (start + 9) or nil, ch = 15 + tile = tp(9) or nil, ch = 15 } -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, diff --git a/gui/launcher.lua b/gui/launcher.lua index ca0597f4b8..91eb3187d2 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -473,21 +473,24 @@ function MainPanel:postUpdateLayout() config:write(self.frame) end -local texpos = dfhack.textures.getThinBordersTexposStart() -local tp = function(offset) +local tp_thin = function(offset) + local texpos = dfhack.textures.getAsset("hack/data/art/border-thin.png", offset) if texpos == -1 then return nil end - return texpos + offset -end - -local H_SPLIT_PEN = dfhack.pen.parse{tile=tp(5), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} -local V_SPLIT_PEN = dfhack.pen.parse{tile=tp(4), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} -local TOP_SPLIT_PEN = dfhack.pen.parse{tile=gui.WINDOW_FRAME.t_frame_pen.tile, - ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} -local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=gui.WINDOW_FRAME.b_frame_pen.tile, - ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} -local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=gui.WINDOW_FRAME.l_frame_pen.tile, - ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} -local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=tp(17), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} + return texpos +end + +local tp_window = function(offset) + local texpos = dfhack.textures.getAsset("hack/data/art/border-window.png", offset) + if texpos == -1 then return nil end + return texpos +end + +local H_SPLIT_PEN = dfhack.pen.parse{tile=tp_thin(5), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} +local V_SPLIT_PEN = dfhack.pen.parse{tile=tp_thin(4), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} +local TOP_SPLIT_PEN = dfhack.pen.parse{tile=tp_window(1), ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} +local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=tp_window(15), ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} +local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=tp_window(7), ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} +local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=tp_thin(17), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} -- paint autocomplete panel border local function paint_vertical_border(rect) diff --git a/unsuspend.lua b/unsuspend.lua index 2d8eb8e310..aceeffe8a3 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -135,17 +135,11 @@ function SuspendOverlay:refresh_screen_buildings() self.screen_buildings = screen_buildings end -local function get_texposes() - local start = dfhack.textures.getMapUnsuspendTexposStart() - local valid = start > 0 - - local function tp(offset) - return valid and start + offset or nil - end - - return tp(3), tp(2), tp(1), tp(0) +local tp = function(offset) + local texpos = dfhack.textures.getAsset("hack/data/art/unsuspend", offset) + if texpos == -1 then return nil end + return texpos end -local PLANNED_TEXPOS, KEPT_SUSPENDED_TEXTPOS, SUSPENDED_TEXPOS, REPEAT_SUSPENDED_TEXPOS = get_texposes() function SuspendOverlay:render_marker(dc, bld, screen_pos) if not bld or #bld.jobs ~= 1 then return end @@ -156,13 +150,13 @@ function SuspendOverlay:render_marker(dc, bld, screen_pos) or not job.flags.suspend then return end - local color, ch, texpos = COLOR_YELLOW, 'x', SUSPENDED_TEXPOS + local color, ch, texpos = COLOR_YELLOW, 'x', tp(1) if buildingplan and buildingplan.isPlannedBuilding(bld) then - color, ch, texpos = COLOR_GREEN, 'P', PLANNED_TEXPOS + color, ch, texpos = COLOR_GREEN, 'P', tp(3) elseif suspendmanager and suspendmanager.isKeptSuspended(job) then - color, ch, texpos = COLOR_WHITE, 'x', KEPT_SUSPENDED_TEXTPOS + color, ch, texpos = COLOR_WHITE, 'x', tp(2) elseif data.suspend_count > 1 then - color, ch, texpos = COLOR_RED, 'X', REPEAT_SUSPENDED_TEXPOS + color, ch, texpos = COLOR_RED, 'X', tp(0) end dc:seek(screen_pos.x, screen_pos.y):tile(ch, texpos, color) end From 9fd31b1bb04642b4d398480ca0d23149917aaecc Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 12 Aug 2023 20:19:51 +0200 Subject: [PATCH 442/732] Update workorder-recheck.rst --- docs/workorder-recheck.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/workorder-recheck.rst b/docs/workorder-recheck.rst index 70f18fdf48..731ca5de96 100644 --- a/docs/workorder-recheck.rst +++ b/docs/workorder-recheck.rst @@ -3,13 +3,13 @@ workorder-recheck .. dfhack-tool:: :summary: Recheck start conditions for a manager workorder. - :tags: unavailable fort workorders + :tags: fort workorders -Sets the status to ``Checking`` (from ``Active``) of the selected work order (in -the ``j-m`` or ``u-m`` screens). This makes the manager reevaluate its -conditions. This is especially useful for an order that had its conditions met -when it was started, but the requisite items have since disappeared and the -workorder is now generating job cancellation spam. +Sets the status to ``Checking`` (from ``Active``) of the selected work order. +This makes the manager reevaluate its conditions. This is especially useful +for an order that had its conditions met when it was started, but the requisite +items have since disappeared and the workorder is now generating job cancellation +spam. Usage ----- From fcb7a53dfcf18818f849bcda8757972a7a22b246 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 12 Aug 2023 20:21:16 +0200 Subject: [PATCH 443/732] remove doc-string from workorder-recheck.lua --- workorder-recheck.lua | 7 ------- 1 file changed, 7 deletions(-) diff --git a/workorder-recheck.lua b/workorder-recheck.lua index cd160b3001..1a55c8d0ca 100644 --- a/workorder-recheck.lua +++ b/workorder-recheck.lua @@ -1,11 +1,4 @@ -- Resets the selected work order to the `Checking` state ---[====[ - -workorder-recheck -================= -Sets the status to ``Checking`` (from ``Active``) of the selected work order. -This makes the manager reevaluate its conditions. -]====] --@ module = true From b860b075321afd20a39d343d8b458439745b5b1a Mon Sep 17 00:00:00 2001 From: shevernitskiy Date: Sun, 13 Aug 2023 07:35:07 +0300 Subject: [PATCH 444/732] single quotes --- gui/civ-alert.lua | 4 ++-- gui/control-panel.lua | 2 +- gui/cp437-table.lua | 2 +- gui/design.lua | 2 +- gui/launcher.lua | 4 ++-- unsuspend.lua | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 049eafd1a3..446e020d31 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -118,12 +118,12 @@ end last_tp_start = last_tp_start or 0 CONFIG_BUTTON_PENS = CONFIG_BUTTON_PENS or {} local function get_button_pen(idx) - local start = dfhack.textures.getAsset("hack/data/art/control-panel.png", 0) + local start = dfhack.textures.getAsset('hack/data/art/control-panel.png', 0) if last_tp_start == start then return CONFIG_BUTTON_PENS[idx] end last_tp_start = start local tp = function(offset) - local texpos = dfhack.textures.getAsset("hack/data/art/control-panel.png", offset) + local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset) if texpos == -1 then return nil end return texpos end diff --git a/gui/control-panel.lua b/gui/control-panel.lua index c1be2b6638..e7fcbb3228 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -154,7 +154,7 @@ end local function get_icon_pens() local tp = function(offset) - local texpos = dfhack.textures.getAsset("hack/data/art/control-panel.png", offset + 10) + local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset + 10) if texpos == -1 then return nil end return texpos end diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index 731ca5ae8a..25b06c9fe6 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -7,7 +7,7 @@ local widgets = require('gui.widgets') local to_pen = dfhack.pen.parse local tp = function(offset) - local texpos = dfhack.textures.getAsset("hack/data/art/border-thin.png", offset) + local texpos = dfhack.textures.getAsset('hack/data/art/border-thin.png', offset) if texpos == -1 then return nil end return texpos end diff --git a/gui/design.lua b/gui/design.lua index 6c20521abf..6888580763 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -116,7 +116,7 @@ end local function get_icon_pens() local tp = function(offset) - local texpos = dfhack.textures.getAsset("hack/data/art/control-panel.png", offset + 10) + local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset + 10) if texpos == -1 then return nil end return texpos end diff --git a/gui/launcher.lua b/gui/launcher.lua index 91eb3187d2..7b948a4b55 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -474,13 +474,13 @@ function MainPanel:postUpdateLayout() end local tp_thin = function(offset) - local texpos = dfhack.textures.getAsset("hack/data/art/border-thin.png", offset) + local texpos = dfhack.textures.getAsset('hack/data/art/border-thin.png', offset) if texpos == -1 then return nil end return texpos end local tp_window = function(offset) - local texpos = dfhack.textures.getAsset("hack/data/art/border-window.png", offset) + local texpos = dfhack.textures.getAsset('hack/data/art/border-window.png', offset) if texpos == -1 then return nil end return texpos end diff --git a/unsuspend.lua b/unsuspend.lua index aceeffe8a3..17a599f83a 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -136,7 +136,7 @@ function SuspendOverlay:refresh_screen_buildings() end local tp = function(offset) - local texpos = dfhack.textures.getAsset("hack/data/art/unsuspend", offset) + local texpos = dfhack.textures.getAsset('hack/data/art/unsuspend', offset) if texpos == -1 then return nil end return texpos end From 97edde0c6d107b6687f06f300f44dd7a2bc6641d Mon Sep 17 00:00:00 2001 From: shevernitskiy Date: Sun, 13 Aug 2023 07:39:27 +0300 Subject: [PATCH 445/732] tiny refactor tp methods --- gui/civ-alert.lua | 3 +-- gui/control-panel.lua | 3 +-- gui/cp437-table.lua | 3 +-- gui/design.lua | 3 +-- gui/launcher.lua | 3 +-- unsuspend.lua | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 446e020d31..9afc2d3453 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -124,8 +124,7 @@ local function get_button_pen(idx) local tp = function(offset) local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset) - if texpos == -1 then return nil end - return texpos + return texpos >= 0 and texpos or nil end CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=tp(6), ch=string.byte('[')} diff --git a/gui/control-panel.lua b/gui/control-panel.lua index e7fcbb3228..4ecf7682cb 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -155,8 +155,7 @@ end local function get_icon_pens() local tp = function(offset) local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset + 10) - if texpos == -1 then return nil end - return texpos + return texpos >= 0 and texpos or nil end local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index 25b06c9fe6..ba9a7352b7 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -8,8 +8,7 @@ local to_pen = dfhack.pen.parse local tp = function(offset) local texpos = dfhack.textures.getAsset('hack/data/art/border-thin.png', offset) - if texpos == -1 then return nil end - return texpos + return texpos >= 0 and texpos or nil end local function get_key_pens(ch) diff --git a/gui/design.lua b/gui/design.lua index 6888580763..04dd572ad6 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -117,8 +117,7 @@ end local function get_icon_pens() local tp = function(offset) local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset + 10) - if texpos == -1 then return nil end - return texpos + return texpos >= 0 and texpos or nil end local enabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, diff --git a/gui/launcher.lua b/gui/launcher.lua index 7b948a4b55..57c325bbc3 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -481,8 +481,7 @@ end local tp_window = function(offset) local texpos = dfhack.textures.getAsset('hack/data/art/border-window.png', offset) - if texpos == -1 then return nil end - return texpos + return texpos >= 0 and texpos or nil end local H_SPLIT_PEN = dfhack.pen.parse{tile=tp_thin(5), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} diff --git a/unsuspend.lua b/unsuspend.lua index 17a599f83a..09f6105554 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -137,8 +137,7 @@ end local tp = function(offset) local texpos = dfhack.textures.getAsset('hack/data/art/unsuspend', offset) - if texpos == -1 then return nil end - return texpos + return texpos >= 0 and texpos or nil end function SuspendOverlay:render_marker(dc, bld, screen_pos) From e5bf1045f028cd2567006770a129babf4a30b3b7 Mon Sep 17 00:00:00 2001 From: shevernitskiy Date: Sun, 13 Aug 2023 08:58:27 +0300 Subject: [PATCH 446/732] more tp refactor --- gui/launcher.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index 57c325bbc3..0e34006792 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -475,8 +475,7 @@ end local tp_thin = function(offset) local texpos = dfhack.textures.getAsset('hack/data/art/border-thin.png', offset) - if texpos == -1 then return nil end - return texpos + return texpos >= 0 and texpos or nil end local tp_window = function(offset) From b2e0ac1d8ffe4ddeb5e920c38393e339ae3e8283 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 13 Aug 2023 10:41:40 +0200 Subject: [PATCH 447/732] adjust widget position depending on tab rows --- workorder-recheck.lua | 52 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/workorder-recheck.lua b/workorder-recheck.lua index 1a55c8d0ca..2983536ca5 100644 --- a/workorder-recheck.lua +++ b/workorder-recheck.lua @@ -2,7 +2,6 @@ --@ module = true -local gui = require('gui') local widgets = require('gui.widgets') local overlay = require('plugins.overlay') @@ -24,23 +23,60 @@ local focusString = 'dwarfmode/Info/WORK_ORDERS/Conditions' RecheckOverlay = defclass(RecheckOverlay, overlay.OverlayWidget) RecheckOverlay.ATTRS{ - default_pos={x=6,y=2}, + default_pos={x=6,y=8}, default_enabled=true, viewscreens=focusString, - frame={w=17, h=3}, - frame_style=gui.MEDIUM_FRAME, - frame_background=gui.CLEAR_PEN, + frame={w=19, h=3}, + -- frame_style=gui.MEDIUM_FRAME, + -- frame_background=gui.CLEAR_PEN, } +local function areTabsInTwoRows() + -- get the tile above the order status icon + local pen = dfhack.screen.readTile(7, 7, false) + -- in graphics mode, `0` when one row, something else when two (`67` aka 'C' from "Creatures") + -- in ASCII mode, `32` aka ' ' when one row, something else when two (`196` aka '-' from tab frame's top) + return (pen.ch ~= 0 and pen.ch ~= 32) +end + +function RecheckOverlay:updateTextButtonFrame() + local twoRows = areTabsInTwoRows() + if (self._twoRows == twoRows) then return false end + + self._twoRows = twoRows + local frame = twoRows + and {b=0, l=0, r=0, h=1} + or {t=0, l=0, r=0, h=1} + self.subviews.button.frame = frame + + return true +end + function RecheckOverlay:init() self:addviews{ - widgets.HotkeyLabel{ - frame={t=0, l=0}, + widgets.TextButton{ + view_id = 'button', + -- frame={t=0, l=0, r=0, h=1}, -- is set in `updateTextButtonFrame()` label='recheck', key='CUSTOM_CTRL_A', on_activate=set_current_inactive, }, } + + self:updateTextButtonFrame() +end + +function RecheckOverlay:onRenderBody(dc) + if (self.frame_rect.y1 == 7) then + -- only apply this logic if the overlay is on the same row as + -- originally thought: just above the order status icon + + if self:updateTextButtonFrame() then + self:updateLayout() + end + end + + RecheckOverlay.super.onRenderBody(dc) end -- ------------------- @@ -53,7 +89,7 @@ if dfhack_flags.module then return end --- Check if on correct screen and perform the action if so +-- Check if on the correct screen and perform the action if so if not dfhack.gui.matchFocusString(focusString) then qerror('workorder-recheck must be run from the manager order conditions view') end From 001525144163f43aa1d8089baa7a7a7bf4f3b2d3 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 13 Aug 2023 12:17:11 +0200 Subject: [PATCH 448/732] Update workorder-recheck.lua Co-authored-by: Myk --- workorder-recheck.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workorder-recheck.lua b/workorder-recheck.lua index 2983536ca5..2c50fbb2f3 100644 --- a/workorder-recheck.lua +++ b/workorder-recheck.lua @@ -76,7 +76,7 @@ function RecheckOverlay:onRenderBody(dc) end end - RecheckOverlay.super.onRenderBody(dc) + RecheckOverlay.super.onRenderBody(self, dc) end -- ------------------- From 8f053f703fa921edf0f6b638da12ee4b689c4ce4 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 13 Aug 2023 12:17:46 +0200 Subject: [PATCH 449/732] Update workorder-recheck.lua Co-authored-by: Myk --- workorder-recheck.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/workorder-recheck.lua b/workorder-recheck.lua index 2c50fbb2f3..8907d1b611 100644 --- a/workorder-recheck.lua +++ b/workorder-recheck.lua @@ -27,8 +27,6 @@ RecheckOverlay.ATTRS{ default_enabled=true, viewscreens=focusString, frame={w=19, h=3}, - -- frame_style=gui.MEDIUM_FRAME, - -- frame_background=gui.CLEAR_PEN, } local function areTabsInTwoRows() From 36a81f20b462cc4ee82f5cd959983a135c336753 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 13 Aug 2023 12:32:09 +0200 Subject: [PATCH 450/732] add is_current_active --- workorder-recheck.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/workorder-recheck.lua b/workorder-recheck.lua index 8907d1b611..7115cdd09c 100644 --- a/workorder-recheck.lua +++ b/workorder-recheck.lua @@ -15,6 +15,12 @@ local function set_current_inactive() end end +local function is_current_active() + local scrConditions = df.global.game.main_interface.info.work_orders.conditions + local order = scrConditions.wq + return order.status.active +end + -- ------------------- -- RecheckOverlay -- @@ -26,7 +32,8 @@ RecheckOverlay.ATTRS{ default_pos={x=6,y=8}, default_enabled=true, viewscreens=focusString, - frame={w=19, h=3}, + -- width is the sum of lengths of `[` + `Ctrl+A` + `: ` + button.label + `]` + frame={w=1 + 6 + 2 + 16 + 1, h=3}, } local function areTabsInTwoRows() @@ -55,9 +62,10 @@ function RecheckOverlay:init() widgets.TextButton{ view_id = 'button', -- frame={t=0, l=0, r=0, h=1}, -- is set in `updateTextButtonFrame()` - label='recheck', + label='request re-check', key='CUSTOM_CTRL_A', on_activate=set_current_inactive, + enabled=is_current_active, }, } From b291421e1c9ea0d777ace41c64e5db133c813bdf Mon Sep 17 00:00:00 2001 From: lethosor Date: Mon, 14 Aug 2023 00:46:22 -0400 Subject: [PATCH 451/732] devel/dump-offsets: use new df.global._fields Ref dfhack/dfhack#3668 --- devel/dump-offsets.lua | 153 ++--------------------------------------- 1 file changed, 4 insertions(+), 149 deletions(-) diff --git a/devel/dump-offsets.lua b/devel/dump-offsets.lua index 9109a00daa..0e3b0d2f0e 100644 --- a/devel/dump-offsets.lua +++ b/devel/dump-offsets.lua @@ -21,155 +21,10 @@ addresses in-game. Passing "all" does this for all globals. ]====] -GLOBALS = { - version = "version", - min_load_version = "min_load_version", - movie_version = "movie_version", - basic_seed = "basic_seed", - enabler = "enabler", - cursor = "cursor", - point = "selection_rect", - gamemode = "gamemode", - gametype = "gametype", - menuposition = "ui_menu_width", - itemmade = "created_item_type", - itemmade_subtype = "created_item_subtype", - itemmade_subcat1 = "created_item_mattype", - itemmade_subcat2 = "created_item_matindex", - itemmade_number = "created_item_count", - mainview = "map_renderer", - d_init = "d_init", - title = "title", - title2 = "title_spaced", - event_flow = "flows", - gps = "gps", - gview = "gview", - init = "init", - texture = "texture", - plot_event = "timed_events", - plotinfo = "plotinfo", - adventure = "adventure", - buildreq = "buildreq", - buildjob_type = "ui_building_assign_type", - buildjob_selected = "ui_building_assign_is_marked", - buildjob_unit = "ui_building_assign_units", - buildjob_item = "ui_building_assign_items", - looklist = "ui_look_list", - game = "game", - world = "world", - year = "cur_year", - season_count = "cur_year_tick", - precise_phase = "cur_year_tick_advmode", - season_timer = "cur_season_tick", - season = "cur_season", - cur_weather = "current_weather", - assignbuildingjobs = "process_jobs", - assigndesjobs = "process_dig", - paused = "pause_state", - modeunit = "ui_selected_unit", - modeview = "ui_unit_view_mode", - modepage = "ui_look_cursor", - modeitem = "ui_building_item_cursor", - addingtask = "ui_workshop_in_add", - modejob = "ui_workshop_job_cursor", - buildjob_assignroom = "ui_building_in_assign", - buildjob_sizeroom = "ui_building_in_resize", - addingtask_sub = "ui_lever_target_type", - buildjob_sizerad = "ui_building_resize_radius", - scrollx = "window_x", - scrolly = "window_y", - scrollz = "window_z", - DEBUG_CONTINUOUS = "debug_nopause", - DEBUG_NOMOOD = "debug_nomoods", - DEBUG_SAFEDWARVES = "debug_combat", - DEBUG_NOANIMALS = "debug_wildlife", - DEBUG_NOTHIRST = "debug_nodrink", - DEBUG_NOHUNGER = "debug_noeat", - DEBUG_NOSLEEP = "debug_nosleep", - DEBUG_VISIBLEAMBUSHERS = "debug_showambush", - DEBUG_QUICKMODE_MINING = "debug_fastmining", - DEBUG_NEVERBERSERK = "debug_noberserk", - DEBUG_MEGAFAST = "debug_turbospeed", - gamemode_cansave = "save_on_exit", - standingorder_butcher = "standing_orders_auto_butcher", - standingorder_collect_web = "standing_orders_auto_collect_webs", - standingorder_fishery = "standing_orders_auto_fishery", - standingorder_kiln = "standing_orders_auto_kiln", - standingorder_kitchen = "standing_orders_auto_kitchen", - standingorder_loom = "standing_orders_auto_loom", - standingorder_other = "standing_orders_auto_other", - standingorder_slaughter = "standing_orders_auto_slaughter", - standingorder_smelter = "standing_orders_auto_smelter", - standingorder_tan = "standing_orders_auto_tan", - standingorder_gatherrefuse_chasm_bones = "standing_orders_dump_bones", - standingorder_gatherrefuse_chasm_corpses = "standing_orders_dump_corpses", - standingorder_gatherrefuse_chasm_strand_tissue = "standing_orders_dump_hair", - standingorder_gatherrefuse_chasm_othernonmetal = "standing_orders_dump_other", - standingorder_gatherrefuse_chasm_shell = "standing_orders_dump_shells", - standingorder_gatherrefuse_chasm_skins = "standing_orders_dump_skins", - standingorder_gatherrefuse_chasm_skulls = "standing_orders_dump_skulls", - standingorder_allharvest = "standing_orders_farmer_harvest", - standingorder_autoforbid_other_items = "standing_orders_forbid_other_dead_items", - standingorder_autoforbid_other_corpse = "standing_orders_forbid_other_nohunt", - standingorder_autoforbid_your_corpse = "standing_orders_forbid_own_dead", - standingorder_autoforbid_your_items = "standing_orders_forbid_own_dead_items", - standingorder_autoforbid_projectile = "standing_orders_forbid_used_ammo", - standingorder_gatheranimals = "standing_orders_gather_animals", - standingorder_gatherbodies = "standing_orders_gather_bodies", - standingorder_gatherfood = "standing_orders_gather_food", - standingorder_gatherfurniture = "standing_orders_gather_furniture", - standingorder_gatherstone = "standing_orders_gather_minerals", - standingorder_gatherrefuse = "standing_orders_gather_refuse", - standingorder_gatherrefuse_outside = "standing_orders_gather_refuse_outside", - standingorder_gatherrefuse_outside_vermin = "standing_orders_gather_vermin_remains", - standingorder_gatherwood = "standing_orders_gather_wood", - option_exceptions = "standing_orders_job_cancel_announce", - standingorder_mixfoods = "standing_orders_mix_food", - standingorder_dyed_clothes = "standing_orders_use_dyed_cloth", - standingorder_zone_drinking = "standing_orders_zoneonly_drink", - standingorder_zone_fishing = "standing_orders_zoneonly_fish", - next_activity_global_id = "activity_next_id", - next_agreement_global_id = "agreement_next_id", - next_army_controller_global_id = "army_controller_next_id", - next_army_global_id = "army_next_id", - next_army_tracking_info_global_id = "army_tracking_info_next_id", - next_art_imagechunk_global_id = "art_image_chunk_next_id", - next_artifact_global_id = "artifact_next_id", - next_belief_system_global_id = "belief_system_next_id", - next_building_global_id = "building_next_id", - next_crime_global_id = "crime_next_id", - next_cultural_identity_global_id = "cultural_identity_next_id", - next_dance_form_global_id = "dance_form_next_id", - next_civ_global_id = "entity_next_id", - next_flow_guide_global_id = "flow_guide_next_id", - next_formation_global_id = "formation_next_id", - next_histeventcol_global_id = "hist_event_collection_next_id", - next_histevent_global_id = "hist_event_next_id", - next_histfig_global_id = "hist_figure_next_id", - next_identity_global_id = "identity_next_id", - next_incident_global_id = "incident_next_id", - next_interaction_instance_global_id = "interaction_instance_next_id", - next_item_global_id = "item_next_id", - next_job_global_id = "job_next_id", - next_machine_global_id = "machine_next_id", - next_musical_form_global_id = "musical_form_next_id", - next_nem_global_id = "nemesis_next_id", - next_occupation_global_id = "occupation_next_id", - next_poetic_form_global_id = "poetic_form_next_id", - next_proj_global_id = "proj_next_id", - next_rhythm_global_id = "rhythm_next_id", - next_scale_global_id = "scale_next_id", - next_schedule_global_id = "schedule_next_id", - next_soul_global_id = "soul_next_id", - next_squad_global_id = "squad_next_id", - next_task_global_id = "task_next_id", - next_unitchunk_global_id = "unit_chunk_next_id", - next_unit_global_id = "unit_next_id", - next_vehicle_global_id = "vehicle_next_id", - next_written_content_global_id = "written_content_next_id", - next_divination_set_global_id = "divination_set_next_id", - next_image_set_global_id = "image_set_next_id", -} +GLOBALS = {} +for k, v in pairs(df.global._fields) do + GLOBALS[v.original_name] = k +end function read_cstr(addr) local s = '' From 1cc7119a7507cbf29f8a314f839a995ab96f886d Mon Sep 17 00:00:00 2001 From: shevernitskiy Date: Mon, 14 Aug 2023 08:18:18 +0300 Subject: [PATCH 452/732] mirgate preloaded assets to lua --- gui/civ-alert.lua | 13 ++++--------- gui/control-panel.lua | 25 ++++++++++--------------- gui/cp437-table.lua | 21 ++++++++------------- gui/design.lua | 25 ++++++++++--------------- gui/launcher.lua | 22 ++++++---------------- unsuspend.lua | 13 +++++++------ 6 files changed, 45 insertions(+), 74 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 9afc2d3453..9c14a9567c 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -118,18 +118,13 @@ end last_tp_start = last_tp_start or 0 CONFIG_BUTTON_PENS = CONFIG_BUTTON_PENS or {} local function get_button_pen(idx) - local start = dfhack.textures.getAsset('hack/data/art/control-panel.png', 0) + local start = gui.tp_control_panel(1) if last_tp_start == start then return CONFIG_BUTTON_PENS[idx] end last_tp_start = start - local tp = function(offset) - local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset) - return texpos >= 0 and texpos or nil - end - - CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=tp(6), ch=string.byte('[')} - CONFIG_BUTTON_PENS[2] = to_pen{tile=tp(9), ch=15} -- gear/masterwork symbol - CONFIG_BUTTON_PENS[3] = to_pen{fg=COLOR_CYAN, tile=tp(7), ch=string.byte(']')} + CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=gui.tp_control_panel(7), ch=string.byte('[')} + CONFIG_BUTTON_PENS[2] = to_pen{tile=gui.tp_control_panel(10), ch=15} -- gear/masterwork symbol + CONFIG_BUTTON_PENS[3] = to_pen{fg=COLOR_CYAN, tile=gui.tp_control_panel(8), ch=string.byte(']')} return CONFIG_BUTTON_PENS[idx] end diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 4ecf7682cb..67241bd614 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -153,31 +153,26 @@ local function save_file(path, save_fn) end local function get_icon_pens() - local tp = function(offset) - local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset + 10) - return texpos >= 0 and texpos or nil - end - local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=tp(0), ch=string.byte('[')} + tile=gui.tp_control_panel(1), ch=string.byte('[')} local enabled_pen_center = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, - tile=tp(1) or nil, ch=251} -- check + tile=gui.tp_control_panel(2) or nil, ch=251} -- check local enabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=tp(2) or nil, ch=string.byte(']')} + tile=gui.tp_control_panel(3) or nil, ch=string.byte(']')} local disabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=tp(3) or nil, ch=string.byte('[')} + tile=gui.tp_control_panel(4) or nil, ch=string.byte('[')} local disabled_pen_center = dfhack.pen.parse{fg=COLOR_RED, - tile=tp(4) or nil, ch=string.byte('x')} + tile=gui.tp_control_panel(5) or nil, ch=string.byte('x')} local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=tp(5) or nil, ch=string.byte(']')} + tile=gui.tp_control_panel(6) or nil, ch=string.byte(']')} local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=tp(6) or nil, ch=string.byte('[')} + tile=gui.tp_control_panel(7) or nil, ch=string.byte('[')} local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=tp(7) or nil, ch=string.byte(']')} + tile=gui.tp_control_panel(8) or nil, ch=string.byte(']')} local help_pen_center = dfhack.pen.parse{ - tile=tp(8) or nil, ch=string.byte('?')} + tile=gui.tp_control_panel(9) or nil, ch=string.byte('?')} local configure_pen_center = dfhack.pen.parse{ - tile=tp(9) or nil, ch=15} -- gear/masterwork symbol + tile=gui.tp_control_panel(10) or nil, ch=15} -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, button_pen_left, button_pen_right, diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index ba9a7352b7..a2abb02696 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -6,28 +6,23 @@ local widgets = require('gui.widgets') local to_pen = dfhack.pen.parse -local tp = function(offset) - local texpos = dfhack.textures.getAsset('hack/data/art/border-thin.png', offset) - return texpos >= 0 and texpos or nil -end - local function get_key_pens(ch) return { - lt=to_pen{tile=tp(0), write_to_lower=true}, - t=to_pen{tile=tp(1), ch=ch, write_to_lower=true, top_of_text=true}, + lt=to_pen{tile=gui.tp_border_thin(1), write_to_lower=true}, + t=to_pen{tile=gui.tp_border_thin(2), ch=ch, write_to_lower=true, top_of_text=true}, t_ascii=to_pen{ch=32}, - rt=to_pen{tile=tp(2), write_to_lower=true}, - lb=to_pen{tile=tp(14), write_to_lower=true}, - b=to_pen{tile=tp(15), ch=ch, write_to_lower=true, bottom_of_text=true}, - rb=to_pen{tile=tp(16), write_to_lower=true}, + rt=to_pen{tile=gui.tp_border_thin(3), write_to_lower=true}, + lb=to_pen{tile=gui.tp_border_thin(15), write_to_lower=true}, + b=to_pen{tile=gui.tp_border_thin(16), ch=ch, write_to_lower=true, bottom_of_text=true}, + rb=to_pen{tile=gui.tp_border_thin(17), write_to_lower=true}, } end local function get_key_hover_pens(ch) return { - t=to_pen{tile=tp(1), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, top_of_text=true}, + t=to_pen{tile=gui.tp_border_thin(2), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, top_of_text=true}, t_ascii=to_pen{fg=COLOR_WHITE, bg=COLOR_RED, ch=ch == 0 and 0 or 32}, - b=to_pen{tile=tp(15), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, bottom_of_text=true}, + b=to_pen{tile=gui.tp_border_thin(16), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, bottom_of_text=true}, } end diff --git a/gui/design.lua b/gui/design.lua index 04dd572ad6..b907ed3421 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -115,32 +115,27 @@ end -- Utilities local function get_icon_pens() - local tp = function(offset) - local texpos = dfhack.textures.getAsset('hack/data/art/control-panel.png', offset + 10) - return texpos >= 0 and texpos or nil - end - local enabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = tp(0) or nil, ch = string.byte('[') } + tile = gui.tp_control_panel(1) or nil, ch = string.byte('[') } local enabled_pen_center = dfhack.pen.parse { fg = COLOR_LIGHTGREEN, - tile = tp(1) or nil, ch = 251 } -- check + tile = gui.tp_control_panel(2) or nil, ch = 251 } -- check local enabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = tp(2) or nil, ch = string.byte(']') } + tile = gui.tp_control_panel(3) or nil, ch = string.byte(']') } local disabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = tp(3) or nil, ch = string.byte('[') } + tile = gui.tp_control_panel(4) or nil, ch = string.byte('[') } local disabled_pen_center = dfhack.pen.parse { fg = COLOR_RED, - tile = tp(4) or nil, ch = string.byte('x') } + tile = gui.tp_control_panel(5) or nil, ch = string.byte('x') } local disabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = tp(5) or nil, ch = string.byte(']') } + tile = gui.tp_control_panel(6) or nil, ch = string.byte(']') } local button_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = tp(6) or nil, ch = string.byte('[') } + tile = gui.tp_control_panel(7) or nil, ch = string.byte('[') } local button_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = tp(7) or nil, ch = string.byte(']') } + tile = gui.tp_control_panel(8) or nil, ch = string.byte(']') } local help_pen_center = dfhack.pen.parse { - tile = tp(8) or nil, ch = string.byte('?') + tile = gui.tp_control_panel(9) or nil, ch = string.byte('?') } local configure_pen_center = dfhack.pen.parse { - tile = tp(9) or nil, ch = 15 + tile = gui.tp_control_panel(10) or nil, ch = 15 } -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, diff --git a/gui/launcher.lua b/gui/launcher.lua index 0e34006792..6e52fa365f 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -473,22 +473,12 @@ function MainPanel:postUpdateLayout() config:write(self.frame) end -local tp_thin = function(offset) - local texpos = dfhack.textures.getAsset('hack/data/art/border-thin.png', offset) - return texpos >= 0 and texpos or nil -end - -local tp_window = function(offset) - local texpos = dfhack.textures.getAsset('hack/data/art/border-window.png', offset) - return texpos >= 0 and texpos or nil -end - -local H_SPLIT_PEN = dfhack.pen.parse{tile=tp_thin(5), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} -local V_SPLIT_PEN = dfhack.pen.parse{tile=tp_thin(4), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} -local TOP_SPLIT_PEN = dfhack.pen.parse{tile=tp_window(1), ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} -local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=tp_window(15), ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} -local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=tp_window(7), ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} -local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=tp_thin(17), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} +local H_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_thin(6), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} +local V_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_thin(5), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} +local TOP_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_window(2), ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} +local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_window(16), ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} +local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_window(8), ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} +local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_thin(18), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} -- paint autocomplete panel border local function paint_vertical_border(rect) diff --git a/unsuspend.lua b/unsuspend.lua index 09f6105554..43d41d1adc 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -12,6 +12,8 @@ if not ok then buildingplan = nil end +local textures = dfhack.textures.loadTileset('hack/data/art/unsuspend.png', 32, 32) + SuspendOverlay = defclass(SuspendOverlay, overlay.OverlayWidget) SuspendOverlay.ATTRS{ viewscreens='dwarfmode', @@ -136,8 +138,7 @@ function SuspendOverlay:refresh_screen_buildings() end local tp = function(offset) - local texpos = dfhack.textures.getAsset('hack/data/art/unsuspend', offset) - return texpos >= 0 and texpos or nil + return dfhack.textures.getTexposByHandle(textures[offset]) end function SuspendOverlay:render_marker(dc, bld, screen_pos) @@ -149,13 +150,13 @@ function SuspendOverlay:render_marker(dc, bld, screen_pos) or not job.flags.suspend then return end - local color, ch, texpos = COLOR_YELLOW, 'x', tp(1) + local color, ch, texpos = COLOR_YELLOW, 'x', tp(2) if buildingplan and buildingplan.isPlannedBuilding(bld) then - color, ch, texpos = COLOR_GREEN, 'P', tp(3) + color, ch, texpos = COLOR_GREEN, 'P', tp(4) elseif suspendmanager and suspendmanager.isKeptSuspended(job) then - color, ch, texpos = COLOR_WHITE, 'x', tp(2) + color, ch, texpos = COLOR_WHITE, 'x', tp(1) elseif data.suspend_count > 1 then - color, ch, texpos = COLOR_RED, 'X', tp(0) + color, ch, texpos = COLOR_RED, 'X', tp(1) end dc:seek(screen_pos.x, screen_pos.y):tile(ch, texpos, color) end From 716c423d07956b0aac50505f0d1c9a9ed4930008 Mon Sep 17 00:00:00 2001 From: shevernitskiy Date: Mon, 14 Aug 2023 12:07:41 +0300 Subject: [PATCH 453/732] support closure as tile arg to get texpos --- gui/civ-alert.lua | 6 +++--- gui/control-panel.lua | 20 ++++++++++---------- gui/cp437-table.lua | 16 ++++++++-------- gui/design.lua | 20 ++++++++++---------- gui/launcher.lua | 12 ++++++------ 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 9c14a9567c..46de596be2 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -122,9 +122,9 @@ local function get_button_pen(idx) if last_tp_start == start then return CONFIG_BUTTON_PENS[idx] end last_tp_start = start - CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=gui.tp_control_panel(7), ch=string.byte('[')} - CONFIG_BUTTON_PENS[2] = to_pen{tile=gui.tp_control_panel(10), ch=15} -- gear/masterwork symbol - CONFIG_BUTTON_PENS[3] = to_pen{fg=COLOR_CYAN, tile=gui.tp_control_panel(8), ch=string.byte(']')} + CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=curry(gui.tp_control_panel, 7), ch=string.byte('[')} + CONFIG_BUTTON_PENS[2] = to_pen{tile=curry(gui.tp_control_panel, 10), ch=15} -- gear/masterwork symbol + CONFIG_BUTTON_PENS[3] = to_pen{fg=COLOR_CYAN, tile=curry(gui.tp_control_panel, 8), ch=string.byte(']')} return CONFIG_BUTTON_PENS[idx] end diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 67241bd614..cb65942d16 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -154,25 +154,25 @@ end local function get_icon_pens() local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=gui.tp_control_panel(1), ch=string.byte('[')} + tile=curry(gui.tp_control_panel, 1), ch=string.byte('[')} local enabled_pen_center = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, - tile=gui.tp_control_panel(2) or nil, ch=251} -- check + tile=curry(gui.tp_control_panel, 2) or nil, ch=251} -- check local enabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=gui.tp_control_panel(3) or nil, ch=string.byte(']')} + tile=curry(gui.tp_control_panel, 3) or nil, ch=string.byte(']')} local disabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=gui.tp_control_panel(4) or nil, ch=string.byte('[')} + tile=curry(gui.tp_control_panel, 4) or nil, ch=string.byte('[')} local disabled_pen_center = dfhack.pen.parse{fg=COLOR_RED, - tile=gui.tp_control_panel(5) or nil, ch=string.byte('x')} + tile=curry(gui.tp_control_panel, 5) or nil, ch=string.byte('x')} local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=gui.tp_control_panel(6) or nil, ch=string.byte(']')} + tile=curry(gui.tp_control_panel, 6) or nil, ch=string.byte(']')} local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=gui.tp_control_panel(7) or nil, ch=string.byte('[')} + tile=curry(gui.tp_control_panel, 7) or nil, ch=string.byte('[')} local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=gui.tp_control_panel(8) or nil, ch=string.byte(']')} + tile=curry(gui.tp_control_panel, 8) or nil, ch=string.byte(']')} local help_pen_center = dfhack.pen.parse{ - tile=gui.tp_control_panel(9) or nil, ch=string.byte('?')} + tile=curry(gui.tp_control_panel, 9) or nil, ch=string.byte('?')} local configure_pen_center = dfhack.pen.parse{ - tile=gui.tp_control_panel(10) or nil, ch=15} -- gear/masterwork symbol + tile=curry(gui.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, button_pen_left, button_pen_right, diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index a2abb02696..a30fcee0e6 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -8,21 +8,21 @@ local to_pen = dfhack.pen.parse local function get_key_pens(ch) return { - lt=to_pen{tile=gui.tp_border_thin(1), write_to_lower=true}, - t=to_pen{tile=gui.tp_border_thin(2), ch=ch, write_to_lower=true, top_of_text=true}, + lt=to_pen{tile=curry(gui.tp_border_thin, 1), write_to_lower=true}, + t=to_pen{tile=curry(gui.tp_border_thin, 2), ch=ch, write_to_lower=true, top_of_text=true}, t_ascii=to_pen{ch=32}, - rt=to_pen{tile=gui.tp_border_thin(3), write_to_lower=true}, - lb=to_pen{tile=gui.tp_border_thin(15), write_to_lower=true}, - b=to_pen{tile=gui.tp_border_thin(16), ch=ch, write_to_lower=true, bottom_of_text=true}, - rb=to_pen{tile=gui.tp_border_thin(17), write_to_lower=true}, + rt=to_pen{tile=curry(gui.tp_border_thin, 3), write_to_lower=true}, + lb=to_pen{tile=curry(gui.tp_border_thin, 15), write_to_lower=true}, + b=to_pen{tile=curry(gui.tp_border_thin, 16), ch=ch, write_to_lower=true, bottom_of_text=true}, + rb=to_pen{tile=curry(gui.tp_border_thin, 17), write_to_lower=true}, } end local function get_key_hover_pens(ch) return { - t=to_pen{tile=gui.tp_border_thin(2), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, top_of_text=true}, + t=to_pen{tile=curry(gui.tp_border_thin, 2), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, top_of_text=true}, t_ascii=to_pen{fg=COLOR_WHITE, bg=COLOR_RED, ch=ch == 0 and 0 or 32}, - b=to_pen{tile=gui.tp_border_thin(16), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, bottom_of_text=true}, + b=to_pen{tile=curry(gui.tp_border_thin, 16), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, bottom_of_text=true}, } end diff --git a/gui/design.lua b/gui/design.lua index b907ed3421..89dd8dbed0 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -116,26 +116,26 @@ end local function get_icon_pens() local enabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = gui.tp_control_panel(1) or nil, ch = string.byte('[') } + tile = curry(gui.tp_control_panel, 1) or nil, ch = string.byte('[') } local enabled_pen_center = dfhack.pen.parse { fg = COLOR_LIGHTGREEN, - tile = gui.tp_control_panel(2) or nil, ch = 251 } -- check + tile = curry(gui.tp_control_panel, 2) or nil, ch = 251 } -- check local enabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = gui.tp_control_panel(3) or nil, ch = string.byte(']') } + tile = curry(gui.tp_control_panel, 3) or nil, ch = string.byte(']') } local disabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = gui.tp_control_panel(4) or nil, ch = string.byte('[') } + tile = curry(gui.tp_control_panel, 4) or nil, ch = string.byte('[') } local disabled_pen_center = dfhack.pen.parse { fg = COLOR_RED, - tile = gui.tp_control_panel(5) or nil, ch = string.byte('x') } + tile = curry(gui.tp_control_panel, 5) or nil, ch = string.byte('x') } local disabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = gui.tp_control_panel(6) or nil, ch = string.byte(']') } + tile = curry(gui.tp_control_panel, 6) or nil, ch = string.byte(']') } local button_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = gui.tp_control_panel(7) or nil, ch = string.byte('[') } + tile = curry(gui.tp_control_panel, 7) or nil, ch = string.byte('[') } local button_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = gui.tp_control_panel(8) or nil, ch = string.byte(']') } + tile = curry(gui.tp_control_panel, 8) or nil, ch = string.byte(']') } local help_pen_center = dfhack.pen.parse { - tile = gui.tp_control_panel(9) or nil, ch = string.byte('?') + tile = curry(gui.tp_control_panel, 9) or nil, ch = string.byte('?') } local configure_pen_center = dfhack.pen.parse { - tile = gui.tp_control_panel(10) or nil, ch = 15 + tile = curry(gui.tp_control_panel, 10) or nil, ch = 15 } -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, diff --git a/gui/launcher.lua b/gui/launcher.lua index 6e52fa365f..56bcc7136f 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -473,12 +473,12 @@ function MainPanel:postUpdateLayout() config:write(self.frame) end -local H_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_thin(6), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} -local V_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_thin(5), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} -local TOP_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_window(2), ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} -local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_window(16), ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} -local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_window(8), ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} -local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=gui.tp_border_thin(18), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} +local H_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_thin, 6), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} +local V_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_thin, 5), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} +local TOP_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_window,2), ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} +local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_window,16), ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} +local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_window,8), ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} +local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_thin, 18), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} -- paint autocomplete panel border local function paint_vertical_border(rect) From 3e1389d2965af61db53eacd1f292a301484434be Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 16 Aug 2023 06:05:12 -0700 Subject: [PATCH 454/732] rename test with same name --- test/assign-minecarts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/assign-minecarts.lua b/test/assign-minecarts.lua index e67a75be09..f9c401a266 100644 --- a/test/assign-minecarts.lua +++ b/test/assign-minecarts.lua @@ -83,7 +83,7 @@ function test.assign_minecart_to_last_route_no_stops_output() function() expect.false_(am.assign_minecart_to_last_route(false)) end) end -function test.assign_minecart_to_last_route_no_minecarts() +function test.assign_minecart_to_last_route_no_minecarts_quiet() mock_routes[1] = {stops={[1]={}}, vehicle_ids={}} mock_routes[0] = mock_routes[1] -- simulate 0-based index expect.false_(am.assign_minecart_to_last_route(true)) From 892390ac38ecde299c806c44ec7dfa851b97cf11 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 17 Aug 2023 17:00:10 -0700 Subject: [PATCH 455/732] add hide-interface --- docs/hide-interface.rst | 22 ++++++++++++++++++++++ hide-interface.lua | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 docs/hide-interface.rst create mode 100644 hide-interface.lua diff --git a/docs/hide-interface.rst b/docs/hide-interface.rst new file mode 100644 index 0000000000..2113080756 --- /dev/null +++ b/docs/hide-interface.rst @@ -0,0 +1,22 @@ +hide-interface +============== + +.. dfhack-tool:: + :summary: Hide the interface layer. + :tags: interface + +This tool simply hides the interface layer so you can view the map unhindered +or take clean fullscreen screenshots. The interface will remain hidden until +you hit :kbd:`Esc` or right click. You can still pause/unpause and move the map +around, so this is an excellent way to sit back and observe fortress life a +little without being distracted by announcements or statistics. + +Note that the interface layer and the map layer cannot be separated in ASCII +mode. This script can only hide the interface in graphics mode. + +Usage +----- + +:: + + hide-interface diff --git a/hide-interface.lua b/hide-interface.lua new file mode 100644 index 0000000000..9808a344d0 --- /dev/null +++ b/hide-interface.lua @@ -0,0 +1,37 @@ +local gui = require('gui') +local widgets = require('gui.widgets') + +TransparentScreen = defclass(TransparentScreen, gui.ZScreen) +TransparentScreen.ATTRS { + focus_path='hide-interface', + pass_movement_keys=true, + pass_mouse_clicks=false, + defocusable=false, +} + +function TransparentScreen:init() + self:addviews{ + widgets.Panel{ + frame_background=gui.TRANSPARENT_PEN, + visible=function() return dfhack.screen.inGraphicsMode() end, + }, + widgets.Panel{ + frame={h=5, w=50}, + frame_background=gui.CLEAR_PEN, + frame_style=gui.FRAME_PANEL, + visible=function() return not dfhack.screen.inGraphicsMode() end, + subviews={ + widgets.Label{ + auto_width=true, + text='Interface cannot be hidden in ASCII mode.', + } + }, + }, + } +end + +function TransparentScreen:onDismiss() + view = nil +end + +view = view and view:raise() or TransparentScreen{}:show() From 340827f573085a97b9a21ba12dce6670cabbced5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 17 Aug 2023 17:29:51 -0700 Subject: [PATCH 456/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index c571289411..9f09192b1e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ Template for new versions: ## New Tools - `devel/scan-vtables`: Scan and dump likely vtable addresses (for memory research) +- `hide-interface`: hide the vanilla UI elements for clean screenshots or laid-back fortress observing ## New Features - `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! From 85c0102829c46a0f01e8c16c22bf8250a8f7a4bc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Aug 2023 09:54:22 -0700 Subject: [PATCH 457/732] active vector is not sorted this was allowing the same unit to appear in the vector twice --- changelog.txt | 1 + docs/fix/retrieve-units.rst | 4 ++-- fix/retrieve-units.lua | 24 +++--------------------- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/changelog.txt b/changelog.txt index 9f09192b1e..710f5a8f0b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -39,6 +39,7 @@ Template for new versions: - `caravan`: Apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices - `suspendmanager`: Improve the detection on "T" and "+" shaped high walls - `starvingdead`: ensure sieges end properly when undead siegers starve +- `fix/retrieve-units`: fix retrieved units sometimes becoming duplicated on the map ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames diff --git a/docs/fix/retrieve-units.rst b/docs/fix/retrieve-units.rst index b9f7f5eb09..4f0f01bb71 100644 --- a/docs/fix/retrieve-units.rst +++ b/docs/fix/retrieve-units.rst @@ -15,8 +15,8 @@ forces them to enter. This can fix issues such as: .. note:: For caravans that are missing entirely, this script may retrieve the - merchants but not the items. Using `fix/stuck-merchants` followed by `force` - to create a new caravan may work better. + merchants but not the items. Using `fix/stuck-merchants` to dismiss the + caravan followed by `force Caravan` to create a new one may work better. Usage ----- diff --git a/fix/retrieve-units.lua b/fix/retrieve-units.lua index f19d5b5751..e851fdc0a1 100644 --- a/fix/retrieve-units.lua +++ b/fix/retrieve-units.lua @@ -3,26 +3,6 @@ -- http://www.bay12forums.com/smf/index.php?topic=163671.0 --@ module = true ---[====[ - -fix/retrieve-units -================== - -This script forces some units off the map to enter the map, which can fix issues -such as the following: - -- Stuck [SIEGE] tags due to invisible armies (or parts of armies) -- Forgotten beasts that never appear -- Packs of wildlife that are missing from the surface or caverns -- Caravans that are partially or completely missing. - -.. note:: - For caravans that are missing entirely, this script may retrieve the - merchants but not the items. Using `fix/stuck-merchants` followed by `force` - to create a new caravan may work better. - -]====] - local utils = require('utils') function shouldRetrieve(unit) @@ -49,7 +29,9 @@ function retrieveUnits() unit.flags1.can_swap = true unit.flags1.hidden_in_ambush = false -- add to active if missing - utils.insert_sorted(df.global.world.units.active, unit, 'id') + if not utils.linear_index(df.global.world.units.active, unit, 'id') then + df.global.world.units.active:insert('#', unit) + end end end end From 3a507721c2557fafd644fc87407079a040dda193 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Aug 2023 13:43:30 -0700 Subject: [PATCH 458/732] recover gracefully when saved frame positions are offscreen --- changelog.txt | 1 + gui/gm-editor.lua | 6 ++++++ gui/launcher.lua | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/changelog.txt b/changelog.txt index 9f09192b1e..4a69bfd048 100644 --- a/changelog.txt +++ b/changelog.txt @@ -39,6 +39,7 @@ Template for new versions: - `caravan`: Apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices - `suspendmanager`: Improve the detection on "T" and "+" shaped high walls - `starvingdead`: ensure sieges end properly when undead siegers starve +- `gui/launcher`, `gui/gm-editor`: recover gracefully when the saved frame position is now offscreen ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 1d0d0b34ae..fa65a76280 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -113,6 +113,12 @@ end function GmEditorUi:init(args) if not next(self.frame) then self.frame = {w=80, h=50} + else + for k,v in pairs(self.frame) do + if v < 0 then + self.frame[k] = 0 + end + end end -- don't appear directly over the current window diff --git a/gui/launcher.lua b/gui/launcher.lua index ca0597f4b8..07f69cfe6e 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -43,6 +43,7 @@ end -- removes duplicate existing history lines and adds the given line to the front local function add_history(hist, hist_set, line) + line = line:trim() if hist_set[line] then for i,v in ipairs(hist) do if v == line then @@ -586,6 +587,12 @@ function LauncherUI:init(args) new_frame = config.data if not next(new_frame) then new_frame = {w=110, h=36} + else + for k,v in pairs(new_frame) do + if v < 0 then + new_frame[k] = 0 + end + end end end main_panel.frame = new_frame From 880688f626e6a7528fefa9e973b5d4c3afff6b03 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Aug 2023 13:45:48 -0700 Subject: [PATCH 459/732] initial version of tutorials-be-gone --- changelog.txt | 1 + docs/tutorials-be-gone.rst | 29 ++++++++++++++ gui/control-panel.lua | 1 + tutorials-be-gone.lua | 80 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 docs/tutorials-be-gone.rst create mode 100644 tutorials-be-gone.lua diff --git a/changelog.txt b/changelog.txt index 9f09192b1e..d268bf768e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools - `devel/scan-vtables`: Scan and dump likely vtable addresses (for memory research) - `hide-interface`: hide the vanilla UI elements for clean screenshots or laid-back fortress observing +- `tutorials-be-gone`: hide the DF tutorial popups; enable in the System tab of `gui/control-panel` ## New Features - `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! diff --git a/docs/tutorials-be-gone.rst b/docs/tutorials-be-gone.rst new file mode 100644 index 0000000000..4e38d859e1 --- /dev/null +++ b/docs/tutorials-be-gone.rst @@ -0,0 +1,29 @@ +tutorials-be-gone +================= + +.. dfhack-tool:: + :summary: Hide new fort tutorial popups. + :tags: fort interface + +If you've played the game before and don't need to see the tutorial popups that +show up on every new fort, ``tutorials-be-gone`` can hide them for you. You can +enable this tool as a system service in the "Services" tab of +`gui/control-panel` so it takes effect for all new or loaded forts. + +Specifically, this tool hides: + +- The popup displayed when creating a new world +- The "Do you want to start a tutorial embark" popup +- Popups displayed the first time you open the labor, burrows, justice, and + other similar screens in a new fort + +Usage +----- + +:: + + enable tutorials-be-gone + tutorials-be-gone + +If you haven't enabled the tool, but you run the command while a fort is +loaded, all future popups for the loaded fort will be hidden. diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 4ee57ea6e5..58e2fc5e2c 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -62,6 +62,7 @@ local SYSTEM_SERVICES = { -- these are fully controlled by the user local SYSTEM_USER_SERVICES = { 'faststart', + 'tutorials-be-gone', 'work-now', } for _,v in ipairs(SYSTEM_USER_SERVICES) do diff --git a/tutorials-be-gone.lua b/tutorials-be-gone.lua new file mode 100644 index 0000000000..63b4a0d5b2 --- /dev/null +++ b/tutorials-be-gone.lua @@ -0,0 +1,80 @@ +--@module = true +--@enable = true + +local gui = require('gui') +local utils = require('utils') + +local GLOBAL_KEY = 'tutorials-be-gone' + +enabled = enabled or false + +function isEnabled() + return enabled +end + +local function is_fort_map_loaded() + return df.global.gamemode == df.game_mode.DWARF and dfhack.isMapLoaded() +end + +local help = df.global.game.main_interface.help + +local function close_help() + help.open = false +end + +function skip_tutorial_prompt(scr) + if help.open and help.context == df.help_context_type.EMBARK_TUTORIAL_CHOICE then + help.context = df.help_context_type.EMBARK_MESSAGE + df.global.gps.mouse_x = df.global.gps.dimx // 2 + df.global.gps.mouse_y = 18 + df.global.enabler.mouse_lbut = 1 + df.global.enabler.mouse_lbut_down = 1 + gui.simulateInput(scr, '_MOUSE_L_DOWN') + end +end + +local function hide_all_popups() + for i,name in ipairs(df.help_context_type) do + if not name:startswith('POPUP_') then goto continue end + utils.insert_sorted(df.global.plotinfo.tutorial_seen, i) + utils.insert_sorted(df.global.plotinfo.tutorial_hide, i) + ::continue:: + end +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if not enabled then return end + + if sc == SC_VIEWSCREEN_CHANGED then + local scr = dfhack.gui.getDFViewscreen(true) + if df.viewscreen_new_regionst:is_instance(scr) then + close_help() + elseif df.viewscreen_choose_start_sitest:is_instance(scr) then + skip_tutorial_prompt(scr) + end + elseif sc == SC_MAP_LOADED and df.global.gamemode == df.game_mode.DWARF then + hide_all_popups() + end +end + +if dfhack_flags.module then + return +end + +local args = {...} +if dfhack_flags and dfhack_flags.enable then + args = {dfhack_flags.enable_state and 'enable' or 'disable'} +end + +if args[1] == "enable" then + enabled = true + if is_fort_map_loaded() then + hide_all_popups() + end +elseif args[1] == "disable" then + enabled = false +elseif is_fort_map_loaded() then + hide_all_popups() +else + qerror('tutorials-be-gone needs a loaded fortress map to work') +end From e8bdb953403e228ee4d0d8f397df44f7f27318f1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Aug 2023 16:40:32 -0700 Subject: [PATCH 460/732] change name to hide-tutorials --- changelog.txt | 2 +- docs/{tutorials-be-gone.rst => hide-tutorials.rst} | 10 +++++----- gui/control-panel.lua | 2 +- tutorials-be-gone.lua => hide-tutorials.lua | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename docs/{tutorials-be-gone.rst => hide-tutorials.rst} (80%) rename tutorials-be-gone.lua => hide-tutorials.lua (94%) diff --git a/changelog.txt b/changelog.txt index d268bf768e..e2217bc85f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,7 @@ Template for new versions: ## New Tools - `devel/scan-vtables`: Scan and dump likely vtable addresses (for memory research) - `hide-interface`: hide the vanilla UI elements for clean screenshots or laid-back fortress observing -- `tutorials-be-gone`: hide the DF tutorial popups; enable in the System tab of `gui/control-panel` +- `hide-tutorials`: hide the DF tutorial popups; enable in the System tab of `gui/control-panel` ## New Features - `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! diff --git a/docs/tutorials-be-gone.rst b/docs/hide-tutorials.rst similarity index 80% rename from docs/tutorials-be-gone.rst rename to docs/hide-tutorials.rst index 4e38d859e1..a1cc934b7b 100644 --- a/docs/tutorials-be-gone.rst +++ b/docs/hide-tutorials.rst @@ -1,12 +1,12 @@ -tutorials-be-gone -================= +hide-tutorials +============== .. dfhack-tool:: :summary: Hide new fort tutorial popups. :tags: fort interface If you've played the game before and don't need to see the tutorial popups that -show up on every new fort, ``tutorials-be-gone`` can hide them for you. You can +show up on every new fort, ``hide-tutorials`` can hide them for you. You can enable this tool as a system service in the "Services" tab of `gui/control-panel` so it takes effect for all new or loaded forts. @@ -22,8 +22,8 @@ Usage :: - enable tutorials-be-gone - tutorials-be-gone + enable hide-tutorials + hide-tutorials If you haven't enabled the tool, but you run the command while a fort is loaded, all future popups for the loaded fort will be hidden. diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 58e2fc5e2c..fba07bd633 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -62,7 +62,7 @@ local SYSTEM_SERVICES = { -- these are fully controlled by the user local SYSTEM_USER_SERVICES = { 'faststart', - 'tutorials-be-gone', + 'hide-tutorials', 'work-now', } for _,v in ipairs(SYSTEM_USER_SERVICES) do diff --git a/tutorials-be-gone.lua b/hide-tutorials.lua similarity index 94% rename from tutorials-be-gone.lua rename to hide-tutorials.lua index 63b4a0d5b2..6887a77df4 100644 --- a/tutorials-be-gone.lua +++ b/hide-tutorials.lua @@ -4,7 +4,7 @@ local gui = require('gui') local utils = require('utils') -local GLOBAL_KEY = 'tutorials-be-gone' +local GLOBAL_KEY = 'hide-tutorials' enabled = enabled or false @@ -76,5 +76,5 @@ elseif args[1] == "disable" then elseif is_fort_map_loaded() then hide_all_popups() else - qerror('tutorials-be-gone needs a loaded fortress map to work') + qerror('hide-tutorials needs a loaded fortress map to work') end From c3715aa1a5264565135907a03973c5e58b426fbb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Aug 2023 17:47:12 -0700 Subject: [PATCH 461/732] clarify that solicited tutorials still work --- docs/hide-tutorials.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/hide-tutorials.rst b/docs/hide-tutorials.rst index a1cc934b7b..417d5e278c 100644 --- a/docs/hide-tutorials.rst +++ b/docs/hide-tutorials.rst @@ -17,6 +17,9 @@ Specifically, this tool hides: - Popups displayed the first time you open the labor, burrows, justice, and other similar screens in a new fort +Note that only unsolicited tutorial popups are hidden. If you directly request +a tutorial page from the help, then it will still function normally. + Usage ----- From f909b6811a977b2f15ee8cccbabd2b67752a1e4f Mon Sep 17 00:00:00 2001 From: shevernitskiy Date: Mon, 28 Aug 2023 06:59:25 +0300 Subject: [PATCH 462/732] move textures to separate lua module --- gui/civ-alert.lua | 9 +++++---- gui/control-panel.lua | 21 +++++++++++---------- gui/cp437-table.lua | 17 +++++++++-------- gui/design.lua | 21 +++++++++++---------- gui/launcher.lua | 13 +++++++------ 5 files changed, 43 insertions(+), 38 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 46de596be2..f043f4c222 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -1,6 +1,7 @@ --@ module=true local gui = require('gui') +local textures = require('gui.textures') local widgets = require('gui.widgets') local overlay = require('plugins.overlay') @@ -118,13 +119,13 @@ end last_tp_start = last_tp_start or 0 CONFIG_BUTTON_PENS = CONFIG_BUTTON_PENS or {} local function get_button_pen(idx) - local start = gui.tp_control_panel(1) + local start = textures.tp_control_panel(1) if last_tp_start == start then return CONFIG_BUTTON_PENS[idx] end last_tp_start = start - CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=curry(gui.tp_control_panel, 7), ch=string.byte('[')} - CONFIG_BUTTON_PENS[2] = to_pen{tile=curry(gui.tp_control_panel, 10), ch=15} -- gear/masterwork symbol - CONFIG_BUTTON_PENS[3] = to_pen{fg=COLOR_CYAN, tile=curry(gui.tp_control_panel, 8), ch=string.byte(']')} + CONFIG_BUTTON_PENS[1] = to_pen{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 7), ch=string.byte('[')} + CONFIG_BUTTON_PENS[2] = to_pen{tile=curry(textures.tp_control_panel, 10), ch=15} -- gear/masterwork symbol + CONFIG_BUTTON_PENS[3] = to_pen{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 8), ch=string.byte(']')} return CONFIG_BUTTON_PENS[idx] end diff --git a/gui/control-panel.lua b/gui/control-panel.lua index cb65942d16..2d32cb2ffe 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -1,5 +1,6 @@ local dialogs = require('gui.dialogs') local gui = require('gui') +local textures = require('gui.textures') local helpdb = require('helpdb') local overlay = require('plugins.overlay') local repeatUtil = require('repeat-util') @@ -154,25 +155,25 @@ end local function get_icon_pens() local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(gui.tp_control_panel, 1), ch=string.byte('[')} + tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} local enabled_pen_center = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, - tile=curry(gui.tp_control_panel, 2) or nil, ch=251} -- check + tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check local enabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(gui.tp_control_panel, 3) or nil, ch=string.byte(']')} + tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} local disabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(gui.tp_control_panel, 4) or nil, ch=string.byte('[')} + tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')} local disabled_pen_center = dfhack.pen.parse{fg=COLOR_RED, - tile=curry(gui.tp_control_panel, 5) or nil, ch=string.byte('x')} + tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(gui.tp_control_panel, 6) or nil, ch=string.byte(']')} + tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(gui.tp_control_panel, 7) or nil, ch=string.byte('[')} + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(gui.tp_control_panel, 8) or nil, ch=string.byte(']')} + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} local help_pen_center = dfhack.pen.parse{ - tile=curry(gui.tp_control_panel, 9) or nil, ch=string.byte('?')} + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} local configure_pen_center = dfhack.pen.parse{ - tile=curry(gui.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol + tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, button_pen_left, button_pen_right, diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index a30fcee0e6..6e0eb5934b 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -2,27 +2,28 @@ local dialog = require('gui.dialogs') local gui = require('gui') +local textures = require('gui.textures') local widgets = require('gui.widgets') local to_pen = dfhack.pen.parse local function get_key_pens(ch) return { - lt=to_pen{tile=curry(gui.tp_border_thin, 1), write_to_lower=true}, - t=to_pen{tile=curry(gui.tp_border_thin, 2), ch=ch, write_to_lower=true, top_of_text=true}, + lt=to_pen{tile=curry(textures.tp_border_thin, 1), write_to_lower=true}, + t=to_pen{tile=curry(textures.tp_border_thin, 2), ch=ch, write_to_lower=true, top_of_text=true}, t_ascii=to_pen{ch=32}, - rt=to_pen{tile=curry(gui.tp_border_thin, 3), write_to_lower=true}, - lb=to_pen{tile=curry(gui.tp_border_thin, 15), write_to_lower=true}, - b=to_pen{tile=curry(gui.tp_border_thin, 16), ch=ch, write_to_lower=true, bottom_of_text=true}, - rb=to_pen{tile=curry(gui.tp_border_thin, 17), write_to_lower=true}, + rt=to_pen{tile=curry(textures.tp_border_thin, 3), write_to_lower=true}, + lb=to_pen{tile=curry(textures.tp_border_thin, 15), write_to_lower=true}, + b=to_pen{tile=curry(textures.tp_border_thin, 16), ch=ch, write_to_lower=true, bottom_of_text=true}, + rb=to_pen{tile=curry(textures.tp_border_thin, 17), write_to_lower=true}, } end local function get_key_hover_pens(ch) return { - t=to_pen{tile=curry(gui.tp_border_thin, 2), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, top_of_text=true}, + t=to_pen{tile=curry(textures.tp_border_thin, 2), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, top_of_text=true}, t_ascii=to_pen{fg=COLOR_WHITE, bg=COLOR_RED, ch=ch == 0 and 0 or 32}, - b=to_pen{tile=curry(gui.tp_border_thin, 16), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, bottom_of_text=true}, + b=to_pen{tile=curry(textures.tp_border_thin, 16), fg=COLOR_WHITE, bg=COLOR_RED, ch=ch, write_to_lower=true, bottom_of_text=true}, } end diff --git a/gui/design.lua b/gui/design.lua index 89dd8dbed0..7affcac542 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -35,6 +35,7 @@ -- END TODOS ================ local gui = require("gui") +local textures = require("gui.textures") local guidm = require("gui.dwarfmode") local widgets = require("gui.widgets") local quickfort = reqscript("quickfort") @@ -116,26 +117,26 @@ end local function get_icon_pens() local enabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = curry(gui.tp_control_panel, 1) or nil, ch = string.byte('[') } + tile = curry(textures.tp_control_panel, 1) or nil, ch = string.byte('[') } local enabled_pen_center = dfhack.pen.parse { fg = COLOR_LIGHTGREEN, - tile = curry(gui.tp_control_panel, 2) or nil, ch = 251 } -- check + tile = curry(textures.tp_control_panel, 2) or nil, ch = 251 } -- check local enabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = curry(gui.tp_control_panel, 3) or nil, ch = string.byte(']') } + tile = curry(textures.tp_control_panel, 3) or nil, ch = string.byte(']') } local disabled_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = curry(gui.tp_control_panel, 4) or nil, ch = string.byte('[') } + tile = curry(textures.tp_control_panel, 4) or nil, ch = string.byte('[') } local disabled_pen_center = dfhack.pen.parse { fg = COLOR_RED, - tile = curry(gui.tp_control_panel, 5) or nil, ch = string.byte('x') } + tile = curry(textures.tp_control_panel, 5) or nil, ch = string.byte('x') } local disabled_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = curry(gui.tp_control_panel, 6) or nil, ch = string.byte(']') } + tile = curry(textures.tp_control_panel, 6) or nil, ch = string.byte(']') } local button_pen_left = dfhack.pen.parse { fg = COLOR_CYAN, - tile = curry(gui.tp_control_panel, 7) or nil, ch = string.byte('[') } + tile = curry(textures.tp_control_panel, 7) or nil, ch = string.byte('[') } local button_pen_right = dfhack.pen.parse { fg = COLOR_CYAN, - tile = curry(gui.tp_control_panel, 8) or nil, ch = string.byte(']') } + tile = curry(textures.tp_control_panel, 8) or nil, ch = string.byte(']') } local help_pen_center = dfhack.pen.parse { - tile = curry(gui.tp_control_panel, 9) or nil, ch = string.byte('?') + tile = curry(textures.tp_control_panel, 9) or nil, ch = string.byte('?') } local configure_pen_center = dfhack.pen.parse { - tile = curry(gui.tp_control_panel, 10) or nil, ch = 15 + tile = curry(textures.tp_control_panel, 10) or nil, ch = 15 } -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, disabled_pen_left, disabled_pen_center, disabled_pen_right, diff --git a/gui/launcher.lua b/gui/launcher.lua index 56bcc7136f..180c6cc46c 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -3,6 +3,7 @@ local dialogs = require('gui.dialogs') local gui = require('gui') +local textures = require('gui.textures') local helpdb = require('helpdb') local json = require('json') local utils = require('utils') @@ -473,12 +474,12 @@ function MainPanel:postUpdateLayout() config:write(self.frame) end -local H_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_thin, 6), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} -local V_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_thin, 5), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} -local TOP_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_window,2), ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} -local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_window,16), ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} -local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_window,8), ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} -local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=curry(gui.tp_border_thin, 18), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} +local H_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_thin, 6), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} +local V_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_thin, 5), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} +local TOP_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_window,2), ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} +local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_window,16), ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} +local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_window,8), ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} +local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_thin, 18), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} -- paint autocomplete panel border local function paint_vertical_border(rect) From af7424f86f2829a68b6b9c0f567389ea321b3c06 Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 27 Aug 2023 22:46:55 -0700 Subject: [PATCH 463/732] Update retrieve-units.rst --- docs/fix/retrieve-units.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fix/retrieve-units.rst b/docs/fix/retrieve-units.rst index 4f0f01bb71..cfaa6d3f37 100644 --- a/docs/fix/retrieve-units.rst +++ b/docs/fix/retrieve-units.rst @@ -16,7 +16,7 @@ forces them to enter. This can fix issues such as: .. note:: For caravans that are missing entirely, this script may retrieve the merchants but not the items. Using `fix/stuck-merchants` to dismiss the - caravan followed by `force Caravan` to create a new one may work better. + caravan followed by ``force Caravan`` to create a new one may work better. Usage ----- From 5a4e659a062e33c1706e5f9db7c0ae60c5864ea4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Aug 2023 18:51:39 -0700 Subject: [PATCH 464/732] implement seeing and searching within bins --- internal/caravan/movegoods.lua | 125 ++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 34 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index c5bac09907..84c141bf29 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -130,6 +130,7 @@ function MoveGoods:init() self.animal_ethics, self.wood_ethics = get_ethics_restrictions() self.banned_items = common.get_banned_items() self.risky_items = common.get_risky_items(self.banned_items) + self.choices_cache = {} self:addviews{ widgets.CycleHotkeyLabel{ @@ -250,9 +251,21 @@ function MoveGoods:init() auto_width=true, }, widgets.ToggleHotkeyLabel{ - view_id='disable_buckets', - frame={l=26, b=2}, - label='Show individual items:', + view_id='group_items', + frame={l=25, b=2, w=24}, + label='Group items:', + key='CUSTOM_CTRL_G', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=true, + on_change=function() self:refresh_list() end, + }, + widgets.ToggleHotkeyLabel{ + view_id='inside_bins', + frame={l=51, b=2, w=28}, + label='See inside bins:', key='CUSTOM_CTRL_I', options={ {label='Yes', value=true, pen=COLOR_GREEN}, @@ -295,7 +308,6 @@ end local function is_tradeable_item(item, depot) if item.flags.hostile or - item.flags.in_inventory or item.flags.removed or item.flags.dead_dwarf or item.flags.spider_web or @@ -311,6 +323,15 @@ local function is_tradeable_item(item, depot) then return false end + if item.flags.in_inventory then + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) + if gref then + local container = df.item.find(gref.item_id) + if container and not df.item_binst:is_instance(container) then + return false + end + end + end if item.flags.in_job then local spec_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) if not spec_ref then return true end @@ -383,14 +404,44 @@ local function is_ethical_product(item, animal_ethics, wood_ethics) (not wood_ethics or not common.has_wood(item)) end -function MoveGoods:cache_choices(disable_buckets) - if self.choices then return self.choices[disable_buckets] end +local function add_words(words, str) + for word in str:gmatch("[%w]+") do + table.insert(words, word:lower()) + end +end + +local function make_bin_search_key(item, desc) + local words = {} + add_words(words, desc) + for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do + add_words(words, common.get_item_description(contained_item)) + end + return table.concat(words, ' ') +end + +local function get_cache_index(group_items, inside_bins) + local val = 1 + if group_items then val = val + 1 end + if inside_bins then val = val + 2 end + return val +end + +function MoveGoods:cache_choices(group_items, inside_bins) + local cache_idx = get_cache_index(group_items, inside_bins) + if self.choices_cache[cache_idx] then return self.choices_cache[cache_idx] end local pending = self.pending_item_ids - local buckets = {} + local groups = {} for _, item in ipairs(df.global.world.items.all) do local item_id = item.id if not item or not is_tradeable_item(item, self.depot) then goto continue end + if inside_bins and df.item_binst:is_instance(item) and + dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINS_ITEM) + then + goto continue + elseif not inside_bins and item.flags.in_inventory then + goto continue + end local value = common.get_perceived_value(item) if value <= 0 then goto continue end local is_pending = not not pending[item_id] or item.flags.in_building @@ -400,16 +451,16 @@ function MoveGoods:cache_choices(disable_buckets) local wear_level = item:getWear() local desc = common.get_item_description(item) local key = ('%s/%d'):format(desc, value) - if buckets[key] then - local bucket = buckets[key] - bucket.data.items[item_id] = {item=item, pending=is_pending, banned=is_banned, requested=is_requested} - bucket.data.quantity = bucket.data.quantity + 1 - bucket.data.selected = bucket.data.selected + (is_pending and 1 or 0) - bucket.data.num_at_depot = bucket.data.num_at_depot + (item.flags.in_building and 1 or 0) - bucket.data.has_forbidden = bucket.data.has_forbidden or is_forbidden - bucket.data.has_banned = bucket.data.has_banned or is_banned - bucket.data.has_risky = bucket.data.has_risky or is_risky - bucket.data.has_requested = bucket.data.has_requested or is_requested + if groups[key] then + local group = groups[key] + group.data.items[item_id] = {item=item, pending=is_pending, banned=is_banned, requested=is_requested} + group.data.quantity = group.data.quantity + 1 + group.data.selected = group.data.selected + (is_pending and 1 or 0) + group.data.num_at_depot = group.data.num_at_depot + (item.flags.in_building and 1 or 0) + group.data.has_forbidden = group.data.has_forbidden or is_forbidden + group.data.has_banned = group.data.has_banned or is_banned + group.data.has_risky = group.data.has_risky or is_risky + group.data.has_requested = group.data.has_requested or is_requested else local is_ethical = is_ethical_product(item, self.animal_ethics, self.wood_ethics) local data = { @@ -431,40 +482,46 @@ function MoveGoods:cache_choices(disable_buckets) ethical=is_ethical, dirty=false, } + local search_key + if not inside_bins and df.item_binst:is_instance(item) then + search_key = make_bin_search_key(item, desc) + else + search_key = common.make_search_key(desc) + end local entry = { - search_key=common.make_search_key(desc), + search_key=search_key, icon=curry(get_entry_icon, data), data=data, } - buckets[key] = entry + groups[key] = entry end ::continue:: end - local bucket_choices, nobucket_choices = {}, {} - for _, bucket in pairs(buckets) do - local data = bucket.data + local group_choices, nogroup_choices = {}, {} + for _, group in pairs(groups) do + local data = group.data for item_id, item_data in pairs(data.items) do - local nobucket_choice = copyall(bucket) - nobucket_choice.icon = curry(get_entry_icon, data, item_id) - nobucket_choice.text = make_choice_text(item_data.item.flags.in_building, data.per_item_value, 1, data.desc) - nobucket_choice.item_id = item_id - table.insert(nobucket_choices, nobucket_choice) + 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.per_item_value, 1, data.desc) + nogroup_choice.item_id = item_id + table.insert(nogroup_choices, nogroup_choice) end data.total_value = data.per_item_value * data.quantity - bucket.text = make_choice_text(data.num_at_depot == data.quantity, data.total_value, data.quantity, data.desc) - table.insert(bucket_choices, bucket) + group.text = make_choice_text(data.num_at_depot == data.quantity, data.total_value, data.quantity, data.desc) + table.insert(group_choices, group) self.value_pending = self.value_pending + (data.per_item_value * data.selected) end - self.choices = {} - self.choices[false] = bucket_choices - self.choices[true] = nobucket_choices - return self:cache_choices(disable_buckets) + self.choices_cache[get_cache_index(true, inside_bins)] = group_choices + self.choices_cache[get_cache_index(false, inside_bins)] = nogroup_choices + return self.choices_cache[cache_idx] end function MoveGoods:get_choices() - local raw_choices = self:cache_choices(self.subviews.disable_buckets:getOptionValue()) + local raw_choices = self:cache_choices(self.subviews.group_items:getOptionValue(), + self.subviews.inside_bins:getOptionValue()) local choices = {} local include_forbidden = not self.subviews.hide_forbidden:getOptionValue() local banned = self.subviews.banned:getOptionValue() From 54704b7c773b5e9c381568851bcd862f6876f1ae Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 28 Aug 2023 15:51:54 -0700 Subject: [PATCH 465/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 01d32e09d4..0233f8034b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -46,6 +46,7 @@ Template for new versions: ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames - `suspendmanager`: display a different color for jobs suspended by suspendmanager +- `caravan`: optionally display items within bins in bring goods to depot screen ## Removed From 024867d5b23ad8dd24c90ed941980dd6a389751d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 28 Aug 2023 17:55:46 -0700 Subject: [PATCH 466/732] allow stockpiles/workshops to be linked by id --- changelog.txt | 1 + internal/quickfort/build.lua | 17 ++++++++++++++--- internal/quickfort/place.lua | 10 ++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 01d32e09d4..0c0ad99f10 100644 --- a/changelog.txt +++ b/changelog.txt @@ -45,6 +45,7 @@ Template for new versions: ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames +- `quickfort`: linked stockpiles and workshops can now be specified by ID instead of only by name. this is mostly useful when dynamically generating blueprints and applying them via the `quickfort` API - `suspendmanager`: display a different color for jobs suspended by suspendmanager ## Removed diff --git a/internal/quickfort/build.lua b/internal/quickfort/build.lua index 53f50b105c..750c6f050b 100644 --- a/internal/quickfort/build.lua +++ b/internal/quickfort/build.lua @@ -313,10 +313,13 @@ local function do_trackstop_adjust(db_entry, bld) local from_names = {} for _,from_name in ipairs(db_entry.route.from_names) do from_names[from_name:lower()] = true + if tonumber(from_name) then + from_names[tonumber(from_name)] = true + end end for _, pile in ipairs(df.global.world.buildings.other.STOCKPILE) do local name = string.lower(pile.name) - if from_names[name] then + if from_names[name] or from_names[pile.id] then stop.stockpiles:insert('#', { new=df.route_stockpile_link, building_id=pile.id, @@ -1146,8 +1149,12 @@ local function create_building(b, cache, dry_run) utils.insert_sorted(bld.profile.links.give_to_pile, to, 'id') utils.insert_sorted(to.links.take_from_workshop, bld, 'id') end + elseif cache.piles[tonumber(recipient)] then + local to = cache.piles[tonumber(recipient)] + utils.insert_sorted(bld.profile.links.give_to_pile, to, 'id') + utils.insert_sorted(to.links.take_from_workshop, bld, 'id') else - dfhack.printerr(('cannot find stockpile named "%s" to give to'):format(recipient)) + dfhack.printerr(('cannot find stockpile with name or id "%s" to give to'):format(recipient)) end end for _,supplier in ipairs(db_entry.links.take_from) do @@ -1157,8 +1164,12 @@ local function create_building(b, cache, dry_run) utils.insert_sorted(bld.profile.links.take_from_pile, from, 'id') utils.insert_sorted(from.links.give_to_workshop, bld, 'id') end + elseif cache.piles[tonumber(supplier)] then + local from = cache.piles[tonumber(supplier)] + utils.insert_sorted(bld.profile.links.take_from_pile, from, 'id') + utils.insert_sorted(from.links.give_to_workshop, bld, 'id') else - dfhack.printerr(('cannot find stockpile named "%s" to take from'):format(supplier)) + dfhack.printerr(('cannot find stockpile with name or id "%s" to take from'):format(supplier)) end end if buildingplan and buildingplan.isEnabled() and diff --git a/internal/quickfort/place.lua b/internal/quickfort/place.lua index 4da103a016..66eccda46c 100644 --- a/internal/quickfort/place.lua +++ b/internal/quickfort/place.lua @@ -347,6 +347,7 @@ function get_stockpiles_by_name() if #pile.name > 0 then table.insert(ensure_key(piles, pile.name), pile) end + piles[pile.id] = pile end return piles end @@ -357,6 +358,7 @@ local function get_workshops_by_name() if #shop.name > 0 then table.insert(ensure_key(shops, shop.name), shop) end + shops[shop.id] = shop end return shops end @@ -364,12 +366,12 @@ end local function get_pile_targets(name, peer_piles, all_piles) if peer_piles[name] then return peer_piles[name], all_piles end all_piles = all_piles or get_stockpiles_by_name() - return all_piles[name], all_piles + return all_piles[name] or (all_piles[tonumber(name)] and {all_piles[tonumber(name)]}), all_piles end local function get_shop_targets(name, all_shops) all_shops = all_shops or get_workshops_by_name() - return all_shops[name], all_shops + return all_shops[name] or (all_shops[tonumber(name)] and {all_shops[tonumber(name)]}), all_shops end -- will link to stockpiles created in this blueprint @@ -394,7 +396,7 @@ local function link_stockpiles(link_data) utils.insert_sorted(node.to.links.take_from_workshop, from, 'id') end else - dfhack.printerr(('cannot find stockpile or workshop named "%s" to take from'):format(name)) + dfhack.printerr(('cannot find stockpile or workshop with name or id "%s" to take from'):format(name)) end end elseif type(node.to) == 'string' then @@ -413,7 +415,7 @@ local function link_stockpiles(link_data) utils.insert_sorted(to.profile.links.take_from_pile, node.from, 'id') end else - dfhack.printerr(('cannot find stockpile or workshop named "%s" to give to'):format(name)) + dfhack.printerr(('cannot find stockpile or workshop with name or id "%s" to give to'):format(name)) end end end From 1f3a8f7846a6d35d61a7d02bdf3d086139aab983 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 11 Aug 2023 20:14:07 -0700 Subject: [PATCH 467/732] Revert "temporarily revert work on predicates" This reverts commit 261a22b50e1ca0a19e4a2c0d901fffa437f60c42. --- internal/caravan/common.lua | 145 ++++++++++++++++++++++++++++++++- internal/caravan/movegoods.lua | 9 +- internal/caravan/trade.lua | 3 + 3 files changed, 151 insertions(+), 6 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 189fdc645d..d5a5170451 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -1,6 +1,7 @@ --@ module = true local dialogs = require('gui.dialogs') +local scriptmanager = require('script-manager') local widgets = require('gui.widgets') CH_UP = string.char(30) @@ -181,7 +182,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=5, l=0, r=0, h=4}, + frame={t=6, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_quality'..suffix, @@ -246,7 +247,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=10, l=0, r=0, h=4}, + frame={t=12, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_value'..suffix, @@ -457,7 +458,96 @@ local function get_ethics_token(animal_ethics, wood_ethics) } end -function get_info_widgets(self, export_agreements) +local PREDICATE_LIBRARY = { + {name='weapons-grade metal', match=function(item) + if item.mat_type ~= 0 then return false end + local flags = df.global.world.raws.inorganics[item.mat_index].material.flags + return flags.IS_METAL and + (flags.ITEMS_METAL or flags.ITEMS_WEAPON or flags.ITEMS_WEAPON_RANGED or flags.ITEMS_AMMO or flags.ITEMS_ARMOR) + end}, +} +for _,item_type in ipairs(df.item_type) do + table.insert(PREDICATE_LIBRARY, { + name=to_item_type_str(item_type), + group='item type', + match=function(item) return item_type == item:getType() end, + }) +end + +local PREDICATES_VAR = 'PREDICATES' + +local function get_user_predicates() + local user_predicates = {} + local load_user_predicates = function(env_name, env) + local predicates = env[PREDICATES_VAR] + if not predicates then return end + if type(predicates) ~= 'table' then + dfhack.printerr( + ('error loading predicates from "%s": %s map is malformed') + :format(env_name, PREDICATES_VAR)) + return + end + for i,predicate in ipairs(predicates) do + if type(predicate) ~= 'table' then + dfhack.printerr(('error loading predicate %s:%d (must be a table)'):format(env_name, i)) + goto continue + end + if type(predicate.name) ~= 'string' or #predicate.name == 0 then + dfhack.printerr(('error loading predicate %s:%d (must have a string "name" field)'):format(env_name, i)) + goto continue + end + if type(predicate.match) ~= 'function' then + dfhack.printerr(('error loading predicate %s:%d (must have a function "match" field)'):format(env_name, i)) + goto continue + end + table.insert(user_predicates, {id=('%s:%s'):format(env_name, predicate.name), name=predicate.name, match=predicate.match}) + ::continue:: + end + end + scriptmanager.foreach_module_script(load_user_predicates) + return user_predicates +end + +local function customize_predicates(predicates, on_close) + local user_predicates = get_user_predicates() + local predicate = nil + if #user_predicates > 0 then + predicate = user_predicates[1] + else + predicate = PREDICATE_LIBRARY[1] + end + predicates[predicate.name] = {match=predicate.match, show=true} + on_close() +end + +local function make_predicate_str(predicates) + local preset, names = nil, {} + for name, predicate in pairs(predicates) do + if not preset then + preset = predicate.preset or '' + end + if #preset > 0 and preset ~= predicate.preset then + preset = '' + end + table.insert(names, name) + end + if preset and #preset > 0 then + return preset + end + if #names > 0 then + return table.concat(names, ', ') + end + return 'All' +end + +local function get_context_predicates(context) + return {} +end + +function get_info_widgets(self, export_agreements, context) + self.predicates = get_context_predicates(context) + local predicate_str = make_predicate_str(self.predicates) + return { widgets.Panel{ frame={t=0, l=0, r=0, h=2}, @@ -546,9 +636,58 @@ function get_info_widgets(self, export_agreements) }, }, }, + widgets.Panel{ + frame={t=13, l=0, r=0, h=2}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Advanced filter:', + }, + widgets.HotkeyLabel{ + frame={t=0, l=18, w=9}, + key='CUSTOM_SHIFT_J', + label='[edit]', + on_activate=function() + customize_predicates(self.predicates, + function() + predicate_str = make_predicate_str(self.predicates) + self:refresh_list() + end) + end, + }, + widgets.HotkeyLabel{ + frame={t=0, l=34, w=10}, + key='CUSTOM_SHIFT_K', + label='[clear]', + text_pen=COLOR_LIGHTRED, + on_activate=function() + self.predicates = {} + predicate_str = make_predicate_str(self.predicates) + self:refresh_list() + end, + enabled=function() return next(self.predicates) end, + }, + widgets.Label{ + frame={t=1, l=2}, + text={{text=function() return predicate_str end}}, + text_pen=COLOR_GREEN, + }, + }, + }, } end +function pass_predicates(item, predicates) + local has_show = false + for _,predicate in pairs(predicates) do + local matches = predicate.match(item) + has_show = has_show or predicate.show + if matches and predicate.show then return true end + if not matches and predicate.hide then return false end + end + return not has_show +end + local function match_risky(item, risky_items) for item_type, subtypes in pairs(risky_items) do for subtype in pairs(subtypes) do diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 84c141bf29..38a0d4accd 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -158,7 +158,7 @@ function MoveGoods:init() on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ - frame={t=2, l=0, w=38, h=14}, + frame={t=2, l=0, w=38, h=16}, subviews=common.get_slider_widgets(self), }, widgets.ToggleHotkeyLabel{ @@ -174,11 +174,11 @@ function MoveGoods:init() on_change=function() self:refresh_list() end, }, widgets.Panel{ - frame={t=4, l=40, r=0, h=12}, + frame={t=4, l=40, r=0, h=15}, subviews=common.get_info_widgets(self, get_export_agreements()), }, widgets.Panel{ - frame={t=17, l=0, r=0, b=6}, + frame={t=19, l=0, r=0, b=6}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', @@ -572,6 +572,9 @@ function MoveGoods:get_choices() goto continue end end + if not common.pass_predicates(data.item, self.predicates) then + goto continue + end table.insert(choices, choice) ::continue:: end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index ec9d27be82..c0dc6902e9 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -438,6 +438,9 @@ function Trade:get_choices() goto continue end end + if not common.pass_predicates(data.item, self.predicates) then + goto continue + end table.insert(choices, choice) ::continue:: end From c214745644ef8380d879ff7444eb2d20c4547551 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 13 Aug 2023 12:19:11 -0700 Subject: [PATCH 468/732] refactor predicate logic out into a separate file --- internal/caravan/common.lua | 109 ++------------------------------ internal/caravan/movegoods.lua | 3 +- internal/caravan/predicates.lua | 104 ++++++++++++++++++++++++++++++ internal/caravan/trade.lua | 3 +- 4 files changed, 114 insertions(+), 105 deletions(-) create mode 100644 internal/caravan/predicates.lua diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index d5a5170451..e8d4d42015 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -1,7 +1,7 @@ --@ module = true local dialogs = require('gui.dialogs') -local scriptmanager = require('script-manager') +local predicates = reqscript('internal/caravan/predicates') local widgets = require('gui.widgets') CH_UP = string.char(30) @@ -458,95 +458,9 @@ local function get_ethics_token(animal_ethics, wood_ethics) } end -local PREDICATE_LIBRARY = { - {name='weapons-grade metal', match=function(item) - if item.mat_type ~= 0 then return false end - local flags = df.global.world.raws.inorganics[item.mat_index].material.flags - return flags.IS_METAL and - (flags.ITEMS_METAL or flags.ITEMS_WEAPON or flags.ITEMS_WEAPON_RANGED or flags.ITEMS_AMMO or flags.ITEMS_ARMOR) - end}, -} -for _,item_type in ipairs(df.item_type) do - table.insert(PREDICATE_LIBRARY, { - name=to_item_type_str(item_type), - group='item type', - match=function(item) return item_type == item:getType() end, - }) -end - -local PREDICATES_VAR = 'PREDICATES' - -local function get_user_predicates() - local user_predicates = {} - local load_user_predicates = function(env_name, env) - local predicates = env[PREDICATES_VAR] - if not predicates then return end - if type(predicates) ~= 'table' then - dfhack.printerr( - ('error loading predicates from "%s": %s map is malformed') - :format(env_name, PREDICATES_VAR)) - return - end - for i,predicate in ipairs(predicates) do - if type(predicate) ~= 'table' then - dfhack.printerr(('error loading predicate %s:%d (must be a table)'):format(env_name, i)) - goto continue - end - if type(predicate.name) ~= 'string' or #predicate.name == 0 then - dfhack.printerr(('error loading predicate %s:%d (must have a string "name" field)'):format(env_name, i)) - goto continue - end - if type(predicate.match) ~= 'function' then - dfhack.printerr(('error loading predicate %s:%d (must have a function "match" field)'):format(env_name, i)) - goto continue - end - table.insert(user_predicates, {id=('%s:%s'):format(env_name, predicate.name), name=predicate.name, match=predicate.match}) - ::continue:: - end - end - scriptmanager.foreach_module_script(load_user_predicates) - return user_predicates -end - -local function customize_predicates(predicates, on_close) - local user_predicates = get_user_predicates() - local predicate = nil - if #user_predicates > 0 then - predicate = user_predicates[1] - else - predicate = PREDICATE_LIBRARY[1] - end - predicates[predicate.name] = {match=predicate.match, show=true} - on_close() -end - -local function make_predicate_str(predicates) - local preset, names = nil, {} - for name, predicate in pairs(predicates) do - if not preset then - preset = predicate.preset or '' - end - if #preset > 0 and preset ~= predicate.preset then - preset = '' - end - table.insert(names, name) - end - if preset and #preset > 0 then - return preset - end - if #names > 0 then - return table.concat(names, ', ') - end - return 'All' -end - -local function get_context_predicates(context) - return {} -end - function get_info_widgets(self, export_agreements, context) - self.predicates = get_context_predicates(context) - local predicate_str = make_predicate_str(self.predicates) + self.predicates = predicates.get_context_predicates(context) + local predicate_str = predicates.make_predicate_str(self.predicates) return { widgets.Panel{ @@ -648,9 +562,9 @@ function get_info_widgets(self, export_agreements, context) key='CUSTOM_SHIFT_J', label='[edit]', on_activate=function() - customize_predicates(self.predicates, + predicates.customize_predicates(self.predicates, function() - predicate_str = make_predicate_str(self.predicates) + predicate_str = predicates.make_predicate_str(self.predicates) self:refresh_list() end) end, @@ -662,7 +576,7 @@ function get_info_widgets(self, export_agreements, context) text_pen=COLOR_LIGHTRED, on_activate=function() self.predicates = {} - predicate_str = make_predicate_str(self.predicates) + predicate_str = predicates.make_predicate_str(self.predicates) self:refresh_list() end, enabled=function() return next(self.predicates) end, @@ -677,17 +591,6 @@ function get_info_widgets(self, export_agreements, context) } end -function pass_predicates(item, predicates) - local has_show = false - for _,predicate in pairs(predicates) do - local matches = predicate.match(item) - has_show = has_show or predicate.show - if matches and predicate.show then return true end - if not matches and predicate.hide then return false end - end - return not has_show -end - local function match_risky(item, risky_items) for item_type, subtypes in pairs(risky_items) do for subtype in pairs(subtypes) do diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 38a0d4accd..754d3a367a 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -3,6 +3,7 @@ local common = reqscript('internal/caravan/common') local gui = require('gui') local overlay = require('plugins.overlay') +local predicates = reqscript('internal/caravan/predicates') local utils = require('utils') local widgets = require('gui.widgets') @@ -572,7 +573,7 @@ function MoveGoods:get_choices() goto continue end end - if not common.pass_predicates(data.item, self.predicates) then + if not predicates.pass_predicates(data.item, self.predicates) then goto continue end table.insert(choices, choice) diff --git a/internal/caravan/predicates.lua b/internal/caravan/predicates.lua new file mode 100644 index 0000000000..a40d4c04da --- /dev/null +++ b/internal/caravan/predicates.lua @@ -0,0 +1,104 @@ +--@ module = true + +local scriptmanager = require('script-manager') + +local function to_item_type_str(item_type) + return string.lower(df.item_type[item_type]):gsub('_', ' ') +end + +local PREDICATE_LIBRARY = { + {name='weapons-grade metal', match=function(item) + if item.mat_type ~= 0 then return false end + local flags = df.global.world.raws.inorganics[item.mat_index].material.flags + return flags.IS_METAL and + (flags.ITEMS_METAL or flags.ITEMS_WEAPON or flags.ITEMS_WEAPON_RANGED or flags.ITEMS_AMMO or flags.ITEMS_ARMOR) + end}, +} +for _,item_type in ipairs(df.item_type) do + table.insert(PREDICATE_LIBRARY, { + name=to_item_type_str(item_type), + group='item type', + match=function(item) return item_type == item:getType() end, + }) +end + +local PREDICATES_VAR = 'PREDICATES' + +local function get_user_predicates() + local user_predicates = {} + local load_user_predicates = function(env_name, env) + local predicates = env[PREDICATES_VAR] + if not predicates then return end + if type(predicates) ~= 'table' then + dfhack.printerr( + ('error loading predicates from "%s": %s map is malformed') + :format(env_name, PREDICATES_VAR)) + return + end + for i,predicate in ipairs(predicates) do + if type(predicate) ~= 'table' then + dfhack.printerr(('error loading predicate %s:%d (must be a table)'):format(env_name, i)) + goto continue + end + if type(predicate.name) ~= 'string' or #predicate.name == 0 then + dfhack.printerr(('error loading predicate %s:%d (must have a string "name" field)'):format(env_name, i)) + goto continue + end + if type(predicate.match) ~= 'function' then + dfhack.printerr(('error loading predicate %s:%d (must have a function "match" field)'):format(env_name, i)) + goto continue + end + table.insert(user_predicates, {id=('%s:%s'):format(env_name, predicate.name), name=predicate.name, match=predicate.match}) + ::continue:: + end + end + scriptmanager.foreach_module_script(load_user_predicates) + return user_predicates +end + +function customize_predicates(predicates, on_close) + local user_predicates = get_user_predicates() + local predicate = nil + if #user_predicates > 0 then + predicate = user_predicates[1] + else + predicate = PREDICATE_LIBRARY[1] + end + predicates[predicate.name] = {match=predicate.match, show=true} + on_close() +end + +function make_predicate_str(predicates) + local preset, names = nil, {} + for name, predicate in pairs(predicates) do + if not preset then + preset = predicate.preset or '' + end + if #preset > 0 and preset ~= predicate.preset then + preset = '' + end + table.insert(names, name) + end + if preset and #preset > 0 then + return preset + end + if #names > 0 then + return table.concat(names, ', ') + end + return 'All' +end + +function get_context_predicates(context) + return {} +end + +function pass_predicates(item, predicates) + local has_show = false + for _,predicate in pairs(predicates) do + local matches = predicate.match(item) + has_show = has_show or predicate.show + if matches and predicate.show then return true end + if not matches and predicate.hide then return false end + end + return not has_show +end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index c0dc6902e9..bff2a1d861 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -8,6 +8,7 @@ local common = reqscript('internal/caravan/common') local gui = require('gui') local overlay = require('plugins.overlay') +local predicates = reqscript('internal/caravan/predicates') local widgets = require('gui.widgets') trader_selected_state = trader_selected_state or {} @@ -438,7 +439,7 @@ function Trade:get_choices() goto continue end end - if not common.pass_predicates(data.item, self.predicates) then + if not predicates.pass_predicates(data.item, self.predicates) then goto continue end table.insert(choices, choice) From f482104d79b3e90d11612b2b31446c458f6472ce Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 29 Aug 2023 01:32:31 -0700 Subject: [PATCH 469/732] hold predicate state in context --- internal/caravan/common.lua | 80 +++++++++++++++++---------------- internal/caravan/movegoods.lua | 6 ++- internal/caravan/predicates.lua | 23 +++++----- internal/caravan/trade.lua | 56 +++++++++++++---------- 4 files changed, 92 insertions(+), 73 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index e8d4d42015..eb4dc7b256 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -458,10 +458,48 @@ local function get_ethics_token(animal_ethics, wood_ethics) } end -function get_info_widgets(self, export_agreements, context) - self.predicates = predicates.get_context_predicates(context) - local predicate_str = predicates.make_predicate_str(self.predicates) +function get_advanced_filter_widgets(self, context) + predicates.init_context_predicates(context) + local predicate_str = predicates.make_predicate_str(context) + return { + widgets.Label{ + frame={t=0, l=0}, + text='Advanced filter:', + }, + widgets.HotkeyLabel{ + frame={t=0, l=18, w=9}, + key='CUSTOM_SHIFT_J', + label='[edit]', + on_activate=function() + predicates.customize_predicates(context, + function() + predicate_str = predicates.make_predicate_str(context) + self:refresh_list() + end) + end, + }, + widgets.HotkeyLabel{ + frame={t=0, l=29, w=10}, + key='CUSTOM_SHIFT_K', + label='[clear]', + text_pen=COLOR_LIGHTRED, + on_activate=function() + context.predicates = {} + predicate_str = predicates.make_predicate_str(context) + self:refresh_list() + end, + enabled=function() return next(context) end, + }, + widgets.Label{ + frame={t=1, l=2}, + text={{text=function() return predicate_str end}}, + text_pen=COLOR_GREEN, + }, + } +end + +function get_info_widgets(self, export_agreements, context) return { widgets.Panel{ frame={t=0, l=0, r=0, h=2}, @@ -552,41 +590,7 @@ function get_info_widgets(self, export_agreements, context) }, widgets.Panel{ frame={t=13, l=0, r=0, h=2}, - subviews={ - widgets.Label{ - frame={t=0, l=0}, - text='Advanced filter:', - }, - widgets.HotkeyLabel{ - frame={t=0, l=18, w=9}, - key='CUSTOM_SHIFT_J', - label='[edit]', - on_activate=function() - predicates.customize_predicates(self.predicates, - function() - predicate_str = predicates.make_predicate_str(self.predicates) - self:refresh_list() - end) - end, - }, - widgets.HotkeyLabel{ - frame={t=0, l=34, w=10}, - key='CUSTOM_SHIFT_K', - label='[clear]', - text_pen=COLOR_LIGHTRED, - on_activate=function() - self.predicates = {} - predicate_str = predicates.make_predicate_str(self.predicates) - self:refresh_list() - end, - enabled=function() return next(self.predicates) end, - }, - widgets.Label{ - frame={t=1, l=2}, - text={{text=function() return predicate_str end}}, - text_pen=COLOR_GREEN, - }, - }, + subviews=get_advanced_filter_widgets(self, context), }, } end diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 754d3a367a..b520c17a6d 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -133,6 +133,8 @@ function MoveGoods:init() self.risky_items = common.get_risky_items(self.banned_items) self.choices_cache = {} + self.predicate_context = {name='movegoods'} + self:addviews{ widgets.CycleHotkeyLabel{ view_id='sort', @@ -176,7 +178,7 @@ function MoveGoods:init() }, widgets.Panel{ frame={t=4, l=40, r=0, h=15}, - subviews=common.get_info_widgets(self, get_export_agreements()), + subviews=common.get_info_widgets(self, get_export_agreements(), self.predicate_context), }, widgets.Panel{ frame={t=19, l=0, r=0, b=6}, @@ -573,7 +575,7 @@ function MoveGoods:get_choices() goto continue end end - if not predicates.pass_predicates(data.item, self.predicates) then + if not predicates.pass_predicates(self.predicate_context, data.item) then goto continue end table.insert(choices, choice) diff --git a/internal/caravan/predicates.lua b/internal/caravan/predicates.lua index a40d4c04da..36745d82ab 100644 --- a/internal/caravan/predicates.lua +++ b/internal/caravan/predicates.lua @@ -22,7 +22,7 @@ for _,item_type in ipairs(df.item_type) do }) end -local PREDICATES_VAR = 'PREDICATES' +local PREDICATES_VAR = 'ITEM_PREDICATES' local function get_user_predicates() local user_predicates = {} @@ -56,7 +56,7 @@ local function get_user_predicates() return user_predicates end -function customize_predicates(predicates, on_close) +function customize_predicates(context, on_close) local user_predicates = get_user_predicates() local predicate = nil if #user_predicates > 0 then @@ -64,13 +64,13 @@ function customize_predicates(predicates, on_close) else predicate = PREDICATE_LIBRARY[1] end - predicates[predicate.name] = {match=predicate.match, show=true} + context.predicates[predicate.name] = {match=predicate.match, show=true} on_close() end -function make_predicate_str(predicates) +function make_predicate_str(context) local preset, names = nil, {} - for name, predicate in pairs(predicates) do + for name, predicate in pairs(context.predicates) do if not preset then preset = predicate.preset or '' end @@ -88,17 +88,20 @@ function make_predicate_str(predicates) return 'All' end -function get_context_predicates(context) - return {} +function init_context_predicates(context) + -- TODO: init according to context.name + context.predicates = {} end -function pass_predicates(item, predicates) +function pass_predicates(context, item) local has_show = false - for _,predicate in pairs(predicates) do - local matches = predicate.match(item) + for _,predicate in pairs(context.predicates) do + local ok, matches = safecall(predicate.match, item) + if not ok then goto continue end has_show = has_show or predicate.show if matches and predicate.show then return true end if not matches and predicate.hide then return false end + ::continue:: end return not has_show end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index bff2a1d861..31a3d56e2e 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -139,10 +139,12 @@ end local STATUS_COL_WIDTH = 7 local VALUE_COL_WIDTH = 6 -local FILTER_HEIGHT = 15 +local FILTER_HEIGHT = 17 function Trade:init() self.cur_page = 1 + self.filters = {'', ''} + self.predicate_contexts = {{name='trade_caravan'}, {name='trade_fort'}} self.animal_ethics = common.is_animal_lover_caravan(trade.mer) self.wood_ethics = common.is_tree_lover_caravan(trade.mer) @@ -152,7 +154,7 @@ function Trade:init() self:addviews{ widgets.CycleHotkeyLabel{ view_id='sort', - frame={l=0, t=0, w=21}, + frame={t=0, l=0, w=21}, label='Sort by:', key='CUSTOM_SHIFT_S', options={ @@ -166,15 +168,9 @@ function Trade:init() initial_option=sort_by_status_desc, on_change=self:callback('refresh_list', 'sort'), }, - widgets.EditField{ - view_id='search', - frame={l=26, t=0}, - label_text='Search: ', - on_char=function(ch) return ch:match('[%l -]') end, - }, widgets.ToggleHotkeyLabel{ view_id='trade_bins', - frame={t=2, l=0, w=36}, + frame={t=0, l=26, w=36}, label='Bins:', key='CUSTOM_SHIFT_B', options={ @@ -184,9 +180,24 @@ function Trade:init() initial_option=false, on_change=function() self:refresh_list() end, }, + widgets.TabBar{ + frame={t=2, l=0}, + labels={ + 'Caravan goods', + 'Fort goods', + }, + on_select=function(idx) + local list = self.subviews.list + self.filters[self.cur_page] = list:getFilter() + list:setFilter(self.filters[idx]) + self.cur_page = idx + self:refresh_list() + end, + get_cur_page=function() return self.cur_page end, + }, widgets.ToggleHotkeyLabel{ view_id='filters', - frame={t=2, l=40, w=36}, + frame={t=5, l=0, w=36}, label='Show filters:', key='CUSTOM_SHIFT_F', options={ @@ -196,17 +207,11 @@ function Trade:init() initial_option=false, on_change=function() self:updateLayout() end, }, - widgets.TabBar{ - frame={t=4, l=0}, - labels={ - 'Caravan goods', - 'Fort goods', - }, - on_select=function(idx) - self.cur_page = idx - self:refresh_list() - end, - get_cur_page=function() return self.cur_page end, + widgets.EditField{ + view_id='search', + frame={t=5, l=40}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ frame={t=7, l=0, r=0, h=FILTER_HEIGHT}, @@ -230,10 +235,15 @@ function Trade:init() visible=function() return self.cur_page == 2 end, subviews=common.get_slider_widgets(self, '2'), }, + widgets.Panel{ + frame={b=0, l=40, r=0, h=2}, + visible=function() return self.cur_page == 1 end, + subviews=common.get_advanced_filter_widgets(self, self.predicate_contexts[1]), + }, widgets.Panel{ frame={t=2, l=40, r=0, h=FILTER_HEIGHT-2}, visible=function() return self.cur_page == 2 end, - subviews=common.get_info_widgets(self, {trade.mer.buy_prices}), + subviews=common.get_info_widgets(self, {trade.mer.buy_prices}, self.predicate_contexts[2]), }, }, }, @@ -439,7 +449,7 @@ function Trade:get_choices() goto continue end end - if not predicates.pass_predicates(data.item, self.predicates) then + if not predicates.pass_predicates(self.predicate_contexts[self.cur_page], data.item) then goto continue end table.insert(choices, choice) From f38d52f21d81fa5b6bf6a5103f01e21efad3d8d2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 29 Aug 2023 02:34:57 -0700 Subject: [PATCH 470/732] prep for advanced filter screen --- internal/caravan/predicates.lua | 58 +++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/internal/caravan/predicates.lua b/internal/caravan/predicates.lua index 36745d82ab..6780e46e55 100644 --- a/internal/caravan/predicates.lua +++ b/internal/caravan/predicates.lua @@ -1,6 +1,8 @@ --@ module = true +local gui = require('gui') local scriptmanager = require('script-manager') +local widgets = require('gui.widgets') local function to_item_type_str(item_type) return string.lower(df.item_type[item_type]):gsub('_', ' ') @@ -8,8 +10,8 @@ end local PREDICATE_LIBRARY = { {name='weapons-grade metal', match=function(item) - if item.mat_type ~= 0 then return false end - local flags = df.global.world.raws.inorganics[item.mat_index].material.flags + if item:getMaterial() ~= 0 then return false end + local flags = df.global.world.raws.inorganics[item:getMaterialIndex()].material.flags return flags.IS_METAL and (flags.ITEMS_METAL or flags.ITEMS_WEAPON or flags.ITEMS_WEAPON_RANGED or flags.ITEMS_AMMO or flags.ITEMS_ARMOR) end}, @@ -56,18 +58,6 @@ local function get_user_predicates() return user_predicates end -function customize_predicates(context, on_close) - local user_predicates = get_user_predicates() - local predicate = nil - if #user_predicates > 0 then - predicate = user_predicates[1] - else - predicate = PREDICATE_LIBRARY[1] - end - context.predicates[predicate.name] = {match=predicate.match, show=true} - on_close() -end - function make_predicate_str(context) local preset, names = nil, {} for name, predicate in pairs(context.predicates) do @@ -89,19 +79,47 @@ function make_predicate_str(context) end function init_context_predicates(context) - -- TODO: init according to context.name + -- TODO: init according to saved preferences associated with context.name context.predicates = {} end function pass_predicates(context, item) - local has_show = false for _,predicate in pairs(context.predicates) do local ok, matches = safecall(predicate.match, item) if not ok then goto continue end - has_show = has_show or predicate.show - if matches and predicate.show then return true end - if not matches and predicate.hide then return false end + if matches ~= predicate.invert then return false end ::continue:: end - return not has_show + return true +end + +AdvancedFilter = defclass(AdvancedFilter, widgets.Window) +AdvancedFilter.ATTRS { + frame_title='Advanced item filters', + frame={w=50, h=45}, + resizable=true, + resize_min={w=50, h=20}, + context=DEFAULT_NIL, + on_change=DEFAULT_NIL, +} + +function AdvancedFilter:init() + self:addviews{ + } +end + +AdvancedFilterScreen = defclass(AdvancedFilterScreen, gui.ZScreenModal) +AdvancedFilterScreen.ATTRS { + focus_path='advanced_item_filter', + context=DEFAULT_NIL, + on_change=DEFAULT_NIL, +} + +function AdvancedFilterScreen:init() + self:addviews{AdvancedFilter{context=self.context, on_change=self.on_change}} +end + +function customize_predicates(context, on_change) + context.user_predicates = context.user_predicates or get_user_predicates() + AdvancedFilterScreen{context=context, on_change=on_change}:show() end From 56538fba30e1378a2d37e70042c0152c73e102b7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 29 Aug 2023 23:29:40 -0700 Subject: [PATCH 471/732] hide advanced widgets for now (revert later) --- internal/caravan/common.lua | 6 ++++-- internal/caravan/movegoods.lua | 6 +++--- internal/caravan/trade.lua | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index eb4dc7b256..1b7cdfdcc3 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -182,7 +182,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=6, l=0, r=0, h=4}, + frame={t=5, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_quality'..suffix, @@ -247,7 +247,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=12, l=0, r=0, h=4}, + frame={t=10, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_value'..suffix, @@ -463,6 +463,7 @@ function get_advanced_filter_widgets(self, context) local predicate_str = predicates.make_predicate_str(context) return { + --[[ widgets.Label{ frame={t=0, l=0}, text='Advanced filter:', @@ -496,6 +497,7 @@ function get_advanced_filter_widgets(self, context) text={{text=function() return predicate_str end}}, text_pen=COLOR_GREEN, }, + --]] } end diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index b520c17a6d..c44ef2d9df 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -161,7 +161,7 @@ function MoveGoods:init() on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ - frame={t=2, l=0, w=38, h=16}, + frame={t=2, l=0, w=38, h=14}, subviews=common.get_slider_widgets(self), }, widgets.ToggleHotkeyLabel{ @@ -177,11 +177,11 @@ function MoveGoods:init() on_change=function() self:refresh_list() end, }, widgets.Panel{ - frame={t=4, l=40, r=0, h=15}, + frame={t=4, l=40, r=0, h=12}, subviews=common.get_info_widgets(self, get_export_agreements(), self.predicate_context), }, widgets.Panel{ - frame={t=19, l=0, r=0, b=6}, + frame={t=17, l=0, r=0, b=6}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 31a3d56e2e..47401f272a 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -139,7 +139,7 @@ end local STATUS_COL_WIDTH = 7 local VALUE_COL_WIDTH = 6 -local FILTER_HEIGHT = 17 +local FILTER_HEIGHT = 15 function Trade:init() self.cur_page = 1 From 478d0237a3452cbc5ad88c2d6e329b41506f7173 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 30 Aug 2023 02:19:51 -0700 Subject: [PATCH 472/732] visually indicate whether autoupdate is on --- changelog.txt | 1 + gui/gm-editor.lua | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 68e2b3d29b..73ad700756 100644 --- a/changelog.txt +++ b/changelog.txt @@ -48,6 +48,7 @@ Template for new versions: - `quickfort`: linked stockpiles and workshops can now be specified by ID instead of only by name. this is mostly useful when dynamically generating blueprints and applying them via the `quickfort` API - `suspendmanager`: display a different color for jobs suspended by suspendmanager - `caravan`: optionally display items within bins in bring goods to depot screen +- `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates ## Removed diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index fa65a76280..231dc8ef11 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -533,6 +533,7 @@ function GmEditorUi:onInput(keys) return true elseif keys[keybindings.autoupdate.key] then self.autoupdate = not self.autoupdate + self:updateTitles() return true elseif keys[keybindings.offset.key] then local trg=self:currentTarget() @@ -601,17 +602,19 @@ end function GmEditorUi:updateTitles() local title = "GameMaster's Editor" if self.read_only then - title = title.." (Read Only)" + title = title.." (Read only)" end for view,_ in pairs(views) do - view.subviews[1].frame_title = title + local window = view.subviews[1] + window.read_only = self.read_only + window.frame_title = title .. (window.autoupdate and ' (Live updates)' or '') end - self.frame_title = title save_config({read_only = self.read_only}) end function GmEditorUi:updateTarget(preserve_pos,reindex) self:verifyStack() local trg=self:currentTarget() + if not trg then return end local filter=self.subviews.filter_input.text:lower() if reindex then From a5589e9e3ee26737d5bfe7a111d9a0b4af73c9f7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 30 Aug 2023 03:15:06 -0700 Subject: [PATCH 473/732] shift right click to quick exit --- changelog.txt | 1 + docs/gui/gm-editor.rst | 6 ++++++ gui/gm-editor.lua | 3 +++ 3 files changed, 10 insertions(+) diff --git a/changelog.txt b/changelog.txt index 68e2b3d29b..fc0b37c714 100644 --- a/changelog.txt +++ b/changelog.txt @@ -45,6 +45,7 @@ Template for new versions: ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames +- `gui/gm-editor`: hold down shift and right click to exit, regardless of how many substructures deep you are - `quickfort`: linked stockpiles and workshops can now be specified by ID instead of only by name. this is mostly useful when dynamically generating blueprints and applying them via the `quickfort` API - `suspendmanager`: display a different color for jobs suspended by suspendmanager - `caravan`: optionally display items within bins in bring goods to depot screen diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index a16862d465..d81c014c2d 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -11,6 +11,12 @@ This editor allows you to inspect or modify almost anything in DF. Press If you just want to browse without fear of accidentally changing anything, hit :kbd:`Ctrl`:kbd:`D` to toggle read-only mode. +Click on fields to edit them or, for structured fields, to inspect their +contents. Right click or hit :kbd:`Esc` to go back to the previous structure +you were inspecting. Right clicking when viewing the structure you started with +will exit the tool. Hold down :kbd:`Shift` and right click to exit, even if you +are inspecting a substructure. + Usage ----- diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index fa65a76280..544b159314 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -515,6 +515,9 @@ function GmEditorUi:onInput(keys) if GmEditorUi.super.onInput(self, keys) then return true end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if dfhack.internal.getModifiers().shift then + return false + end if self.subviews.pages:getSelected()==2 then self.subviews.pages:setSelected(1) else From 731059c5e69d82dfeedb690c4bbe72d8a682e1b4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 30 Aug 2023 03:32:09 -0700 Subject: [PATCH 474/732] clean up after emigrating citizens --- changelog.txt | 2 ++ emigration.lua | 32 +++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 68e2b3d29b..c1f4dbf475 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,8 @@ Template for new versions: - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly - `caravan`: Correct price adjustment values in trade agreement details screen - `caravan`: Apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices +- `emigration`: fix errors loading forts after dwarves assigned to work details or workshops have emigrated +- `emigration`: fix citizens sometimes "emigrating" to the fortress site - `suspendmanager`: Improve the detection on "T" and "+" shaped high walls - `starvingdead`: ensure sieges end properly when undead siegers starve - `fix/retrieve-units`: fix retrieved units sometimes becoming duplicated on the map diff --git a/emigration.lua b/emigration.lua index 4bad19ca94..742e1a147e 100644 --- a/emigration.lua +++ b/emigration.lua @@ -56,6 +56,34 @@ function desert(u,method,civ) dfhack.buildings.setOwner(temp_bld, nil) end + -- remove from workshop profiles + for _, bld in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do + for k, v in ipairs(bld.profile.permitted_workers) do + if v == u.id then + bld.profile.permitted_workers:erase(k) + break + end + end + end + for _, bld in ipairs(df.global.world.buildings.other.FURNACE_ANY) do + for k, v in ipairs(bld.profile.permitted_workers) do + if v == u.id then + bld.profile.permitted_workers:erase(k) + break + end + end + end + + -- disassociate from work details + for _, detail in ipairs(df.global.plotinfo.hauling.work_details) do + for k, v in ipairs(detail.assigned_units) do + if v == u.id then + detail.assigned_units:erase(k) + break + end + end + end + -- erase the unit from the fortress entity for k,v in ipairs(fort_ent.histfig_ids) do if v == hf_id then @@ -99,6 +127,7 @@ function desert(u,method,civ) -- try to find a new site for the unit to join for k,v in ipairs(df.global.world.entities.all[hf.civ_id].site_links) do + local site_id = df.global.plotinfo.site_id if v.type == df.entity_site_link_type.Claim and v.target ~= site_id then newsite_id = v.target break @@ -116,7 +145,7 @@ function desert(u,method,civ) df.global.world.history.events:insert("#", {new = df.history_event_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, hfid = hf_id, state = 1, reason = -1, site = newsite_id}) end end - print(line) + print(dfhack.df2console(line)) dfhack.gui.showAnnouncement(line, COLOR_WHITE) end @@ -210,6 +239,7 @@ if args[1] == "enable" then elseif args[1] == "disable" then enabled = false else + print('emigration is ' .. (enabled and 'enabled' or 'not enabled')) return end From f903536c986429971ad7db4d60e1f2c3098fc1c2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 31 Aug 2023 10:18:28 -0700 Subject: [PATCH 475/732] cancel TradeAtDepot jobs if all caravans are leaving --- caravan.lua | 22 +++++++++++++++++++++- changelog.txt | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/caravan.lua b/caravan.lua index 2d8d29f241..26d704b023 100644 --- a/caravan.lua +++ b/caravan.lua @@ -90,6 +90,26 @@ function commands.leave(...) for id, car in pairs(caravans_from_ids{...}) do car.trade_state = df.caravan_state.T_trade_state.Leaving end + local still_needs_broker = false + for _,car in ipairs(caravans) do + if car.trade_state == df.caravan_state.T_trade_state.Approaching or + car.trade_state == df.caravan_state.T_trade_state.AtDepot + then + still_needs_broker = true + break + end + end + if not still_needs_broker then + for _,depot in ipairs(df.global.world.buildings.other.TRADE_DEPOT) do + depot.trade_flags.trader_requested = false + for _, job in ipairs(depot.jobs) do + if job.job_type == df.job_type.TradeAtDepot then + dfhack.job.removeJob(job) + break + end + end + end + end end local function isDisconnectedPackAnimal(unit) @@ -114,7 +134,7 @@ end local function rejoin_pack_animals() print('Reconnecting disconnected pack animals...') local found = false - for _, unit in pairs(df.global.world.units.active) do + for _, unit in ipairs(df.global.world.units.active) do if unit.flags1.merchant and isDisconnectedPackAnimal(unit) then local dragger = unit.following print((' %s <-> %s'):format( diff --git a/changelog.txt b/changelog.txt index b4987a92f0..d94ae95c0d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,7 @@ Template for new versions: - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly - `caravan`: Correct price adjustment values in trade agreement details screen - `caravan`: Apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices +- `caravan`: cancel any active TradeAtDepot jobs if all caravans are instructed to leave - `emigration`: fix errors loading forts after dwarves assigned to work details or workshops have emigrated - `emigration`: fix citizens sometimes "emigrating" to the fortress site - `suspendmanager`: Improve the detection on "T" and "+" shaped high walls From 93bd315f198843bef8caa98fc68dea7400f03422 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 31 Aug 2023 11:12:59 -0700 Subject: [PATCH 476/732] cancel old dig jobs when tile designation is changed --- changelog.txt | 1 + internal/quickfort/dig.lua | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index b4987a92f0..625c58a340 100644 --- a/changelog.txt +++ b/changelog.txt @@ -43,6 +43,7 @@ Template for new versions: - `suspendmanager`: Improve the detection on "T" and "+" shaped high walls - `starvingdead`: ensure sieges end properly when undead siegers starve - `fix/retrieve-units`: fix retrieved units sometimes becoming duplicated on the map +- `quickfort`: cancel old dig jobs that point to a tile when a new designation is applied to the tile - `gui/launcher`, `gui/gm-editor`: recover gracefully when the saved frame position is now offscreen ## Misc Improvements diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index a307986739..4aa7e33d39 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -109,8 +109,6 @@ local function clear_designation(flags, occupancy) occupancy.carve_track_west = 0 end -local values = nil - local values_run = { dig_default=df.tile_dig_designation.Default, dig_channel=df.tile_dig_designation.Channel, @@ -164,6 +162,8 @@ local values_undo = { traffic_restricted=0, } +local values = values_run + -- these functions return a function if a designation needs to be made; else nil local function do_mine(digctx) if digctx.on_map_edge then return nil end @@ -731,9 +731,20 @@ local function get_track_direction(x, y, width, height) return {north=north, east=east, south=south, west=west} end +local function get_dig_job_map() + local job_map = {} + for _, job in utils.listpairs(df.global.world.jobs.list) do + if df.job_type.attrs[job.job_type].is_designation then + ensure_keys(job_map, job.pos.z, job.pos.y)[job.pos.x] = job + end + end + return job_map +end + local function do_run_impl(zlevel, grid, ctx) local stats = ctx.stats ctx.bounds = ctx.bounds or quickfort_map.MapBoundsChecker{} + local job_map = get_dig_job_map() for y, row in pairs(grid) do for x, cell_and_text in pairs(row) do local cell, text = cell_and_text.cell, cell_and_text.text @@ -820,7 +831,14 @@ local function do_run_impl(zlevel, grid, ctx) stats.dig_protected_engraving.value = stats.dig_protected_engraving.value + 1 else - if not ctx.dry_run then action_fn() end + if not ctx.dry_run then + local existing_dig_job = safe_index(job_map, extent_pos.z, extent_pos.y, extent_pos.x) + if existing_dig_job then + print(('removing existing job at %d, %d, %d'):format(extent_pos.x, extent_pos.y, extent_pos.z)) + dfhack.job.removeJob(existing_dig_job) + end + action_fn() + end stats.dig_designated.value = stats.dig_designated.value + 1 end From b2610819a69569fbfa8389c38fe4f5e2e5b9fcc8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 31 Aug 2023 11:25:26 -0700 Subject: [PATCH 477/732] adapt gui/overlay to new "function" frames --- gui/overlay.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/overlay.lua b/gui/overlay.lua index f5d33c0244..017c66738c 100644 --- a/gui/overlay.lua +++ b/gui/overlay.lua @@ -2,7 +2,6 @@ --@ module = true local gui = require('gui') -local guidm = require('gui.dwarfmode') local widgets = require('gui.widgets') local overlay = require('plugins.overlay') @@ -11,7 +10,7 @@ local DIALOG_WIDTH = 59 local LIST_HEIGHT = 14 local HIGHLIGHT_TILE = df.global.init.load_bar_texpos[1] -local SHADOW_FRAME = copyall(gui.PANEL_FRAME) +local SHADOW_FRAME = gui.PANEL_FRAME() SHADOW_FRAME.signature_pen = false local to_pen = dfhack.pen.parse From 143098d3bd04676f31addcb542ba39efaae3f2f9 Mon Sep 17 00:00:00 2001 From: shevernitskiy Date: Fri, 1 Sep 2023 18:22:37 +0300 Subject: [PATCH 478/732] use reserved range for tileset --- unsuspend.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsuspend.lua b/unsuspend.lua index 43d41d1adc..2f38fe1252 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -12,7 +12,7 @@ if not ok then buildingplan = nil end -local textures = dfhack.textures.loadTileset('hack/data/art/unsuspend.png', 32, 32) +local textures = dfhack.textures.loadTileset('hack/data/art/unsuspend.png', 32, 32, true) SuspendOverlay = defclass(SuspendOverlay, overlay.OverlayWidget) SuspendOverlay.ATTRS{ From 59dc3bac9bff84667877ab8f61adbb77f3324418 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 1 Sep 2023 18:10:26 -0700 Subject: [PATCH 479/732] mark set-orientation as available --- changelog.txt | 1 + docs/set-orientation.rst | 2 +- set-orientation.lua | 34 ++-------------------------------- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5644840043..0e0a118a0a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,7 @@ Template for new versions: - `devel/scan-vtables`: Scan and dump likely vtable addresses (for memory research) - `hide-interface`: hide the vanilla UI elements for clean screenshots or laid-back fortress observing - `hide-tutorials`: hide the DF tutorial popups; enable in the System tab of `gui/control-panel` +- `set-orientation`: tinker with romantic inclinations (reinstated from back catalog of tools) ## New Features - `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! diff --git a/docs/set-orientation.rst b/docs/set-orientation.rst index c1c549252a..9244627d7a 100644 --- a/docs/set-orientation.rst +++ b/docs/set-orientation.rst @@ -3,7 +3,7 @@ set-orientation .. dfhack-tool:: :summary: Alter a unit's romantic inclinations. - :tags: unavailable fort armok units + :tags: fort armok units This tool lets you tinker with the interest levels your dwarves have towards dwarves of the same/different sex. diff --git a/set-orientation.lua b/set-orientation.lua index 05eedd683f..19583f7913 100644 --- a/set-orientation.lua +++ b/set-orientation.lua @@ -1,34 +1,4 @@ --- Edit a unit's orientation --- Not to be confused with kane_t's script of the same name --@ module = true -local help = [====[ - -set-orientation -=============== -Edit a unit's orientation. -Interest levels are 0 for Uninterested, 1 for Romance, 2 for Marry. - -:unit : - The given unit will be affected. - If not found/provided, the script will try defaulting to the currently selected unit. -:male : - Set the interest level towards male sexes -:female : - Set the interest level towards female sexes -:opposite : - Set the interest level towards the opposite sex to the unit -:same : - Set the interest level towards the same sex as the unit -:random: - Randomise the unit's interest towards both sexes, respecting their ORIENTATION token odds. - -Other arguments: - -:help: - Shows this help page. -:view: - Print the unit's orientation values in the console. -]====] local utils = require 'utils' @@ -48,7 +18,7 @@ rng = rng or dfhack.random.new(nil, 10) -- General function used for rolling weighted tables function weightedRoll(weightedTable) local maxWeight = 0 - for index, result in ipairs(weightedTable) do + for _, result in ipairs(weightedTable) do maxWeight = maxWeight + result.weight end @@ -224,7 +194,7 @@ function main(...) -- Help if args.help then - print(help) + print(dfhack.script_help()) return end From 575302e7ee990d73dcd21290d19e4032cb54543d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 1 Sep 2023 18:26:21 -0700 Subject: [PATCH 480/732] don't conflict with default keybindings --- changelog.txt | 1 + gui/design.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 5644840043..77ca3dcd73 100644 --- a/changelog.txt +++ b/changelog.txt @@ -54,6 +54,7 @@ Template for new versions: - `suspendmanager`: display a different color for jobs suspended by suspendmanager - `caravan`: optionally display items within bins in bring goods to depot screen - `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates +- `gui/design`: change "auto commit" hotkey from ``c`` to ``Alt-c`` to avoid conflict with the default keybinding for z-level down ## Removed diff --git a/gui/design.lua b/gui/design.lua index 7affcac542..66ef48ddb3 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -895,7 +895,7 @@ function GenericOptionsPanel:init() }, widgets.ToggleHotkeyLabel { view_id = "autocommit_designation_label", - key = "CUSTOM_C", + key = "CUSTOM_ALT_C", label = "Auto-Commit: ", active = true, enabled = function() return self.design_panel.shape.max_points end, From 20b52399d8050a92f6e4273d8b7454533c7e5252 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 2 Sep 2023 02:38:35 -0700 Subject: [PATCH 481/732] close dialog with ctrl-D instead of left arrow --- changelog.txt | 1 + gui/quickfort.lua | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0b3a41b2b2..3c5523ce7c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -52,6 +52,7 @@ Template for new versions: - `devel/lsmem`: added support for filtering by memory addresses and filenames - `gui/gm-editor`: hold down shift and right click to exit, regardless of how many substructures deep you are - `quickfort`: linked stockpiles and workshops can now be specified by ID instead of only by name. this is mostly useful when dynamically generating blueprints and applying them via the `quickfort` API +- `gui/quickfort`: blueprint details screen can now be closed with Ctrl-D (the same hotkey used to open the details) - `suspendmanager`: display a different color for jobs suspended by suspendmanager - `caravan`: optionally display items within bins in bring goods to depot screen - `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 84a75eed45..13014a2c75 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -46,12 +46,12 @@ BlueprintDetails.ATTRS{ -- adds hint about left arrow being a valid "exit" key for this dialog function BlueprintDetails:onRenderFrame(dc, rect) BlueprintDetails.super.onRenderFrame(self, dc, rect) - dc:seek(rect.x1+2, rect.y2):string('Left arrow', dc.cur_key_pen): + dc:seek(rect.x1+2, rect.y2):string('Ctrl+D', dc.cur_key_pen): string(': Back', COLOR_GREY) end function BlueprintDetails:onInput(keys) - if keys.KEYBOARD_CURSOR_LEFT or keys.SELECT + if keys.CUSTOM_CTRL_D or keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R_DOWN then self:dismiss() end @@ -367,9 +367,7 @@ function Quickfort:init() end function Quickfort:get_summary_label() - if self.mode == 'config' then - return 'Blueprint configures game, not map.' - elseif self.mode == 'notes' then + if self.mode == 'notes' then return 'Blueprint shows help text.' end return 'Reposition with the mouse.' From a9649237c8a90550486c6cce64b6226a3285b27b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 2 Sep 2023 03:25:08 -0700 Subject: [PATCH 482/732] clarify error message when a mod cannot be found --- gui/mod-manager.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index ace0c39b88..08e9ae5b03 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -171,7 +171,9 @@ local function load_preset(idx) for _, v in ipairs(failures) do failures_str = failures_str .. v .. "\n" end - dialogs.showMessage("Warning", "Failed to load some mods", COLOR_LIGHTRED) + dialogs.showMessage("Warning", + "Failed to load some mods. Please re-create your default preset.", + COLOR_LIGHTRED) end end From a7424436aa4ff766cb8502c29aa60d4bdc7fb7cd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 2 Sep 2023 04:00:16 -0700 Subject: [PATCH 483/732] protect against out of range vector access in modded games where there are invalid plant indices in the Wood vector --- changelog.txt | 1 + gui/sandbox.lua | 2 ++ 2 files changed, 3 insertions(+) diff --git a/changelog.txt b/changelog.txt index 3c5523ce7c..41701c8788 100644 --- a/changelog.txt +++ b/changelog.txt @@ -47,6 +47,7 @@ Template for new versions: - `fix/retrieve-units`: fix retrieved units sometimes becoming duplicated on the map - `quickfort`: cancel old dig jobs that point to a tile when a new designation is applied to the tile - `gui/launcher`, `gui/gm-editor`: recover gracefully when the saved frame position is now offscreen +- `gui/sandbox`: correctly load equipment materials in modded games that categorize non-wood plants as wood ## Misc Improvements - `devel/lsmem`: added support for filtering by memory addresses and filenames diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 01903da6b6..ff74ceb31a 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -317,12 +317,14 @@ local function scan_organic(cat, vec, start_idx, base, do_insert) local indexes = MAT_TABLE.organic_indexes[cat] for idx = start_idx,#indexes-1 do local matindex = indexes[idx] + if #vec <= matindex then goto continue end local organic = vec[matindex] for offset, mat in ipairs(organic.material) do if do_insert(mat, base + offset, matindex) then return matindex end end + ::continue:: end return 0 end From abd57e0f0ecfaa474884fcbee9a9ed23b5cb36d6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 3 Sep 2023 07:47:29 -0700 Subject: [PATCH 484/732] reword preferences label and help text --- gui/control-panel.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 2bbfe9c3c8..24689c0c6b 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -73,9 +73,9 @@ table.sort(SYSTEM_SERVICES) local PREFERENCES = { ['dfhack']={ - HIDE_CONSOLE_ON_STARTUP={label='Hide console on startup', type='bool', default=true, + HIDE_CONSOLE_ON_STARTUP={label='Hide console on startup (MS Windows only)', type='bool', default=true, desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, - HIDE_ARMOK_TOOLS={label='Hide "armok" tools in command lists', type='bool', default=false, + HIDE_ARMOK_TOOLS={label='Mortal mode: hide "armok" tools', type='bool', default=false, desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.'}, }, ['gui']={ @@ -98,7 +98,7 @@ local CPP_PREFERENCES = { label='Prevent duplicate key events', type='bool', default=true, - desc='Whether to pass key events through to DF when DFHack keybindings are triggered.', + desc='Whether to additionally pass key events through to DF when DFHack keybindings are triggered.', init_fmt=':lua dfhack.internal.setSuppressDuplicateKeyboardEvents(%s)', get_fn=dfhack.internal.getSuppressDuplicateKeyboardEvents, set_fn=dfhack.internal.setSuppressDuplicateKeyboardEvents, From aa5adbb5077b4502c3dce9e178ea652d245e2059 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 3 Sep 2023 08:26:57 -0700 Subject: [PATCH 485/732] support removing river sources --- changelog.txt | 1 + gui/liquids.lua | 49 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index 41701c8788..52534e826a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: - `caravan`: optionally display items within bins in bring goods to depot screen - `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates - `gui/design`: change "auto commit" hotkey from ``c`` to ``Alt-c`` to avoid conflict with the default keybinding for z-level down +- `gui/liquids`: support removing river sources by converting them into stone floors ## Removed diff --git a/gui/liquids.lua b/gui/liquids.lua index 8adb71d56a..4eda71242a 100644 --- a/gui/liquids.lua +++ b/gui/liquids.lua @@ -68,7 +68,7 @@ function SpawnLiquid:init() { label = "Magma", value = df.tile_liquid.Magma, pen = COLOR_RED }, { label = "River", value = df.tiletype.RiverSource, pen = COLOR_BLUE }, }, - initial_option = 0, + initial_option = df.tile_liquid.Water, on_change = function(new, _) self.type = new self.tile = SpawnLiquidCursor[new] @@ -84,10 +84,11 @@ function SpawnLiquid:init() { label = "Click", value = SpawnLiquidPaintMode.CLICK, pen = COLOR_WHITE }, { label = "Drag ", value = SpawnLiquidPaintMode.DRAG, pen = COLOR_WHITE }, }, - initial_option = 1, + initial_option = SpawnLiquidPaintMode.DRAG, on_change = function(new, _) self.paint_mode = new end, }, widgets.CycleHotkeyLabel{ + view_id = 'mode1', frame = {l = 18, b = 2}, label = 'Mode:', auto_width = true, @@ -98,9 +99,28 @@ function SpawnLiquid:init() { label = "Remove", value = SpawnLiquidMode.REMOVE, pen = COLOR_WHITE }, { label = "Clean ", value = SpawnLiquidMode.CLEAN, pen = COLOR_WHITE }, }, - initial_option = 1, - on_change = function(new, _) self.mode = new end, - disabled = function() return self.type == df.tiletype.RiverSource end + initial_option = SpawnLiquidMode.SET, + on_change = function(new, _) + self.mode = new + self.subviews.mode2:setOption(new) + end, + visible = function() return self.type ~= df.tiletype.RiverSource end + }, + widgets.CycleHotkeyLabel{ + view_id = 'mode2', + frame = {l = 18, b = 2}, + label = 'Mode:', + auto_width = true, + key = 'CUSTOM_X', + options = { + { label = "Set ", value = SpawnLiquidMode.SET, pen = COLOR_WHITE }, + { label = "Remove", value = SpawnLiquidMode.REMOVE, pen = COLOR_WHITE }, + }, + on_change = function(new, _) + self.mode = new + self.subviews.mode1:setOption(new) + end, + visible = function() return self.type == df.tiletype.RiverSource end }, } end @@ -143,9 +163,22 @@ function SpawnLiquid:spawn(pos) tile.water_salt = false tile.water_stagnant = false elseif self.type == df.tiletype.RiverSource then - map_block.tiletype[pos.x % 16][pos.y % 16] = df.tiletype.RiverSource - - liquids.spawnLiquid(pos, 7, df.tile_liquid.Water) + if self.mode == SpawnLiquidMode.REMOVE then + local commands = { + 'f', 'any', ';', + 'f', 'sp', 'river_source', ';', + 'p', 'any', ';', + 'p', 's', 'floor', ';', + 'p', 'sp', 'normal', ';', + 'p', 'm', 'stone', ';', + } + dfhack.run_command('tiletypes-command', table.unpack(commands)) + dfhack.run_command('tiletypes-here', '--quiet', ('--cursor=%d,%d,%d'):format(pos2xyz(pos))) + liquids.spawnLiquid(pos, 0, df.tile_liquid.Water) + else + map_block.tiletype[pos.x % 16][pos.y % 16] = df.tiletype.RiverSource + liquids.spawnLiquid(pos, 7, df.tile_liquid.Water) + end else liquids.spawnLiquid(pos, self:getLiquidLevel(pos), self.type) end From 214c433ca9c62d9431ea6f19f4a81b6e850f582c Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 3 Sep 2023 19:10:23 +0200 Subject: [PATCH 486/732] workorder-recheck.rst: add an `Overlay` section --- docs/workorder-recheck.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/workorder-recheck.rst b/docs/workorder-recheck.rst index 731ca5de96..9685823351 100644 --- a/docs/workorder-recheck.rst +++ b/docs/workorder-recheck.rst @@ -17,3 +17,9 @@ Usage :: workorder-recheck + +Overlay +------- + +The position of the "request re-check" text that appears when a workorder +conditions window is open is configurable via `gui/overlay`. From 44372a7c9e8b1f8f1e44e33b901b31e641b4d676 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 3 Sep 2023 16:48:27 -0700 Subject: [PATCH 487/732] separate search key into separate terms for move goods --- internal/caravan/common.lua | 14 +++++++++----- internal/caravan/movegoods.lua | 10 ++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 1b7cdfdcc3..88527c3a1f 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -13,12 +13,16 @@ local to_pen = dfhack.pen.parse SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} ALL_PEN = to_pen{ch='+', fg=COLOR_LIGHTGREEN} -function make_search_key(str) - local out = '' - for c in str:gmatch("[%w%s]") do - out = out .. c:lower() +function add_words(words, str) + for word in str:gmatch("[%w]+") do + table.insert(words, word:lower()) end - return out +end + +function make_search_key(str) + local words = {} + add_words(words, str) + return table.concat(words, ' ') end local function get_broker_skill() diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index c44ef2d9df..62858ee221 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -407,17 +407,11 @@ local function is_ethical_product(item, animal_ethics, wood_ethics) (not wood_ethics or not common.has_wood(item)) end -local function add_words(words, str) - for word in str:gmatch("[%w]+") do - table.insert(words, word:lower()) - end -end - local function make_bin_search_key(item, desc) local words = {} - add_words(words, desc) + common.add_words(words, desc) for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do - add_words(words, common.get_item_description(contained_item)) + common.add_words(words, common.get_item_description(contained_item)) end return table.concat(words, ' ') end From 5865b6c8d60f958b73d1d0cddec73f518b04663b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 3 Sep 2023 18:38:07 -0700 Subject: [PATCH 488/732] also check for secrets in scrolls --- changelog.txt | 1 + docs/necronomicon.rst | 8 +++++--- necronomicon.lua | 34 +++++++++++++++++++--------------- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/changelog.txt b/changelog.txt index 52534e826a..883567e534 100644 --- a/changelog.txt +++ b/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: - `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates - `gui/design`: change "auto commit" hotkey from ``c`` to ``Alt-c`` to avoid conflict with the default keybinding for z-level down - `gui/liquids`: support removing river sources by converting them into stone floors +- `necronomicon`: report on secrets of life and death contained in scrolls ## Removed diff --git a/docs/necronomicon.rst b/docs/necronomicon.rst index f64622380f..0e784443a0 100644 --- a/docs/necronomicon.rst +++ b/docs/necronomicon.rst @@ -6,8 +6,9 @@ necronomicon :tags: fort inspection productivity items Lists all books in the fortress that contain the secrets to life and death. -To find the books in fortress mode, go to the Written content submenu in Objects (O). -Tablets are not shown by default, because dwarves cannot read the secrets from a slab in fort mode. +To find the books in fortress mode, go to the Written content submenu in +Objects (O). Slabs are not shown by default since dwarves cannot read secrets +from a slab in fort mode. Usage ----- @@ -20,4 +21,5 @@ Options ------- ``-s``, ``--include-slabs`` - Also list slabs that contain the secrets of life and death. Note that dwarves cannot read the secrets from a slab in fort mode. + Also list slabs that contain the secrets of life and death. Note that + dwarves cannot read the secrets from a slab in fort mode. diff --git a/necronomicon.lua b/necronomicon.lua index 3b6d37d489..68ef8d083c 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -1,17 +1,16 @@ +-- lists books that contain secrets of life and death. -- Author: Ajhaa --- lists books that contain secrets of life and death -local utils = require("utils") local argparse = require("argparse") - function get_book_interactions(item) - local book_interactions = {} + local title, book_interactions = nil, {} for _, improvement in ipairs(item.improvements) do if improvement._type == df.itemimprovement_pagesst or improvement._type == df.itemimprovement_writingst then for _, content_id in ipairs(improvement.contents) do local written_content = df.written_content.find(content_id) + title = written_content.title for _, ref in ipairs (written_content.refs) do if ref._type == df.general_ref_interactionst then @@ -23,7 +22,7 @@ function get_book_interactions(item) end end - return book_interactions + return title, book_interactions end function check_slab_secrets(item) @@ -47,7 +46,7 @@ function print_interactions(interactions) for _, str in ipairs(interaction.str) do local _, e = string.find(str.value, "ADV_NAME") if e then - print("\t", string.sub(str.value, e + 2, #str.value - 1)) + print(" " .. string.sub(str.value, e + 2, #str.value - 1)) end end end @@ -55,23 +54,28 @@ end function necronomicon(include_slabs) if include_slabs then - print("SLABS:") + print("Slabs:") + print() for _, item in ipairs(df.global.world.items.other.SLAB) do if check_slab_secrets(item) then local artifact = get_item_artifact(item) local name = dfhack.TranslateName(artifact.name) - print(dfhack.df2console(name)) + print(" " .. dfhack.df2console(name)) end end print() end - print("BOOKS:") - for _, item in ipairs(df.global.world.items.other.BOOK) do - local interactions = get_book_interactions(item) + print("Books and Scrolls:") + print() + for _, vec in ipairs{df.global.world.items.other.BOOK, df.global.world.items.other.TOOL} do + for _, item in ipairs(vec) do + local title, interactions = get_book_interactions(item) - if next(interactions) ~= nil then - print(item.title) - print_interactions(interactions) + if next(interactions) ~= nil then + print(" " .. dfhack.df2console(title)) + print_interactions(interactions) + print() + end end end end @@ -91,5 +95,5 @@ if help or cmd == "help" then elseif cmd == nil or cmd == "" then necronomicon(include_slabs) else - print("necronomicon: Invalid argument \"" .. cmd .. "\"") + print(('necronomicon: Invalid argument: "%s"'):format(cmd)) end From 657f94c1f562abb8d2ad2247fd009c93da9ec9dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 04:05:51 +0000 Subject: [PATCH 489/732] [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.23.3 → 0.26.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.23.3...0.26.3) - [github.com/Lucas-C/pre-commit-hooks: v1.5.1 → v1.5.4](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.5.1...v1.5.4) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b9b1e7b84..a76c1f8a22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.23.3 + rev: 0.26.3 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.1 + rev: v1.5.4 hooks: - id: forbid-tabs exclude_types: From 9b6102d54de8296da5b418dea4aa96e5fb5a16d9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 5 Sep 2023 01:16:07 -0700 Subject: [PATCH 490/732] add note to makeown docs that it can fix DF bug 10921 --- changelog.txt | 3 +++ docs/makeown.rst | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/changelog.txt b/changelog.txt index 883567e534..41082f185f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -61,6 +61,9 @@ Template for new versions: - `gui/liquids`: support removing river sources by converting them into stone floors - `necronomicon`: report on secrets of life and death contained in scrolls +## Documentation +- `makeown`: note that ``makeown`` fixes DF bug 10921, where you request workers from your holdings but they come as merchants and are not functional citizens + ## Removed # 50.09-r2 diff --git a/docs/makeown.rst b/docs/makeown.rst index b47f78eb34..7e2a53ba8a 100644 --- a/docs/makeown.rst +++ b/docs/makeown.rst @@ -8,6 +8,11 @@ makeown Select a unit in the UI and run this tool to converts that unit to be a fortress citizen (if sentient). It also removes their foreign affiliation, if any. +This tool also fixes :bug:`10921`, where you request workers from your +holdings, but they come with the "Merchant" profession and are unable to +complete jobs in your fort. Select those units and run `makeown` to convert +them into functional citizens. + Usage ----- From 5407080e61e49f3cddc762ccbb7dfa124070363d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 6 Sep 2023 00:37:45 -0700 Subject: [PATCH 491/732] add --unban option and remove unused code also rewrite docs with more explanation about why the tool exists --- ban-cooking.lua | 265 +++++++++++-------------------------------- changelog.txt | 1 + docs/ban-cooking.rst | 42 +++++-- 3 files changed, 100 insertions(+), 208 deletions(-) diff --git a/ban-cooking.lua b/ban-cooking.lua index 65f7200291..eca6b63c57 100644 --- a/ban-cooking.lua +++ b/ban-cooking.lua @@ -2,63 +2,76 @@ -- based on ban-cooking.rb by Putnam: https://github.com/DFHack/scripts/pull/427/files -- Putnams work completed by TBSTeun -local options = {} local argparse = require('argparse') + local kitchen = df.global.plotinfo.kitchen -local already_banned = {} + +local options = {} +local banned = {} local count = 0 local function make_key(mat_type, mat_index, type, subtype) return ('%s:%s:%s:%s'):format(mat_type, mat_index, type, subtype) end -local function reaction_product_id_contains(reaction_product, str) - for _, s in ipairs(reaction_product.id) do - if s.value == str then - return true - end - end - - return false -end - local function ban_cooking(print_name, mat_type, mat_index, type, subtype) local key = make_key(mat_type, mat_index, type, subtype) - -- Skip adding a new entry further below, if the item is already banned. - if already_banned[key] then + -- Skip adding a new entry further below if there's nothing to do + if (banned[key] and not options.unban) or (not banned[key] and options.unban) then return end - -- The item hasn't already been banned, so we do that here by appending its values to the various arrays + -- The item hasn't already been (un)banned, so we do that here by appending/removing + -- its values to/from the various arrays count = count + 1 if options.verbose then - print(print_name .. ' has been banned!') + print(print_name .. ' has been ' .. (options.unban and 'un' or '') .. 'banned!') end - kitchen.mat_types:insert('#', mat_type) - kitchen.mat_indices:insert('#', mat_index) - kitchen.item_types:insert('#', type) - kitchen.item_subtypes:insert('#', subtype) - kitchen.exc_types:insert('#', df.kitchen_exc_type.Cook) - - already_banned[key] = {} - already_banned[key].mat_type = mat_type - already_banned[key].mat_index = mat_index - already_banned[key].type = type - already_banned[key].subtype = subtype + if options.unban then + for i, mtype in ipairs(kitchen.mat_types) do + if mtype == mat_type and + kitchen.mat_indices[i] == mat_index and + kitchen.item_types[i] == type and + kitchen.item_subtypes[i] == subtype and + kitchen.exc_types[i] == df.kitchen_exc_type.Cook + then + kitchen.mat_types:erase(i) + kitchen.mat_indices:erase(i) + kitchen.item_types:erase(i) + kitchen.item_subtypes:erase(i) + kitchen.exc_types:erase(i) + break + end + end + banned[key] = nil + else + kitchen.mat_types:insert('#', mat_type) + kitchen.mat_indices:insert('#', mat_index) + kitchen.item_types:insert('#', type) + kitchen.item_subtypes:insert('#', subtype) + kitchen.exc_types:insert('#', df.kitchen_exc_type.Cook) + banned[key] = { + mat_type=mat_type, + mat_index=mat_index, + type=type, + subtype=subtype, + } + end end --- Iterate over the elements of the kitchen.item_types list -for i = 0, #kitchen.item_types - 1 do - -- Check if the kitchen.exc_types[i] element is equal to :Cook - if kitchen.exc_types[i] == df.kitchen_exc_type.Cook then - -- Add a new element to the already_banned dictionary - already_banned_key = make_key(kitchen.mat_types[i], kitchen.mat_indices[i], kitchen.item_types[i], kitchen.item_subtypes[i]) - if not already_banned[already_banned_key] then - already_banned[already_banned_key] = {} - already_banned[already_banned_key].mat_type = kitchen.mat_types[i] - already_banned[already_banned_key].mat_index = kitchen.mat_indices[i] - already_banned[already_banned_key].type = kitchen.item_types[i] - already_banned[already_banned_key].subtype = kitchen.item_subtypes[i] +local function init_banned() + -- Iterate over the elements of the kitchen.item_types list + for i in ipairs(kitchen.item_types) do + if kitchen.exc_types[i] == df.kitchen_exc_type.Cook then + local key = make_key(kitchen.mat_types[i], kitchen.mat_indices[i], kitchen.item_types[i], kitchen.item_subtypes[i]) + if not banned[key] then + banned[key] = { + mat_type=kitchen.mat_types[i], + mat_index=kitchen.mat_indices[i], + type=kitchen.item_types[i], + subtype=kitchen.item_subtypes[i], + } + end end end end @@ -267,157 +280,9 @@ funcs.fruit = function() end end -funcs.show = function() - -- First put together a dictionary/hash table - local type_list = {} - local matinfo = nil - - -- cycle through all plants - for i, p in ipairs(df.global.world.raws.plants.all) do - -- The below three if statements initialize the dictionary/hash tables for their respective (cookable) plant/drink/seed - -- And yes, this will create and then overwrite an entry when there is no (cookable) plant/drink/seed item for a specific plant, - -- but since the -1 type and -1 index can't be added to the ban list, it's inconsequential to check for non-existent (cookable) plant/drink/seed items here - - -- need to create a key to reference values in the types_list dictionary. - key_basic_mat = p.material_defs.type["basic_mat"] .. p.material_defs.idx.basic_mat - if not(type_list[key_basic_mat]) then - -- Initialize the type_list dictionary with a new element - type_list[key_basic_mat] = {} - end - - -- need to create a key to reference values in the types_list dictionary. - key_drink = p.material_defs.type["drink"] .. p.material_defs.idx.drink - if not(type_list[key_drink]) then - type_list[key_drink] = {} - end - - -- need to create a key to reference values in the types_list dictionary. - key_seed = p.material_defs.type["seed"] .. p.material_defs.idx.seed - if not(type_list[key_seed]) then - type_list[key_seed] = {} - end - - type_list[key_basic_mat].text = p.name .. " basic" - -- basic materials for plants always appear to use the 'PLANT' item type tag - type_list[key_basic_mat]["type"] = "PLANT" - -- item subtype of 'PLANT' types appears to always be -1, as there is no growth array entry for the 'PLANT' - type_list[key_basic_mat]["subtype"] = -1 - - type_list[key_drink]["text"] = p.name .. " drink" - -- drink materials for plants always appear to use the 'DRINK' item type tag - type_list[key_drink]["type"] = "DRINK" - -- item subtype of 'Drink' types appears to always be -1, as there is no growth array entry for the 'Drink' - type_list[key_drink]["subtype"] = -1 - - type_list[key_seed]["text"] = p.name .. " seed" - -- drink materials for plants always appear to use the 'SEEDS' item type tag - type_list[key_seed]["type"] = "SEEDS" - -- item subtype of 'SEEDS' types appears to always be -1, as there is no growth array entry for the 'SEEDS' - type_list[key_seed]["subtype"] = -1 - - for r, g in ipairs(p.growths) do - matinfo = dfhack.matinfo.decode(g) - local m = matinfo.material - - if m.flags["EDIBLE_COOKED"] and m.flags["LEAF_MAT"] then - for j, s in ipairs(p.material) do - if m.id == s.id then - - -- need to create a key to reference values in the types_list dictionary. - local key_matinfo_type = matinfo.type .. i - - if not(type_list[key_matinfo_type]) then - type_list[key_matinfo_type] = {} - else - print('Key exists for ' .. p.name, type_list[key_matinfo_type]["text"], type_list[key_matinfo_type]["type"], j + matinfo.type) - end - - type_list[key_matinfo_type]["text"] = p.name .. " " .. m.id .. " growth" - -- item type for plant materials listed in the growths array appear to always use the 'PLANT_GROWTH' item type tag - type_list[key_matinfo_type]["type"] = "PLANT_GROWTH" - -- item subtype is equal to the array index of the cookable item in the growths table - type_list[key_matinfo_type]["subtype"] = r - end - end - end - end - end - -- cycle through all creatures - for i, c in ipairs(df.global.world.raws.creatures.all) do - for j, m in ipairs(c.material) do - -- need to create a key to reference values in the types_list dictionary. - local key_matinfo_type = j + matinfo.type .. i - - if m.reaction_product and m.reaction_product.id and reaction_product_id_contains(m.reaction_product, "CHEESE_MAT") then - if not(type_list[key_matinfo_type]) then - type_list[key_matinfo_type] = {} - end - - type_list[key_matinfo_type]["text"] = c.name[1] .. " milk" - -- item type for milk appears to use the 'LIQUID_MISC' tag - type_list[key_matinfo_type]["type"] = "LIQUID_MISC" - type_list[key_matinfo_type]["subtype"] = -1 - - end - - if m.reaction_product and m.reaction_product.id and reaction_product_id_contains(m.reaction_product, "SOAP_MAT") then - if not(type_list[key_matinfo_type]) then - type_list[key_matinfo_type] = {} - end - - type_list[key_matinfo_type]["text"] = c.name[1] .. " tallow" - -- item type for milk appears to use the 'GLOB' tag - type_list[key_matinfo_type]["type"] = "GLOB" - type_list[key_matinfo_type]["subtype"] = -1 - end - end - end - - local output = {} - - for i, b in pairs(already_banned) do - -- initialize our output string with the array entry position (largely stays the same for each item on successive runs, except when items are added/removed) - local cur = '' - -- initialize our key for accessing our stored items info - local key = b.mat_type .. b.mat_index - - -- It shouldn't be possible for there to not be a matching key entry by this point, but we'll be kinda safe here - if type_list[key] then - -- Add the item name to the first part of the string - cur = type_list[key]['text'] .. ' |type ' - - if type_list[key]['type'] == df.item_type[b.type] then - cur = cur .. 'match: ' .. type_list[key]['type'] - else - -- Aw crap. The item type we EXpected doesn't match up with the ACtual item type. - cur = cur .. "error: ex;" .. type_list[key]["type"] .. "/ac;" .. df.item_type[b.type] - end - - cur = cur .. "|subtype " - if type_list[key]["subtype"] == b.subtype then - -- item sub type is a match, so we print that it's a match, as well as the item subtype index number (-1 means there is no subtype for this item) - cur = cur .. "match: " .. type_list[key]["subtype"] - else - -- Something went wrong, and the EXpected item subtype index value doesn't match the ACtual index value - cur = cur .. "error: ex;" .. type_list[key]["subtype"] .. "/ac;" .. b.subtype - end - else - -- There's no entry for this item in our calculated list of cookable items. So, it's not a plant, alcohol, tallow, or milk. It's likely that it's a meat that has been banned. - cur = cur .. '|"' .. '[' .. b.mat_type .. ', ' .. b.mat_index .. ']' .. ' unknown banned material type (meat?) " ' .. '|item type: "' .. tostring(df.item_type[b.type]) .. '"|item subtype: "' .. tostring(b.subtype) - end - - table.insert(output, cur) - end - - table.sort(output, function(a, b) return a < b end) - - for k, v in pairs(output) do - print(k .. ": |" .. v) - end -end - -local commands = argparse.processArgsGetopt({...}, { +local classes = argparse.processArgsGetopt({...}, { {'h', 'help', handler=function() options.help = true end}, + {'u', 'unban', handler=function() options.unban = true end}, {'v', 'verbose', handler=function() options.verbose = true end}, }) @@ -426,18 +291,18 @@ if options.help == true then return end -if commands[1] == 'all' then - for func, _ in pairs(funcs) do - if func ~= 'show' then - funcs[func]() - end - end -end +init_banned() -for _, v in ipairs(commands) do - if funcs[v] then - funcs[v]() +if classes[1] == 'all' then + for _, func in pairs(funcs) do + func() + end +else + for _, v in ipairs(classes) do + if funcs[v] then + funcs[v]() + end end end -print('banned ' .. count .. ' items.') +print((options.unban and 'un' or '') .. 'banned ' .. count .. ' types.') diff --git a/changelog.txt b/changelog.txt index 41082f185f..844303bc48 100644 --- a/changelog.txt +++ b/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: - `gui/design`: change "auto commit" hotkey from ``c`` to ``Alt-c`` to avoid conflict with the default keybinding for z-level down - `gui/liquids`: support removing river sources by converting them into stone floors - `necronomicon`: report on secrets of life and death contained in scrolls +- `ban-cooking`: add ``--unban`` option for removing kitchen bans ## Documentation - `makeown`: note that ``makeown`` fixes DF bug 10921, where you request workers from your holdings but they come as merchants and are not functional citizens diff --git a/docs/ban-cooking.rst b/docs/ban-cooking.rst index afcf9fc86c..5ec01c5394 100644 --- a/docs/ban-cooking.rst +++ b/docs/ban-cooking.rst @@ -2,11 +2,33 @@ ban-cooking =========== .. dfhack-tool:: - :summary: Protect entire categories of ingredients from being cooked. + :summary: Protect useful items from being cooked. :tags: fort productivity items plants -This tool provides a far more convenient way to ban cooking categories of foods -than the native kitchen interface. +Some cookable ingredients have other important uses. For example, seeds can be +cooked, but if you cook them all, then your farmers will have nothing to plant +in the fields. Similarly, thread can be cooked, but if you do that, then your +weavers will have nothing to weave into cloth and your doctors will have +nothing to use for stitching up injured dwarves. + +If you open the Kitchen screen, you can select individual item types and choose +to ban them from cooking. To prevent all your booze from being cooked, for +example, you'd select the Booze tab and then click each of the visible types of +booze to prevent them from being cooked. Only types that you have in stock are +shown, so if you acquire a different type of booze in the future, you have to +come back to this screen and ban the new types. + +Instead of doing all that clicking, ``ban-cooking`` can ban entire classes of +items (e.g. all types of booze) in one go. It can even ban types that you don't +have in stock yet, so when you *do* get some in stock, they will already be +banned. It will never ban items that are only good for eating or cooking, like +meat or non-plantable nuts. It is usually a good idea to run +``ban-cooking all`` as one of your first actions in a new fort. + +If you want to re-enable cooking for a banned item type, you can go to the +Kitchen screen and un-ban whatever you like by clicking on the "cook" +icon. You can also un-ban an entire class of items with the +``ban-cooking --unban`` option. Usage ----- @@ -14,11 +36,12 @@ Usage :: ban-cooking [ ...] [] -Valid types are ``booze``, ``brew``, ``fruit``, ``honey``, ``milk``, ``mill``, -``oil``, ``seeds`` (i.e. non-tree plants with seeds), ``tallow``, and -``thread``. It is possible to include multiple types or all types in a single ban-cooking -call: ``ban-cooking oil tallow`` will ban both oil and tallow from cooking. -``ban-cooking all`` will ban all types from cooking. +Valid types are ``booze``, ``brew`` (brewable plants), ``fruit``, ``honey``, +``milk``, ``mill`` (millable plants), ``oil``, ``seeds`` (plantable seeds), +``tallow``, and ``thread``. It is possible to include multiple types or all +types in a single ban-cooking command: ``ban-cooking oil tallow`` will ban both +oil and tallow from cooking. ``ban-cooking all`` will ban all of the above +types. Examples:: @@ -31,5 +54,8 @@ Note that this exact command can be enabled via the ``Autostart`` tab of Options ------- +``-u``, ``--unban`` + Un-ban the indicated item types. + ``-v``, ``--verbose`` Print each ban as it happens. From 3c1e93dc52d41911eb6a3c6db32e30dcc4116e25 Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Wed, 6 Sep 2023 12:00:34 -0600 Subject: [PATCH 492/732] Update minimum default to correct value --- docs/autofish.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/autofish.rst b/docs/autofish.rst index 0b8758bb05..702c686379 100644 --- a/docs/autofish.rst +++ b/docs/autofish.rst @@ -26,7 +26,7 @@ Usage on hand in your fortress. Fishing will be disabled when the amount of fish goes above this value. - ``min`` (default: 50) controls the minimum fish you want before restarting + ``min`` (default: 75) controls the minimum fish you want before restarting fishing. Use ``--toggle-raw``(``-r``) (default: on) to toggle letting the script From fd60489e5d767b1ed78d04018856658ee1053f27 Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 6 Sep 2023 12:18:03 -0700 Subject: [PATCH 493/732] Update ban-cooking.rst Fix formatting --- docs/ban-cooking.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ban-cooking.rst b/docs/ban-cooking.rst index 5ec01c5394..d096f72a19 100644 --- a/docs/ban-cooking.rst +++ b/docs/ban-cooking.rst @@ -34,6 +34,7 @@ Usage ----- :: + ban-cooking [ ...] [] Valid types are ``booze``, ``brew`` (brewable plants), ``fruit``, ``honey``, From 628adeb0ed71567b8099214604340202aea3c60a Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:31:52 -0600 Subject: [PATCH 494/732] Additional formatting Broke out the parameters and options flags for easier readability --- docs/autofish.rst | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/autofish.rst b/docs/autofish.rst index 702c686379..dab089f5c8 100644 --- a/docs/autofish.rst +++ b/docs/autofish.rst @@ -12,6 +12,7 @@ collecting too many rotten fish. Usage ----- + ``enable autofish`` Enable the script ``disable autofish`` @@ -22,16 +23,23 @@ Usage ``autofish [min] []`` Change autofish settings. - ``max`` (default: 100) controls the maximum amount of fish you want to keep - on hand in your fortress. Fishing will be disabled when the amount of fish - goes above this value. +Positional Parameters +--------------------- + +``max`` + (default: 100) controls the maximum amount of fish you want to keep on hand + in your fortress. Fishing will be disabled when the amount of fish goes + above this value. + +``min`` + (default: 75) controls the minimum fish you want before restarting fishing. - ``min`` (default: 75) controls the minimum fish you want before restarting - fishing. +Options +------- - Use ``--toggle-raw``(``-r``) (default: on) to toggle letting the script - also count your raw fish as part of your quota. Use it a second time to - disable this. +``-r``, ``--toggle-raw`` + (default: on) to toggle letting the script also count your raw fish as part + of your quota. Use it a second time to disable this. Examples -------- From 16b77541abf43e14a7283f04bdce8964db6bba22 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 6 Sep 2023 13:37:11 -0700 Subject: [PATCH 495/732] bump changelog to 50.09-r3 --- changelog.txt | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/changelog.txt b/changelog.txt index 844303bc48..b8de21c835 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,7 +27,19 @@ Template for new versions: # Future ## New Tools -- `devel/scan-vtables`: Scan and dump likely vtable addresses (for memory research) + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 50.09-r3 + +## New Tools +- `devel/scan-vtables`: scan and dump likely vtable addresses (for memory research) - `hide-interface`: hide the vanilla UI elements for clean screenshots or laid-back fortress observing - `hide-tutorials`: hide the DF tutorial popups; enable in the System tab of `gui/control-panel` - `set-orientation`: tinker with romantic inclinations (reinstated from back catalog of tools) @@ -37,12 +49,12 @@ Template for new versions: ## Fixes - `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly -- `caravan`: Correct price adjustment values in trade agreement details screen -- `caravan`: Apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices +- `caravan`: correct price adjustment values in trade agreement details screen +- `caravan`: apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices - `caravan`: cancel any active TradeAtDepot jobs if all caravans are instructed to leave - `emigration`: fix errors loading forts after dwarves assigned to work details or workshops have emigrated - `emigration`: fix citizens sometimes "emigrating" to the fortress site -- `suspendmanager`: Improve the detection on "T" and "+" shaped high walls +- `suspendmanager`: improve the detection on "T" and "+" shaped high walls - `starvingdead`: ensure sieges end properly when undead siegers starve - `fix/retrieve-units`: fix retrieved units sometimes becoming duplicated on the map - `quickfort`: cancel old dig jobs that point to a tile when a new designation is applied to the tile @@ -59,13 +71,6 @@ Template for new versions: - `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates - `gui/design`: change "auto commit" hotkey from ``c`` to ``Alt-c`` to avoid conflict with the default keybinding for z-level down - `gui/liquids`: support removing river sources by converting them into stone floors -- `necronomicon`: report on secrets of life and death contained in scrolls -- `ban-cooking`: add ``--unban`` option for removing kitchen bans - -## Documentation -- `makeown`: note that ``makeown`` fixes DF bug 10921, where you request workers from your holdings but they come as merchants and are not functional citizens - -## Removed # 50.09-r2 From 4dfb75dd1b0df0af4fb9b5a04f5a2dbba19dff11 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 6 Sep 2023 15:31:40 -0700 Subject: [PATCH 496/732] don't match items in unit inventories --- internal/caravan/movegoods.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 62858ee221..96c8ae6417 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -328,11 +328,10 @@ local function is_tradeable_item(item, depot) end if item.flags.in_inventory then local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) - if gref then - local container = df.item.find(gref.item_id) - if container and not df.item_binst:is_instance(container) then - return false - end + if not gref then return false end + local container = df.item.find(gref.item_id) + if not container or not df.item_binst:is_instance(container) then + return false end end if item.flags.in_job then From 6b0718ae7437195fbc84259e3a348f3f7b609bb0 Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Wed, 6 Sep 2023 16:58:11 -0600 Subject: [PATCH 497/732] Make raw fish option a command line toggle. --- autofish.lua | 25 +++++++++++++++++-------- docs/autofish.rst | 6 +++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/autofish.lua b/autofish.lua index cdbf872e2c..f173a5a4b7 100644 --- a/autofish.lua +++ b/autofish.lua @@ -1,5 +1,5 @@ -- handles automatic fishing jobs to limit the number of fish the fortress keeps on hand --- autofish [enable | disable] [min] [--include-raw | -r] +-- autofish [enable | disable] [min] --@ enable=true --@ module=true @@ -206,10 +206,21 @@ if dfhack_flags and dfhack_flags.enable then args = {dfhack_flags.enable_state and "enable" or "disable"} end --- find flags in args: +-- lookup to convert arguments to bool values. +local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true, + ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false} + local positionals = argparse.processArgsGetopt(args, - {{"r", "toggle-raw", - handler=function() s_useRaw = not s_useRaw end} + {{"r", "raw", hasArg=true, + handler=function(optArg) + optArg=string.lower(optArg) + if toBool[optArg] ~= nil then + set_useRaw(toBool[optArg]) + else + qerror("Invalid argument to --raw \"".. optArg .."\". expected boolean") + return + end + end} }) load_state() @@ -226,8 +237,9 @@ elseif positionals[1] == "status" then print_status() return +-- positionals is an empty table if no positional arguments are set elseif positionals ~= nil then - -- positionals is a number? + -- check to see if passed args are numbers if positionals[1] and tonumber(positionals[1]) then -- assume we're changing setting: local newval = tonumber(positionals[1]) @@ -235,9 +247,6 @@ elseif positionals ~= nil then if not positionals[2] then set_minFish(math.floor(newval * 0.75)) end - else - -- invalid or no argument - return end if positionals[2] and tonumber(positionals[2]) then diff --git a/docs/autofish.rst b/docs/autofish.rst index dab089f5c8..c4a00bdc28 100644 --- a/docs/autofish.rst +++ b/docs/autofish.rst @@ -37,9 +37,9 @@ Positional Parameters Options ------- -``-r``, ``--toggle-raw`` - (default: on) to toggle letting the script also count your raw fish as part - of your quota. Use it a second time to disable this. +``r``, ``--raw `` + (default: on) Set whether or not raw fish should be counted in the running + total of fish in your fortress. Examples -------- From bab5b4c87159923149d7436dffa435a99483fc58 Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sat, 9 Sep 2023 12:45:26 -0600 Subject: [PATCH 498/732] Update autofish.lua Missed optional argument [] Co-authored-by: Myk --- autofish.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autofish.lua b/autofish.lua index f173a5a4b7..12d0f45fd6 100644 --- a/autofish.lua +++ b/autofish.lua @@ -1,5 +1,5 @@ -- handles automatic fishing jobs to limit the number of fish the fortress keeps on hand --- autofish [enable | disable] [min] +-- autofish [enable | disable] [min] [] --@ enable=true --@ module=true From 48bb812f5ee68c25c883f3990163310481f52a7d Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sat, 9 Sep 2023 12:45:55 -0600 Subject: [PATCH 499/732] Update docs/autofish.rst fix text formatting of command output Co-authored-by: Myk --- docs/autofish.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/autofish.rst b/docs/autofish.rst index c4a00bdc28..48d23bee81 100644 --- a/docs/autofish.rst +++ b/docs/autofish.rst @@ -37,7 +37,7 @@ Positional Parameters Options ------- -``r``, ``--raw `` +``r``, ``--raw (true | false)`` (default: on) Set whether or not raw fish should be counted in the running total of fish in your fortress. From 2006c7b9fd963c0e2a999151ce45bf9155c363f5 Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sat, 9 Sep 2023 12:46:16 -0600 Subject: [PATCH 500/732] Update docs/autofish.rst fix wording of command description Co-authored-by: Myk --- docs/autofish.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/autofish.rst b/docs/autofish.rst index 48d23bee81..25e39b82f5 100644 --- a/docs/autofish.rst +++ b/docs/autofish.rst @@ -38,7 +38,7 @@ Options ------- ``r``, ``--raw (true | false)`` - (default: on) Set whether or not raw fish should be counted in the running + (default: ``true``) Set whether or not raw fish should be counted in the running total of fish in your fortress. Examples From 068fb57744cbd2855ce6eb38ba6b7b10b13bf19e Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sat, 9 Sep 2023 12:49:35 -0600 Subject: [PATCH 501/732] remove return, qerror throws --- autofish.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/autofish.lua b/autofish.lua index 12d0f45fd6..aa11d72fe4 100644 --- a/autofish.lua +++ b/autofish.lua @@ -218,7 +218,6 @@ local positionals = argparse.processArgsGetopt(args, set_useRaw(toBool[optArg]) else qerror("Invalid argument to --raw \"".. optArg .."\". expected boolean") - return end end} }) From d9e6935c212eaefc93fd3c3a1827c706893ddb90 Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sat, 9 Sep 2023 12:52:27 -0600 Subject: [PATCH 502/732] forgot to update the example texts --- docs/autofish.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/autofish.rst b/docs/autofish.rst index 25e39b82f5..97c9f71ecf 100644 --- a/docs/autofish.rst +++ b/docs/autofish.rst @@ -46,7 +46,7 @@ Examples ``enable autofish`` Enables the script. -``autofish -r 150`` +``autofish 150 -r true`` Sets your maximum fish to 150, and enables counting raw fish. ``autofish 300 250`` Sets your maximum fish to 300 and minimum to 250. From 1bf1f8fbc12740e6e3696557deb6e75fa28aefec Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:43:29 -0600 Subject: [PATCH 503/732] add autofish changes to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index b8de21c835..af5d25f089 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- ``autofish``: changed ``--raw`` argument format to allow explicit setting ## Removed From 36bf786b180744dd4f1d10f2831bb8008cffa700 Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:46:23 -0600 Subject: [PATCH 504/732] Update changelog.txt Co-authored-by: Myk --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index af5d25f089..03dc537e9d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,7 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements -- ``autofish``: changed ``--raw`` argument format to allow explicit setting +- `autofish`: changed ``--raw`` argument format to allow explicit setting to on or off ## Removed From 472a0c4a79f558fea3f17d665b4301d7b492627e Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sun, 10 Sep 2023 00:21:31 -0600 Subject: [PATCH 505/732] update autofish to use argparse.boolean --- autofish.lua | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/autofish.lua b/autofish.lua index aa11d72fe4..d2e67e73e0 100644 --- a/autofish.lua +++ b/autofish.lua @@ -206,23 +206,16 @@ if dfhack_flags and dfhack_flags.enable then args = {dfhack_flags.enable_state and "enable" or "disable"} end --- lookup to convert arguments to bool values. -local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true, - ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false} - +-- handle options flags local positionals = argparse.processArgsGetopt(args, {{"r", "raw", hasArg=true, handler=function(optArg) - optArg=string.lower(optArg) - if toBool[optArg] ~= nil then - set_useRaw(toBool[optArg]) - else - qerror("Invalid argument to --raw \"".. optArg .."\". expected boolean") - end + return argparse.boolean(optArg, "raw") end} }) load_state() +-- handle the rest of the arguments if positionals[1] == "enable" then enabled = true From 5a89438bf81dbeb498bc47fe7695401fbd713f71 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 10 Sep 2023 01:09:07 -0700 Subject: [PATCH 506/732] trade non-liquid/powder goods inside of barrels and pots --- changelog.txt | 1 + internal/caravan/movegoods.lua | 56 +++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/changelog.txt b/changelog.txt index 03dc537e9d..0f0fcde242 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: ## Misc Improvements - `autofish`: changed ``--raw`` argument format to allow explicit setting to on or off +- `caravan`: move goods to depot screen can now see/search/trade items inside of barrels and pots ## Removed diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index 96c8ae6417..fa39d219b3 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -14,9 +14,10 @@ local widgets = require('gui.widgets') MoveGoods = defclass(MoveGoods, widgets.Window) MoveGoods.ATTRS { frame_title='Move goods to/from depot', - frame={w=84, h=46}, + frame={w=85, h=46}, resizable=true, resize_min={h=35}, + frame_inset={l=1, t=1, b=1, r=0}, pending_item_ids=DEFAULT_NIL, depot=DEFAULT_NIL, } @@ -177,7 +178,7 @@ function MoveGoods:init() on_change=function() self:refresh_list() end, }, widgets.Panel{ - frame={t=4, l=40, r=0, h=12}, + frame={t=4, l=40, r=1, h=12}, subviews=common.get_info_widgets(self, get_export_agreements(), self.predicate_context), }, widgets.Panel{ @@ -241,7 +242,7 @@ function MoveGoods:init() widgets.Label{ frame={l=0, b=4, h=1, r=0}, text={ - 'Total value of trade items:', + 'Total value of items marked for trade:', {gap=1, text=function() return common.obfuscate_value(self.value_pending) end}, }, @@ -266,9 +267,9 @@ function MoveGoods:init() on_change=function() self:refresh_list() end, }, widgets.ToggleHotkeyLabel{ - view_id='inside_bins', - frame={l=51, b=2, w=28}, - label='See inside bins:', + view_id='inside_containers', + frame={l=51, b=2, w=30}, + label='Inside containers:', key='CUSTOM_CTRL_I', options={ {label='Yes', value=true, pen=COLOR_GREEN}, @@ -309,6 +310,13 @@ function MoveGoods:refresh_list(sort_widget, sort_fn) list:setFilter(saved_filter) end +local function is_container(item) + return item and ( + df.item_binst:is_instance(item) or + item:isFoodStorage() + ) +end + local function is_tradeable_item(item, depot) if item.flags.hostile or item.flags.removed or @@ -329,8 +337,7 @@ local function is_tradeable_item(item, depot) if item.flags.in_inventory then local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) if not gref then return false end - local container = df.item.find(gref.item_id) - if not container or not df.item_binst:is_instance(container) then + if not is_container(df.item.find(gref.item_id)) or item:isLiquidPowder() then return false end end @@ -406,7 +413,7 @@ local function is_ethical_product(item, animal_ethics, wood_ethics) (not wood_ethics or not common.has_wood(item)) end -local function make_bin_search_key(item, desc) +local function make_container_search_key(item, desc) local words = {} common.add_words(words, desc) for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do @@ -415,15 +422,22 @@ local function make_bin_search_key(item, desc) return table.concat(words, ' ') end -local function get_cache_index(group_items, inside_bins) +local function get_cache_index(group_items, inside_containers) local val = 1 if group_items then val = val + 1 end - if inside_bins then val = val + 2 end + if inside_containers then val = val + 2 end return val end -function MoveGoods:cache_choices(group_items, inside_bins) - local cache_idx = get_cache_index(group_items, inside_bins) +local function contains_non_liquid_powder(container) + for _, item in ipairs(dfhack.items.getContainedItems(container)) do + if not item:isLiquidPowder() then return true end + end + return false +end + +function MoveGoods:cache_choices(group_items, inside_containers) + local cache_idx = get_cache_index(group_items, inside_containers) if self.choices_cache[cache_idx] then return self.choices_cache[cache_idx] end local pending = self.pending_item_ids @@ -431,11 +445,9 @@ function MoveGoods:cache_choices(group_items, inside_bins) for _, item in ipairs(df.global.world.items.all) do local item_id = item.id if not item or not is_tradeable_item(item, self.depot) then goto continue end - if inside_bins and df.item_binst:is_instance(item) and - dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINS_ITEM) - then + if inside_containers and is_container(item) and contains_non_liquid_powder(item) then goto continue - elseif not inside_bins and item.flags.in_inventory then + elseif not inside_containers and item.flags.in_inventory then goto continue end local value = common.get_perceived_value(item) @@ -479,8 +491,8 @@ function MoveGoods:cache_choices(group_items, inside_bins) dirty=false, } local search_key - if not inside_bins and df.item_binst:is_instance(item) then - search_key = make_bin_search_key(item, desc) + if not inside_containers and is_container(item) then + search_key = make_container_search_key(item, desc) else search_key = common.make_search_key(desc) end @@ -510,14 +522,14 @@ function MoveGoods:cache_choices(group_items, inside_bins) self.value_pending = self.value_pending + (data.per_item_value * data.selected) end - self.choices_cache[get_cache_index(true, inside_bins)] = group_choices - self.choices_cache[get_cache_index(false, inside_bins)] = nogroup_choices + self.choices_cache[get_cache_index(true, inside_containers)] = group_choices + self.choices_cache[get_cache_index(false, inside_containers)] = nogroup_choices return self.choices_cache[cache_idx] end function MoveGoods:get_choices() local raw_choices = self:cache_choices(self.subviews.group_items:getOptionValue(), - self.subviews.inside_bins:getOptionValue()) + self.subviews.inside_containers:getOptionValue()) local choices = {} local include_forbidden = not self.subviews.hide_forbidden:getOptionValue() local banned = self.subviews.banned:getOptionValue() From 2b81eab62e0695faf54725914ccc974e6250a2c8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 10 Sep 2023 03:42:20 -0700 Subject: [PATCH 507/732] add missing tags for gui/design --- docs/gui/design.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 5bbbce4ecc..2c2e2feb8b 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -4,7 +4,7 @@ gui/design .. dfhack-tool:: :summary: Design designation utility with shapes. - + :tags: fort design productivity map This tool provides a point and click interface to make designating shapes and patterns easier. Supports both digging designations and placing constructions. From ea4e5602b66e5642dd147588d52971908b8b7899 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 10 Sep 2023 03:44:17 -0700 Subject: [PATCH 508/732] show tagged tools as autocomplete options when a tag is typed --- changelog.txt | 1 + gui/launcher.lua | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0f0fcde242..7491c9e391 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Misc Improvements - `autofish`: changed ``--raw`` argument format to allow explicit setting to on or off - `caravan`: move goods to depot screen can now see/search/trade items inside of barrels and pots +- `gui/launcher`: show tagged tools in the autocomplete list when a tag name is typed ## Removed diff --git a/gui/launcher.lua b/gui/launcher.lua index 72fb848a6a..cfb8d38efc 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -414,7 +414,7 @@ function HelpPanel:add_output(output) if text_len > SCROLLBACK_CHARS then text = text:sub(-SCROLLBACK_CHARS) local text_diff = text_len - #text - HelpPanel_update_label(label, label.text_to_wrap:sub(text_len - #text)) + HelpPanel_update_label(label, label.text_to_wrap:sub(text_diff)) text_height = label:getTextHeight() label:scroll('end') line_num = label.start_line_num @@ -699,30 +699,36 @@ local function add_top_related_entries(entries, entry, n) end function LauncherUI:update_autocomplete(firstword) + local includes = {{str=firstword, types='command'}} local excludes + if helpdb.is_tag(firstword) then + table.insert(includes, {tag=firstword, types='command'}) + end if not dev_mode then excludes = {tag={'dev', 'unavailable'}} - if dfhack.getHideArmokTools() then + if dfhack.getHideArmokTools() and firstword ~= 'armok' then table.insert(excludes.tag, 'armok') end end - local entries = helpdb.search_entries({str=firstword, types='command'}, excludes) + local entries = helpdb.search_entries(includes, excludes) -- if firstword is in the list, extract it so we can add it to the top later -- even if it's not in the list, add it back anyway if it's a valid db entry -- (e.g. if it's a dev script that we masked out) to show that it's a valid -- command - local found = extract_entry(entries,firstword) or helpdb.is_entry(firstword) + local found = extract_entry(entries, firstword) or helpdb.is_entry(firstword) sort_by_freq(entries) - if found then + if helpdb.is_tag(firstword) then + self.subviews.autocomplete_label:setText("Tagged tools") + elseif found then table.insert(entries, 1, firstword) - self.subviews.autocomplete_label:setText("Similar scripts") + self.subviews.autocomplete_label:setText("Similar tools") add_top_related_entries(entries, firstword, 20) else self.subviews.autocomplete_label:setText("Suggestions") end if #firstword == 0 then - self.subviews.autocomplete_label:setText("All scripts") + self.subviews.autocomplete_label:setText("All tools") end self.subviews.autocomplete:set_options(entries, found) From d4660e198c3c4c6dbf792a2ce3502bc7b8b35bef Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:47:23 -0600 Subject: [PATCH 509/732] fix a bug that caused raw flag to not be settable --- autofish.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autofish.lua b/autofish.lua index d2e67e73e0..b80c3d1b08 100644 --- a/autofish.lua +++ b/autofish.lua @@ -210,7 +210,8 @@ end local positionals = argparse.processArgsGetopt(args, {{"r", "raw", hasArg=true, handler=function(optArg) - return argparse.boolean(optArg, "raw") + local val = argparse.boolean(optArg, "raw") + set_useRaw(val) end} }) From 9324d299cf5369426ef97efd8f57d7aec6d80585 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 13 Sep 2023 23:50:10 -0700 Subject: [PATCH 510/732] fix gui/suspendmanager formatting and tags --- docs/gui/suspendmanager.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/gui/suspendmanager.rst b/docs/gui/suspendmanager.rst index 4940dc7c30..24b924b6a7 100644 --- a/docs/gui/suspendmanager.rst +++ b/docs/gui/suspendmanager.rst @@ -3,13 +3,13 @@ gui/suspendmanager .. dfhack-tool:: :summary: Intelligently suspend and unsuspend jobs. - + :tags: fort jobs This is the graphical configuration interface for the `suspendmanager` automation tool. Usage -===== +----- :: From 2ecd256e7a15fd9134e0a27fbcacae419cf94a23 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 13 Sep 2023 23:50:51 -0700 Subject: [PATCH 511/732] fix gui/autofish formatting --- docs/gui/autofish.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/autofish.rst b/docs/gui/autofish.rst index 739f137d7a..c216c55a16 100644 --- a/docs/gui/autofish.rst +++ b/docs/gui/autofish.rst @@ -11,7 +11,7 @@ should also count your raw fish. You can also check whether or not autofish is currently fishing or not. Usage -===== +----- :: From 0f457b56595f0e7eaba8300a6870af406b1154f6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 14 Sep 2023 12:37:08 -0700 Subject: [PATCH 512/732] update changelog for 50.09-r4 --- changelog.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7491c9e391..30e25f696f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -32,13 +32,17 @@ Template for new versions: ## Fixes +## Misc Improvements + +## Removed + +# 50.09-r4 + ## Misc Improvements - `autofish`: changed ``--raw`` argument format to allow explicit setting to on or off - `caravan`: move goods to depot screen can now see/search/trade items inside of barrels and pots - `gui/launcher`: show tagged tools in the autocomplete list when a tag name is typed -## Removed - # 50.09-r3 ## New Tools From 8016747ae999147c147ddd62c3b1d8a9a701b0b3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 16 Sep 2023 05:21:16 -0500 Subject: [PATCH 513/732] update fix/general-strike make less aggressive should close #3779 --- changelog.txt | 1 + fix/general-strike.lua | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 30e25f696f..08e8ff9f89 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- 'fix/general-strike: make less aggressive about trying to fix problems that don't exist yet ## Misc Improvements diff --git a/fix/general-strike.lua b/fix/general-strike.lua index 9807112b22..c3b3b10b66 100644 --- a/fix/general-strike.lua +++ b/fix/general-strike.lua @@ -6,10 +6,16 @@ local argparse = require('argparse') local function fix_seeds(quiet) local count = 0 for _,v in ipairs(df.global.world.items.other.SEEDS) do - if not v.flags.in_building then + if (not v.flags.in_job) and (not v.flags.in_building) then local bld = dfhack.items.getHolderBuilding(v) if bld and bld:isFarmPlot() then v.flags.in_building = true + for _,i in ipairs(bld.contained_items) do + print (('%d %d'):format(i.item.id, v.id)) + if i.item.id == v.id then + i.use_mode = 2 + end + end count = count + 1 end end From 9373e8547679767a7f7c7d138eeb405d4c7d3088 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 16 Sep 2023 05:22:30 -0500 Subject: [PATCH 514/732] remove errant debugging print --- fix/general-strike.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/fix/general-strike.lua b/fix/general-strike.lua index c3b3b10b66..5081251c2d 100644 --- a/fix/general-strike.lua +++ b/fix/general-strike.lua @@ -11,7 +11,6 @@ local function fix_seeds(quiet) if bld and bld:isFarmPlot() then v.flags.in_building = true for _,i in ipairs(bld.contained_items) do - print (('%d %d'):format(i.item.id, v.id)) if i.item.id == v.id then i.use_mode = 2 end From e0fdd68f8824a2af050bf4f71769a50d0dd3bfcb Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 16 Sep 2023 14:45:01 +0300 Subject: [PATCH 515/732] Added orders-reevaluate option to request manager orders conditions recheck once per month. --- gui/control-panel.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 24689c0c6b..eb3e67b05a 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -127,6 +127,9 @@ local REPEATS = { ['orders-sort']={ desc='Sort manager orders by repeat frequency so one-time orders can be completed.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, + ['orders-reevaluate']={ + desc='Invalidates manager orders making it necessary to recheck conditions.', + command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'reset', ']'}}, ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, From e0591830b72cdfaec5c9bdb1bf713a74fe744788 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 17 Sep 2023 23:51:58 -0700 Subject: [PATCH 516/732] bump changelog to 50.10-r1 --- changelog.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 08e8ff9f89..4970b5eac7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,12 +31,16 @@ Template for new versions: ## New Features ## Fixes -- 'fix/general-strike: make less aggressive about trying to fix problems that don't exist yet ## Misc Improvements ## Removed +# 50.10-r1 + +## Fixes +- 'fix/general-strike: fix issue where too many seeds were getting planted in farm plots + # 50.09-r4 ## Misc Improvements From c7456909bc0ed5c9d0248f1762921b09a57a3f87 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 18 Sep 2023 11:44:32 +0300 Subject: [PATCH 517/732] Added 'once a month' clarification to orders-reevaluate. --- gui/control-panel.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index eb3e67b05a..8a39294c50 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -128,7 +128,7 @@ local REPEATS = { desc='Sort manager orders by repeat frequency so one-time orders can be completed.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, ['orders-reevaluate']={ - desc='Invalidates manager orders making it necessary to recheck conditions.', + desc='Invalidates manager orders once a month making it necessary to recheck conditions.', command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'reset', ']'}}, ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', From bec158e26692100d8a3f77619ae769223d4b4d0e Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 18 Sep 2023 13:42:10 +0300 Subject: [PATCH 518/732] Changed 'reset' to 'recheck'. --- gui/control-panel.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 8a39294c50..bdc5a980cb 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -129,7 +129,7 @@ local REPEATS = { command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, ['orders-reevaluate']={ desc='Invalidates manager orders once a month making it necessary to recheck conditions.', - command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'reset', ']'}}, + command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, From d8acfef36d108bbea0f68a481f54769b6d7873bc Mon Sep 17 00:00:00 2001 From: Mikhail Panov Date: Fri, 22 Sep 2023 18:48:44 +0300 Subject: [PATCH 519/732] Removed workorder-recheck.lua. Work order manager enhancement branch moves it's code to orders.lua plugin. --- gui/control-panel.lua | 2 +- workorder-recheck.lua | 103 ------------------------------------------ 2 files changed, 1 insertion(+), 104 deletions(-) delete mode 100644 workorder-recheck.lua diff --git a/gui/control-panel.lua b/gui/control-panel.lua index bdc5a980cb..fb248e5cc4 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -128,7 +128,7 @@ local REPEATS = { desc='Sort manager orders by repeat frequency so one-time orders can be completed.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, ['orders-reevaluate']={ - desc='Invalidates manager orders once a month making it necessary to recheck conditions.', + desc='Invalidates work orders once a month forcing manager to recheck conditions.', command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', diff --git a/workorder-recheck.lua b/workorder-recheck.lua deleted file mode 100644 index 7115cdd09c..0000000000 --- a/workorder-recheck.lua +++ /dev/null @@ -1,103 +0,0 @@ --- Resets the selected work order to the `Checking` state - ---@ module = true - -local widgets = require('gui.widgets') -local overlay = require('plugins.overlay') - -local function set_current_inactive() - local scrConditions = df.global.game.main_interface.info.work_orders.conditions - if scrConditions.open then - local order = scrConditions.wq - order.status.active = false - else - qerror("Order conditions is not open") - end -end - -local function is_current_active() - local scrConditions = df.global.game.main_interface.info.work_orders.conditions - local order = scrConditions.wq - return order.status.active -end - --- ------------------- --- RecheckOverlay --- - -local focusString = 'dwarfmode/Info/WORK_ORDERS/Conditions' - -RecheckOverlay = defclass(RecheckOverlay, overlay.OverlayWidget) -RecheckOverlay.ATTRS{ - default_pos={x=6,y=8}, - default_enabled=true, - viewscreens=focusString, - -- width is the sum of lengths of `[` + `Ctrl+A` + `: ` + button.label + `]` - frame={w=1 + 6 + 2 + 16 + 1, h=3}, -} - -local function areTabsInTwoRows() - -- get the tile above the order status icon - local pen = dfhack.screen.readTile(7, 7, false) - -- in graphics mode, `0` when one row, something else when two (`67` aka 'C' from "Creatures") - -- in ASCII mode, `32` aka ' ' when one row, something else when two (`196` aka '-' from tab frame's top) - return (pen.ch ~= 0 and pen.ch ~= 32) -end - -function RecheckOverlay:updateTextButtonFrame() - local twoRows = areTabsInTwoRows() - if (self._twoRows == twoRows) then return false end - - self._twoRows = twoRows - local frame = twoRows - and {b=0, l=0, r=0, h=1} - or {t=0, l=0, r=0, h=1} - self.subviews.button.frame = frame - - return true -end - -function RecheckOverlay:init() - self:addviews{ - widgets.TextButton{ - view_id = 'button', - -- frame={t=0, l=0, r=0, h=1}, -- is set in `updateTextButtonFrame()` - label='request re-check', - key='CUSTOM_CTRL_A', - on_activate=set_current_inactive, - enabled=is_current_active, - }, - } - - self:updateTextButtonFrame() -end - -function RecheckOverlay:onRenderBody(dc) - if (self.frame_rect.y1 == 7) then - -- only apply this logic if the overlay is on the same row as - -- originally thought: just above the order status icon - - if self:updateTextButtonFrame() then - self:updateLayout() - end - end - - RecheckOverlay.super.onRenderBody(self, dc) -end - --- ------------------- - -OVERLAY_WIDGETS = { - recheck=RecheckOverlay, -} - -if dfhack_flags.module then - return -end - --- Check if on the correct screen and perform the action if so -if not dfhack.gui.matchFocusString(focusString) then - qerror('workorder-recheck must be run from the manager order conditions view') -end - -set_current_inactive() From 3fb36edc030f487a063243348bc306fe0a473134 Mon Sep 17 00:00:00 2001 From: Mikhail Panov Date: Fri, 22 Sep 2023 19:03:55 +0300 Subject: [PATCH 520/732] Removed workorder-recheck.rst. --- docs/workorder-recheck.rst | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 docs/workorder-recheck.rst diff --git a/docs/workorder-recheck.rst b/docs/workorder-recheck.rst deleted file mode 100644 index 9685823351..0000000000 --- a/docs/workorder-recheck.rst +++ /dev/null @@ -1,25 +0,0 @@ -workorder-recheck -================= - -.. dfhack-tool:: - :summary: Recheck start conditions for a manager workorder. - :tags: fort workorders - -Sets the status to ``Checking`` (from ``Active``) of the selected work order. -This makes the manager reevaluate its conditions. This is especially useful -for an order that had its conditions met when it was started, but the requisite -items have since disappeared and the workorder is now generating job cancellation -spam. - -Usage ------ - -:: - - workorder-recheck - -Overlay -------- - -The position of the "request re-check" text that appears when a workorder -conditions window is open is configurable via `gui/overlay`. From a10400f78fd8fdb600602ee9ac4bb1c536942915 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 24 Sep 2023 13:47:06 -0700 Subject: [PATCH 521/732] show total grid sizes --- changelog.txt | 1 + devel/inspect-screen.lua | 52 ++++++++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4970b5eac7..f4898dcd82 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `devel/inspect-screen`: display total grid size for UI and map layers ## Removed diff --git a/devel/inspect-screen.lua b/devel/inspect-screen.lua index 586166c398..3510cf0468 100644 --- a/devel/inspect-screen.lua +++ b/devel/inspect-screen.lua @@ -1,7 +1,7 @@ -- Read from the screen and display info about the tiles local gui = require('gui') -local utils = require('utils') +local guidm = require('gui.dwarfmode') local widgets = require('gui.widgets') local overlay = require('plugins.overlay') @@ -19,32 +19,42 @@ function Inspect:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text={'Current screen: ', {text=scr_name, pen=COLOR_CYAN}}}, + text={'Current screen: ', {text=scr_name, pen=COLOR_CYAN}}, + }, widgets.CycleHotkeyLabel{ view_id='layer', frame={t=2, l=0}, key='CUSTOM_CTRL_A', label='Inspect layer:', options={{label='UI', value='ui'}, 'map'}, - enabled=self:callback('is_unfrozen')}, + enabled=self:callback('is_unfrozen'), + }, widgets.CycleHotkeyLabel{ view_id='empties', frame={t=3, l=0}, key='CUSTOM_CTRL_E', label='Empty elements:', - options={'hide', 'show'}}, + options={'hide', 'show'}, + }, widgets.ToggleHotkeyLabel{ view_id='freeze', frame={t=4, l=0}, key='CUSTOM_CTRL_F', label='Freeze current tile:', - initial_option=false}, + initial_option=false, + }, widgets.Label{ frame={t=6}, - text={{text=self:callback('get_mouse_pos')}}}, + text={{text=self:callback('get_grid_size')}}, + }, + widgets.Label{ + frame={t=7}, + text={{text=self:callback('get_mouse_pos')}}, + }, widgets.Label{ view_id='report', - frame={t=8},}, + frame={t=9}, + }, } end @@ -56,6 +66,15 @@ function Inspect:do_refresh() return self:is_unfrozen() and not self:getMouseFramePos() end +function Inspect:get_grid_size() + if self.subviews.layer:getOptionValue() == 'ui' then + local width, height = dfhack.screen.getWindowSize() + return ('UI grid size: %d x %d'):format(width, height) + end + local layout = guidm.getPanelLayout() + return ('Map grid size: %d x %d'):format(layout.map.width, layout.map.height) +end + local cur_mouse_pos = {x=-1, y=-1} function Inspect:get_mouse_pos() local pos, text = cur_mouse_pos, '' @@ -294,13 +313,12 @@ local function get_map_report(show_empty) end function Inspect:onRenderBody() - if self:do_refresh() then - local show_empty = self.subviews.empties:getOptionValue() == 'show' - local report = self.subviews.layer:getOptionValue() == 'ui' and - get_ui_report(show_empty) or get_map_report(show_empty) - self.subviews.report:setText(report) - self:updateLayout() - end + if not self:do_refresh() then return end + local show_empty = self.subviews.empties:getOptionValue() == 'show' + local report = self.subviews.layer:getOptionValue() == 'ui' and + get_ui_report(show_empty) or get_map_report(show_empty) + self.subviews.report:setText(report) + self:updateLayout() end function Inspect:onInput(keys) @@ -313,18 +331,16 @@ function Inspect:onInput(keys) end end -InspectScreen = defclass(InspectScreen, gui.ZScreen) +InspectScreen = defclass(InspectScreen, gui.ZScreenModal) InspectScreen.ATTRS{ focus_string='inspect-screen', - force_pause=true, - pass_mouse_clicks=false, } function InspectScreen:init() -- prevent hotspot widgets from reacting overlay.register_trigger_lock_screen(self) - self:addviews{Inspect{view_id='main'}} + self:addviews{Inspect{}} end function InspectScreen:onDismiss() From f06180326506b8d2b9ee99d0f88dc299482fdf8a Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 15:25:16 +0100 Subject: [PATCH 522/732] suspendmanager now checks for unsupported tiles --- suspendmanager.lua | 209 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 208 insertions(+), 1 deletion(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 4f650f8b27..16c0d6927d 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -36,6 +36,7 @@ REASON = { ERASE_DESIGNATION = 4, --- Blocks a dead end (either a corridor or on top of a wall) DEADEND = 5, + UNSUPPORTED = 6, } REASON_TEXT = { @@ -44,6 +45,7 @@ REASON_TEXT = { [REASON.RISK_BLOCKING] = 'blocking', [REASON.ERASE_DESIGNATION] = 'designation', [REASON.DEADEND] = 'dead end', + [REASON.UNSUPPORTED] = 'unsupported', } --- Description of suspension @@ -52,7 +54,8 @@ REASON_TEXT = { REASON_DESCRIPTION = { [REASON.RISK_BLOCKING] = 'May block another build job', [REASON.ERASE_DESIGNATION] = 'Waiting for carve/smooth/engrave', - [REASON.DEADEND] = 'Blocks another build job' + [REASON.DEADEND] = 'Blocks another build job', + [REASON.UNSUPPORTED] = 'Construction is unsupported' } --- Suspension reasons from an external source @@ -161,6 +164,77 @@ local CONSTRUCTION_IMPASSABLE = utils.invert{ df.construction_type.Fortification, } +local CONSTRUCTION_WALL_SUPPORT = utils.invert{ + df.construction_type.Wall, + df.construction_type.Fortification, + df.construction_type.UpStair, + df.construction_type.UpDownStair, +} + +local CONSTRUCTION_FLOOR_SUPPORT = utils.invert{ + df.construction_type.FLOOR, + df.construction_type.DownStair, + df.construction_type.Ramp, + df.construction_type.TrackN, + df.construction_type.TrackS, + df.construction_type.TrackE, + df.construction_type.TrackW, + df.construction_type.TrackNS, + df.construction_type.TrackNE, + df.construction_type.TrackSE, + df.construction_type.TrackSW, + df.construction_type.TrackEW, + df.construction_type.TrackNSE, + df.construction_type.TrackNSW, + df.construction_type.TrackNEW, + df.construction_type.TrackSEW, + df.construction_type.TrackNSEW, + df.construction_type.TrackRampN, + df.construction_type.TrackRampS, + df.construction_type.TrackRampE, + df.construction_type.TrackRampW, + df.construction_type.TrackRampNS, + df.construction_type.TrackRampNE, + df.construction_type.TrackRampNW, + df.construction_type.TrackRampSE, + df.construction_type.TrackRampSW, + df.construction_type.TrackRampEW, + df.construction_type.TrackRampNSE, + df.construction_type.TrackRampNSW, + df.construction_type.TrackRampNEW, + df.construction_type.TrackRampSEW, + df.construction_type.TrackRampNSEW, +} + +-- all the tiletype shapes which provide support as if a wall +-- note that these shapes act as if there is a floor above them, +-- (including an up stair with no down stair above) which then connects +-- orthogonally at that level. +-- see: https://dwarffortresswiki.org/index.php/DF2014:Cave-in +local TILETYPE_SHAPE_WALL_SUPPORT = utils.invert{ + df.tiletype_shape.WALL, + df.tiletype_shape.FORTIFICATION, + df.tiletype_shape.STAIR_UP, + df.tiletype_shape.STAIR_UPDOWN, +} + +-- all the tiletype shapes which provide support as if it were a floor. +-- Tested as of v50.10 - YES, twigs do provide orthogonal support like a floor. +local TILETYPE_SHAPE_FLOOR_SUPPORT = utils.invert{ + df.tiletype_shape.FLOOR, + df.tiletype_shape.STAIR_DOWN, + df.tiletype_shape.RAMP, + df.tiletype_shape.BOULDER, + df.tiletype_shape.PEBBLES, + df.tiletype_shape.SAPLING, + df.tiletype_shape.BROOK_BED, + df.tiletype_shape.BROOK_TOP, + df.tiletype_shape.SHRUB, + df.tiletype_shape.TWIG, + df.tiletype_shape.BRANCH, + df.tiletype_shape.TRUNK_BRANCH, +} + local BUILDING_IMPASSABLE = utils.invert{ df.building_type.Floodgate, df.building_type.Statue, @@ -266,6 +340,134 @@ local function neighbours(pos) } end +--- list neighbour coordinates of pos which if is a Wall, will support a Wall at pos +---@param pos coord +---@return table +local function neighboursWallSupportsWall(pos) + return { + {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x+1, y=pos.y, z=pos.z}, + {x=pos.x, y=pos.y-1, z=pos.z}, + {x=pos.x, y=pos.y+1, z=pos.z}, + {x=pos.x-1, y=pos.y, z=pos.z-1}, + {x=pos.x+1, y=pos.y, z=pos.z-1}, + {x=pos.x, y=pos.y-1, z=pos.z-1}, + {x=pos.x, y=pos.y+1, z=pos.z-1}, + {x=pos.x-1, y=pos.y, z=pos.z+1}, + {x=pos.x+1, y=pos.y, z=pos.z+1}, + {x=pos.x, y=pos.y-1, z=pos.z+1}, + {x=pos.x, y=pos.y+1, z=pos.z+1}, + {x=pos.x, y=pos.y, z=pos.z-1}, + {x=pos.x, y=pos.y, z=pos.z+1}, + } +end + +--- list neighbour coordinates of pos which if is a Floor, will support a Wall at pos +---@param pos coord +---@return table +local function neighboursFloorSupportsWall(pos) + return { + {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x+1, y=pos.y, z=pos.z}, + {x=pos.x, y=pos.y-1, z=pos.z}, + {x=pos.x, y=pos.y+1, z=pos.z}, + {x=pos.x, y=pos.y, z=pos.z+1}, + {x=pos.x-1, y=pos.y, z=pos.z+1}, + {x=pos.x+1, y=pos.y, z=pos.z+1}, + {x=pos.x, y=pos.y-1, z=pos.z+1}, + {x=pos.x, y=pos.y+1, z=pos.z+1}, + } +end + +--- list neighbour coordinates of pos which if is a Wall, will support a Floor at pos +---@param pos coord +---@return table +local function neighboursWallSupportsFloor(pos) + return { + {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x+1, y=pos.y, z=pos.z}, + {x=pos.x, y=pos.y-1, z=pos.z}, + {x=pos.x, y=pos.y+1, z=pos.z}, + } +end + +--- list neighbour coordinates of pos which if is a Floor, will support a Floor at pos +---@param pos coord +---@return table +local function neighboursFloorSupportsFloor(pos) + return { + {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x+1, y=pos.y, z=pos.z}, + {x=pos.x, y=pos.y-1, z=pos.z}, + {x=pos.x, y=pos.y+1, z=pos.z}, + {x=pos.x, y=pos.y, z=pos.z+1}, + {x=pos.x-1, y=pos.y, z=pos.z+1}, + {x=pos.x+1, y=pos.y, z=pos.z+1}, + {x=pos.x, y=pos.y-1, z=pos.z+1}, + {x=pos.x, y=pos.y+1, z=pos.z+1}, + } +end + +local function tileHasSupportBuilding(pos) + local bld = dfhack.buildings.findAtTile(pos) + if bld then + return bld:getType() == df.building_type.Support and bld.flags.exists + end + return false +end + +--- +local function constructionIsUnsupported(job) + if job.job_type ~= df.job_type.ConstructBuilding then return false end + + local building = dfhack.job.getHolder(job) + if not building or building:getType() ~= df.building_type.Construction then return false end + + local pos = {x=building.centerx, y=building.centery,z=building.z} + + -- find out what type of construction + local constr_type = building:getSubtype() + if CONSTRUCTION_FLOOR_SUPPORT[constr_type] then + for _,n in pairs(neighboursWallSupportsFloor(pos)) do + local tt = dfhack.maps.getTileType(n) + if tt then + local attrs = df.tiletype.attrs[tt] + if TILETYPE_SHAPE_WALL_SUPPORT[attrs.shape] then return false end + end + end + for _,n in pairs(neighboursFloorSupportsFloor(pos)) do + local tt = dfhack.maps.getTileType(n) + if tt then + local attrs = df.tiletype.attrs[tt] + if TILETYPE_SHAPE_FLOOR_SUPPORT[attrs.shape] then return false end + end + end + -- check for a support building below the tile + if tileHasSupportBuilding({x=pos.x, y=pos.y, z=pos.z-1}) then return false end + return true + elseif CONSTRUCTION_WALL_SUPPORT[constr_type] then + for _,n in pairs(neighboursWallSupportsWall(pos)) do + local tt = dfhack.maps.getTileType(n) + if tt then + local attrs = df.tiletype.attrs[tt] + if TILETYPE_SHAPE_WALL_SUPPORT[attrs.shape] then return false end + end + end + for _,n in pairs(neighboursFloorSupportsWall(pos)) do + local tt = dfhack.maps.getTileType(n) + if tt then + local attrs = df.tiletype.attrs[tt] + if TILETYPE_SHAPE_FLOOR_SUPPORT[attrs.shape] then return false end + end + end + -- check for a support building below and above the tile + if tileHasSupportBuilding({x=pos.x, y=pos.y, z=pos.z-1}) then return false end + if tileHasSupportBuilding({x=pos.x, y=pos.y, z=pos.z+1}) then return false end + return true + end + return false +end + --- Get the amount of risk a tile is to be blocked --- -1: There is a nearby walkable area with no plan to build a wall --- >=0: Surrounded by either unwalkable tiles, or tiles that will be constructed @@ -433,6 +635,11 @@ function SuspendManager:refresh() end end + -- Check for construction jobs which may be unsupported + if constructionIsUnsupported(job) then + self.suspensions[job.id]=REASON.UNSUPPORTED + end + if not self.preventBlocking then goto continue end -- Internal reasons to suspend a job From f761683bebad7f77c1f50c64ea3e78f94ff48aa3 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 15:47:57 +0100 Subject: [PATCH 523/732] add suspendmanager unsupported condition improvement to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index f4898dcd82..5efb93672f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: ## Misc Improvements - `devel/inspect-screen`: display total grid size for UI and map layers +- `suspendmanager`: now suspends constructions that would cave-in immediately on completion ## Removed From e49605726d470f72916c3deea591b6e64d4f4e34 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 16:29:10 +0100 Subject: [PATCH 524/732] cleaned up the control flow of function constructionIsUnsupported(job) for reading clarity --- suspendmanager.lua | 76 ++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 16c0d6927d..bd26c7ee1b 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -408,6 +408,23 @@ local function neighboursFloorSupportsFloor(pos) } end +local function tileHasSupportWall(pos) + local tt = dfhack.maps.getTileType(pos) + if tt then + local attrs = df.tiletype.attrs[tt] + if TILETYPE_SHAPE_WALL_SUPPORT[attrs.shape] then return true end + end + return false +end + +local function tileHasSupportFloor(pos) + local tt = dfhack.maps.getTileType(pos) + if tt then + local attrs = df.tiletype.attrs[tt] + if TILETYPE_SHAPE_FLOOR_SUPPORT[attrs.shape] then return true end + end +end + local function tileHasSupportBuilding(pos) local bld = dfhack.buildings.findAtTile(pos) if bld then @@ -427,45 +444,32 @@ local function constructionIsUnsupported(job) -- find out what type of construction local constr_type = building:getSubtype() + local wall_would_support = {} + local floor_would_support = {} + local supportbld_would_support = {} + if CONSTRUCTION_FLOOR_SUPPORT[constr_type] then - for _,n in pairs(neighboursWallSupportsFloor(pos)) do - local tt = dfhack.maps.getTileType(n) - if tt then - local attrs = df.tiletype.attrs[tt] - if TILETYPE_SHAPE_WALL_SUPPORT[attrs.shape] then return false end - end - end - for _,n in pairs(neighboursFloorSupportsFloor(pos)) do - local tt = dfhack.maps.getTileType(n) - if tt then - local attrs = df.tiletype.attrs[tt] - if TILETYPE_SHAPE_FLOOR_SUPPORT[attrs.shape] then return false end - end - end - -- check for a support building below the tile - if tileHasSupportBuilding({x=pos.x, y=pos.y, z=pos.z-1}) then return false end - return true + wall_would_support = neighboursWallSupportsFloor(pos) + floor_would_support = neighboursFloorSupportsFloor(pos) + supportbld_would_support = {{x=pos.x, y=pos.y, z=pos.z-1}} elseif CONSTRUCTION_WALL_SUPPORT[constr_type] then - for _,n in pairs(neighboursWallSupportsWall(pos)) do - local tt = dfhack.maps.getTileType(n) - if tt then - local attrs = df.tiletype.attrs[tt] - if TILETYPE_SHAPE_WALL_SUPPORT[attrs.shape] then return false end - end - end - for _,n in pairs(neighboursFloorSupportsWall(pos)) do - local tt = dfhack.maps.getTileType(n) - if tt then - local attrs = df.tiletype.attrs[tt] - if TILETYPE_SHAPE_FLOOR_SUPPORT[attrs.shape] then return false end - end - end - -- check for a support building below and above the tile - if tileHasSupportBuilding({x=pos.x, y=pos.y, z=pos.z-1}) then return false end - if tileHasSupportBuilding({x=pos.x, y=pos.y, z=pos.z+1}) then return false end - return true + wall_would_support = neighboursWallSupportsWall(pos) + floor_would_support = neighboursFloorSupportsWall(pos) + supportbld_would_support = {{x=pos.x, y=pos.y, z=pos.z-1}, {x=pos.x, y=pos.y, z=pos.z+1}} + else return false -- some unknown construction - don't suspend end - return false + + for _,n in pairs(wall_would_support) do + if tileHasSupportWall(n) then return false end + end + for _,n in pairs(floor_would_support) do + if tileHasSupportFloor(n) then return false end + end + -- check for a support building below the tile + for _,n in pairs(supportbld_would_support) do + if tileHasSupportBuilding(n) then return false end + end + return true end --- Get the amount of risk a tile is to be blocked From 72f6b743bf6e41e35a0233e1b6a16db1460715d5 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 16:49:49 +0100 Subject: [PATCH 525/732] fixed typo df.construction_type.FLOOR -> ...Floor --- suspendmanager.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index bd26c7ee1b..f407ad0761 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -172,7 +172,7 @@ local CONSTRUCTION_WALL_SUPPORT = utils.invert{ } local CONSTRUCTION_FLOOR_SUPPORT = utils.invert{ - df.construction_type.FLOOR, + df.construction_type.Floor, df.construction_type.DownStair, df.construction_type.Ramp, df.construction_type.TrackN, @@ -447,7 +447,7 @@ local function constructionIsUnsupported(job) local wall_would_support = {} local floor_would_support = {} local supportbld_would_support = {} - + if CONSTRUCTION_FLOOR_SUPPORT[constr_type] then wall_would_support = neighboursWallSupportsFloor(pos) floor_would_support = neighboursFloorSupportsFloor(pos) From db0f4a3c79171d250f1b375a3e82d3e8e1adb9b0 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 17:04:33 +0100 Subject: [PATCH 526/732] SuspendManager:refresh() moved constructionIsUnsupported() check after self.preventBlocking check --- suspendmanager.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index f407ad0761..f33273ff43 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -639,10 +639,7 @@ function SuspendManager:refresh() end end - -- Check for construction jobs which may be unsupported - if constructionIsUnsupported(job) then - self.suspensions[job.id]=REASON.UNSUPPORTED - end + if not self.preventBlocking then goto continue end @@ -651,6 +648,11 @@ function SuspendManager:refresh() self.suspensions[job.id]=REASON.RISK_BLOCKING end + -- Check for construction jobs which may be unsupported + if constructionIsUnsupported(job) then + self.suspensions[job.id]=REASON.UNSUPPORTED + end + -- If this job is a dead end, mark jobs leading to it as dead end self:suspendDeadend(job) From 2f4a6e5f22771ccc239b7a24ddf876a78cf6141b Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 17:10:27 +0100 Subject: [PATCH 527/732] suspendmanager: added comment and updated doc --- docs/suspendmanager.rst | 2 ++ suspendmanager.lua | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/suspendmanager.rst b/docs/suspendmanager.rst index f36c8e3c9c..fc26461833 100644 --- a/docs/suspendmanager.rst +++ b/docs/suspendmanager.rst @@ -14,6 +14,8 @@ This tool will watch your active jobs and: - suspend construction jobs on top of a smoothing, engraving or track carving designation. This prevents the construction job from being completed first, which would erase the designation. +- suspend construction jobs that would cave in immediately on completion, + such as when building walls or floors next to grates/bars. Usage ----- diff --git a/suspendmanager.lua b/suspendmanager.lua index f33273ff43..458425b1d1 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -36,6 +36,7 @@ REASON = { ERASE_DESIGNATION = 4, --- Blocks a dead end (either a corridor or on top of a wall) DEADEND = 5, + --- Would cave in immediately on completion UNSUPPORTED = 6, } From 01481c6ec1273f054cce09c098e0d8765216d38b Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 17:26:39 +0100 Subject: [PATCH 528/732] fixed directions for supporting connections --- suspendmanager.lua | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 458425b1d1..a37cd28dee 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -346,20 +346,20 @@ end ---@return table local function neighboursWallSupportsWall(pos) return { - {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x-1, y=pos.y, z=pos.z}, -- orthogonal same level {x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}, - {x=pos.x-1, y=pos.y, z=pos.z-1}, + {x=pos.x-1, y=pos.y, z=pos.z-1}, -- orthogonal level below {x=pos.x+1, y=pos.y, z=pos.z-1}, {x=pos.x, y=pos.y-1, z=pos.z-1}, {x=pos.x, y=pos.y+1, z=pos.z-1}, - {x=pos.x-1, y=pos.y, z=pos.z+1}, + {x=pos.x-1, y=pos.y, z=pos.z+1}, -- orthogonal level above {x=pos.x+1, y=pos.y, z=pos.z+1}, {x=pos.x, y=pos.y-1, z=pos.z+1}, {x=pos.x, y=pos.y+1, z=pos.z+1}, - {x=pos.x, y=pos.y, z=pos.z-1}, - {x=pos.x, y=pos.y, z=pos.z+1}, + {x=pos.x, y=pos.y, z=pos.z-1}, -- directly below + {x=pos.x, y=pos.y, z=pos.z+1}, -- directly above } end @@ -368,12 +368,12 @@ end ---@return table local function neighboursFloorSupportsWall(pos) return { - {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x-1, y=pos.y, z=pos.z}, -- orthogonal same level {x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}, - {x=pos.x, y=pos.y, z=pos.z+1}, - {x=pos.x-1, y=pos.y, z=pos.z+1}, + {x=pos.x, y=pos.y, z=pos.z+1}, -- directly above + {x=pos.x-1, y=pos.y, z=pos.z+1}, --orthogonal level above {x=pos.x+1, y=pos.y, z=pos.z+1}, {x=pos.x, y=pos.y-1, z=pos.z+1}, {x=pos.x, y=pos.y+1, z=pos.z+1}, @@ -385,10 +385,15 @@ end ---@return table local function neighboursWallSupportsFloor(pos) return { - {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x-1, y=pos.y, z=pos.z}, -- orthogonal same level {x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}, + {x=pos.x-1, y=pos.y, z=pos.z-1}, -- orthogonal level below + {x=pos.x+1, y=pos.y, z=pos.z-1}, + {x=pos.x, y=pos.y-1, z=pos.z-1}, + {x=pos.x, y=pos.y+1, z=pos.z-1}, + {x=pos.x, y=pos.y, z=pos.z-1}, -- directly below } end @@ -397,15 +402,10 @@ end ---@return table local function neighboursFloorSupportsFloor(pos) return { - {x=pos.x-1, y=pos.y, z=pos.z}, + {x=pos.x-1, y=pos.y, z=pos.z}, -- orthogonal same level {x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}, - {x=pos.x, y=pos.y, z=pos.z+1}, - {x=pos.x-1, y=pos.y, z=pos.z+1}, - {x=pos.x+1, y=pos.y, z=pos.z+1}, - {x=pos.x, y=pos.y-1, z=pos.z+1}, - {x=pos.x, y=pos.y+1, z=pos.z+1}, } end From eab1120473c4844aeffc319c15388cc8c7bcdf6b Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:35:56 +0200 Subject: [PATCH 529/732] Restore the "kept suspended" color --- unsuspend.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsuspend.lua b/unsuspend.lua index 2f38fe1252..2a56f5dea9 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -154,7 +154,7 @@ function SuspendOverlay:render_marker(dc, bld, screen_pos) if buildingplan and buildingplan.isPlannedBuilding(bld) then color, ch, texpos = COLOR_GREEN, 'P', tp(4) elseif suspendmanager and suspendmanager.isKeptSuspended(job) then - color, ch, texpos = COLOR_WHITE, 'x', tp(1) + color, ch, texpos = COLOR_WHITE, 'x', tp(3) elseif data.suspend_count > 1 then color, ch, texpos = COLOR_RED, 'X', tp(1) end From 7c924218d3772b38f894a42da65a755155efacdb Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 18:06:21 +0100 Subject: [PATCH 530/732] constructionIsUnsupported() - early return if unreachable to reduce spam --- suspendmanager.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/suspendmanager.lua b/suspendmanager.lua index a37cd28dee..2de83aecec 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -409,6 +409,13 @@ local function neighboursFloorSupportsFloor(pos) } end +local function hasWalkableNeighbour(pos) + for _,n in pairs(neighbours(pos)) do + if (walkable(n)) then return true end + end + return false +end + local function tileHasSupportWall(pos) local tt = dfhack.maps.getTileType(pos) if tt then @@ -443,6 +450,10 @@ local function constructionIsUnsupported(job) local pos = {x=building.centerx, y=building.centery,z=building.z} + -- if no neighbour is walkable it can't be constructed now anyways, + -- this early return helps reduce "spam" + if not hasWalkableNeighbour(pos) then return false end + -- find out what type of construction local constr_type = building:getSubtype() local wall_would_support = {} From 4ac36ac4e8b5b796ae1ff5c54d2504e9d1b3ab4c Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 18:19:51 +0100 Subject: [PATCH 531/732] suspendmanager - clarified 'unsupported' reason description --- suspendmanager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 2de83aecec..f799e119f5 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -56,7 +56,7 @@ REASON_DESCRIPTION = { [REASON.RISK_BLOCKING] = 'May block another build job', [REASON.ERASE_DESIGNATION] = 'Waiting for carve/smooth/engrave', [REASON.DEADEND] = 'Blocks another build job', - [REASON.UNSUPPORTED] = 'Construction is unsupported' + [REASON.UNSUPPORTED] = 'Would collapse immediately' } --- Suspension reasons from an external source From 6d3bd57dbc7a2bebe9cb047f67354358eeab41e3 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Mon, 25 Sep 2023 19:11:04 +0100 Subject: [PATCH 532/732] reverted walkable neighbour early return --- suspendmanager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index f799e119f5..9f97fd4127 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -452,7 +452,7 @@ local function constructionIsUnsupported(job) -- if no neighbour is walkable it can't be constructed now anyways, -- this early return helps reduce "spam" - if not hasWalkableNeighbour(pos) then return false end + -- if not hasWalkableNeighbour(pos) then return false end -- commented out pending `walkable()` fix -- find out what type of construction local constr_type = building:getSubtype() From eb3b1fceb6380276a479359ebb51c061db4ec24b Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Tue, 26 Sep 2023 08:49:26 +0100 Subject: [PATCH 533/732] walkable() now correct and new function to ignore case where that tile is tree branch --- suspendmanager.lua | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/suspendmanager.lua b/suspendmanager.lua index 9f97fd4127..a3d751d349 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -308,25 +308,19 @@ end --- Check if the tile can be walked on ---@param pos coord local function walkable(pos) + return dfhack.maps.getTileBlock(pos).walkable[pos.x % 16][pos.y % 16] > 0 +end + +--- Check if the tile is suitable tile to stand on for construction (walkable & not a tree branch) +---@param pos coord +local function isSuitableAccess(pos) local tt = dfhack.maps.getTileType(pos) - if not tt then - return false - end local attrs = df.tiletype.attrs[tt] - if attrs.shape == df.tiletype_shape.BRANCH or attrs.shape == df.tiletype_shape.TRUNK_BRANCH then -- Branches can be walked on, but most of the time we can assume that it's not a suitable access. return false end - - local shape_attrs = df.tiletype_shape.attrs[attrs.shape] - - if not shape_attrs.walkable then - return false - end - - local building = dfhack.buildings.findAtTile(pos) - return not building or not building.flags.exists or not isImpassable(building) + return walkable(pos) end --- List neighbour coordinates of a position @@ -452,7 +446,7 @@ local function constructionIsUnsupported(job) -- if no neighbour is walkable it can't be constructed now anyways, -- this early return helps reduce "spam" - -- if not hasWalkableNeighbour(pos) then return false end -- commented out pending `walkable()` fix + if not hasWalkableNeighbour(pos) then return false end -- find out what type of construction local constr_type = building:getSubtype() @@ -516,7 +510,7 @@ local function riskBlocking(job) local pos = {x=building.centerx,y=building.centery,z=building.z} -- The construction is on a non walkable tile, it can't get worst - if not walkable(pos) then return false end + if not isSuitableAccess(pos) then return false end --- Get self risk of being blocked local risk = riskOfStuckConstructionAt(pos) @@ -544,7 +538,7 @@ function SuspendManager:suspendDeadend(start_job) ---@type building? local exit = nil for _,neighbourPos in pairs(neighbours(pos)) do - if not walkable(neighbourPos) then + if not isSuitableAccess(neighbourPos) then -- non walkable neighbour, not an exit goto continue end From 263cfcff29865965c09187b8e12f87e45429fcf5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 26 Sep 2023 03:56:40 -0700 Subject: [PATCH 534/732] clean up dump-offsets --- devel/dump-offsets.lua | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/devel/dump-offsets.lua b/devel/dump-offsets.lua index 0e3b0d2f0e..5f49ce1662 100644 --- a/devel/dump-offsets.lua +++ b/devel/dump-offsets.lua @@ -1,26 +1,5 @@ -- Dump all global addresses ---[====[ - -devel/dump-offsets -================== - -.. warning:: - - THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS. - - Running this script on a new DF version will NOT - MAKE IT RUN CORRECTLY if any data structures - changed, thus possibly leading to CRASHES AND/OR - PERMANENT SAVE CORRUPTION. - -This dumps the contents of the table of global addresses (new in 0.44.01). - -Passing global names as arguments calls setAddress() to set those globals' -addresses in-game. Passing "all" does this for all globals. - -]====] - GLOBALS = {} for k, v in pairs(df.global._fields) do GLOBALS[v.original_name] = k @@ -50,13 +29,11 @@ else search = {0x12345678, 0x87654321, 0x89abcdef} end -local addrs = {} function save_addr(name, addr) print((""):format(name, addr)) if iargs[name] or iargs.all then ms.found_offset(name, addr) end - addrs[name] = addr end local extended = false From 58e24f9bbc908050e6074d05794af58dbad96175 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 26 Sep 2023 04:03:55 -0700 Subject: [PATCH 535/732] clean up points code --- points.lua | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/points.lua b/points.lua index b7d2647039..40dd9c3ce7 100644 --- a/points.lua +++ b/points.lua @@ -1,20 +1,7 @@ --- Set available points at the embark screen --- http://www.bay12forums.com/smf/index.php?topic=135506.msg4925005#msg4925005 ---[====[ - -points -====== -Sets available points at the embark screen to the specified number. Eg. -``points 1000000`` would allow you to buy everything, or ``points 0`` would -make life quite difficult. - -]====] - if dfhack.isWorldLoaded() then df.global.world.worldgen.worldgen_parms.embark_points = tonumber(...) - local scr = dfhack.gui.getCurViewscreen() + local scr = dfhack.gui.getDFViewscreen() if df.viewscreen_setupdwarfgamest:is_instance(scr) then - local scr = scr --as:df.viewscreen_setupdwarfgamest scr.points_remaining = tonumber(...) end else From 572c3abc0b72eb1e7f05c01130e9a87f3adfaef8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 26 Sep 2023 04:05:30 -0700 Subject: [PATCH 536/732] adjust to new mouse event semantics --- devel/inspect-screen.lua | 2 +- exportlegends.lua | 4 ++-- gui/autochop.lua | 2 +- gui/autodump.lua | 4 ++-- gui/blueprint.lua | 4 ++-- gui/control-panel.lua | 8 ++++---- gui/design.lua | 6 +++--- gui/gm-editor.lua | 2 +- gui/gm-unit.lua | 2 +- gui/liquids.lua | 6 +++--- gui/mass-remove.lua | 4 ++-- gui/masspit.lua | 2 +- gui/overlay.lua | 4 ++-- gui/quickfort.lua | 6 +++--- gui/sandbox.lua | 12 ++++++------ gui/seedwatch.lua | 2 +- gui/unit-syndromes.lua | 2 +- hide-tutorials.lua | 2 +- internal/caravan/trade.lua | 6 +++--- internal/gm-unit/editor_body.lua | 2 +- test/gui/blueprint.lua | 2 +- 21 files changed, 42 insertions(+), 42 deletions(-) diff --git a/devel/inspect-screen.lua b/devel/inspect-screen.lua index 3510cf0468..36d42564be 100644 --- a/devel/inspect-screen.lua +++ b/devel/inspect-screen.lua @@ -325,7 +325,7 @@ function Inspect:onInput(keys) if Inspect.super.onInput(self, keys) then return true end - if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then + if keys._MOUSE_L and not self:getMouseFramePos() then self.subviews.freeze:cycle() return true end diff --git a/exportlegends.lua b/exportlegends.lua index 69633a6320..22373fd7a5 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -1070,7 +1070,7 @@ function LegendsOverlay:init() end function LegendsOverlay:onInput(keys) - if keys._MOUSE_L_DOWN and progress_percent < 0 and + if keys._MOUSE_L and progress_percent < 0 and self.subviews.button_mask:getMousePos() and self.subviews.do_export:getOptionValue() then @@ -1102,7 +1102,7 @@ end function DoneMaskOverlay:onInput(keys) if progress_percent >= 0 then - if keys.LEAVESCREEN or (keys._MOUSE_L_DOWN and self:getMousePos()) then + if keys.LEAVESCREEN or (keys._MOUSE_L and self:getMousePos()) then return true end end diff --git a/gui/autochop.lua b/gui/autochop.lua index 12122875c8..5fc6a858db 100644 --- a/gui/autochop.lua +++ b/gui/autochop.lua @@ -101,7 +101,7 @@ function BurrowSettings:commit() end function BurrowSettings:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then self:hide() return true end diff --git a/gui/autodump.lua b/gui/autodump.lua index a0ab71f686..bb81432b0b 100644 --- a/gui/autodump.lua +++ b/gui/autodump.lua @@ -267,11 +267,11 @@ end function Autodump:onInput(keys) if Autodump.super.onInput(self, keys) then return true end - if keys._MOUSE_R_DOWN and self.mark then + if keys._MOUSE_R and self.mark then self.mark = nil self:updateLayout() return true - elseif keys._MOUSE_L_DOWN then + elseif keys._MOUSE_L then if self:getMouseFramePos() then return true end local pos = dfhack.gui.getMousePos() if not pos then diff --git a/gui/blueprint.lua b/gui/blueprint.lua index 0335e1b19f..f05d4bada5 100644 --- a/gui/blueprint.lua +++ b/gui/blueprint.lua @@ -459,7 +459,7 @@ end function Blueprint:onInput(keys) if Blueprint.super.onInput(self, keys) then return true end - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then if self:is_setting_start_pos() then self.subviews.startpos.option_idx = 1 self.saved_cursor = nil @@ -474,7 +474,7 @@ function Blueprint:onInput(keys) end local pos = nil - if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then + if keys._MOUSE_L and not self:getMouseFramePos() then pos = dfhack.gui.getMousePos() if pos then guidm.setCursorPos(pos) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 24689c0c6b..bd7a49ee48 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -250,7 +250,7 @@ end function ConfigPanel:onInput(keys) local handled = ConfigPanel.super.onInput(self, keys) - if keys._MOUSE_L_DOWN then + if keys._MOUSE_L then local list = self.subviews.list.list local idx = list:getIdxUnderMouse() if idx then @@ -603,7 +603,7 @@ function IntegerInputDialog:onInput(keys) if keys.SELECT then self:hide(self.subviews.input_edit.text) return true - elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + elseif keys.LEAVESCREEN or keys._MOUSE_R then self:hide() return true end @@ -638,7 +638,7 @@ end function Preferences:onInput(keys) -- call grandparent's onInput since we don't want ConfigPanel's processing local handled = Preferences.super.super.onInput(self, keys) - if keys._MOUSE_L_DOWN then + if keys._MOUSE_L then local list = self.subviews.list.list local idx = list:getIdxUnderMouse() if idx then @@ -787,7 +787,7 @@ end function RepeatAutostart:onInput(keys) -- call grandparent's onInput since we don't want ConfigPanel's processing local handled = RepeatAutostart.super.super.onInput(self, keys) - if keys._MOUSE_L_DOWN then + if keys._MOUSE_L then local list = self.subviews.list.list local idx = list:getIdxUnderMouse() if idx then diff --git a/gui/design.lua b/gui/design.lua index 66ef48ddb3..a0a215d4ad 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1386,7 +1386,7 @@ function Design:onInput(keys) -- return -- end - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then -- Close help window if open if view.help_window.visible then self:dismiss_help() return true end @@ -1438,7 +1438,7 @@ function Design:onInput(keys) local pos = nil - if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then + if keys._MOUSE_L and not self:getMouseFramePos() then pos = getMousePoint() if not pos then return true end guidm.setCursorPos(dfhack.gui.getMousePos()) @@ -1446,7 +1446,7 @@ function Design:onInput(keys) pos = Point(guidm.getCursorPos()) end - if keys._MOUSE_L_DOWN and pos then + if keys._MOUSE_L and pos then -- TODO Refactor this a bit if self.shape.max_points and #self.marks == self.shape.max_points and self.placing_mark.active then self.marks[self.placing_mark.index] = pos diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 0814d84d83..380e9cdd3c 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -514,7 +514,7 @@ end function GmEditorUi:onInput(keys) if GmEditorUi.super.onInput(self, keys) then return true end - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then if dfhack.internal.getModifiers().shift then return false end diff --git a/gui/gm-unit.lua b/gui/gm-unit.lua index f37ec6f546..b83ed49cf9 100644 --- a/gui/gm-unit.lua +++ b/gui/gm-unit.lua @@ -147,7 +147,7 @@ end function Editor_Unit:onInput(keys) local pages = self.subviews.pages if pages:getSelected() == 1 or - (not keys.LEAVESCREEN and not keys._MOUSE_R_DOWN) then + (not keys.LEAVESCREEN and not keys._MOUSE_R) then return Editor_Unit.super.onInput(self, keys) end local page = pages:getSelectedPage() diff --git a/gui/liquids.lua b/gui/liquids.lua index 4eda71242a..f5d90483fb 100644 --- a/gui/liquids.lua +++ b/gui/liquids.lua @@ -210,7 +210,7 @@ function SpawnLiquid:onRenderFrame(dc, rect) local mouse_pos = dfhack.gui.getMousePos() if self.is_dragging then - if df.global.enabler.mouse_lbut == 0 then + if df.global.enabler.mouse_lbut_down == 0 then self.is_dragging = false elseif mouse_pos and not self:getMouseFramePos() then self:spawn(mouse_pos) @@ -228,7 +228,7 @@ end function SpawnLiquid:onInput(keys) if SpawnLiquid.super.onInput(self, keys) then return true end - if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then + if keys._MOUSE_L and not self:getMouseFramePos() then local mouse_pos = dfhack.gui.getMousePos() if self.paint_mode == SpawnLiquidPaintMode.CLICK and mouse_pos then @@ -258,7 +258,7 @@ function SpawnLiquid:onInput(keys) end -- TODO: Holding the mouse down causes event spam. - if keys._MOUSE_L and not self:getMouseFramePos() then + if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then if self.paint_mode == SpawnLiquidPaintMode.DRAG then self.is_dragging = true return true diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 3c2781a380..4be9fb338a 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -175,7 +175,7 @@ end function MassRemove:onInput(keys) if MassRemove.super.onInput(self, keys) then return true end - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then if self.mark then self.mark = nil self:updateLayout() @@ -185,7 +185,7 @@ function MassRemove:onInput(keys) end local pos = nil - if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then + if keys._MOUSE_L and not self:getMouseFramePos() then pos = dfhack.gui.getMousePos() end if not pos then return false end diff --git a/gui/masspit.lua b/gui/masspit.lua index 464701781d..f7d77176bb 100644 --- a/gui/masspit.lua +++ b/gui/masspit.lua @@ -186,7 +186,7 @@ end function Masspit:onInput(keys) if Masspit.super.onInput(self, keys) then return true end - if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then + if keys._MOUSE_L and not self:getMouseFramePos() then if self.subviews.pages:getSelected() == 1 then local building = dfhack.buildings.findAtTile(dfhack.gui.getMousePos()) diff --git a/gui/overlay.lua b/gui/overlay.lua index 017c66738c..a0c5719495 100644 --- a/gui/overlay.lua +++ b/gui/overlay.lua @@ -57,7 +57,7 @@ DraggablePanel.ATTRS{ } function DraggablePanel:onInput(keys) - if keys._MOUSE_L_DOWN then + if keys._MOUSE_L then local rect = self.frame_rect local x,y = self:getMousePos(gui.ViewRect{rect=rect}) if x then @@ -281,7 +281,7 @@ function OverlayConfig:onInput(keys) return true end end - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then self:dismiss() return true end diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 13014a2c75..2d713c7b3e 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -52,7 +52,7 @@ end function BlueprintDetails:onInput(keys) if keys.CUSTOM_CTRL_D or keys.SELECT - or keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + or keys.LEAVESCREEN or keys._MOUSE_R then self:dismiss() end end @@ -211,7 +211,7 @@ function BlueprintDialog:onInput(keys) details:show() -- for testing self._details = details - elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + elseif keys.LEAVESCREEN or keys._MOUSE_R then self:dismiss() if self.on_cancel then self.on_cancel() @@ -606,7 +606,7 @@ function Quickfort:onInput(keys) return true end - if keys._MOUSE_L_DOWN and not self:getMouseFramePos() then + if keys._MOUSE_L and not self:getMouseFramePos() then local pos = dfhack.gui.getMousePos() if pos then self:commit() diff --git a/gui/sandbox.lua b/gui/sandbox.lua index ff74ceb31a..b491e1b899 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -121,7 +121,7 @@ function Sandbox:init() key='CUSTOM_SHIFT_U', label="Spawn unit", on_activate=function() - df.global.enabler.mouse_lbut = 0 + df.global.enabler.mouse_lbut_down = 0 clear_arena_action() view:sendInputToParent{ARENA_CREATE_CREATURE=true} df.global.game.main_interface.arena_unit.editing_filter = true @@ -162,7 +162,7 @@ function Sandbox:init() key='CUSTOM_SHIFT_T', label="Spawn tree", on_activate=function() - df.global.enabler.mouse_lbut = 0 + df.global.enabler.mouse_lbut_down = 0 clear_arena_action() view:sendInputToParent{ARENA_CREATE_TREE=true} df.global.game.main_interface.arena_tree.editing_filter = true @@ -189,11 +189,11 @@ function Sandbox:init() end function Sandbox:onInput(keys) - if keys._MOUSE_R_DOWN and self:getMouseFramePos() then + if keys._MOUSE_R and self:getMouseFramePos() then clear_arena_action() return false end - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then if is_arena_action_in_progress() then clear_arena_action() return true @@ -204,7 +204,7 @@ function Sandbox:onInput(keys) if Sandbox.super.onInput(self, keys) then return true end - if keys._MOUSE_L then + if keys._MOUSE_L_DOWN then if self:getMouseFramePos() then return true end for _,mask_panel in ipairs(self.interface_masks) do if mask_panel:getMousePos() then return true end @@ -252,7 +252,7 @@ InterfaceMask.ATTRS{ } function InterfaceMask:onInput(keys) - return keys._MOUSE_L and self:getMousePos() + return keys._MOUSE_L_DOWN and self:getMousePos() end --------------------- diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua index 30ae9dc8d0..612b42ee48 100644 --- a/gui/seedwatch.lua +++ b/gui/seedwatch.lua @@ -78,7 +78,7 @@ function SeedSettings:commit() end function SeedSettings:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then self:hide() return true end diff --git a/gui/unit-syndromes.lua b/gui/unit-syndromes.lua index 93a3476a7c..994f967c08 100644 --- a/gui/unit-syndromes.lua +++ b/gui/unit-syndromes.lua @@ -441,7 +441,7 @@ function UnitSyndromes:push_state() end function UnitSyndromes:onInput(keys) - if keys._MOUSE_R_DOWN then + if keys._MOUSE_R then self:previous_page() return true end diff --git a/hide-tutorials.lua b/hide-tutorials.lua index 6887a77df4..f31a6072f2 100644 --- a/hide-tutorials.lua +++ b/hide-tutorials.lua @@ -29,7 +29,7 @@ function skip_tutorial_prompt(scr) df.global.gps.mouse_y = 18 df.global.enabler.mouse_lbut = 1 df.global.enabler.mouse_lbut_down = 1 - gui.simulateInput(scr, '_MOUSE_L_DOWN') + gui.simulateInput(scr, '_MOUSE_L') end end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 47401f272a..9c8650eef3 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -525,7 +525,7 @@ end function TradeScreen:onInput(keys) if self.reset_pending then return false end local handled = TradeScreen.super.onInput(self, keys) - if keys._MOUSE_L_DOWN and not self.trade_window:getMouseFramePos() then + if keys._MOUSE_L and not self.trade_window:getMouseFramePos() then -- "trade" or "offer" buttons may have been clicked and we need to reset the cache self.reset_pending = true end @@ -780,7 +780,7 @@ end function TradeOverlay:onInput(keys) if TradeOverlay.super.onInput(self, keys) then return true end - if keys._MOUSE_L_DOWN then + if keys._MOUSE_L then if dfhack.internal.getModifiers().shift then handle_shift_click_on_render = true copyGoodflagState() @@ -819,7 +819,7 @@ end function TradeBannerOverlay:onInput(keys) if TradeBannerOverlay.super.onInput(self, keys) then return true end - if keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + if keys._MOUSE_R or keys.LEAVESCREEN then if view then view:dismiss() end diff --git a/internal/gm-unit/editor_body.lua b/internal/gm-unit/editor_body.lua index 9c250d3828..b6e8935308 100644 --- a/internal/gm-unit/editor_body.lua +++ b/internal/gm-unit/editor_body.lua @@ -163,7 +163,7 @@ function Editor_Body_Modifier:init(args) end function Editor_Body_Modifier:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then self:setFocus(false) self.visible = false else diff --git a/test/gui/blueprint.lua b/test/gui/blueprint.lua index 55651719da..fcf45a932f 100644 --- a/test/gui/blueprint.lua +++ b/test/gui/blueprint.lua @@ -297,7 +297,7 @@ function test.set_with_mouse() mock.patch(dfhack.gui, 'getMousePos', mock.func(pos), function() local view = load_ui() - view:onInput({_MOUSE_L_DOWN=true}) + view:onInput({_MOUSE_L=true}) expect.table_eq(pos, view.mark, comment) send_keys('LEAVESCREEN') -- cancel selection send_keys('LEAVESCREEN') -- cancel out of UI From 2dcf6d903e7f3ba069a7f65d8c3cc1c73e32e774 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 26 Sep 2023 04:19:42 -0700 Subject: [PATCH 537/732] reinstate startdwarf and add scrollbar overlay --- changelog.txt | 2 + docs/startdwarf.rst | 27 +++++++++---- startdwarf.lua | 86 ++++++++++++++++++++++++++++++++++------ test/startdwarf.lua | 96 ++++----------------------------------------- 4 files changed, 103 insertions(+), 108 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5efb93672f..c0604214da 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,8 +27,10 @@ Template for new versions: # Future ## New Tools +- `startdwarf`: (reinstated) set number of starting dwarves ## New Features +- `startdwarf`: overlay scrollbar so you can scroll through your starting dwarves if they don't all fit on the screen ## Fixes diff --git a/docs/startdwarf.rst b/docs/startdwarf.rst index 4f63442a99..339ceed4bd 100644 --- a/docs/startdwarf.rst +++ b/docs/startdwarf.rst @@ -2,15 +2,16 @@ startdwarf ========== .. dfhack-tool:: - :summary: Increase the number of dwarves you embark with. - :tags: unavailable embark fort armok + :summary: Change the number of dwarves you embark with. + :tags: embark fort armok -You must use this tool before embarking (e.g. at the site selection screen or -any time before) to change the number of dwarves you embark with from the -default of 7. +You must use this tool before you get to the embark preparation screen (e.g. at +the site selection screen or any time before) to change the number of dwarves +you embark with from the default of 7. The value that you set will remain in +effect until DF is restarted (or you use `startdwarf` to set a new value). -Note that the game requires that you embark with no fewer than 7 dwarves, so -this tool can only increase the starting dwarf count, not decrease it. +The maximum number of dwarves you can have is 32,767, but that is far more than +the game can handle. Usage ----- @@ -24,6 +25,18 @@ Examples ``startdwarf 10`` Start with a few more warm bodies to help you get started. +``startdwarf 1`` + Hermit fort! (also see the `hermit` tool for keeping it that way) ``startdwarf 500`` Start with a teeming army of dwarves (leading to immediate food shortage and FPS issues). + +Overlay +------- + +The vanilla DF screen doesn't provide a way to scroll through the starting +dwarves, so if you start with more dwarves than can fit on your screen, this +tool provides a scrollbar that you can use to scroll through them. The vanilla +list was *not* designed for scrolling, so there is some odd behavior. When you +click on a dwarf to set skills, the list will jump so that the dwarf you +clicked on will be at the top of the page. diff --git a/startdwarf.lua b/startdwarf.lua index 143dce23ff..5f779a5933 100644 --- a/startdwarf.lua +++ b/startdwarf.lua @@ -1,17 +1,79 @@ --- change number of dwarves on initial embark +--@ module=true -local addr = dfhack.internal.getAddress('start_dwarf_count') -if not addr then - qerror('start_dwarf_count address not available - cannot patch') +local argparse = require('argparse') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +StartDwarfOverlay = defclass(StartDwarfOverlay, overlay.OverlayWidget) +StartDwarfOverlay.ATTRS{ + default_pos={x=5, y=9}, + default_enabled=true, + viewscreens='setupdwarfgame/Dwarves', + frame={w=5, h=10}, +} + +function StartDwarfOverlay:init() + self:addviews{ + widgets.Scrollbar{ + view_id='scrollbar', + frame={r=0, t=0, w=2, b=0}, + on_scroll=self:callback('on_scrollbar'), + }, + } +end + +function StartDwarfOverlay:on_scrollbar(scroll_spec) + local scr = dfhack.gui.getDFViewscreen(true) + local _, sh = dfhack.screen.getWindowSize() + local list_height = sh - 17 + local num_units = #scr.s_unit + local units_per_page = list_height // 3 + + local v = 0 + if tonumber(scroll_spec) then + v = tonumber(scroll_spec) - 1 + elseif scroll_spec == 'down_large' then + v = scr.selected_u + units_per_page // 2 + elseif scroll_spec == 'up_large' then + v = scr.selected_u - units_per_page // 2 + elseif scroll_spec == 'down_small' then + v = scr.selected_u + 1 + elseif scroll_spec == 'up_small' then + v = scr.selected_u - 1 + end + + scr.selected_u = math.max(0, math.min(num_units-1, v)) +end + +function StartDwarfOverlay:render(dc) + local sw, sh = dfhack.screen.getWindowSize() + local list_height = sh - 17 + local scr = dfhack.gui.getDFViewscreen(true) + local num_units = #scr.s_unit + local units_per_page = list_height // 3 + local scrollbar = self.subviews.scrollbar + self.frame.w = sw // 2 - 4 + self.frame.h = list_height + self:updateLayout() + + local top = math.min(scr.selected_u + 1, num_units - units_per_page + 1) + scrollbar:update(top, units_per_page, num_units) + + StartDwarfOverlay.super.render(self, dc) +end + +OVERLAY_WIDGETS = { + overlay=StartDwarfOverlay, +} + +if dfhack_flags.module then + return end -local num = tonumber(({...})[1]) -if not num or num < 7 then - qerror('argument must be a number no less than 7') +local num = argparse.positiveInt(({...})[1]) +if num > 32767 then + qerror(('value must be no more than 32,767: %d'):format(num)) end +df.global.start_dwarf_count = num -dfhack.with_temp_object(df.new('uint32_t'), function(temp) - temp.value = num - local temp_size, temp_addr = temp:sizeof() - dfhack.internal.patchMemory(addr, temp_addr, temp_size) -end) +print(('starting dwarf count set to %d. good luck!'):format(num)) diff --git a/test/startdwarf.lua b/test/startdwarf.lua index af58ce9198..511b325d35 100644 --- a/test/startdwarf.lua +++ b/test/startdwarf.lua @@ -1,104 +1,22 @@ -local utils = require('utils') - -local function with_patches(callback, custom_mocks) - dfhack.with_temp_object(df.new('uint32_t'), function(temp_out) - local originalPatchMemory = dfhack.internal.patchMemory - local function safePatchMemory(target, source, length) - -- only allow patching the expected address - otherwise a buggy - -- script could corrupt the test environment - if target ~= utils.addressof(temp_out) then - return expect.fail(('attempted to patch invalid address 0x%x: expected 0x%x'):format(target, utils.addressof(temp_out))) - end - return originalPatchMemory(target, source, length) - end - local mocks = { - getAddress = mock.func(utils.addressof(temp_out)), - patchMemory = mock.observe_func(safePatchMemory), - } - if custom_mocks then - for k, v in pairs(custom_mocks) do - mocks[k] = v - end - end - mock.patch({ - {dfhack.internal, 'getAddress', mocks.getAddress}, - {dfhack.internal, 'patchMemory', mocks.patchMemory}, - }, function() - callback(mocks, temp_out) - end) - end) -end +config.target = 'startdwarf' local function run_startdwarf(...) return dfhack.run_script('startdwarf', ...) end -local function test_early_error(args, expected_message, custom_mocks) - with_patches(function(mocks, temp_out) - temp_out.value = 12345 - - expect.error_match(expected_message, function() - run_startdwarf(table.unpack(args)) - end) - - expect.eq(mocks.getAddress.call_count, 1, 'getAddress was not called') - expect.table_eq(mocks.getAddress.call_args[1], {'start_dwarf_count'}) - - expect.eq(mocks.patchMemory.call_count, 0, 'patchMemory was called unexpectedly') - - -- make sure the script didn't attempt to write in some other way - expect.eq(temp_out.value, 12345, 'memory was changed unexpectedly') - end, custom_mocks) -end - -local function test_invalid_args(args, expected_message) - test_early_error(args, expected_message) -end - -local function test_patch_successful(expected_value) - with_patches(function(mocks, temp_out) - run_startdwarf(tostring(expected_value)) - expect.eq(temp_out.value, expected_value) - - expect.eq(mocks.getAddress.call_count, 1, 'getAddress was not called') - expect.table_eq(mocks.getAddress.call_args[1], {'start_dwarf_count'}) - - expect.eq(mocks.patchMemory.call_count, 1, 'patchMemory was not called') - expect.eq(mocks.patchMemory.call_args[1][1], utils.addressof(temp_out), - 'patchMemory called with wrong destination') - -- skip checking source (arg 2) because it has already been freed by the script - expect.eq(mocks.patchMemory.call_args[1][3], df.sizeof(temp_out), - 'patchMemory called with wrong length') - end) -end - function test.no_arg() - test_invalid_args({}, 'must be a number') + expect.error_match('expected positive integer', run_startdwarf) end function test.not_number() - test_invalid_args({'a'}, 'must be a number') + expect.error_match('expected positive integer', curry(run_startdwarf, 'a')) end function test.too_small() - test_invalid_args({'4'}, 'less than 7') - test_invalid_args({'6'}, 'less than 7') - test_invalid_args({'-1'}, 'less than 7') -end - -function test.missing_address() - test_early_error({}, 'address not available', {getAddress = mock.func(nil)}) - test_early_error({'8'}, 'address not available', {getAddress = mock.func(nil)}) -end - -function test.exactly_7() - test_patch_successful(7) -end - -function test.above_7() - test_patch_successful(10) + expect.error_match('expected positive integer', curry(run_startdwarf, '0')) + expect.error_match('expected positive integer', curry(run_startdwarf, '-1')) end -function test.uint8_overflow() - test_patch_successful(257) +function test.too_big() + expect.error_match('value must be no more than', curry(run_startdwarf, '32768')) end From 1cc37bc82f51db0eb24875f298ce57a65210ad1d Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Wed, 27 Sep 2023 10:35:43 +0100 Subject: [PATCH 538/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 5efb93672f..5fb2bd346e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `suspendmanager`: fixed a bug where floor grates, bars, bridges etc. wouldn't be recognised as walkable, leading to unnecessary suspensions in certain cases. ## Misc Improvements - `devel/inspect-screen`: display total grid size for UI and map layers From 26643dc55f15e0ddcaf3933ab3315266f6c2c22d Mon Sep 17 00:00:00 2001 From: lethosor Date: Wed, 27 Sep 2023 19:57:32 -0400 Subject: [PATCH 539/732] sc: skip temp_save vectors --- devel/sc.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/sc.lua b/devel/sc.lua index 3d0b286f22..1f91a1a8e7 100644 --- a/devel/sc.lua +++ b/devel/sc.lua @@ -127,7 +127,7 @@ local function check_container(obj, path) if df.isvalid(v) == 'ref' then local s, a = v:sizeof() - if v and v._kind == 'container' and k ~= 'bad' then + if v and v._kind == 'container' and k ~= 'bad' and k ~= 'temp_save' then if tostring(v._type):sub(1,6) == 'vector' and check_vectors and not is_valid_vector(a) then local key = tostring(obj._type) .. '.' .. k if not checkedp[key] then From 99872eda34bebc56d59ec58b85823c25070afa65 Mon Sep 17 00:00:00 2001 From: lethosor Date: Wed, 27 Sep 2023 19:57:43 -0400 Subject: [PATCH 540/732] sc: log path to mis-sized objects --- devel/sc.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/sc.lua b/devel/sc.lua index 1f91a1a8e7..6cf8aab27b 100644 --- a/devel/sc.lua +++ b/devel/sc.lua @@ -162,7 +162,7 @@ local function check_container(obj, path) --print (' OK') else bold (t) - err (' NOT OK '.. s .. ' ' .. s2) + err (' NOT OK '.. s .. ' ' .. s2 .. ' at ' .. path .. '.' .. k) end end From 85e61428786db2a010080967a76e0ceeac92f29c Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 27 Sep 2023 23:01:04 -0700 Subject: [PATCH 541/732] Update gui/control-panel.lua --- gui/control-panel.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index fb248e5cc4..2ee28bd60f 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -128,7 +128,7 @@ local REPEATS = { desc='Sort manager orders by repeat frequency so one-time orders can be completed.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, ['orders-reevaluate']={ - desc='Invalidates work orders once a month forcing manager to recheck conditions.', + desc='Invalidates work orders once a month, allowing conditions to be rechecked.', command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', From a4bdfe0aedacc4c7fa735af82068efb6e139564d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 29 Sep 2023 20:55:58 -0700 Subject: [PATCH 542/732] first draft of the display furniture selection --- caravan.lua | 2 + internal/caravan/pedestal.lua | 670 ++++++++++++++++++++++++++++++++++ 2 files changed, 672 insertions(+) create mode 100644 internal/caravan/pedestal.lua diff --git a/caravan.lua b/caravan.lua index 26d704b023..de8c4e32f9 100644 --- a/caravan.lua +++ b/caravan.lua @@ -2,6 +2,7 @@ --@ module = true local movegoods = reqscript('internal/caravan/movegoods') +local pedestal = reqscript('internal/caravan/pedestal') local trade = reqscript('internal/caravan/trade') local tradeagreement = reqscript('internal/caravan/tradeagreement') @@ -20,6 +21,7 @@ OVERLAY_WIDGETS = { tradeagreement=tradeagreement.TradeAgreementOverlay, movegoods=movegoods.MoveGoodsOverlay, assigntrade=movegoods.AssignTradeOverlay, + displayitemselector=pedestal.PedestalOverlay, } INTERESTING_FLAGS = { diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua new file mode 100644 index 0000000000..755706621f --- /dev/null +++ b/internal/caravan/pedestal.lua @@ -0,0 +1,670 @@ +--@ module=true + +-- TODO: this should be moved to stocks once the item filter code is moved there + +local common = reqscript('internal/caravan/common') +local gui = require('gui') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') + +local STATUS = { + NONE={label='Unknown', value=0}, + ASSIGNED_HERE={label='Assigned here', value=1}, + ASSIGNED_THERE={label='Assigned elsewhere', value=2}, + AVAILABLE={label='', value=3}, +} +local STATUS_REVMAP = {} +for k, v in pairs(STATUS) do + STATUS_REVMAP[v.value] = k +end + +-- ------------------- +-- AssignItems +-- + +AssignItems = defclass(AssignItems, widgets.Window) +AssignItems.ATTRS { + frame_title='Assign items for display', + frame={w=80, h=46}, + resizable=true, + resize_min={h=25}, + frame_inset={l=1, t=1, b=1, r=0}, +} + +local STATUS_COL_WIDTH = 18 +local VALUE_COL_WIDTH = 9 + +local function sort_noop(a, b) + -- this function is used as a marker and never actually gets called + error('sort_noop should not be called') +end + +local function sort_base(a, b) + return a.data.desc < b.data.desc +end + +local function sort_by_name_desc(a, b) + if a.search_key == b.search_key then + return sort_base(a, b) + end + return a.search_key < b.search_key +end + +local function sort_by_name_asc(a, b) + if a.search_key == b.search_key then + return sort_base(a, b) + end + return a.search_key > b.search_key +end + +local function sort_by_value_desc(a, b) + if a.data.value == b.data.value then + return sort_by_name_desc(a, b) + end + return a.data.value > b.data.value +end + +local function sort_by_value_asc(a, b) + if a.data.value == b.data.value then + return sort_by_name_desc(a, b) + end + return a.data.value < b.data.value +end + +local function sort_by_status_desc(a, b) + if a.data.status == b.data.status then + return sort_by_value_desc(a, b) + end + return a.data.status < b.data.status +end + +local function sort_by_status_asc(a, b) + if a.data.status == b.data.status then + return sort_by_value_desc(a, b) + end + return a.data.status > b.data.status +end + +local function get_assigned_value(display_bld) + local value = 0 + for _, item_id in ipairs(display_bld.displayed_items) do + local item = df.item.find(item_id) + if item then + value = value + common.get_perceived_value(item) + end + end + return value +end + +local function get_containing_temple_or_guildhall(display_bld) + local loc_id = nil + for _, relation in ipairs(display_bld.relations) do + if relation.location_id > -1 then + loc_id = relation.location_id + end + end + if not loc_id then return end + local site = df.global.world.world_data.active_site[0] + local location = utils.binsearch(site.buildings, loc_id, 'id') + if not location then return end + local loc_type = location:getType() + if loc_type ~= df.abstract_building_type.GUILDHALL and loc_type ~= df.abstract_building_type.TEMPLE then + return + end + return location +end + +local function to_title_case(str) + str = str:gsub('(%a)([%w_]*)', + function (first, rest) return first:upper()..rest:lower() end) + str = str:gsub('_', ' ') + return str +end + +-- returns the value of items assigned to the display but not yet in the display +local function get_pending_value(display_bld) + local value = get_assigned_value(display_bld) + for _, contained_item in ipairs(display_bld.contained_items) do + if contained_item.use_mode ~= 0 or + not contained_item.item.flags.in_building + then + goto continue + end + value = value - common.get_perceived_value(contained_item.item) + ::continue:: + end + return value +end + +local difficulty = df.global.plotinfo.main.custom_difficulty +local function get_expected_location_tier(display_bld) + local location = get_containing_temple_or_guildhall(display_bld) + if not location then return '' end + local loc_type = to_title_case(df.abstract_building_type[location:getType()]) + local pending_value = get_pending_value(display_bld) // #display_bld.relations + local value = location.contents.location_value + pending_value + if loc_type == 'Guildhall' then + if value >= difficulty.grand_guildhall_value then + return 'Grand Guildhall' + elseif value >= difficulty.guildhall_value then + return loc_type + end + else + if value >= difficulty.temple_complex_value then + return 'Temple Complex' + elseif value >= difficulty.temple_value then + return loc_type + end + end + return 'Meeting Hall' +end + +function AssignItems:init() + self.bld = dfhack.gui.getSelectedBuilding(true) + if not self.bld or not df.building_display_furniturest:is_instance(self.bld) then + qerror('No display furniture selected') + end + + self.choices_cache = {} + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={l=0, t=0, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_S', + options={ + {label='status'..common.CH_DN, value=sort_by_status_desc}, + {label='status'..common.CH_UP, value=sort_by_status_asc}, + {label='value'..common.CH_DN, value=sort_by_value_desc}, + {label='value'..common.CH_UP, value=sort_by_value_asc}, + {label='name'..common.CH_DN, value=sort_by_name_desc}, + {label='name'..common.CH_UP, value=sort_by_name_asc}, + }, + initial_option=sort_by_status_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={l=26, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, + }, + widgets.Panel{ + frame={t=2, l=0, w=38, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_quality', + frame={l=0, t=0, w=18}, + label='Min quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_SHIFT_X', + options={ + {label='Ordinary', value=0}, + {label='-Well Crafted-', value=1}, + {label='+Finely Crafted+', value=2}, + {label='*Superior*', value=3}, + {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, + {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, + {label='Artifact', value=6}, + }, + initial_option=0, + on_change=function(val) + if self.subviews.max_quality:getOptionValue() < val then + self.subviews.max_quality:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_quality', + frame={r=1, t=0, w=18}, + label='Max quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Q', + key='CUSTOM_SHIFT_W', + options={ + {label='Ordinary', value=0}, + {label='-Well Crafted-', value=1}, + {label='+Finely Crafted+', value=2}, + {label='*Superior*', value=3}, + {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, + {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, + {label='Artifact', value=6}, + }, + initial_option=6, + on_change=function(val) + if self.subviews.min_quality:getOptionValue() > val then + self.subviews.min_quality:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=7, + get_left_idx_fn=function() + return self.subviews.min_quality:getOptionValue() + 1 + end, + get_right_idx_fn=function() + return self.subviews.max_quality:getOptionValue() + 1 + end, + on_left_change=function(idx) self.subviews.min_quality:setOption(idx-1, true) end, + on_right_change=function(idx) self.subviews.max_quality:setOption(idx-1, true) end, + }, + }, + }, + widgets.ToggleHotkeyLabel{ + view_id='hide_forbidden', + frame={t=2, l=40, w=28}, + label='Hide forbidden items:', + key='CUSTOM_SHIFT_F', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=false, + on_change=function() self:refresh_list() end, + }, + widgets.Panel{ + frame={t=7, l=0, r=0, b=7}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_status', + frame={t=0, l=0, w=7}, + options={ + {label='status', value=sort_noop}, + {label='status'..common.CH_DN, value=sort_by_status_desc}, + {label='status'..common.CH_UP, value=sort_by_status_asc}, + }, + initial_option=sort_by_status_desc, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_status'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_value', + frame={t=0, l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+1-6, w=6}, + options={ + {label='value', value=sort_noop}, + {label='value'..common.CH_DN, value=sort_by_value_desc}, + {label='value'..common.CH_UP, value=sort_by_value_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_value'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_name', + frame={t=0, l=STATUS_COL_WIDTH+2+VALUE_COL_WIDTH+2, w=5}, + options={ + {label='name', value=sort_noop}, + {label='name'..common.CH_DN, value=sort_by_name_desc}, + {label='name'..common.CH_UP, value=sort_by_name_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_name'), + }, + widgets.FilteredList{ + view_id='list', + frame={l=0, t=2, r=0, b=0}, + on_submit=self:callback('toggle_item'), + on_submit2=self:callback('toggle_range'), + on_select=self:callback('select_item'), + }, + } + }, + widgets.Label{ + frame={l=0, b=5, h=1, r=0}, + text={ + 'Total value of assigned items:', + {gap=1, + text=function() return common.obfuscate_value(get_assigned_value(self.bld)) end}, + }, + }, + widgets.Label{ + frame={l=0, b=4, h=1, r=0}, + text={ + {gap=7, + text='Expected location tier:'}, + {gap=1, + text=function() return get_expected_location_tier(self.bld) end}, + }, + visible=function() return get_containing_temple_or_guildhall(self.bld) end, + }, + widgets.HotkeyLabel{ + frame={l=0, b=2}, + label='Select all/none', + key='CUSTOM_CTRL_A', + on_activate=self:callback('toggle_visible'), + auto_width=true, + }, + widgets.ToggleHotkeyLabel{ + view_id='inside_containers', + frame={l=33, b=2, w=34}, + label='See inside containers:', + key='CUSTOM_CTRL_I', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=true, + on_change=function() self:refresh_list() end, + }, + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap='Click to assign/unassign. Shift click to assign/unassign a range of items.', + }, + } + + -- replace the FilteredList's built-in EditField with our own + self.subviews.list.list.frame.t = 0 + self.subviews.list.edit.visible = false + self.subviews.list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + + self.subviews.list:setChoices(self:get_choices()) +end + +function AssignItems:refresh_list(sort_widget, sort_fn) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == sort_noop then + self.subviews[sort_widget]:cycle() + return + end + for _,widget_name in ipairs{'sort', 'sort_status', 'sort_value', 'sort_name'} do + self.subviews[widget_name]:setOption(sort_fn) + end + local list = self.subviews.list + local saved_filter = list:getFilter() + list:setFilter('') + list:setChoices(self:get_choices(), list:getSelected()) + list:setFilter(saved_filter) +end + +local function is_container(item) + return item and ( + df.item_binst:is_instance(item) or + item:isFoodStorage() + ) +end + +local function is_displayable_item(item, display_bld) + if not item or + item.flags.hostile or + item.flags.removed or + item.flags.dead_dwarf or + item.flags.spider_web or + item.flags.construction or + item.flags.encased or + item.flags.unk12 or + item.flags.murder or + item.flags.trader or + item.flags.owned or + item.flags.garbage_collect or + item.flags.on_fire or + item.flags.in_chest + then + return false + end + if item.flags.in_job then + local spec_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if not spec_ref then return false end + if spec_ref.data.job.job_type ~= df.job_type.PutItemOnDisplay then return false end + elseif item.flags.in_inventory then + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) + if not gref then return false end + if not is_container(df.item.find(gref.item_id)) or item:isLiquidPowder() then + return false + end + end + if item.flags.in_building then + local bld = dfhack.items.getHolderBuilding(item) + if not bld then return false end + for _, contained_item in ipairs(bld.contained_items) do + if contained_item.use_mode == 0 then return true end + -- building construction materials + if item == contained_item.item then return false end + end + end + return dfhack.maps.canWalkBetween(xyz2pos(dfhack.items.getPosition(item)), + xyz2pos(display_bld.centerx, display_bld.centery, display_bld.z)) +end + +local function get_display_bld_id(item) + local assigned_bld_ref = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_DISPLAY_FURNITURE) + if assigned_bld_ref then return assigned_bld_ref.building_id end +end + +local function get_status(item, display_bld) + local display_bld_id = get_display_bld_id(item) + if display_bld_id == display_bld.id then + return STATUS.ASSIGNED_HERE.value + elseif display_bld_id then + return STATUS.ASSIGNED_THERE.value + end + return STATUS.AVAILABLE.value +end + +local function make_choice_text(data) + 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, text=data.desc}, + } +end + +local function make_container_search_key(item, desc) + local words = {} + common.add_words(words, desc) + for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do + common.add_words(words, common.get_item_description(contained_item)) + end + return table.concat(words, ' ') +end + +local function contains_non_liquid_powder(container) + for _, item in ipairs(dfhack.items.getContainedItems(container)) do + if not item:isLiquidPowder() then return true end + end + return false +end + +function AssignItems:cache_choices(inside_containers) + if self.choices_cache[inside_containers] then return self.choices_cache[inside_containers] end + + local choices = {} + for _, item in ipairs(df.global.world.items.all) do + if not is_displayable_item(item, self.bld) then goto continue end + if inside_containers and is_container(item) and contains_non_liquid_powder(item) then + goto continue + elseif not inside_containers and item.flags.in_inventory then + goto continue + end + local value = common.get_perceived_value(item) + local desc = common.get_item_description(item) + local status = get_status(item, self.bld) + local data = { + item=item, + desc=desc, + value=value, + status=status, + quality=item.flags.artifact and 6 or item:getQuality(), + } + local search_key + if not inside_containers and is_container(item) then + search_key = make_container_search_key(item, desc) + else + search_key = common.make_search_key(desc) + end + local entry = { + search_key=search_key, + text=make_choice_text(data), + data=data, + } + table.insert(choices, entry) + ::continue:: + end + + self.choices_cache[inside_containers] = choices + return choices +end + +function AssignItems:get_choices() + local raw_choices = self:cache_choices(self.subviews.inside_containers:getOptionValue()) + local choices = {} + local include_forbidden = not self.subviews.hide_forbidden:getOptionValue() + local min_quality = self.subviews.min_quality:getOptionValue() + local max_quality = self.subviews.max_quality:getOptionValue() + for _,choice in ipairs(raw_choices) do + local data = choice.data + if not include_forbidden then + if data.item.flags.forbid then + goto continue + end + end + if min_quality > data.quality then goto continue end + if max_quality < data.quality then goto continue end + table.insert(choices, choice) + ::continue:: + end + table.sort(choices, self.subviews.sort:getOptionValue()) + return choices +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 + bld.displayed_items:erase(idx) + end +end + +local function detach_item(item) + if item.flags.in_job then + local spec_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if spec_ref then + dfhack.job.removeJob(spec_ref.data.job) + end + end + local display_bld_id = get_display_bld_id(item) + if not display_bld_id then return end + for idx = #item.general_refs-1, 0, -1 do + local ref = item.general_refs[idx] + if df.general_ref_building_display_furniturest:is_instance(ref) then + unassign_item(df.building.find(ref.building_id), item) + item.general_refs:erase(idx) + ref:delete() + end + end +end + +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) + item.flags.forbid = false + item.flags.in_building = false +end + +function AssignItems:toggle_item_base(choice, target_value) + local true_value = STATUS.ASSIGNED_HERE.value + + if target_value == nil then + target_value = choice.data.status ~= true_value + end + + if target_value and choice.data.status == true_value then + return target_value + end + if not target_value and choice.data.status ~= true_value then + return target_value + end + + local item = choice.data.item + detach_item(item) + + if target_value then + attach_item(item, self.bld) + end + + choice.data.status = get_status(item, self.bld) + + return target_value +end + +function AssignItems:select_item(idx, choice) + if not dfhack.internal.getModifiers().shift then + self.prev_list_idx = self.subviews.list.list:getSelected() + end +end + +function AssignItems:toggle_item(idx, choice) + self:toggle_item_base(choice) +end + +function AssignItems:toggle_range(idx, choice) + if not self.prev_list_idx then + self:toggle_item(idx, choice) + return + end + local choices = self.subviews.list:getVisibleChoices() + local list_idx = self.subviews.list.list:getSelected() + local target_value + for i = list_idx, self.prev_list_idx, list_idx < self.prev_list_idx and 1 or -1 do + target_value = self:toggle_item_base(choices[i], target_value) + end + self.prev_list_idx = list_idx +end + +function AssignItems:toggle_visible() + local target_value + for _, choice in ipairs(self.subviews.list:getVisibleChoices()) do + target_value = self:toggle_item_base(choice, target_value) + end +end + +-- ------------------- +-- AssignItemsModal +-- + +AssignItemsModal = defclass(AssignItemsModal, gui.ZScreenModal) +AssignItemsModal.ATTRS { + focus_path='pedestal/assignitems', +} + +function AssignItemsModal:init() + self:addviews{AssignItems{}} +end + +-- ------------------- +-- PedestalOverlay +-- + +PedestalOverlay = defclass(PedestalOverlay, overlay.OverlayWidget) +PedestalOverlay.ATTRS{ + default_pos={x=-40, y=34}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING/DisplayFurniture', + frame={w=23, h=1}, + frame_background=gui.CLEAR_PEN, +} + +local function is_valid_building() + local bld = dfhack.gui.getSelectedBuilding(true) +return bld and bld:getBuildStage() == bld:getMaxBuildStage() +end + +function PedestalOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='DFHack assign items', + key='CUSTOM_CTRL_T', + visible=is_valid_building, + on_activate=function() AssignItemsModal{}:show() end, + }, + } +end From 9c3c9a1a06fc836dd06512034c79fdb64659ecb1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 29 Sep 2023 21:02:32 -0700 Subject: [PATCH 543/732] frames --- internal/caravan/pedestal.lua | 148 ++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index 755706621f..b6f2bf6231 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -26,7 +26,7 @@ end AssignItems = defclass(AssignItems, widgets.Window) AssignItems.ATTRS { frame_title='Assign items for display', - frame={w=80, h=46}, + frame={w=74, h=46}, resizable=true, resize_min={h=25}, frame_inset={l=1, t=1, b=1, r=0}, @@ -192,84 +192,90 @@ function AssignItems:init() on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ - frame={t=2, l=0, w=38, h=4}, + frame={t=2, l=0, w=70, h=6}, + frame_style=gui.FRAME_INTERIOR, subviews={ - widgets.CycleHotkeyLabel{ - view_id='min_quality', - frame={l=0, t=0, w=18}, - label='Min quality:', - label_below=true, - key_back='CUSTOM_SHIFT_Z', - key='CUSTOM_SHIFT_X', - options={ - {label='Ordinary', value=0}, - {label='-Well Crafted-', value=1}, - {label='+Finely Crafted+', value=2}, - {label='*Superior*', value=3}, - {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, - {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, - {label='Artifact', value=6}, + widgets.Panel{ + frame={t=0, l=0, w=38, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_quality', + frame={l=0, t=0, w=18}, + label='Min quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_SHIFT_X', + options={ + {label='Ordinary', value=0}, + {label='-Well Crafted-', value=1}, + {label='+Finely Crafted+', value=2}, + {label='*Superior*', value=3}, + {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, + {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, + {label='Artifact', value=6}, + }, + initial_option=0, + on_change=function(val) + if self.subviews.max_quality:getOptionValue() < val then + self.subviews.max_quality:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_quality', + frame={r=1, t=0, w=18}, + label='Max quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Q', + key='CUSTOM_SHIFT_W', + options={ + {label='Ordinary', value=0}, + {label='-Well Crafted-', value=1}, + {label='+Finely Crafted+', value=2}, + {label='*Superior*', value=3}, + {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, + {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, + {label='Artifact', value=6}, + }, + initial_option=6, + on_change=function(val) + if self.subviews.min_quality:getOptionValue() > val then + self.subviews.min_quality:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=7, + get_left_idx_fn=function() + return self.subviews.min_quality:getOptionValue() + 1 + end, + get_right_idx_fn=function() + return self.subviews.max_quality:getOptionValue() + 1 + end, + on_left_change=function(idx) self.subviews.min_quality:setOption(idx-1, true) end, + on_right_change=function(idx) self.subviews.max_quality:setOption(idx-1, true) end, + }, }, - initial_option=0, - on_change=function(val) - if self.subviews.max_quality:getOptionValue() < val then - self.subviews.max_quality:setOption(val) - end - self:refresh_list() - end, }, - widgets.CycleHotkeyLabel{ - view_id='max_quality', - frame={r=1, t=0, w=18}, - label='Max quality:', - label_below=true, - key_back='CUSTOM_SHIFT_Q', - key='CUSTOM_SHIFT_W', + widgets.ToggleHotkeyLabel{ + view_id='hide_forbidden', + frame={t=0, l=40, w=28}, + label='Hide forbidden items:', + key='CUSTOM_SHIFT_F', options={ - {label='Ordinary', value=0}, - {label='-Well Crafted-', value=1}, - {label='+Finely Crafted+', value=2}, - {label='*Superior*', value=3}, - {label=common.CH_EXCEPTIONAL..'Exceptional'..common.CH_EXCEPTIONAL, value=4}, - {label=common.CH_MONEY..'Masterful'..common.CH_MONEY, value=5}, - {label='Artifact', value=6}, + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} }, - initial_option=6, - on_change=function(val) - if self.subviews.min_quality:getOptionValue() > val then - self.subviews.min_quality:setOption(val) - end - self:refresh_list() - end, - }, - widgets.RangeSlider{ - frame={l=0, t=3}, - num_stops=7, - get_left_idx_fn=function() - return self.subviews.min_quality:getOptionValue() + 1 - end, - get_right_idx_fn=function() - return self.subviews.max_quality:getOptionValue() + 1 - end, - on_left_change=function(idx) self.subviews.min_quality:setOption(idx-1, true) end, - on_right_change=function(idx) self.subviews.max_quality:setOption(idx-1, true) end, + initial_option=false, + on_change=function() self:refresh_list() end, }, }, }, - widgets.ToggleHotkeyLabel{ - view_id='hide_forbidden', - frame={t=2, l=40, w=28}, - label='Hide forbidden items:', - key='CUSTOM_SHIFT_F', - options={ - {label='Yes', value=true, pen=COLOR_GREEN}, - {label='No', value=false} - }, - initial_option=false, - on_change=function() self:refresh_list() end, - }, widgets.Panel{ - frame={t=7, l=0, r=0, b=7}, + frame={t=9, l=0, r=0, b=7}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', @@ -353,7 +359,7 @@ function AssignItems:init() }, widgets.WrappedLabel{ frame={b=0, l=0, r=0}, - text_to_wrap='Click to assign/unassign. Shift click to assign/unassign a range of items.', + text_to_wrap='Click to assign/unassign. Shift click to assign/unassign a range.', }, } From 044bd5c9dc2f2344b2dd17003a8b8e8680103f2a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 29 Sep 2023 21:14:05 -0700 Subject: [PATCH 544/732] add docs for the new pedestal screen --- docs/caravan.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/caravan.rst b/docs/caravan.rst index 7e0fdf15de..d45432619d 100644 --- a/docs/caravan.rst +++ b/docs/caravan.rst @@ -94,3 +94,15 @@ Trade agreement A small panel is shown with a hotkey (``Ctrl-A``) for selecting all/none in the currently shown category. + +Display furniture +````````````````` + +A button is added to the screen when you are viewing display furniture +(pedestals and display cases) where you can launch an item assignment GUI. + +The dialog allows you to sort by name, value, or where the item is currently +assigned for display. + +You can search by name, and you can filter by item quality and by whether the +item is forbidden. From c26b63b727dbc2855e9faeccb6e5ff2212f482d6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 29 Sep 2023 21:15:37 -0700 Subject: [PATCH 545/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 288eba9479..ada35e9866 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features - `startdwarf`: overlay scrollbar so you can scroll through your starting dwarves if they don't all fit on the screen +- A new searchable, sortable, filterable dialog for selecting items for display on pedestals and display cases ## Fixes - `suspendmanager`: fixed a bug where floor grates, bars, bridges etc. wouldn't be recognised as walkable, leading to unnecessary suspensions in certain cases. From 44fd440bdce7e174e47a92dc59de42e12ec2364d Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Sat, 30 Sep 2023 14:53:52 +0100 Subject: [PATCH 546/732] added preserve-tombs to fort services list --- gui/control-panel.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 31e025918f..1ca6ed3b21 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -30,6 +30,7 @@ local FORT_SERVICES = { 'hermit', 'misery', 'nestboxes', + 'preserve-tombs', 'prioritize', 'seedwatch', 'starvingdead', From 55d0463cdb67165f746a3f2c0f4dd87c6ef86eec Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Sep 2023 23:54:57 -0700 Subject: [PATCH 547/732] use common dialog input handler code so mouse clicks don't bleed through --- gui/quickfort.lua | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 2d713c7b3e..ca27ca6c51 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -211,13 +211,7 @@ function BlueprintDialog:onInput(keys) details:show() -- for testing self._details = details - elseif keys.LEAVESCREEN or keys._MOUSE_R then - self:dismiss() - if self.on_cancel then - self.on_cancel() - end - else - self:inputToSubviews(keys) + elseif BlueprintDialog.super.onInput(self, keys) then local prev_filter_text = filter_text -- save the filter if it was updated so we always have the most recent -- text for the next invocation of the dialog @@ -229,6 +223,7 @@ function BlueprintDialog:onInput(keys) -- otherwise, save the new selected item save_selection(self.subviews.list) end + return true end end From 43a22b312666e6ca0478987942676b86b1d77631 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 1 Oct 2023 01:14:39 -0700 Subject: [PATCH 548/732] get any citizen if no unit is specified --- modtools/create-item.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index b6325657c4..031f5c56b1 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -274,7 +274,7 @@ local function createItem(mat, itemType, quality, creator, description, amount) end local function get_first_citizen() - local citizens = dfhack.units.getCitizens() + local citizens = dfhack.units.getCitizens(true) if not citizens or not citizens[1] then qerror('Could not choose a creator unit. Please select one in the UI') end From 8259d95e9ee9d88c88f32b22608d0d8c977aac18 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 1 Oct 2023 01:34:12 -0700 Subject: [PATCH 549/732] align gui/sandbox with mouse button changes --- gui/sandbox.lua | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index b491e1b899..854173d836 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -121,8 +121,8 @@ function Sandbox:init() key='CUSTOM_SHIFT_U', label="Spawn unit", on_activate=function() - df.global.enabler.mouse_lbut_down = 0 clear_arena_action() + gui.markMouseClicksHandled{_MOUSE_L=true} view:sendInputToParent{ARENA_CREATE_CREATURE=true} df.global.game.main_interface.arena_unit.editing_filter = true end, @@ -162,8 +162,8 @@ function Sandbox:init() key='CUSTOM_SHIFT_T', label="Spawn tree", on_activate=function() - df.global.enabler.mouse_lbut_down = 0 clear_arena_action() + gui.markMouseClicksHandled{_MOUSE_L=true} view:sendInputToParent{ARENA_CREATE_TREE=true} df.global.game.main_interface.arena_tree.editing_filter = true end, @@ -204,7 +204,7 @@ function Sandbox:onInput(keys) if Sandbox.super.onInput(self, keys) then return true end - if keys._MOUSE_L_DOWN then + if keys._MOUSE_L then if self:getMouseFramePos() then return true end for _,mask_panel in ipairs(self.interface_masks) do if mask_panel:getMousePos() then return true end @@ -251,10 +251,6 @@ InterfaceMask.ATTRS{ frame_background=gui.TRANSPARENT_PEN, } -function InterfaceMask:onInput(keys) - return keys._MOUSE_L_DOWN and self:getMousePos() -end - --------------------- -- SandboxScreen -- From 4250c075d237b4da1efed8a9fd4af302b3f0c8c4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 1 Oct 2023 10:17:27 -0700 Subject: [PATCH 550/732] add some color and ability to include unreachable items but still exclude hidden items like unseen demon slabs --- internal/caravan/pedestal.lua | 47 +++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index b6f2bf6231..a7842891f9 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -26,7 +26,7 @@ end AssignItems = defclass(AssignItems, widgets.Window) AssignItems.ATTRS { frame_title='Assign items for display', - frame={w=74, h=46}, + frame={w=76, h=46}, resizable=true, resize_min={h=25}, frame_inset={l=1, t=1, b=1, r=0}, @@ -192,7 +192,7 @@ function AssignItems:init() on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ - frame={t=2, l=0, w=70, h=6}, + frame={t=2, l=0, w=72, h=6}, frame_style=gui.FRAME_INTERIOR, subviews={ widgets.Panel{ @@ -260,9 +260,21 @@ function AssignItems:init() }, }, }, + widgets.ToggleHotkeyLabel{ + view_id='hide_unreachable', + frame={t=0, l=40, w=30}, + label='Hide unreachable items:', + key='CUSTOM_SHIFT_U', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=true, + on_change=function() self:refresh_list() end, + }, widgets.ToggleHotkeyLabel{ view_id='hide_forbidden', - frame={t=0, l=40, w=28}, + frame={t=2, l=40, w=28}, label='Hide forbidden items:', key='CUSTOM_SHIFT_F', options={ @@ -324,7 +336,7 @@ function AssignItems:init() frame={l=0, b=5, h=1, r=0}, text={ 'Total value of assigned items:', - {gap=1, + {gap=1, pen=COLOR_GREEN, text=function() return common.obfuscate_value(get_assigned_value(self.bld)) end}, }, }, @@ -333,7 +345,7 @@ function AssignItems:init() text={ {gap=7, text='Expected location tier:'}, - {gap=1, + {gap=1, pen=COLOR_GREEN, text=function() return get_expected_location_tier(self.bld) end}, }, visible=function() return get_containing_temple_or_guildhall(self.bld) end, @@ -396,7 +408,7 @@ local function is_container(item) ) end -local function is_displayable_item(item, display_bld) +local function is_displayable_item(item) if not item or item.flags.hostile or item.flags.removed or @@ -425,6 +437,9 @@ local function is_displayable_item(item, display_bld) 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 bld = dfhack.items.getHolderBuilding(item) if not bld then return false end @@ -434,8 +449,7 @@ local function is_displayable_item(item, display_bld) if item == contained_item.item then return false end end end - return dfhack.maps.canWalkBetween(xyz2pos(dfhack.items.getPosition(item)), - xyz2pos(display_bld.centerx, display_bld.centery, display_bld.z)) + return true end local function get_display_bld_id(item) @@ -477,12 +491,12 @@ local function contains_non_liquid_powder(container) return false end -function AssignItems:cache_choices(inside_containers) +function AssignItems:cache_choices(inside_containers, display_bld) if self.choices_cache[inside_containers] then return self.choices_cache[inside_containers] end local choices = {} for _, item in ipairs(df.global.world.items.all) do - if not is_displayable_item(item, self.bld) then goto continue end + if not is_displayable_item(item) then goto continue end if inside_containers and is_container(item) and contains_non_liquid_powder(item) then goto continue elseif not inside_containers and item.flags.in_inventory then @@ -491,12 +505,15 @@ function AssignItems:cache_choices(inside_containers) local value = common.get_perceived_value(item) local desc = common.get_item_description(item) local status = get_status(item, self.bld) + local reachable = dfhack.maps.canWalkBetween(xyz2pos(dfhack.items.getPosition(item)), + xyz2pos(display_bld.centerx, display_bld.centery, display_bld.z)) local data = { item=item, desc=desc, value=value, status=status, quality=item.flags.artifact and 6 or item:getQuality(), + reachable=reachable, } local search_key if not inside_containers and is_container(item) then @@ -518,18 +535,16 @@ function AssignItems:cache_choices(inside_containers) end function AssignItems:get_choices() - local raw_choices = self:cache_choices(self.subviews.inside_containers:getOptionValue()) + local raw_choices = self:cache_choices(self.subviews.inside_containers:getOptionValue(), self.bld) local choices = {} + local include_unreachable = not self.subviews.hide_unreachable:getOptionValue() local include_forbidden = not self.subviews.hide_forbidden:getOptionValue() local min_quality = self.subviews.min_quality:getOptionValue() local max_quality = self.subviews.max_quality:getOptionValue() for _,choice in ipairs(raw_choices) do local data = choice.data - if not include_forbidden then - if data.item.flags.forbid then - goto continue - end - end + if not include_unreachable and not data.reachable then goto continue end + if not include_forbidden and data.item.flags.forbid then goto continue end if min_quality > data.quality then goto continue end if max_quality < data.quality then goto continue end table.insert(choices, choice) From 7911f758979f1a8e9bf4d2ca893a3c1d9c9a43aa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 1 Oct 2023 12:56:21 -0700 Subject: [PATCH 551/732] adjust hide-tutorials to new embark message behavior --- hide-tutorials.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hide-tutorials.lua b/hide-tutorials.lua index f31a6072f2..c1fac3a980 100644 --- a/hide-tutorials.lua +++ b/hide-tutorials.lua @@ -23,10 +23,18 @@ local function close_help() end function skip_tutorial_prompt(scr) - if help.open and help.context == df.help_context_type.EMBARK_TUTORIAL_CHOICE then + if not help.open then return end + local mouse_y = 23 + if help.context == df.help_context_type.EMBARK_TUTORIAL_CHOICE then help.context = df.help_context_type.EMBARK_MESSAGE + -- dialog behavior changes for the button click, but the button is still + -- in the "tutorial choice" button position + mouse_y = 18 + end + if help.context == df.help_context_type.EMBARK_MESSAGE then df.global.gps.mouse_x = df.global.gps.dimx // 2 - df.global.gps.mouse_y = 18 + df.global.gps.mouse_y = mouse_y + df.global.enabler.tracking_on = 1 df.global.enabler.mouse_lbut = 1 df.global.enabler.mouse_lbut_down = 1 gui.simulateInput(scr, '_MOUSE_L') From bcfbfe51ba2256b0cfe3f172f51dea29d370cd82 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 1 Oct 2023 13:34:51 -0700 Subject: [PATCH 552/732] bump changelog to 50.11-r1 --- changelog.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index ada35e9866..f7a2e75445 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 50.11-r1 + ## New Tools - `startdwarf`: (reinstated) set number of starting dwarves @@ -40,8 +52,6 @@ Template for new versions: - `devel/inspect-screen`: display total grid size for UI and map layers - `suspendmanager`: now suspends constructions that would cave-in immediately on completion -## Removed - # 50.10-r1 ## Fixes From 1d3dd5fe2a7066e99810059b15626c05a3978b63 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 1 Oct 2023 16:29:10 -0700 Subject: [PATCH 553/732] use new centralized mouse handling --- gui/cp437-table.lua | 4 ---- gui/sandbox.lua | 2 -- hide-tutorials.lua | 3 --- 3 files changed, 9 deletions(-) diff --git a/gui/cp437-table.lua b/gui/cp437-table.lua index 0340c4dbda..667e028913 100644 --- a/gui/cp437-table.lua +++ b/gui/cp437-table.lua @@ -118,10 +118,6 @@ function CPDialog:submit() keys[i] = k end - -- ensure clicks on "submit" don't bleed through - df.global.enabler.mouse_lbut = 0 - df.global.enabler.mouse_lbut_down = 0 - local screen = self.parent_view local parent = screen._native.parent dfhack.screen.hideGuard(screen, function() diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 854173d836..cb136f41ba 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -122,7 +122,6 @@ function Sandbox:init() label="Spawn unit", on_activate=function() clear_arena_action() - gui.markMouseClicksHandled{_MOUSE_L=true} view:sendInputToParent{ARENA_CREATE_CREATURE=true} df.global.game.main_interface.arena_unit.editing_filter = true end, @@ -163,7 +162,6 @@ function Sandbox:init() label="Spawn tree", on_activate=function() clear_arena_action() - gui.markMouseClicksHandled{_MOUSE_L=true} view:sendInputToParent{ARENA_CREATE_TREE=true} df.global.game.main_interface.arena_tree.editing_filter = true end, diff --git a/hide-tutorials.lua b/hide-tutorials.lua index c1fac3a980..ef855539e4 100644 --- a/hide-tutorials.lua +++ b/hide-tutorials.lua @@ -34,9 +34,6 @@ function skip_tutorial_prompt(scr) if help.context == df.help_context_type.EMBARK_MESSAGE then df.global.gps.mouse_x = df.global.gps.dimx // 2 df.global.gps.mouse_y = mouse_y - df.global.enabler.tracking_on = 1 - df.global.enabler.mouse_lbut = 1 - df.global.enabler.mouse_lbut_down = 1 gui.simulateInput(scr, '_MOUSE_L') end end From d2ad86165e89dc3b0f262eea00db8e2347cc4421 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 1 Oct 2023 23:27:35 -0700 Subject: [PATCH 554/732] don't dismiss a nil view --- internal/caravan/trade.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 9c8650eef3..6d5f3a8d89 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -534,7 +534,7 @@ end function TradeScreen:onRenderFrame() if not df.global.game.main_interface.trade.open then - view:dismiss() + if view then view:dismiss() end elseif self.reset_pending then self.reset_pending = nil self.trade_window:reset_cache() From 3f8904fe1231444021fd180bd6aeb425d3076f00 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 05:44:04 +0000 Subject: [PATCH 555/732] [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.26.3 → 0.27.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.26.3...0.27.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a76c1f8a22..ac959adc36 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.26.3 + rev: 0.27.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 76ba6e9b5c272ef99af6ed5d1183073f32e03a7b Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Tue, 3 Oct 2023 22:53:49 +0200 Subject: [PATCH 556/732] Fix nil access of tiletype and tileblock --- changelog.txt | 1 + suspendmanager.lua | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index f7a2e75445..3c4fcc1e01 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `suspendmanager`: fix errors when constructing near the map edge ## Misc Improvements diff --git a/suspendmanager.lua b/suspendmanager.lua index a3d751d349..283d99234f 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -308,13 +308,20 @@ end --- Check if the tile can be walked on ---@param pos coord local function walkable(pos) - return dfhack.maps.getTileBlock(pos).walkable[pos.x % 16][pos.y % 16] > 0 + local tileblock = dfhack.maps.getTileBlock(pos) + return tileblock and tileblock.walkable[pos.x % 16][pos.y % 16] > 0 end --- Check if the tile is suitable tile to stand on for construction (walkable & not a tree branch) ---@param pos coord local function isSuitableAccess(pos) local tt = dfhack.maps.getTileType(pos) + + if not tt then + -- no tiletype, likely out of bound + return false + end + local attrs = df.tiletype.attrs[tt] if attrs.shape == df.tiletype_shape.BRANCH or attrs.shape == df.tiletype_shape.TRUNK_BRANCH then -- Branches can be walked on, but most of the time we can assume that it's not a suitable access. @@ -425,6 +432,7 @@ local function tileHasSupportFloor(pos) local attrs = df.tiletype.attrs[tt] if TILETYPE_SHAPE_FLOOR_SUPPORT[attrs.shape] then return true end end + return false end local function tileHasSupportBuilding(pos) From 345d63761c28232a10fe37b18a12948b607b20ea Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:16:17 -0600 Subject: [PATCH 557/732] update to reflect changes in naming --- autofish.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autofish.lua b/autofish.lua index b80c3d1b08..dc7814a1e7 100644 --- a/autofish.lua +++ b/autofish.lua @@ -73,7 +73,7 @@ end function toggle_fishing_labour(state) -- pass true to state to turn on, otherwise disable -- find all work details that have fishing enabled: - local work_details = df.global.plotinfo.hauling.work_details + local work_details = df.global.plotinfo.labor_info.work_details for _,v in pairs(work_details) do if v.allowed_labors.FISH then -- set limited to true just in case a custom work detail is being From b8c7ff4072fc09532e84a1e89ccccb710664431e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 5 Oct 2023 13:29:01 -0700 Subject: [PATCH 558/732] fix clicks not getting cleared on first handle --- changelog.txt | 1 + gui/sandbox.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 3c4fcc1e01..4722c3b630 100644 --- a/changelog.txt +++ b/changelog.txt @@ -32,6 +32,7 @@ Template for new versions: ## Fixes - `suspendmanager`: fix errors when constructing near the map edge +- `gui/sandbox`: fix scrollbar moving double distance on click ## Misc Improvements diff --git a/gui/sandbox.lua b/gui/sandbox.lua index cb136f41ba..3866fd3096 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -209,6 +209,7 @@ function Sandbox:onInput(keys) end end view:sendInputToParent(keys) + return true end function Sandbox:find_zombie_syndrome() From 9c331bdda12aedb111fdc0dc0327817274049935 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sun, 10 Sep 2023 19:28:46 -0500 Subject: [PATCH 559/732] Use pathability groups (thanks @myk002) to detect stranded citizens --- warn-stranded.lua | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 warn-stranded.lua diff --git a/warn-stranded.lua b/warn-stranded.lua new file mode 100644 index 0000000000..74e181224e --- /dev/null +++ b/warn-stranded.lua @@ -0,0 +1,46 @@ +-- Detects and alerts when a citizen is stranded +-- by Azrazalea + +-- Taken from warn-starving +local function getSexString(sex) + local sym = df.pronoun_type.attrs[sex].symbol + if not sym then + return "" + end + return "("..sym..")" +end + +function doCheck() + local grouped = {} + local citizens = dfhack.units.getCitizens() + + -- Pathability group calculation is from gui/pathable + for _, unit in pairs(citizens) do + local target = unit.pos + local block = dfhack.maps.getTileBlock(target) + local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 + local groupTable = grouped[walkGroup] + + if groupTable == nil then + grouped[walkGroup] = { unit } + else + table.insert(groupTable, unit) + end + end + + local strandedUnits = {} + + for _, units in pairs(grouped) do + if #units == 1 then + table.insert(strandedUnits, units[1]) + end + end + + print("Number of stranded: ") + print(#strandedUnits) + for _, unit in pairs(strandedUnits) do + print('['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit)) + end +end + +doCheck() From 09a3431efaffd109bcdb1ef2f0bfcf27a608679c Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sun, 10 Sep 2023 20:13:51 -0500 Subject: [PATCH 560/732] Add documentation and enable warn-stranded in control panel --- changelog.txt | 3 ++ docs/warn-stranded.rst | 27 +++++++++++++++++ gui/control-panel.lua | 3 ++ warn-stranded.lua | 68 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 docs/warn-stranded.rst diff --git a/changelog.txt b/changelog.txt index 4722c3b630..dde33631bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,9 @@ Template for new versions: ## New Features +## New Scripts +- `warn-stranded`: new repeatable maintenance script to check for stranded units, based off warn-starving + ## Fixes - `suspendmanager`: fix errors when constructing near the map edge - `gui/sandbox`: fix scrollbar moving double distance on click diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst new file mode 100644 index 0000000000..b5db2dd496 --- /dev/null +++ b/docs/warn-stranded.rst @@ -0,0 +1,27 @@ +warn-stranded +============= + +.. dfhack-tool:: + :summary: Reports citizens that are stranded and can't reach any other unit + :tags: fort units + +If any (live) units are stranded the game will pause and you'll get a warning dialog telling you +which units are isolated. This gives you a chance to rescue them before +they get overly stressed or start starving. + +You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. + +Usage +----- + +:: + + warn-stranded + +Examples +-------- + +``warn-stranded`` + +Options +------- diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 1ca6ed3b21..228dd875a2 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -134,6 +134,9 @@ local REPEATS = { ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, + ['warn-stranded']={ + desc='Show a warning dialog when units are stranded from all others.' + command={'--time', '300', '--timeUnits', 'ticks', '--command', '[', 'warn-stranded', ']'}}, ['empty-wheelbarrows']={ desc='Empties wheelbarrows which have rocks stuck in them.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, diff --git a/warn-stranded.lua b/warn-stranded.lua index 74e181224e..b08b0be080 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -1,7 +1,41 @@ -- Detects and alerts when a citizen is stranded -- by Azrazalea +-- Heavily based off of warn-starving +-- Thanks myk002 for telling me about pathability groups! +--@ module = true + +local gui = require 'gui' +local utils = require 'utils' +local widgets = require 'gui.widgets' + +warning = defclass(warning, gui.ZScreen) +warning.ATTRS = { + focus_path='warn-stranded', + force_pause=true, + pass_mouse_clicks=false, +} + +function warning:init(info) + local main = widgets.Window{ + frame={w=80, h=18}, + frame_title='Stranded Citizen Warning', + resizable=true, + autoarrange_subviews=true + } + + main:addviews{ + widgets.WrappedLabel{ + text_to_wrap=table.concat(info.messages, NEWLINE), + } + } + + self:addviews{main} +end + +function warning:onDismiss() + view = nil +end --- Taken from warn-starving local function getSexString(sex) local sym = df.pronoun_type.attrs[sex].symbol if not sym then @@ -30,17 +64,37 @@ function doCheck() local strandedUnits = {} + for _, units in pairs(grouped) do if #units == 1 then table.insert(strandedUnits, units[1]) end end - print("Number of stranded: ") - print(#strandedUnits) - for _, unit in pairs(strandedUnits) do - print('['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit)) - end + if #strandedUnits > 0 then + dfhack.color(COLOR_LIGHTMAGENTA) + + local messages = {} + local preface = "Number of stranded: "..#strandedUnits + print(dfhack.df2console(preface)) + table.insert(messages, preface) + for _, unit in pairs(strandedUnits) do + local unitString = '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) + print(dfhack.df2console(unitString)) + table.insert(messages, unitString) + end + + dfhack.color() + return warning{messages=messages}:show() + end +end + +if dfhack_flags.module then + return +end + +if not dfhack.isMapLoaded() then + qerror('warn-stranded requires a map to be loaded') end -doCheck() +view = view and view:raise() or doCheck() From 97910b8a920630b30ccbc41afb60fd73fbae29e8 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sun, 10 Sep 2023 23:17:05 -0500 Subject: [PATCH 561/732] warn-stranded: Add GUI to allow persistently ignoring units in --- docs/warn-stranded.rst | 11 ++- warn-stranded.lua | 181 +++++++++++++++++++++++++++++++++-------- 2 files changed, 158 insertions(+), 34 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index b5db2dd496..6242068f87 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -11,17 +11,24 @@ they get overly stressed or start starving. You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. +If you ignore a unit, either call ``warn-stranded clear`` in the dfhack console or if you have multiple +stranded you can toggle/clear all units in the warning dialog. + Usage ----- :: - warn-stranded + warn-stranded [clear] Examples -------- -``warn-stranded`` +``warn-stranded clear`` + Clear all ignored units and then check for ones that are stranded. Options ------- + +``clear`` + Will clear all ignored units so that warnings will be displayed again. diff --git a/warn-stranded.lua b/warn-stranded.lua index b08b0be080..5b23d1ac50 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -1,6 +1,7 @@ -- Detects and alerts when a citizen is stranded -- by Azrazalea --- Heavily based off of warn-starving +-- Logic heavily based off of warn-starving +-- GUI heavily based off of autobutcher -- Thanks myk002 for telling me about pathability groups! --@ module = true @@ -8,6 +9,16 @@ local gui = require 'gui' local utils = require 'utils' local widgets = require 'gui.widgets' +local function clear() + dfhack.persistent.delete('warnStrandedIgnore') +end + +local args = utils.invert({...}) +if args.clear then + clear() +end + + warning = defclass(warning, gui.ZScreen) warning.ATTRS = { focus_path='warn-stranded', @@ -16,24 +27,36 @@ warning.ATTRS = { } function warning:init(info) - local main = widgets.Window{ - frame={w=80, h=18}, - frame_title='Stranded Citizen Warning', - resizable=true, - autoarrange_subviews=true - } - - main:addviews{ - widgets.WrappedLabel{ - text_to_wrap=table.concat(info.messages, NEWLINE), - } + self:addviews{ + widgets.Window{ + view_id = 'main', + frame={w=80, h=18}, + frame_title='Stranded Citizen Warning', + resizable=true, + subviews = { + widgets.Label{ + frame = { l = 0, t = 0}, + text_pen = COLOR_CYAN, + text = 'Number Stranded: '..#info.units, + }, + widgets.List{ + view_id = 'list', + frame = { t = 3, b = 5 }, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + }, + widgets.Label{ + view_id = 'bottom_ui', + frame = { b = 0, h = 1 }, + text = 'filled by updateBottom()' + } + } + } } - self:addviews{main} -end - -function warning:onDismiss() - view = nil + self.units = info.units + self:initListChoices() + self:updateBottom() end local function getSexString(sex) @@ -44,6 +67,113 @@ local function getSexString(sex) return "("..sym..")" end +local function getUnitDescription(unit) + return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. + ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) +end + + +local function unitIgnored(unit) + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + if currentIgnore == nil then return false end + + local tbl = string.gmatch(currentIgnore['value'], '%d+') + local index = 1 + for id in tbl do + if tonumber(id) == unit.id then + return true, index + end + index = index + 1 + end + + return false +end + +local function toggleUnitIgnore(unit) + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + local tbl = {} + + if currentIgnore == nil then + currentIgnore = { key = 'warnStrandedIgnore' } + else + local index = 1 + for v in string.gmatch(currentIgnore['value'], '%d+') do + tbl[index] = v + index = index + 1 + end + end + + local ignored, index = unitIgnored(unit) + + if ignored then + table.remove(tbl, index) + else + table.insert(tbl, unit.id) + end + + dfhack.persistent.delete('warnStrandedIgnore') + currentIgnore.value = table.concat(tbl, ' ') + dfhack.persistent.save(currentIgnore) +end + +function warning:initListChoices() + local choices = {} + for _, unit in pairs(self.units) do + local text = '' + + dfhack.printerr('Ignored: ', unitIgnored(unit)) + + if unitIgnored(unit) then + text = '[IGNORED] ' + end + + text = text..getUnitDescription(unit) + table.insert(choices, { text = text, unit = unit }) + end + local list = self.subviews.list + list:setChoices(choices, 1) +end + +function warning:updateBottom() + self.subviews.bottom_ui:setText( + { + { key = 'SELECT', text = ': Toggle ignore unit', on_activate = self:callback('onIgnore') }, ' ', + { key = 'CUSTOM_SHIFT_I', text = ': Ignore all', on_activate = self:callback('onIgnoreAll') }, ' ', + { key = 'CUSTOM_SHIFT_C', text = ': Clear all ignored', on_activate = self:callback('onClear') }, + } + ) +end + +function warning:onIgnore() + local index, choice = self.subviews.list:getSelected() + local unit = choice.unit + + toggleUnitIgnore(unit) + self:initListChoices() +end + +function warning:onIgnoreAll() + local choices = self.subviews.list:getChoices() + + for _, choice in pairs(choices) do + if not unitIgnored(choice.unit) then + toggleUnitIgnore(choice.unit) + end + end + + self:dismiss() +end + +function warning:onClear() + clear() + self:initListChoices() + self:updateBottom() +end + +function warning:onDismiss() + view = nil +end + function doCheck() local grouped = {} local citizens = dfhack.units.getCitizens() @@ -66,26 +196,13 @@ function doCheck() for _, units in pairs(grouped) do - if #units == 1 then + if #units == 1 and not unitIgnored(units[1]) then table.insert(strandedUnits, units[1]) end end if #strandedUnits > 0 then - dfhack.color(COLOR_LIGHTMAGENTA) - - local messages = {} - local preface = "Number of stranded: "..#strandedUnits - print(dfhack.df2console(preface)) - table.insert(messages, preface) - for _, unit in pairs(strandedUnits) do - local unitString = '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) - print(dfhack.df2console(unitString)) - table.insert(messages, unitString) - end - - dfhack.color() - return warning{messages=messages}:show() + return warning{units=strandedUnits}:show() end end From 91ac082fb1486c8eaa325b63c915ee65a1e9f32c Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 00:26:37 -0500 Subject: [PATCH 562/732] Apply suggestions from code review Co-authored-by: Myk --- changelog.txt | 2 +- docs/warn-stranded.rst | 7 +------ warn-stranded.lua | 10 ++-------- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/changelog.txt b/changelog.txt index dde33631bb..d247ec6801 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,7 +31,7 @@ Template for new versions: ## New Features ## New Scripts -- `warn-stranded`: new repeatable maintenance script to check for stranded units, based off warn-starving +- `warn-stranded`: new repeatable maintenance script to check for stranded units, based off `warn-starving` ## Fixes - `suspendmanager`: fix errors when constructing near the map edge diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 6242068f87..db9fd82a1e 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -5,7 +5,7 @@ warn-stranded :summary: Reports citizens that are stranded and can't reach any other unit :tags: fort units -If any (live) units are stranded the game will pause and you'll get a warning dialog telling you +If any (live) units are stranded, the game will pause and you'll get a warning dialog telling you which units are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. @@ -27,8 +27,3 @@ Examples ``warn-stranded clear`` Clear all ignored units and then check for ones that are stranded. -Options -------- - -``clear`` - Will clear all ignored units so that warnings will be displayed again. diff --git a/warn-stranded.lua b/warn-stranded.lua index 5b23d1ac50..51834721ac 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -179,17 +179,11 @@ function doCheck() local citizens = dfhack.units.getCitizens() -- Pathability group calculation is from gui/pathable - for _, unit in pairs(citizens) do + for _, unit in ipairs(citizens) do local target = unit.pos local block = dfhack.maps.getTileBlock(target) local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - local groupTable = grouped[walkGroup] - - if groupTable == nil then - grouped[walkGroup] = { unit } - else - table.insert(groupTable, unit) - end + table.insert(ensure_key(grouped, walkGroup), unit) end local strandedUnits = {} From fa394505e3b3db5a98198a83acc299443a80f9f5 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 01:02:57 -0500 Subject: [PATCH 563/732] Manual fixes from review --- docs/warn-stranded.rst | 3 +- warn-stranded.lua | 162 ++++++++++++++++++++--------------------- 2 files changed, 78 insertions(+), 87 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index db9fd82a1e..4af7ee7b1a 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -2,7 +2,7 @@ warn-stranded ============= .. dfhack-tool:: - :summary: Reports citizens that are stranded and can't reach any other unit + :summary: Reports citizens that are stranded and can't reach any other unit. :tags: fort units If any (live) units are stranded, the game will pause and you'll get a warning dialog telling you @@ -26,4 +26,3 @@ Examples ``warn-stranded clear`` Clear all ignored units and then check for ones that are stranded. - diff --git a/warn-stranded.lua b/warn-stranded.lua index 51834721ac..ab12f2a880 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -13,58 +13,58 @@ local function clear() dfhack.persistent.delete('warnStrandedIgnore') end -local args = utils.invert({...}) -if args.clear then - clear() -end - - -warning = defclass(warning, gui.ZScreen) -warning.ATTRS = { - focus_path='warn-stranded', - force_pause=true, - pass_mouse_clicks=false, -} +warning = defclass(warning, gui.ZScreenModal) function warning:init(info) - self:addviews{ - widgets.Window{ - view_id = 'main', - frame={w=80, h=18}, - frame_title='Stranded Citizen Warning', - resizable=true, - subviews = { - widgets.Label{ - frame = { l = 0, t = 0}, - text_pen = COLOR_CYAN, - text = 'Number Stranded: '..#info.units, - }, - widgets.List{ - view_id = 'list', - frame = { t = 3, b = 5 }, - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, - }, - widgets.Label{ - view_id = 'bottom_ui', - frame = { b = 0, h = 1 }, - text = 'filled by updateBottom()' - } - } - } - } - - self.units = info.units - self:initListChoices() - self:updateBottom() + self:addviews{ + widgets.Window{ + view_id = 'main', + frame={w=80, h=18}, + frame_title='Stranded Citizen Warning', + resizable=true, + subviews = { + widgets.Label{ + frame = { l=0, t=0}, + text_pen = COLOR_CYAN, + text = 'Number Stranded: '..#info.units, + }, + widgets.List{ + view_id = 'list', + frame = { t = 3, l=0 }, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + }, + widgets.HotkeyLabel{ + frame = { b=3, l=0}, + key='SELECT', + label='Toggle Ignore', + on_activate=self:callback('onIgnore'), + }, + widgets.HotkeyLabel{ + frame = { b=2, l=0 }, + key = 'CUSTOM_SHIFT_I', + label = 'Ignore All', + on_activate = self:callback('onIgnoreAll') }, + widgets.HotkeyLabel{ + frame = { b=1, l=0 }, + key = 'CUSTOM_SHIFT_C', + label = 'Clear All Ignored', + on_activate = self:callback('onClear'), + }, + } + } + } + + self.units = info.units + self:initListChoices() end local function getSexString(sex) - local sym = df.pronoun_type.attrs[sex].symbol - if not sym then - return "" - end - return "("..sym..")" + local sym = df.pronoun_type.attrs[sex].symbol + if not sym then + return "" + end + return "("..sym..")" end local function getUnitDescription(unit) @@ -98,8 +98,8 @@ local function toggleUnitIgnore(unit) else local index = 1 for v in string.gmatch(currentIgnore['value'], '%d+') do - tbl[index] = v - index = index + 1 + tbl[index] = v + index = index + 1 end end @@ -118,11 +118,9 @@ end function warning:initListChoices() local choices = {} - for _, unit in pairs(self.units) do + for _, unit in ipairs(self.units) do local text = '' - dfhack.printerr('Ignored: ', unitIgnored(unit)) - if unitIgnored(unit) then text = '[IGNORED] ' end @@ -134,16 +132,6 @@ function warning:initListChoices() list:setChoices(choices, 1) end -function warning:updateBottom() - self.subviews.bottom_ui:setText( - { - { key = 'SELECT', text = ': Toggle ignore unit', on_activate = self:callback('onIgnore') }, ' ', - { key = 'CUSTOM_SHIFT_I', text = ': Ignore all', on_activate = self:callback('onIgnoreAll') }, ' ', - { key = 'CUSTOM_SHIFT_C', text = ': Clear all ignored', on_activate = self:callback('onClear') }, - } - ) -end - function warning:onIgnore() local index, choice = self.subviews.list:getSelected() local unit = choice.unit @@ -155,7 +143,7 @@ end function warning:onIgnoreAll() local choices = self.subviews.list:getChoices() - for _, choice in pairs(choices) do + for _, choice in ipairs(choices) do if not unitIgnored(choice.unit) then toggleUnitIgnore(choice.unit) end @@ -167,45 +155,49 @@ end function warning:onClear() clear() self:initListChoices() - self:updateBottom() end function warning:onDismiss() - view = nil + view = nil end function doCheck() - local grouped = {} - local citizens = dfhack.units.getCitizens() - - -- Pathability group calculation is from gui/pathable - for _, unit in ipairs(citizens) do - local target = unit.pos - local block = dfhack.maps.getTileBlock(target) - local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - table.insert(ensure_key(grouped, walkGroup), unit) - end + local grouped = {} + local citizens = dfhack.units.getCitizens() + + -- Pathability group calculation is from gui/pathable + for _, unit in ipairs(citizens) do + local target = xyz2pos(dfhack.units.getPosition(unit)) + local block = dfhack.maps.getTileBlock(target) + local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 + table.insert(ensure_key(grouped, walkGroup), unit) + end - local strandedUnits = {} + local strandedUnits = {} - for _, units in pairs(grouped) do - if #units == 1 and not unitIgnored(units[1]) then - table.insert(strandedUnits, units[1]) - end - end + for _, units in pairs(grouped) do + if #units == 1 and not unitIgnored(units[1]) then + table.insert(strandedUnits, units[1]) + end + end - if #strandedUnits > 0 then - return warning{units=strandedUnits}:show() + if #strandedUnits > 0 then + return warning{units=strandedUnits}:show() end end if dfhack_flags.module then - return + return end if not dfhack.isMapLoaded() then - qerror('warn-stranded requires a map to be loaded') + qerror('warn-stranded requires a map to be loaded') +end + +local args = utils.invert({...}) +if args.clear then + clear() end view = view and view:raise() or doCheck() From 3850f5849eeaf0f88cb99cc9fbd3cb4b03cae112 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 18:21:11 -0500 Subject: [PATCH 564/732] Second round review fixes --- gui/control-panel.lua | 2 +- warn-stranded.lua | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 228dd875a2..c4cccc281d 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -135,7 +135,7 @@ local REPEATS = { desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, ['warn-stranded']={ - desc='Show a warning dialog when units are stranded from all others.' + desc='Show a warning dialog when units are stranded from all others.', command={'--time', '300', '--timeUnits', 'ticks', '--command', '[', 'warn-stranded', ']'}}, ['empty-wheelbarrows']={ desc='Empties wheelbarrows which have rocks stuck in them.', diff --git a/warn-stranded.lua b/warn-stranded.lua index ab12f2a880..ee513dc559 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -1,8 +1,6 @@ -- Detects and alerts when a citizen is stranded --- by Azrazalea -- Logic heavily based off of warn-starving -- GUI heavily based off of autobutcher --- Thanks myk002 for telling me about pathability groups! --@ module = true local gui = require 'gui' From 1e38a3a7131ea96edbc003b160668a55200faee2 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 18:21:50 -0500 Subject: [PATCH 565/732] Fix indentation --- warn-stranded.lua | 268 +++++++++++++++++++++++----------------------- 1 file changed, 134 insertions(+), 134 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index ee513dc559..6288fad5e2 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,194 +8,194 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local function clear() - dfhack.persistent.delete('warnStrandedIgnore') + dfhack.persistent.delete('warnStrandedIgnore') end warning = defclass(warning, gui.ZScreenModal) function warning:init(info) - self:addviews{ - widgets.Window{ - view_id = 'main', - frame={w=80, h=18}, - frame_title='Stranded Citizen Warning', - resizable=true, - subviews = { - widgets.Label{ - frame = { l=0, t=0}, - text_pen = COLOR_CYAN, - text = 'Number Stranded: '..#info.units, - }, - widgets.List{ - view_id = 'list', - frame = { t = 3, l=0 }, - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, - }, - widgets.HotkeyLabel{ - frame = { b=3, l=0}, - key='SELECT', - label='Toggle Ignore', - on_activate=self:callback('onIgnore'), - }, - widgets.HotkeyLabel{ - frame = { b=2, l=0 }, - key = 'CUSTOM_SHIFT_I', - label = 'Ignore All', - on_activate = self:callback('onIgnoreAll') }, - widgets.HotkeyLabel{ - frame = { b=1, l=0 }, - key = 'CUSTOM_SHIFT_C', - label = 'Clear All Ignored', - on_activate = self:callback('onClear'), - }, - } - } - } - - self.units = info.units - self:initListChoices() + self:addviews{ + widgets.Window{ + view_id = 'main', + frame={w=80, h=18}, + frame_title='Stranded Citizen Warning', + resizable=true, + subviews = { + widgets.Label{ + frame = { l=0, t=0}, + text_pen = COLOR_CYAN, + text = 'Number Stranded: '..#info.units, + }, + widgets.List{ + view_id = 'list', + frame = { t = 3, l=0 }, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + }, + widgets.HotkeyLabel{ + frame = { b=3, l=0}, + key='SELECT', + label='Toggle Ignore', + on_activate=self:callback('onIgnore'), + }, + widgets.HotkeyLabel{ + frame = { b=2, l=0 }, + key = 'CUSTOM_SHIFT_I', + label = 'Ignore All', + on_activate = self:callback('onIgnoreAll') }, + widgets.HotkeyLabel{ + frame = { b=1, l=0 }, + key = 'CUSTOM_SHIFT_C', + label = 'Clear All Ignored', + on_activate = self:callback('onClear'), + }, + } + } + } + + self.units = info.units + self:initListChoices() end local function getSexString(sex) - local sym = df.pronoun_type.attrs[sex].symbol - if not sym then - return "" - end - return "("..sym..")" + local sym = df.pronoun_type.attrs[sex].symbol + if not sym then + return "" + end + return "("..sym..")" end local function getUnitDescription(unit) - return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. - ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) + return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. + ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) end local function unitIgnored(unit) - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - if currentIgnore == nil then return false end - - local tbl = string.gmatch(currentIgnore['value'], '%d+') - local index = 1 - for id in tbl do - if tonumber(id) == unit.id then - return true, index - end - index = index + 1 - end - - return false + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + if currentIgnore == nil then return false end + + local tbl = string.gmatch(currentIgnore['value'], '%d+') + local index = 1 + for id in tbl do + if tonumber(id) == unit.id then + return true, index + end + index = index + 1 + end + + return false end local function toggleUnitIgnore(unit) - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - local tbl = {} - - if currentIgnore == nil then - currentIgnore = { key = 'warnStrandedIgnore' } - else - local index = 1 - for v in string.gmatch(currentIgnore['value'], '%d+') do - tbl[index] = v - index = index + 1 - end - end - - local ignored, index = unitIgnored(unit) - - if ignored then - table.remove(tbl, index) - else - table.insert(tbl, unit.id) - end - - dfhack.persistent.delete('warnStrandedIgnore') - currentIgnore.value = table.concat(tbl, ' ') - dfhack.persistent.save(currentIgnore) + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + local tbl = {} + + if currentIgnore == nil then + currentIgnore = { key = 'warnStrandedIgnore' } + else + local index = 1 + for v in string.gmatch(currentIgnore['value'], '%d+') do + tbl[index] = v + index = index + 1 + end + end + + local ignored, index = unitIgnored(unit) + + if ignored then + table.remove(tbl, index) + else + table.insert(tbl, unit.id) + end + + dfhack.persistent.delete('warnStrandedIgnore') + currentIgnore.value = table.concat(tbl, ' ') + dfhack.persistent.save(currentIgnore) end function warning:initListChoices() - local choices = {} - for _, unit in ipairs(self.units) do - local text = '' - - if unitIgnored(unit) then - text = '[IGNORED] ' - end - - text = text..getUnitDescription(unit) - table.insert(choices, { text = text, unit = unit }) - end - local list = self.subviews.list - list:setChoices(choices, 1) + local choices = {} + for _, unit in ipairs(self.units) do + local text = '' + + if unitIgnored(unit) then + text = '[IGNORED] ' + end + + text = text..getUnitDescription(unit) + table.insert(choices, { text = text, unit = unit }) + end + local list = self.subviews.list + list:setChoices(choices, 1) end function warning:onIgnore() - local index, choice = self.subviews.list:getSelected() - local unit = choice.unit + local index, choice = self.subviews.list:getSelected() + local unit = choice.unit - toggleUnitIgnore(unit) - self:initListChoices() + toggleUnitIgnore(unit) + self:initListChoices() end function warning:onIgnoreAll() - local choices = self.subviews.list:getChoices() + local choices = self.subviews.list:getChoices() - for _, choice in ipairs(choices) do - if not unitIgnored(choice.unit) then - toggleUnitIgnore(choice.unit) - end - end + for _, choice in ipairs(choices) do + if not unitIgnored(choice.unit) then + toggleUnitIgnore(choice.unit) + end + end - self:dismiss() + self:dismiss() end function warning:onClear() - clear() - self:initListChoices() + clear() + self:initListChoices() end function warning:onDismiss() - view = nil + view = nil end function doCheck() - local grouped = {} - local citizens = dfhack.units.getCitizens() + local grouped = {} + local citizens = dfhack.units.getCitizens() - -- Pathability group calculation is from gui/pathable - for _, unit in ipairs(citizens) do - local target = xyz2pos(dfhack.units.getPosition(unit)) - local block = dfhack.maps.getTileBlock(target) - local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - table.insert(ensure_key(grouped, walkGroup), unit) - end + -- Pathability group calculation is from gui/pathable + for _, unit in ipairs(citizens) do + local target = xyz2pos(dfhack.units.getPosition(unit)) + local block = dfhack.maps.getTileBlock(target) + local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 + table.insert(ensure_key(grouped, walkGroup), unit) + end - local strandedUnits = {} + local strandedUnits = {} - for _, units in pairs(grouped) do - if #units == 1 and not unitIgnored(units[1]) then - table.insert(strandedUnits, units[1]) - end - end + for _, units in pairs(grouped) do + if #units == 1 and not unitIgnored(units[1]) then + table.insert(strandedUnits, units[1]) + end + end - if #strandedUnits > 0 then - return warning{units=strandedUnits}:show() - end + if #strandedUnits > 0 then + return warning{units=strandedUnits}:show() + end end if dfhack_flags.module then - return + return end if not dfhack.isMapLoaded() then - qerror('warn-stranded requires a map to be loaded') + qerror('warn-stranded requires a map to be loaded') end local args = utils.invert({...}) if args.clear then - clear() + clear() end view = view and view:raise() or doCheck() From d3b7828036782f9b38e2e1c23d311535f6960bd6 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 21:06:49 -0500 Subject: [PATCH 566/732] Refactor: Main group is biggest group, lists all stranded groups --- warn-stranded.lua | 142 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 114 insertions(+), 28 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 6288fad5e2..5e331def13 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -21,39 +21,40 @@ function warning:init(info) frame_title='Stranded Citizen Warning', resizable=true, subviews = { - widgets.Label{ - frame = { l=0, t=0}, - text_pen = COLOR_CYAN, - text = 'Number Stranded: '..#info.units, - }, widgets.List{ view_id = 'list', - frame = { t = 3, l=0 }, + frame = { t = 1, l=0 }, text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, }, widgets.HotkeyLabel{ - frame = { b=3, l=0}, + frame = { b=4, l=0}, key='SELECT', label='Toggle Ignore', on_activate=self:callback('onIgnore'), }, widgets.HotkeyLabel{ - frame = { b=2, l=0 }, + frame = { b=3, l=0 }, key = 'CUSTOM_SHIFT_I', label = 'Ignore All', on_activate = self:callback('onIgnoreAll') }, widgets.HotkeyLabel{ - frame = { b=1, l=0 }, + frame = { b=2, l=0 }, key = 'CUSTOM_SHIFT_C', label = 'Clear All Ignored', on_activate = self:callback('onClear'), }, + widgets.HotkeyLabel{ + frame = { b=1, l=0}, + key = 'CUSTOM_Z', + label = 'Zoom to unit', + on_activate = self:callback('onZoom'), + } } } } - self.units = info.units + self.groups = info.groups self:initListChoices() end @@ -116,23 +117,35 @@ end function warning:initListChoices() local choices = {} - for _, unit in ipairs(self.units) do - local text = '' - if unitIgnored(unit) then - text = '[IGNORED] ' + for groupIndex, group in ipairs(self.groups) do + local groupDesignation = nil + + if group['mainGroup'] then + groupDesignation = ' (Main Group)' + else + groupDesignation = ' (Group '..groupIndex..')' end - text = text..getUnitDescription(unit) - table.insert(choices, { text = text, unit = unit }) + for _, unit in ipairs(group['units']) do + local text = '' + + if unitIgnored(unit) then + text = '[IGNORED] ' + end + + text = text..getUnitDescription(unit)..groupDesignation + table.insert(choices, { text = text, data = {unit = unit, group = index} }) + end end + local list = self.subviews.list list:setChoices(choices, 1) end function warning:onIgnore() local index, choice = self.subviews.list:getSelected() - local unit = choice.unit + local unit = choice.data['unit'] toggleUnitIgnore(unit) self:initListChoices() @@ -142,8 +155,8 @@ function warning:onIgnoreAll() local choices = self.subviews.list:getChoices() for _, choice in ipairs(choices) do - if not unitIgnored(choice.unit) then - toggleUnitIgnore(choice.unit) + if not unitIgnored(choice.data['unit']) then + toggleUnitIgnore(choice.data['unit']) end end @@ -155,33 +168,106 @@ function warning:onClear() self:initListChoices() end +function warning:onZoom() + local index, choice = self.subviews.list:getSelected() + local unit = choice.data['unit'] + + local target = xyz2pos(dfhack.units.getPosition(unit)) + dfhack.gui.revealInDwarfmodeMap(target, true) +end + function warning:onDismiss() view = nil end -function doCheck() - local grouped = {} +local function compareGroups(group_one, group_two) + return #group_one['units'] > #group_two['units'] +end + +local function getStrandedUnits() + local grouped = { n = 0 } local citizens = dfhack.units.getCitizens() + -- Don't use ignored units to determine if there are any stranded units + -- but keep them to display later + local ignoredGroup = {} + -- Pathability group calculation is from gui/pathable for _, unit in ipairs(citizens) do local target = xyz2pos(dfhack.units.getPosition(unit)) local block = dfhack.maps.getTileBlock(target) local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - table.insert(ensure_key(grouped, walkGroup), unit) + + if unitIgnored(unit) then + table.insert(ensure_key(ignoredGroup, walkGroup), unit) + else + table.insert(ensure_key(grouped, walkGroup), unit) + grouped['n'] = grouped['n'] + 1 + end + end + + -- No one is stranded, so stop here + if grouped['n'] <= 1 then + return false, {} + end + + -- We needed the table for easy grouping + -- Now let us get an array so we can sort easily + local rawGroups = {} + for index, units in pairs(grouped) do + if not (index == 'n') then + table.insert(rawGroups, { units = units, walkGroup = index }) + end end - local strandedUnits = {} + -- This data structure is super easy to sort from biggest to smallest + -- Our group number is just the array index and is sorted for us + table.sort(rawGroups, compareGroups) + + -- The biggest group is not stranded + mainGroup = rawGroups[1]['walkGroup'] + table.remove(rawGroups, 1) + -- Merge ignoredGroup with grouped + for index, units in pairs(ignoredGroup) do + local groupIndex = nil + + -- Handle ignored units in mainGroup by shifting other groups down + -- We need to list them so they can be toggled + if index == mainGroup then + table.insert(rawGroups, 1, { units = {}, walkGroup = mainGroup, mainGroup = true }) + groupIndex = 1 + end - for _, units in pairs(grouped) do - if #units == 1 and not unitIgnored(units[1]) then - table.insert(strandedUnits, units[1]) + -- Find matching group + for i, group in ipairs(rawGroups) do + if group[walkGroup] == index then + groupIndex = i + end + end + + -- No matching group + if groupIndex == nil then + table.insert(rawGroups, { units = {}, walkGroup = index }) + end + + -- Put all the units in the appropriate group + for _, unit in ipairs(units) do + table.insert(rawGroups[groupIndex]['units'], unit) end end - if #strandedUnits > 0 then - return warning{units=strandedUnits}:show() + -- Key = group number (not pathability group number) + -- Value = { units = , walkGroup = , mainGroup = } + return true, rawGroups +end + + +function doCheck() + local result, strandedGroups = getStrandedUnits() + + if result then + return warning{groups=strandedGroups}:show() end end From 07a2bfb4d94536f6196b407cc7e8ac0373bbd651 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 22:37:13 -0500 Subject: [PATCH 567/732] Bugfix: Use coherent method to determine if no stranded units --- warn-stranded.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 5e331def13..cca630e1a9 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -185,7 +185,8 @@ local function compareGroups(group_one, group_two) end local function getStrandedUnits() - local grouped = { n = 0 } + local groupCount = 0 + local grouped = {} local citizens = dfhack.units.getCitizens() -- Don't use ignored units to determine if there are any stranded units @@ -202,12 +203,14 @@ local function getStrandedUnits() table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) - grouped['n'] = grouped['n'] + 1 + if #grouped[walkGroup] == 1 then + groupCount = groupCount + 1 + end end end -- No one is stranded, so stop here - if grouped['n'] <= 1 then + if groupCount <= 1 then return false, {} end From f08f09e04e4577ccb0325fb650e5d409879ac2d2 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 15 Sep 2023 01:23:44 -0500 Subject: [PATCH 568/732] Add command-line status command with ids and walkGroup options This duplicates too much code and needs a documentation update but it works --- warn-stranded.lua | 71 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index cca630e1a9..ad426d8c73 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -211,16 +211,14 @@ local function getStrandedUnits() -- No one is stranded, so stop here if groupCount <= 1 then - return false, {} + return false, ignoredGroup end -- We needed the table for easy grouping -- Now let us get an array so we can sort easily local rawGroups = {} for index, units in pairs(grouped) do - if not (index == 'n') then - table.insert(rawGroups, { units = units, walkGroup = index }) - end + table.insert(rawGroups, { units = units, walkGroup = index }) end -- This data structure is super easy to sort from biggest to smallest @@ -244,7 +242,7 @@ local function getStrandedUnits() -- Find matching group for i, group in ipairs(rawGroups) do - if group[walkGroup] == index then + if group.walkGroup == index then groupIndex = i end end @@ -252,6 +250,7 @@ local function getStrandedUnits() -- No matching group if groupIndex == nil then table.insert(rawGroups, { units = {}, walkGroup = index }) + groupIndex = #rawGroups end -- Put all the units in the appropriate group @@ -283,8 +282,68 @@ if not dfhack.isMapLoaded() then end local args = utils.invert({...}) -if args.clear then + +if args.clear or args.all then clear() end +if args.status then + local result, strandedGroups = getStrandedUnits() + + if not result then + print('No citizens are currently stranded.') + + -- We have some ignored citizens + if not (next(strandedGroups) == nil) then + print('\nIgnored citizens:') + + for walkGroup, units in pairs(strandedGroups) do + for _, unit in ipairs(units) do + local text = '' + + if args.ids then + text = text..'|'..unit.id..'| ' + end + + text = text..getUnitDescription(unit)..' {'..walkGroup..'}' + print(text) + end + end + end + + return false + end + + for groupIndex, group in ipairs(strandedGroups) do + local groupDesignation = nil + + if group['mainGroup'] then + groupDesignation = ' (Main Group)' + else + groupDesignation = ' (Group '..groupIndex..')' + end + + if args.walk_groups then + groupDesignation = groupDesignation..' {'..group.walkGroup..'}' + end + + for _, unit in ipairs(group['units']) do + local text = '' + + if unitIgnored(unit) then + text = '[IGNORED] ' + end + + if args.ids then + text = text..'|'..unit.id..'| ' + end + + text = text..getUnitDescription(unit)..groupDesignation + print(text) + end + end + + return true +end + view = view and view:raise() or doCheck() From 15807043f43c89deda7190e0de7350e0f9251418 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 15 Sep 2023 01:31:13 -0500 Subject: [PATCH 569/732] Flip order to be ascending by group size This means the ones that are "most stranded" will be near the top --- warn-stranded.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index ad426d8c73..9283c2cd0b 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -181,7 +181,7 @@ function warning:onDismiss() end local function compareGroups(group_one, group_two) - return #group_one['units'] > #group_two['units'] + return #group_one['units'] < #group_two['units'] end local function getStrandedUnits() @@ -227,7 +227,7 @@ local function getStrandedUnits() -- The biggest group is not stranded mainGroup = rawGroups[1]['walkGroup'] - table.remove(rawGroups, 1) + table.remove(rawGroups, #rawGroups) -- Merge ignoredGroup with grouped for index, units in pairs(ignoredGroup) do From ceb7413a615f2f8e3378641df14153ec796ff78b Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 15 Sep 2023 03:00:38 -0500 Subject: [PATCH 570/732] Refactor to better reuse code and organize --- warn-stranded.lua | 266 +++++++++++++++++++++++++++------------------- 1 file changed, 158 insertions(+), 108 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 9283c2cd0b..74412222ce 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -6,11 +6,125 @@ local gui = require 'gui' local utils = require 'utils' local widgets = require 'gui.widgets' +local args = nil +-- =============================================== +-- Utility Functions +-- =============================================== + +-- Clear the ignore list local function clear() dfhack.persistent.delete('warnStrandedIgnore') end +-- Taken from warn-starving +local function getSexString(sex) + local sym = df.pronoun_type.attrs[sex].symbol + + if sym then + return "("..sym..")" + else + return "" + end +end + +-- Partially taken from warn-starving +local function getUnitDescription(unit) + return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. + ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) +end + +-- Use group data, index, and command arguments to generate a group +-- designation string. +local function getGroupDesignation(group, groupIndex) + local groupDesignation = '' + + if group['mainGroup'] then + groupDesignation = ' (Main Group)' + else + groupDesignation = ' (Group '..groupIndex..')' + end + + if args.walk_groups then + groupDesignation = groupDesignation..' {'..group.walkGroup..'}' + end + + return groupDesignation +end + +-- Check for and potentially add unit.id to text. Controlled by command args. +local function addId(text, unit) + if args.ids then + return text..'|'..unit.id..'| ' + else + return text + end +end + +-- Uses persistent API. Low-level, deserializes 'warnStrandedIgnored' key and +-- will return an initialized empty warnStrandedIgnored table if needed. +-- Performance characterstics unknown of persistent API +local function deserializeIgnoredUnits() + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + if currentIgnore == nil then return {} end + + local tbl = {} + + for v in string.gmatch(currentIgnore['value'], '%d+') do + table.insert(tbl, v) + end + + return tbl +end + +-- Uses persistent API. Deserializes 'warnStrandedIgnore' key to determine if unit is ignored +-- deserializedIgnores is optional but allows us to only call deserialize once like an explicit cache. +local function unitIgnored(unit, deserializedIgnores) + local ignores = deserializedIgnores or deserializeIgnoredUnits() + + for index, id in ipairs(ignores) do + if tonumber(id) == unit.id then + return true, index + end + end + + return false +end + +-- Check for and potentially add [IGNORED] to text. Controlled by command args. +-- Optional deserializedIgnores allows us to call deserialize once for a group of operations +local function addIgnored(text, unit, deserializedIgnores) + if unitIgnored(unit, deserializedIgnores) then + return text..'[IGNORED] ' + end + + return text +end + +-- Uses persistent API. Toggles a unit's ignored status by deserializing 'warnStrandedIgnore' key +-- then serializing the resulting table after the toggle. +-- Optional cache parameter could affect data integrity. Make sure you don't need data reloaded +-- before using it. Calling several times in a row can use the return result of the function +-- as input to the next call. +local function toggleUnitIgnore(unit, deserializedIgnores) + local ignores = deserializedIgnores or deserializeIgnoredUnits() + local is_ignored, index = unitIgnored(unit, ignores) + + if is_ignored then + table.remove(ignores, index) + else + table.insert(ignores, unit.id) + end + + dfhack.persistent.delete('warnStrandedIgnore') + dfhack.persistent.save({key = 'warnStrandedIgnore', value = table.concat(ignores, ' ')}) + + return ignores +end + +-- =============================================================== +-- Graphical Interface +-- =============================================================== warning = defclass(warning, gui.ZScreenModal) function warning:init(info) @@ -58,83 +172,21 @@ function warning:init(info) self:initListChoices() end -local function getSexString(sex) - local sym = df.pronoun_type.attrs[sex].symbol - if not sym then - return "" - end - return "("..sym..")" -end - -local function getUnitDescription(unit) - return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. - ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) -end - - -local function unitIgnored(unit) - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - if currentIgnore == nil then return false end - - local tbl = string.gmatch(currentIgnore['value'], '%d+') - local index = 1 - for id in tbl do - if tonumber(id) == unit.id then - return true, index - end - index = index + 1 - end - - return false -end - -local function toggleUnitIgnore(unit) - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - local tbl = {} - - if currentIgnore == nil then - currentIgnore = { key = 'warnStrandedIgnore' } - else - local index = 1 - for v in string.gmatch(currentIgnore['value'], '%d+') do - tbl[index] = v - index = index + 1 - end - end - - local ignored, index = unitIgnored(unit) - - if ignored then - table.remove(tbl, index) - else - table.insert(tbl, unit.id) - end - - dfhack.persistent.delete('warnStrandedIgnore') - currentIgnore.value = table.concat(tbl, ' ') - dfhack.persistent.save(currentIgnore) -end function warning:initListChoices() local choices = {} for groupIndex, group in ipairs(self.groups) do - local groupDesignation = nil - - if group['mainGroup'] then - groupDesignation = ' (Main Group)' - else - groupDesignation = ' (Group '..groupIndex..')' - end + local groupDesignation = getGroupDesignation(group, groupIndex) + local ignoresCache = deserializeIgnoredUnits() for _, unit in ipairs(group['units']) do local text = '' - if unitIgnored(unit) then - text = '[IGNORED] ' - end - + text = addIgnored(text, unit, ignoresCache) + text = addId(text, unit) text = text..getUnitDescription(unit)..groupDesignation + table.insert(choices, { text = text, data = {unit = unit, group = index} }) end end @@ -153,10 +205,12 @@ end function warning:onIgnoreAll() local choices = self.subviews.list:getChoices() + local ignoresCache = deserializeIgnoredUnits() for _, choice in ipairs(choices) do - if not unitIgnored(choice.data['unit']) then - toggleUnitIgnore(choice.data['unit']) + -- We don't want to flip ignored units to unignored + if not unitIgnored(choice.data['unit'], ignoresCache) then + ignoresCache = toggleUnitIgnore(choice.data['unit'], ignoresCache) end end @@ -180,6 +234,10 @@ function warning:onDismiss() view = nil end +-- ====================================================================== +-- Core Logic +-- ====================================================================== + local function compareGroups(group_one, group_two) return #group_one['units'] < #group_two['units'] end @@ -192,6 +250,7 @@ local function getStrandedUnits() -- Don't use ignored units to determine if there are any stranded units -- but keep them to display later local ignoredGroup = {} + local ignoresCache = deserializeIgnoredUnits() -- Pathability group calculation is from gui/pathable for _, unit in ipairs(citizens) do @@ -199,10 +258,12 @@ local function getStrandedUnits() local block = dfhack.maps.getTileBlock(target) local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - if unitIgnored(unit) then + if unitIgnored(unit, ignoresCache) then table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) + + -- Count each new group if #grouped[walkGroup] == 1 then groupCount = groupCount + 1 end @@ -281,69 +342,58 @@ if not dfhack.isMapLoaded() then qerror('warn-stranded requires a map to be loaded') end -local args = utils.invert({...}) +-- ========================================================================= +-- Command Line Interface +-- ========================================================================= -if args.clear or args.all then +args = utils.invert({...}) + +if args.clear then clear() end if args.status then local result, strandedGroups = getStrandedUnits() - if not result then - print('No citizens are currently stranded.') + if result then + local ignoresCache = deserializeIgnoredUnits() - -- We have some ignored citizens - if not (next(strandedGroups) == nil) then - print('\nIgnored citizens:') + for groupIndex, group in ipairs(strandedGroups) do + local groupDesignation = getGroupDesignation(group, groupIndex) - for walkGroup, units in pairs(strandedGroups) do - for _, unit in ipairs(units) do - local text = '' + for _, unit in ipairs(group['units']) do + local text = '' - if args.ids then - text = text..'|'..unit.id..'| ' - end + text = addIgnored(text, unit, ignoresCache) + text = addId(text, unit) - text = text..getUnitDescription(unit)..' {'..walkGroup..'}' - print(text) - end + print(text..getUnitDescription(unit)..groupDesignation) end end - return false + return true end - for groupIndex, group in ipairs(strandedGroups) do - local groupDesignation = nil - if group['mainGroup'] then - groupDesignation = ' (Main Group)' - else - groupDesignation = ' (Group '..groupIndex..')' - end + print('No citizens are currently stranded.') - if args.walk_groups then - groupDesignation = groupDesignation..' {'..group.walkGroup..'}' - end + -- We have some ignored citizens + if not (next(strandedGroups) == nil) then + print('\nIgnored citizens:') - for _, unit in ipairs(group['units']) do - local text = '' + for walkGroup, units in pairs(strandedGroups) do + for _, unit in ipairs(units) do + local text = '' - if unitIgnored(unit) then - text = '[IGNORED] ' - end + text = addId(text, unit) - if args.ids then - text = text..'|'..unit.id..'| ' + text = text..getUnitDescription(unit)..' {'..walkGroup..'}' + print(text) end - - text = text..getUnitDescription(unit)..groupDesignation - print(text) end end - return true + return false end view = view and view:raise() or doCheck() From e2cf30f9ae6d3d95c168b374c7d4be4a0add2eb5 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 18 Sep 2023 00:35:28 -0500 Subject: [PATCH 571/732] Add ignore and unignore command line commands --- warn-stranded.lua | 140 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 134 insertions(+), 6 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 74412222ce..a4ac34792f 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -6,7 +6,9 @@ local gui = require 'gui' local utils = require 'utils' local widgets = require 'gui.widgets' -local args = nil +local argparse = require 'argparse' +local args = {...} +local args_walk_groups, args_ids, args_clear, args_group = false, false, false, false -- =============================================== -- Utility Functions @@ -45,7 +47,7 @@ local function getGroupDesignation(group, groupIndex) groupDesignation = ' (Group '..groupIndex..')' end - if args.walk_groups then + if args_walk_groups then groupDesignation = groupDesignation..' {'..group.walkGroup..'}' end @@ -54,7 +56,7 @@ end -- Check for and potentially add unit.id to text. Controlled by command args. local function addId(text, unit) - if args.ids then + if args_ids then return text..'|'..unit.id..'| ' else return text @@ -325,6 +327,64 @@ local function getStrandedUnits() return true, rawGroups end +local function findCitizen(unitId) + local citizens = dfhack.units.getCitizens() + + for _, citizen in ipairs(citizens) do + if citizen.id == unitId then return citizen end + end + + return nil +end + +local function ignoreGroup(groups, groupNumber) + local ignored = deserializeIgnoredUnits() + + if groupNumber > #groups then + print('Group '..groupNumber..' does not exist') + return false + end + + if groups[groupNumber]['mainGroup'] then + print('Group '..groupNumber..' is the main group of dwarves. Not ignoring.') + return false + end + + for _, unit in ipairs(groups[groupNumber]['units']) do + if unitIgnored(unit, ignored) then + print('Unit '..unit.id..' already ignored, doing nothing to them.') + else + print('Ignoring unit '..unit.id) + toggleUnitIgnore(unit, ignored) + end + end + + return true +end + +local function unignoreGroup(groups, groupNumber) + local ignored = deserializeIgnoredUnits() + + if groupNumber > #groups then + print('Group '..groupNumber..' does not exist') + return false + end + + if group[groupNumber]['mainGroup'] then + print('Group '..groupNumber..' is the main group of dwarves. Unignoring.') + end + + for _, unit in ipairs(groups[groupNumber]['units']) do + if unitIgnored(unit, ignored) then + print('Unignoring unit '..unit.id) + toggleUnitIgnore(unit, ignored) + else + print('Unit '..unit.id..' not already ignored, doing nothing to them.') + end + end + + return true +end function doCheck() local result, strandedGroups = getStrandedUnits() @@ -346,13 +406,19 @@ end -- Command Line Interface -- ========================================================================= -args = utils.invert({...}) +local positionals = argparse.processArgsGetopt(args, { + {'w', 'walkgroups', handler=function() args_walk_groups = true end}, + {'i', 'ids', handler=function() args_ids = true end}, + {'c', 'clear', handler=function() args_clear = true end}, + {'g', 'group', handler=function() args_group = true end}, +}) -if args.clear then +if args_clear then + print('Clearing unit ignore list.') clear() end -if args.status then +if positionals[1] == 'status' then local result, strandedGroups = getStrandedUnits() if result then @@ -396,4 +462,66 @@ if args.status then return false end +if positionals[1] == 'ignore' then + local parameter = tonumber(positionals[2]) + + if parameter and not args_group then + local citizen = findCitizen(parameter) + + if citizen == nil then + print('No citizen with unit id '..parameter..' found in the fortress') + return false + + end + + if unitIgnored(citizen) then + print('Unit '..parameter..' is already ignored. You may want to use the unignore command.') + return false + end + + print('Ignoring unit '..parameter) + toggleUnitIgnore(citizen) + return true + elseif parameter and args_group then + print('Ignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() + return ignoreGroup(strandedCitizens, parameter) + else + print('Must provide unit or group id to the ignore command.') + end + + return false +end + +if positionals[1] == 'unignore' then + local parameter = tonumber(positionals[2]) + + if parameter and not args_group then + local citizen = findCitizen(parameter) + + if citizen == nil then + print('No citizen with unit id '..parameter..' found in the fortress') + return false + + end + + if unitIgnored(citizen) == false then + print('Unit '..parameter..' is not ignored. You may want to use the ignore command.') + return false + end + + print('Unignoring unit '..parameter) + toggleUnitIgnore(citizen) + return true + elseif parameter and args_group then + print('Unignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() + return unignoreGroup(strandedCitizens, parameter) + else + print('Must provide unit id to ignore command.') + end + + return false +end + view = view and view:raise() or doCheck() From 453a842f3c07bd67f1be6ffe3f28b65ad38d9265 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 19 Sep 2023 00:16:06 -0500 Subject: [PATCH 572/732] Improve GUI, now decently happy with it --- warn-stranded.lua | 57 ++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index a4ac34792f..edf928fd63 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -133,39 +133,45 @@ function warning:init(info) self:addviews{ widgets.Window{ view_id = 'main', - frame={w=80, h=18}, + frame={w=80, h=25}, + min_size={w=60, h=25}, frame_title='Stranded Citizen Warning', resizable=true, + autoarrange_subviews=true, subviews = { widgets.List{ + frame={h=15}, view_id = 'list', - frame = { t = 1, l=0 }, text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + on_submit=self:callback('onIgnore'), }, - widgets.HotkeyLabel{ - frame = { b=4, l=0}, - key='SELECT', - label='Toggle Ignore', - on_activate=self:callback('onIgnore'), + widgets.Panel{ + frame={h=5}, + autoarrange_subviews=true, + subviews = { + widgets.HotkeyLabel{ + key='SELECT', + label='Toggle Ignore', + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_SHIFT_I', + label = 'Ignore All', + on_activate = self:callback('onIgnoreAll'), + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_SHIFT_C', + label = 'Clear All Ignored', + on_activate = self:callback('onClear'), + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_Z', + label = 'Zoom to unit', + on_activate = self:callback('onZoom'), + } + } }, - widgets.HotkeyLabel{ - frame = { b=3, l=0 }, - key = 'CUSTOM_SHIFT_I', - label = 'Ignore All', - on_activate = self:callback('onIgnoreAll') }, - widgets.HotkeyLabel{ - frame = { b=2, l=0 }, - key = 'CUSTOM_SHIFT_C', - label = 'Clear All Ignored', - on_activate = self:callback('onClear'), - }, - widgets.HotkeyLabel{ - frame = { b=1, l=0}, - key = 'CUSTOM_Z', - label = 'Zoom to unit', - on_activate = self:callback('onZoom'), - } + } } } @@ -197,8 +203,7 @@ function warning:initListChoices() list:setChoices(choices, 1) end -function warning:onIgnore() - local index, choice = self.subviews.list:getSelected() +function warning:onIgnore(_, choice) local unit = choice.data['unit'] toggleUnitIgnore(unit) From f96cdfa8ce9a56083f8363456ebc9ac4ee2e3d05 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 19 Sep 2023 00:55:45 -0500 Subject: [PATCH 573/732] Add proper toggle group option to GUI only This could be added to command line but we already have ignore group and unignore group which seems to handle the use cases. --- warn-stranded.lua | 66 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index edf928fd63..bdbe7dd017 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -124,6 +124,44 @@ local function toggleUnitIgnore(unit, deserializedIgnores) return ignores end +-- Does the usual GUI pattern when groups can be in a partial state +-- Will ignore everything, unless all units in group are already ignored +-- If all units in the group are ignored, then it will unignore all of them +local function toggleGroup(groups, groupNumber) + local ignored = deserializeIgnoredUnits() + + if groupNumber > #groups then + print('Group '..groupNumber..' does not exist') + return false + end + + if groups[groupNumber]['mainGroup'] then + print('Group '..groupNumber..' is the main group of dwarves. Cannot toggle.') + return false + end + + local group = groups[groupNumber] + + local allIgnored = true + for _, unit in ipairs(group['units']) do + if not unitIgnored(unit, ignored) then + allIgnored = false + goto process + end + end + ::process:: + + for _, unit in ipairs(group['units']) do + local isIgnored = unitIgnored(unit, ignored) + + if allIgnored == isIgnored then + toggleUnitIgnore(unit, ignored) + end + end + + return true +end + -- =============================================================== -- Graphical Interface -- =============================================================== @@ -154,6 +192,11 @@ function warning:init(info) key='SELECT', label='Toggle Ignore', }, + widgets.HotkeyLabel{ + key='CUSTOM_G', + label='Toggle Group', + on_activate = self:callback('onToggleGroup'), + }, widgets.HotkeyLabel{ key = 'CUSTOM_SHIFT_I', label = 'Ignore All', @@ -195,7 +238,7 @@ function warning:initListChoices() text = addId(text, unit) text = text..getUnitDescription(unit)..groupDesignation - table.insert(choices, { text = text, data = {unit = unit, group = index} }) + table.insert(choices, { text = text, data = {unit = unit, group = groupIndex} }) end end @@ -237,6 +280,14 @@ function warning:onZoom() dfhack.gui.revealInDwarfmodeMap(target, true) end +function warning:onToggleGroup() + local index, choice = self.subviews.list:getSelected() + local group = choice.data['group'] + + toggleGroup(self.groups, group) + self:initListChoices() +end + function warning:onDismiss() view = nil end @@ -411,12 +462,13 @@ end -- Command Line Interface -- ========================================================================= -local positionals = argparse.processArgsGetopt(args, { - {'w', 'walkgroups', handler=function() args_walk_groups = true end}, - {'i', 'ids', handler=function() args_ids = true end}, - {'c', 'clear', handler=function() args_clear = true end}, - {'g', 'group', handler=function() args_group = true end}, -}) +local options = { + {'w', 'walkgroups', handler=function() args_walk_groups = true end}, + {'i', 'ids', handler=function() args_ids = true end}, + {'c', 'clear', handler=function() args_clear = true end}, + {'g', 'group', handler=function() args_group = true end}, +} +local positionals = argparse.processArgsGetopt(args, options) if args_clear then print('Clearing unit ignore list.') From 2f1a75164265919a649b32625a7c12a349d10bf7 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 19 Sep 2023 01:13:48 -0500 Subject: [PATCH 574/732] Update help --- docs/warn-stranded.rst | 43 ++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 4af7ee7b1a..0519da5458 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -5,24 +5,47 @@ warn-stranded :summary: Reports citizens that are stranded and can't reach any other unit. :tags: fort units -If any (live) units are stranded, the game will pause and you'll get a warning dialog telling you -which units are isolated. This gives you a chance to rescue them before -they get overly stressed or start starving. +If any (live) units are stranded from the main group, the game will pause and you'll get a warning dialog telling you +which units are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. -You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. +Each unit will be put into a group with the other units stranded together. + +There is a command line interface that can print status of units without pausing or bringing up a window. + +The GUI and command-line both also have the ability to ignore units so they don't trigger a pause and window. -If you ignore a unit, either call ``warn-stranded clear`` in the dfhack console or if you have multiple -stranded you can toggle/clear all units in the warning dialog. +You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. Usage ----- -:: +``warn-stranded -[wicg] [status|ignore|unignore] `` + + -w, --walkgroups: List the raw pathability walkgroup number of each unit in all views. + + -i, --ids: List the id of each unit in all views. - warn-stranded [clear] + -g, --group: Only affects ignore/unignore. Interpret positional argument as group ID and perform operation to the entire group. + + -c, --clear: Clear the entire ignore list first before doing anything else. Examples -------- -``warn-stranded clear`` - Clear all ignored units and then check for ones that are stranded. +``warn-stranded -c`` + Clear all ignored units and then check for ones that are stranded. + +``warn-stranded -wi`` + Standard GUI invocation, but list walkgroups and ids in the table. + +``warn-stranded -wic status`` + Clear all ignored units. Then list all stranded units and all ignored units. Include walkgroups and ids in the output. + +``warn-stranded ignore 1`` + Ignore unit with id 1. + +``warn-stranded ignore -g 2`` + Ignore stranded unit group 2. + +``warn-stranded unignore [-g] 1`` + Unignore unit or stranded group 1. From 6b78003e814917cadea4620450d78f3290f2cd4c Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 19 Sep 2023 01:28:06 -0500 Subject: [PATCH 575/732] Fix minor display bug from self-review --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index bdbe7dd017..63bede9c39 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -345,7 +345,7 @@ local function getStrandedUnits() table.sort(rawGroups, compareGroups) -- The biggest group is not stranded - mainGroup = rawGroups[1]['walkGroup'] + mainGroup = rawGroups[#rawGroups]['walkGroup'] table.remove(rawGroups, #rawGroups) -- Merge ignoredGroup with grouped From 4b86576c1f0b37f32c2706e2fa0e8335ff37631c Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 21:48:16 -0500 Subject: [PATCH 576/732] Update help to match new usage/commands after review --- docs/warn-stranded.rst | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 0519da5458..3232d0662e 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -5,8 +5,9 @@ warn-stranded :summary: Reports citizens that are stranded and can't reach any other unit. :tags: fort units -If any (live) units are stranded from the main group, the game will pause and you'll get a warning dialog telling you -which units are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. +If any (live) groups of units are stranded from the main (largest) group, +the game will pause and you'll get a warning dialog telling you which units are isolated. +This gives you a chance to rescue them before they get overly stressed or start starving. Each unit will be put into a group with the other units stranded together. @@ -19,33 +20,30 @@ You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Ma Usage ----- -``warn-stranded -[wicg] [status|ignore|unignore] `` +:: - -w, --walkgroups: List the raw pathability walkgroup number of each unit in all views. - - -i, --ids: List the id of each unit in all views. - - -g, --group: Only affects ignore/unignore. Interpret positional argument as group ID and perform operation to the entire group. - - -c, --clear: Clear the entire ignore list first before doing anything else. + warn-stranded + warn-stranded status + warn-stranded clear + warn-stranded (ignore|ignoregroup|unignore|unignoregroup) Examples -------- -``warn-stranded -c`` - Clear all ignored units and then check for ones that are stranded. +``warn-stranded status`` + List all stranded units and all ignored units. Includes unit ids in the output. -``warn-stranded -wi`` - Standard GUI invocation, but list walkgroups and ids in the table. - -``warn-stranded -wic status`` - Clear all ignored units. Then list all stranded units and all ignored units. Include walkgroups and ids in the output. +``warn-stranded clear`` + Clear(unignore) all ignored units. ``warn-stranded ignore 1`` Ignore unit with id 1. -``warn-stranded ignore -g 2`` +``warn-stranded ignoregroup 2`` Ignore stranded unit group 2. -``warn-stranded unignore [-g] 1`` - Unignore unit or stranded group 1. +``warn-stranded unignore 1`` + Unignore unit with id 1. + +``warn-stranded unignoregroup 3`` + Unignore stranded unit group 3. From 15d42a361cc57f23abd33d04f2d06adcacc3c305 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 22:15:09 -0500 Subject: [PATCH 577/732] Implement remaining review changes --- warn-stranded.lua | 145 +++++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 74 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 63bede9c39..d1585acb2c 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,7 +8,6 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -local args_walk_groups, args_ids, args_clear, args_group = false, false, false, false -- =============================================== -- Utility Functions @@ -38,7 +37,7 @@ end -- Use group data, index, and command arguments to generate a group -- designation string. -local function getGroupDesignation(group, groupIndex) +local function getGroupDesignation(group, groupIndex, walkGroup) local groupDesignation = '' if group['mainGroup'] then @@ -47,20 +46,16 @@ local function getGroupDesignation(group, groupIndex) groupDesignation = ' (Group '..groupIndex..')' end - if args_walk_groups then + if walkGroup then groupDesignation = groupDesignation..' {'..group.walkGroup..'}' end return groupDesignation end --- Check for and potentially add unit.id to text. Controlled by command args. +-- Add unit.id to text local function addId(text, unit) - if args_ids then - return text..'|'..unit.id..'| ' - else - return text - end + return text..'|'..unit.id..'| ' end -- Uses persistent API. Low-level, deserializes 'warnStrandedIgnored' key and @@ -93,7 +88,7 @@ local function unitIgnored(unit, deserializedIgnores) return false end --- Check for and potentially add [IGNORED] to text. Controlled by command args. +-- Check for and potentially add [IGNORED] to text. -- Optional deserializedIgnores allows us to call deserialize once for a group of operations local function addIgnored(text, unit, deserializedIgnores) if unitIgnored(unit, deserializedIgnores) then @@ -155,7 +150,7 @@ local function toggleGroup(groups, groupNumber) local isIgnored = unitIgnored(unit, ignored) if allIgnored == isIgnored then - toggleUnitIgnore(unit, ignored) + ignored = toggleUnitIgnore(unit, ignored) end end @@ -183,6 +178,9 @@ function warning:init(info) text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, on_submit=self:callback('onIgnore'), + on_select=self:callback('onZoom'), + on_double_click=self:callback('onIgnore'), + on_double_click2=self:callback('onToggleGroup'), }, widgets.Panel{ frame={h=5}, @@ -190,28 +188,27 @@ function warning:init(info) subviews = { widgets.HotkeyLabel{ key='SELECT', - label='Toggle Ignore', + label='Toggle ignore', }, widgets.HotkeyLabel{ key='CUSTOM_G', - label='Toggle Group', + label='Toggle group', on_activate = self:callback('onToggleGroup'), }, widgets.HotkeyLabel{ key = 'CUSTOM_SHIFT_I', - label = 'Ignore All', + label = 'Ignore all', on_activate = self:callback('onIgnoreAll'), }, widgets.HotkeyLabel{ key = 'CUSTOM_SHIFT_C', - label = 'Clear All Ignored', + label = 'Clear all ignored', on_activate = self:callback('onClear'), }, - widgets.HotkeyLabel{ - key = 'CUSTOM_Z', - label = 'Zoom to unit', - on_activate = self:callback('onZoom'), - } + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap='Click to ignore/unignore unit. Shift doubleclick to ignore/unignore a group of units.', + }, } }, @@ -235,7 +232,6 @@ function warning:initListChoices() local text = '' text = addIgnored(text, unit, ignoresCache) - text = addId(text, unit) text = text..getUnitDescription(unit)..groupDesignation table.insert(choices, { text = text, data = {unit = unit, group = groupIndex} }) @@ -433,7 +429,7 @@ local function unignoreGroup(groups, groupNumber) for _, unit in ipairs(groups[groupNumber]['units']) do if unitIgnored(unit, ignored) then print('Unignoring unit '..unit.id) - toggleUnitIgnore(unit, ignored) + ignored = toggleUnitIgnore(unit, ignored) else print('Unit '..unit.id..' not already ignored, doing nothing to them.') end @@ -462,19 +458,15 @@ end -- Command Line Interface -- ========================================================================= -local options = { - {'w', 'walkgroups', handler=function() args_walk_groups = true end}, - {'i', 'ids', handler=function() args_ids = true end}, - {'c', 'clear', handler=function() args_clear = true end}, - {'g', 'group', handler=function() args_group = true end}, -} local positionals = argparse.processArgsGetopt(args, options) -if args_clear then +if positionals[1] == 'clear' then print('Clearing unit ignore list.') - clear() + return clear() end +local parameter = tonumber(positionals[2]) + if positionals[1] == 'status' then local result, strandedGroups = getStrandedUnits() @@ -482,7 +474,7 @@ if positionals[1] == 'status' then local ignoresCache = deserializeIgnoredUnits() for groupIndex, group in ipairs(strandedGroups) do - local groupDesignation = getGroupDesignation(group, groupIndex) + local groupDesignation = getGroupDesignation(group, groupIndex, true) for _, unit in ipairs(group['units']) do local text = '' @@ -509,8 +501,8 @@ if positionals[1] == 'status' then local text = '' text = addId(text, unit) - text = text..getUnitDescription(unit)..' {'..walkGroup..'}' + print(text) end end @@ -520,65 +512,70 @@ if positionals[1] == 'status' then end if positionals[1] == 'ignore' then - local parameter = tonumber(positionals[2]) + if not parameter then + print('Must provide unit id to the ignore command.') + return false + end - if parameter and not args_group then - local citizen = findCitizen(parameter) + local citizen = findCitizen(parameter) - if citizen == nil then - print('No citizen with unit id '..parameter..' found in the fortress') - return false + if citizen == nil then + print('No citizen with unit id '..parameter..' found in the fortress') + return false + end - end + if unitIgnored(citizen) then + print('Unit '..parameter..' is already ignored. You may want to use the unignore command.') + return false + end - if unitIgnored(citizen) then - print('Unit '..parameter..' is already ignored. You may want to use the unignore command.') - return false - end + print('Ignoring unit '..parameter) + toggleUnitIgnore(citizen) + return true +end - print('Ignoring unit '..parameter) - toggleUnitIgnore(citizen) - return true - elseif parameter and args_group then - print('Ignoring group '..parameter) - local _, strandedCitizens = getStrandedUnits() - return ignoreGroup(strandedCitizens, parameter) - else - print('Must provide unit or group id to the ignore command.') +if positionals[1] == 'ignoregroup' then + if not parameter then + print('Must provide group id to the ignoregroup command.') end - return false + print('Ignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() + return ignoreGroup(strandedCitizens, parameter) end if positionals[1] == 'unignore' then - local parameter = tonumber(positionals[2]) + if not parameter then + print('Must provide unit id to unignore command.') + return false + end - if parameter and not args_group then - local citizen = findCitizen(parameter) + local citizen = findCitizen(parameter) - if citizen == nil then - print('No citizen with unit id '..parameter..' found in the fortress') - return false + if citizen == nil then + print('No citizen with unit id '..parameter..' found in the fortress') + return false + end - end + if unitIgnored(citizen) == false then + print('Unit '..parameter..' is not ignored. You may want to use the ignore command.') + return false + end - if unitIgnored(citizen) == false then - print('Unit '..parameter..' is not ignored. You may want to use the ignore command.') - return false - end + print('Unignoring unit '..parameter) + toggleUnitIgnore(citizen) + return true +end - print('Unignoring unit '..parameter) - toggleUnitIgnore(citizen) - return true - elseif parameter and args_group then - print('Unignoring group '..parameter) - local _, strandedCitizens = getStrandedUnits() - return unignoreGroup(strandedCitizens, parameter) - else - print('Must provide unit id to ignore command.') +if positionals[1] == 'unignoregroup' then + if not parameter then + print('Must provide group id to unignoregroup command.') + return false end - return false + print('Unignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() + return unignoreGroup(strandedCitizens, parameter) end view = view and view:raise() or doCheck() From b7ab4115c7f6d9403f1fcf17b34417787da8741e Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 22:34:23 -0500 Subject: [PATCH 578/732] Fix first round of testing bugs post review changes --- warn-stranded.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index d1585acb2c..1f1f8a40b1 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -207,7 +207,7 @@ function warning:init(info) }, widgets.WrappedLabel{ frame={b=0, l=0, r=0}, - text_to_wrap='Click to ignore/unignore unit. Shift doubleclick to ignore/unignore a group of units.', + text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', }, } }, @@ -458,7 +458,7 @@ end -- Command Line Interface -- ========================================================================= -local positionals = argparse.processArgsGetopt(args, options) +local positionals = argparse.processArgsGetopt(args, {}) if positionals[1] == 'clear' then print('Clearing unit ignore list.') From 3609a8d636c6305bfbd516dc184c8d061336310f Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 22:41:30 -0500 Subject: [PATCH 579/732] Fix typo in unignore group --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 1f1f8a40b1..1b80385ddc 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -422,7 +422,7 @@ local function unignoreGroup(groups, groupNumber) return false end - if group[groupNumber]['mainGroup'] then + if groups[groupNumber]['mainGroup'] then print('Group '..groupNumber..' is the main group of dwarves. Unignoring.') end From 81a24a850b25a195dec3fe780aaa22e2aca01bc8 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 22:56:09 -0500 Subject: [PATCH 580/732] Remove jarring selection reset on every action --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 1b80385ddc..bdb56747aa 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -239,7 +239,7 @@ function warning:initListChoices() end local list = self.subviews.list - list:setChoices(choices, 1) + list:setChoices(choices) end function warning:onIgnore(_, choice) From 71cd71885c10357cec9d264d2ea9331fe592003f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Oct 2023 11:48:06 -0700 Subject: [PATCH 581/732] use capital letter for class name --- warn-starving.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/warn-starving.lua b/warn-starving.lua index 2b08042030..225d41cfd7 100644 --- a/warn-starving.lua +++ b/warn-starving.lua @@ -27,14 +27,14 @@ if args.sane then checkOnlySane = true end -warning = defclass(warning, gui.ZScreen) -warning.ATTRS = { +Warning = defclass(Warning, gui.ZScreen) +Warning.ATTRS = { focus_path='warn-starving', force_pause=true, pass_mouse_clicks=false, } -function warning:init(info) +function Warning:init(info) local main = widgets.Window{ frame={w=80, h=18}, frame_title='Warning', @@ -51,7 +51,7 @@ function warning:init(info) self:addviews{main} end -function warning:onDismiss() +function Warning:onDismiss() view = nil end @@ -111,7 +111,7 @@ function doCheck() print(dfhack.df2console(msg)) end dfhack.color() - return warning{messages=messages}:show() + return Warning{messages=messages}:show() end end From b74a2930dfc80d21295101d1c8a3d3a5fb2eb428 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Oct 2023 16:12:50 -0700 Subject: [PATCH 582/732] clean up remove-wear code --- remove-wear.lua | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/remove-wear.lua b/remove-wear.lua index 58406a1b6d..90621b54c9 100644 --- a/remove-wear.lua +++ b/remove-wear.lua @@ -1,33 +1,21 @@ -- Reset items in your fort to 0 wear -- original author: Laggy, edited by expwnent -local help = [====[ - -remove-wear -=========== -Sets the wear on items in your fort to zero. Usage: - -:remove-wear all: - Removes wear from all items in your fort. -:remove-wear ID1 ID2 ...: - Removes wear from items with the given ID numbers. - -]====] local args = {...} local count = 0 -if args[1] == 'help' then - print(help) +if not args[1] or args[1] == 'help' or args[1] == '-h' or args[1] == '--help' then + print(dfhack.script_help()) return elseif args[1] == 'all' or args[1] == '-all' then for _, item in ipairs(df.global.world.items.all) do - if item.wear > 0 then --hint:df.item_actual + if item:getWear() > 0 then --hint:df.item_actual item:setWear(0) count = count + 1 end end else - for i, arg in ipairs(args) do + for _, arg in ipairs(args) do local item_id = tonumber(arg) if item_id then local item = df.item.find(item_id) @@ -43,4 +31,4 @@ else end end -print('remove-wear: removed wear from '..count..' objects') +print('remove-wear: removed wear from '..tostring(count)..' items') From cc238577a0d0b5e8787770e6bac87e37ed0460a7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Oct 2023 16:37:50 -0700 Subject: [PATCH 583/732] more configurability for drain-aquifer --- changelog.txt | 3 ++ docs/drain-aquifer.rst | 32 +++++++++++++++-- drain-aquifer.lua | 78 ++++++++++++++++++++++++++++-------------- gui/control-panel.lua | 1 + 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4722c3b630..1aa41616c4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,9 @@ Template for new versions: ## New Tools ## New Features +- `drain-aquifer`: gained ability to drain just above or below a certain z-level +- `drain-aquifer`: new option to drain all layers except for the first N aquifer layers, in case you want some aquifer layers but not too many +- `gui/control-panel`: ``drain-aquifer --top 2`` added as an autostart option ## Fixes - `suspendmanager`: fix errors when constructing near the map edge diff --git a/docs/drain-aquifer.rst b/docs/drain-aquifer.rst index efd2ca58f2..b614a36306 100644 --- a/docs/drain-aquifer.rst +++ b/docs/drain-aquifer.rst @@ -2,14 +2,40 @@ drain-aquifer ============= .. dfhack-tool:: - :summary: Remove all aquifers on the map. + :summary: Remove some or all aquifers on the map. :tags: fort armok map -This tool irreversibly removes all 'aquifer' tags from the map blocks. +This tool irreversibly removes 'aquifer' tags from map blocks. Also see +`prospect` for discovering the range of layers that currently have aquifers. Usage ----- :: - drain-aquifer + drain-aquifer [] + +Examples +-------- + +``drain-aquifer`` + Remove all aquifers on the map. +``drain-aquifer --top 2`` + Remove all aquifers on the map except for the top 2 levels of aquifer. +``drain-aquifer -d`` + Remove all aquifers on the current z-level and below. + +Options +------- + +``-t``, ``--top `` + Remove all aquifers on the map except for the top ```` levels, + starting from the first level that has an aquifer tile. Note that there may + be less than ```` levels of aquifer after the command is run if the + levels of aquifer are not contiguous. +``-d``, ``--zdown`` + Remove all aquifers on the current z-level and below. +``-u``, ``--zup`` + Remove all aquifers on the current z-level and above. +``-z``, ``--cur-zlevel`` + Remove all aquifers on the current z-level. diff --git a/drain-aquifer.lua b/drain-aquifer.lua index c7e3c6c98e..7e92293e39 100644 --- a/drain-aquifer.lua +++ b/drain-aquifer.lua @@ -1,40 +1,68 @@ --- Remove all aquifers from the map ---[====[ +local argparse = require('argparse') -drain-aquifer -============= -Remove all 'aquifer' tags from the map blocks. Irreversible. - -]====] +local zmin = 0 +local zmax = df.global.world.map.z_count - 1 local function drain() local layers = {} --as:bool[] local layer_count = 0 local tile_count = 0 - for k, block in ipairs(df.global.world.map.map_blocks) do - if block.flags.has_aquifer then - block.flags.has_aquifer = false - block.flags.check_aquifer = false - - for x, row in ipairs(block.designation) do - for y, tile in ipairs(row) do - if tile.water_table then - tile.water_table = false - tile_count = tile_count + 1 - end + for _, block in ipairs(df.global.world.map.map_blocks) do + if not block.flags.has_aquifer then goto continue end + if block.map_pos.z < zmin or block.map_pos.z > zmax then goto continue end + + block.flags.has_aquifer = false + block.flags.check_aquifer = false + + for _, row in ipairs(block.designation) do + for _, tile in ipairs(row) do + if tile.water_table then + tile.water_table = false + tile_count = tile_count + 1 end end + end - if not layers[block.map_pos.z] then - layers[block.map_pos.z] = true - layer_count = layer_count + 1 - end + if not layers[block.map_pos.z] then + layers[block.map_pos.z] = true + layer_count = layer_count + 1 end + ::continue:: end - print("Cleared "..tile_count.." aquifer tile"..((tile_count ~= 1) and "s" or "").. - " in "..layer_count.." layer"..((layer_count ~= 1) and "s" or "")..".") + print(('Cleared %d aquifer tile%s in %d layer%s.'):format( + tile_count, (tile_count ~= 1) and 's' or '', layer_count, (layer_count ~= 1) and 's' or '')) +end + +local help = false +local top = 0 + +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() help = true end}, + {'t', 'top', hasArg=true, handler=function(optarg) top = argparse.nonnegativeInt(optarg, 'top') end}, + {'d', 'zdown', handler=function() zmax = df.global.window_z end}, + {'u', 'zup', handler=function() zmin = df.global.window_z end}, + {'z', 'cur-zlevel', handler=function() zmax, zmin = df.global.window_z, df.global.window_z end}, +}) + +if help or positionals[1] == 'help' then + print(dfhack.script_help()) + return +end + +if top > 0 then + zmax = -1 + for _, block in ipairs(df.global.world.map.map_blocks) do + if block.flags.has_aquifer and zmax < block.map_pos.z then + zmax = block.map_pos.z + end + end + zmax = zmax - top + if zmax < zmin then + print('No aquifer levels need draining') + return + end end -drain(...) +drain() diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 1ca6ed3b21..96050eb591 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -46,6 +46,7 @@ local FORT_AUTOSTART = { 'ban-cooking all', 'buildingplan set boulders false', 'buildingplan set logs false', + 'drain-aquifer --top 2', 'fix/blood-del fort', 'light-aquifers-only fort', } From 509cef40c123b6a8118bd8fe0f8cb8a7c0301ee4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Oct 2023 16:49:44 -0700 Subject: [PATCH 584/732] update add-recipe --- add-recipe.lua | 29 +---------------------------- changelog.txt | 1 + docs/add-recipe.rst | 23 +++++++++++++---------- 3 files changed, 15 insertions(+), 38 deletions(-) diff --git a/add-recipe.lua b/add-recipe.lua index da626216ad..222dc5511f 100644 --- a/add-recipe.lua +++ b/add-recipe.lua @@ -1,25 +1,4 @@ -- Script to add unknown crafting recipes to the player's civ. ---[====[ -add-recipe -========== -Adds unknown weapon and armor crafting recipes to your civ. -E.g. some civilizations never learn to craft high boots. This script can -help with that, and more. Only weapons, armor, and tools are currently supported; -things such as instruments are not. Available options: - -* ``add-recipe all`` adds *all* available weapons and armor, including exotic items - like blowguns, two-handed swords, and capes. - -* ``add-recipe native`` adds only native (but unknown) crafting recipes. Civilizations - pick randomly from a pool of possible recipes, which means not all civs get - high boots, for instance. This command gives you all the recipes your - civilisation could have gotten. - -* ``add-recipe single `` adds a single item by the given - item token. For example:: - - add-recipe single SHOES:ITEM_SHOES_BOOTS -]====] local itemDefs = df.global.world.raws.itemdefs local resources = df.historical_entity.find(df.global.plotinfo.civ_id).resources @@ -125,7 +104,6 @@ function addItems(category, exotic) return added end - function printItems(itemList) for _, v in ipairs(itemList) do local v = v --as:df.itemdef_weaponst @@ -192,10 +170,5 @@ elseif (cmd == "native") then elseif (cmd == "single") then addSingleItem(args[2]) else - print("Available options:\n" - .."all: adds all supported crafting recipes.\n" - .."native: adds only unknown native recipes (eg. high boots for " - .."some dwarves)\n" - .."single: adds a specific item by itemstring (eg. " - .."SHOES:ITEM_SHOES_BOOTS)") + print(dfhack.script_help()) end diff --git a/changelog.txt b/changelog.txt index 4722c3b630..e70b474f5a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) ## New Features diff --git a/docs/add-recipe.rst b/docs/add-recipe.rst index 2263dafe39..c4e3f567dd 100644 --- a/docs/add-recipe.rst +++ b/docs/add-recipe.rst @@ -3,26 +3,29 @@ add-recipe .. dfhack-tool:: :summary: Add crafting recipes to a civ. - :tags: unavailable adventure fort gameplay + :tags: adventure fort gameplay -Civilizations pick randomly from a pool of possible recipes, which means not all -civs get high boots, for instance. This script can help fix that. Only weapons, -armor, and tools are currently supported; things such as instruments are not. +Civilizations pick randomly from a pool of possible recipes, which means, for +example, not all civs get high boots. This script can help fix that. Only +weapons, armor, and tools are currently supported; dynamically generated item +types like instruments are not. Usage ----- +:: + + add-recipe (all|native) + add-recipe single + +Examples +-------- + ``add-recipe native`` Add all crafting recipes that your civ could have chosen from its pool, but did not. ``add-recipe all`` Add *all* available weapons and armor, including exotic items like blowguns, two-handed swords, and capes. -``add-recipe single `` - Add a single item by the given item token. - -Example -------- - ``add-recipe single SHOES:ITEM_SHOES_BOOTS`` Allow your civ to craft high boots. From 3255e5755026cc728fe300e1bd7254fbb205e398 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 6 Oct 2023 16:52:59 -0500 Subject: [PATCH 585/732] Update docs/warn-stranded.rst wording from review Co-authored-by: Myk --- docs/warn-stranded.rst | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 3232d0662e..50bbeead40 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -2,18 +2,18 @@ warn-stranded ============= .. dfhack-tool:: - :summary: Reports citizens that are stranded and can't reach any other unit. + :summary: Reports citizens that are stranded and can't reach any other citizens. :tags: fort units -If any (live) groups of units are stranded from the main (largest) group, -the game will pause and you'll get a warning dialog telling you which units are isolated. +If any (live) groups of fort citizens are stranded from the main (largest) group, +the game will pause and you'll get a warning dialog telling you which citizens are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. -Each unit will be put into a group with the other units stranded together. +Each ciitizen will be put into a group with the other citizens stranded together. -There is a command line interface that can print status of units without pausing or bringing up a window. +There is a command line interface that can print status of citizens without pausing or bringing up a window. -The GUI and command-line both also have the ability to ignore units so they don't trigger a pause and window. +The GUI and command-line both also have the ability to ignore citizens so they don't trigger a pause and window. You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. @@ -30,20 +30,24 @@ Usage Examples -------- +``warn-stranded`` + Standard command that checks for standed citizens and causes a window to pop up with a warning if any are stranded. + Does nothing when there are no unignored stranded citizens. + ``warn-stranded status`` - List all stranded units and all ignored units. Includes unit ids in the output. + List all stranded citizens and all ignored citizens. Includes citizen unit ids. ``warn-stranded clear`` - Clear(unignore) all ignored units. + Clear(unignore) all ignored citizens. ``warn-stranded ignore 1`` - Ignore unit with id 1. + Ignore citizen with unit id 1. ``warn-stranded ignoregroup 2`` - Ignore stranded unit group 2. + Ignore stranded citizen group 2. ``warn-stranded unignore 1`` - Unignore unit with id 1. + Unignore citizen with unit id 1. ``warn-stranded unignoregroup 3`` - Unignore stranded unit group 3. + Unignore stranded citizen group 3. From 748bff59f9a84c593b15e49567fb012f351a7f33 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 6 Oct 2023 23:37:58 -0500 Subject: [PATCH 586/732] Attempt to implement review refactors --- warn-stranded.lua | 274 ++++++++++++++++++++++------------------------ 1 file changed, 132 insertions(+), 142 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index bdb56747aa..670b6b41c2 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,6 +8,8 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} +local scriptPrefix = 'warn-stranded' +local ignoresCache = {} -- =============================================== -- Utility Functions @@ -15,7 +17,7 @@ local args = {...} -- Clear the ignore list local function clear() - dfhack.persistent.delete('warnStrandedIgnore') + dfhack.persistent.delete(scriptPrefix) end -- Taken from warn-starving @@ -31,8 +33,9 @@ end -- Partially taken from warn-starving local function getUnitDescription(unit) - return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. - ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) + return ('[%s] %s %s'):format(dfhack.units.getProfessionName(unit), + dfhack.TranslateName(dfhack.units.getVisibleName(unit)), + getSexString(unit.sex)) end -- Use group data, index, and command arguments to generate a group @@ -58,73 +61,67 @@ local function addId(text, unit) return text..'|'..unit.id..'| ' end --- Uses persistent API. Low-level, deserializes 'warnStrandedIgnored' key and --- will return an initialized empty warnStrandedIgnored table if needed. --- Performance characterstics unknown of persistent API -local function deserializeIgnoredUnits() - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - if currentIgnore == nil then return {} end +-- =============================================== +-- Persistence API +-- =============================================== +-- Optional refresh parameter forces us to load from API instead of using cache + +-- Uses persistent API. Low-level, gets all entries currently in our persistent table +-- will return an empty array if needed. Clears and adds entries to our cache. +-- Returns the new global ignoresCache value +local function getIgnoredUnits() + local ignores = dfhack.persistent.get_all(scriptPrefix) + if ignores == nil then return {} end - local tbl = {} + ignoresCache = {} - for v in string.gmatch(currentIgnore['value'], '%d+') do - table.insert(tbl, v) + for _, entry in ipairs(ignores) do + unit_id = entry.ints[1] + ignoresCache[unit_id] = entry end - return tbl + return ignoresCache end --- Uses persistent API. Deserializes 'warnStrandedIgnore' key to determine if unit is ignored --- deserializedIgnores is optional but allows us to only call deserialize once like an explicit cache. -local function unitIgnored(unit, deserializedIgnores) - local ignores = deserializedIgnores or deserializeIgnoredUnits() - - for index, id in ipairs(ignores) do - if tonumber(id) == unit.id then - return true, index - end - end +-- Uses persistent API. Optional refresh parameter forces us to load from API, +-- instead of using our cache. +-- Returns the persistent entry or nil +local function unitIgnored(unit, refresh) + if refresh then getIgnoredUnits() end - return false + return ignoresCache[unit.id] end -- Check for and potentially add [IGNORED] to text. --- Optional deserializedIgnores allows us to call deserialize once for a group of operations -local function addIgnored(text, unit, deserializedIgnores) - if unitIgnored(unit, deserializedIgnores) then +local function addIgnored(text, unit, refresh) + if unitIgnored(unit, refresh) then return text..'[IGNORED] ' end return text end --- Uses persistent API. Toggles a unit's ignored status by deserializing 'warnStrandedIgnore' key --- then serializing the resulting table after the toggle. --- Optional cache parameter could affect data integrity. Make sure you don't need data reloaded --- before using it. Calling several times in a row can use the return result of the function --- as input to the next call. -local function toggleUnitIgnore(unit, deserializedIgnores) - local ignores = deserializedIgnores or deserializeIgnoredUnits() - local is_ignored, index = unitIgnored(unit, ignores) - - if is_ignored then - table.remove(ignores, index) +-- Uses persistent API. Toggles a unit's ignored status by deleting the entry from the persistence API +-- and from the ignoresCache table. +-- Returns true if the unit was already ignored, false if it wasn't. +local function toggleUnitIgnore(unit, refresh) + local entry = unitIgnored(unit, refresh) + + if entry then + entry:delete() + table.remove(ignoresCache, unit.id) + return true else - table.insert(ignores, unit.id) + entry = dfhack.persistent.save({key = scriptPrefix, ints = {unit.id}}) + ignoresCache[unit.id] = entry + return false end - - dfhack.persistent.delete('warnStrandedIgnore') - dfhack.persistent.save({key = 'warnStrandedIgnore', value = table.concat(ignores, ' ')}) - - return ignores end -- Does the usual GUI pattern when groups can be in a partial state -- Will ignore everything, unless all units in group are already ignored -- If all units in the group are ignored, then it will unignore all of them local function toggleGroup(groups, groupNumber) - local ignored = deserializeIgnoredUnits() - if groupNumber > #groups then print('Group '..groupNumber..' does not exist') return false @@ -139,7 +136,7 @@ local function toggleGroup(groups, groupNumber) local allIgnored = true for _, unit in ipairs(group['units']) do - if not unitIgnored(unit, ignored) then + if not unitIgnored(unit) then allIgnored = false goto process end @@ -147,10 +144,10 @@ local function toggleGroup(groups, groupNumber) ::process:: for _, unit in ipairs(group['units']) do - local isIgnored = unitIgnored(unit, ignored) + local isIgnored = unitIgnored(unit) if allIgnored == isIgnored then - ignored = toggleUnitIgnore(unit, ignored) + ignored = toggleUnitIgnore(unit) end end @@ -160,68 +157,63 @@ end -- =============================================================== -- Graphical Interface -- =============================================================== -warning = defclass(warning, gui.ZScreenModal) - -function warning:init(info) +WarningWindow = defclass(WarningWindow, widgets.Window) +WarningWindow.ATTRS{ + frame={w=80, h=25}, + min_size={w=60, h=25}, + frame_title='Stranded Citizen Warning', + resizable=true, + autoarrange_subviews=true, +} + +function WarningWindow:init() self:addviews{ - widgets.Window{ - view_id = 'main', - frame={w=80, h=25}, - min_size={w=60, h=25}, - frame_title='Stranded Citizen Warning', - resizable=true, + widgets.List{ + frame={h=15}, + view_id = 'list', + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + on_submit=self:callback('onIgnore'), + on_select=self:callback('onZoom'), + on_double_click=self:callback('onIgnore'), + on_double_click2=self:callback('onToggleGroup'), + }, + widgets.Panel{ + frame={h=5}, autoarrange_subviews=true, subviews = { - widgets.List{ - frame={h=15}, - view_id = 'list', - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, - on_submit=self:callback('onIgnore'), - on_select=self:callback('onZoom'), - on_double_click=self:callback('onIgnore'), - on_double_click2=self:callback('onToggleGroup'), + widgets.HotkeyLabel{ + key='SELECT', + label='Toggle ignore', }, - widgets.Panel{ - frame={h=5}, - autoarrange_subviews=true, - subviews = { - widgets.HotkeyLabel{ - key='SELECT', - label='Toggle ignore', - }, - widgets.HotkeyLabel{ - key='CUSTOM_G', - label='Toggle group', - on_activate = self:callback('onToggleGroup'), - }, - widgets.HotkeyLabel{ - key = 'CUSTOM_SHIFT_I', - label = 'Ignore all', - on_activate = self:callback('onIgnoreAll'), - }, - widgets.HotkeyLabel{ - key = 'CUSTOM_SHIFT_C', - label = 'Clear all ignored', - on_activate = self:callback('onClear'), - }, - widgets.WrappedLabel{ - frame={b=0, l=0, r=0}, - text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', - }, - } + widgets.HotkeyLabel{ + key='CUSTOM_G', + label='Toggle group', + on_activate = self:callback('onToggleGroup'), + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_SHIFT_I', + label = 'Ignore all', + on_activate = self:callback('onIgnoreAll'), + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_SHIFT_C', + label = 'Clear all ignored', + on_activate = self:callback('onClear'), + }, + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', }, - } - } + }, } self.groups = info.groups self:initListChoices() end - -function warning:initListChoices() +function WarningWindow:initListChoices() local choices = {} for groupIndex, group in ipairs(self.groups) do @@ -242,33 +234,32 @@ function warning:initListChoices() list:setChoices(choices) end -function warning:onIgnore(_, choice) +function WarningWindow:onIgnore(_, choice) local unit = choice.data['unit'] toggleUnitIgnore(unit) self:initListChoices() end -function warning:onIgnoreAll() +function WarningWindow:onIgnoreAll() local choices = self.subviews.list:getChoices() - local ignoresCache = deserializeIgnoredUnits() for _, choice in ipairs(choices) do -- We don't want to flip ignored units to unignored - if not unitIgnored(choice.data['unit'], ignoresCache) then - ignoresCache = toggleUnitIgnore(choice.data['unit'], ignoresCache) + if not unitIgnored(choice.data['unit']) then + toggleUnitIgnore(choice.data['unit']) end end self:dismiss() end -function warning:onClear() +function WarningWindow:onClear() clear() self:initListChoices() end -function warning:onZoom() +function WarningWindow:onZoom() local index, choice = self.subviews.list:getSelected() local unit = choice.data['unit'] @@ -276,7 +267,7 @@ function warning:onZoom() dfhack.gui.revealInDwarfmodeMap(target, true) end -function warning:onToggleGroup() +function WarningWindow:onToggleGroup() local index, choice = self.subviews.list:getSelected() local group = choice.data['group'] @@ -284,7 +275,13 @@ function warning:onToggleGroup() self:initListChoices() end -function warning:onDismiss() +WarningScreen = defclass(WarningScreen, gui.ZScreenModal) + +function WarningScreen:init(info) + self:addviews{WarningWindow{info}} +end + +function WarningScreen:onDismiss() view = nil end @@ -304,7 +301,6 @@ local function getStrandedUnits() -- Don't use ignored units to determine if there are any stranded units -- but keep them to display later local ignoredGroup = {} - local ignoresCache = deserializeIgnoredUnits() -- Pathability group calculation is from gui/pathable for _, unit in ipairs(citizens) do @@ -312,7 +308,7 @@ local function getStrandedUnits() local block = dfhack.maps.getTileBlock(target) local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - if unitIgnored(unit, ignoresCache) then + if unitIgnored(unit) then table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) @@ -390,8 +386,6 @@ local function findCitizen(unitId) end local function ignoreGroup(groups, groupNumber) - local ignored = deserializeIgnoredUnits() - if groupNumber > #groups then print('Group '..groupNumber..' does not exist') return false @@ -403,11 +397,11 @@ local function ignoreGroup(groups, groupNumber) end for _, unit in ipairs(groups[groupNumber]['units']) do - if unitIgnored(unit, ignored) then + if unitIgnored(unit) then print('Unit '..unit.id..' already ignored, doing nothing to them.') else print('Ignoring unit '..unit.id) - toggleUnitIgnore(unit, ignored) + toggleUnitIgnore(unit) end end @@ -415,8 +409,6 @@ local function ignoreGroup(groups, groupNumber) end local function unignoreGroup(groups, groupNumber) - local ignored = deserializeIgnoredUnits() - if groupNumber > #groups then print('Group '..groupNumber..' does not exist') return false @@ -427,9 +419,9 @@ local function unignoreGroup(groups, groupNumber) end for _, unit in ipairs(groups[groupNumber]['units']) do - if unitIgnored(unit, ignored) then + if unitIgnored(unit) then print('Unignoring unit '..unit.id) - ignored = toggleUnitIgnore(unit, ignored) + ignored = toggleUnitIgnore(unit) else print('Unit '..unit.id..' not already ignored, doing nothing to them.') end @@ -442,7 +434,7 @@ function doCheck() local result, strandedGroups = getStrandedUnits() if result then - return warning{groups=strandedGroups}:show() + return WarningScreen{groups=strandedGroups}:show() end end @@ -459,27 +451,23 @@ end -- ========================================================================= local positionals = argparse.processArgsGetopt(args, {}) +local parameter = tonumber(positionals[2]) if positionals[1] == 'clear' then print('Clearing unit ignore list.') - return clear() -end - -local parameter = tonumber(positionals[2]) + clear() -if positionals[1] == 'status' then +elseif positionals[1] == 'status' then local result, strandedGroups = getStrandedUnits() if result then - local ignoresCache = deserializeIgnoredUnits() - for groupIndex, group in ipairs(strandedGroups) do local groupDesignation = getGroupDesignation(group, groupIndex, true) for _, unit in ipairs(group['units']) do local text = '' - text = addIgnored(text, unit, ignoresCache) + text = addIgnored(text, unit) text = addId(text, unit) print(text..getUnitDescription(unit)..groupDesignation) @@ -508,10 +496,7 @@ if positionals[1] == 'status' then end end - return false -end - -if positionals[1] == 'ignore' then +elseif positionals[1] == 'ignore' then if not parameter then print('Must provide unit id to the ignore command.') return false @@ -531,20 +516,17 @@ if positionals[1] == 'ignore' then print('Ignoring unit '..parameter) toggleUnitIgnore(citizen) - return true -end -if positionals[1] == 'ignoregroup' then +elseif positionals[1] == 'ignoregroup' then if not parameter then print('Must provide group id to the ignoregroup command.') end print('Ignoring group '..parameter) local _, strandedCitizens = getStrandedUnits() - return ignoreGroup(strandedCitizens, parameter) -end + ignoreGroup(strandedCitizens, parameter) -if positionals[1] == 'unignore' then +elseif positionals[1] == 'unignore' then if not parameter then print('Must provide unit id to unignore command.') return false @@ -564,18 +546,26 @@ if positionals[1] == 'unignore' then print('Unignoring unit '..parameter) toggleUnitIgnore(citizen) - return true -end -if positionals[1] == 'unignoregroup' then +elseif positionals[1] == 'unignoregroup' then if not parameter then print('Must provide group id to unignoregroup command.') return false end print('Unignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() - return unignoreGroup(strandedCitizens, parameter) + unignoreGroup(strandedCitizens, parameter) +else + view = view and view:raise() or doCheck() end -view = view and view:raise() or doCheck() +-- Load ignores list on save game load +dfhack.onStateChange[scriptPrefix] = function(state_change) + if state_change ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + getIgnoredUnits() +end From d1c182916da25ddc2d2f5feff5b15094a321c129 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 6 Oct 2023 23:40:15 -0500 Subject: [PATCH 587/732] Remove attempt at using const --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 670b6b41c2..eaff6f45b8 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,7 +8,7 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -local scriptPrefix = 'warn-stranded' +local scriptPrefix = 'warn-stranded' local ignoresCache = {} -- =============================================== From f3befea75a1b6215b25af0df5c75fd398b1d38af Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sat, 7 Oct 2023 00:39:02 -0500 Subject: [PATCH 588/732] Fix post-review-change bugs --- warn-stranded.lua | 52 +++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index eaff6f45b8..7546d89846 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -1,15 +1,15 @@ -- Detects and alerts when a citizen is stranded -- Logic heavily based off of warn-starving -- GUI heavily based off of autobutcher ---@ module = true +--@module = true local gui = require 'gui' local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -local scriptPrefix = 'warn-stranded' -local ignoresCache = {} +scriptPrefix = 'warn-stranded' +ignoresCache = ignoresCache or {} -- =============================================== -- Utility Functions @@ -17,7 +17,10 @@ local ignoresCache = {} -- Clear the ignore list local function clear() - dfhack.persistent.delete(scriptPrefix) + for index, entry in pairs(ignoresCache) do + entry:delete() + ignoresCache[index] = nil + end end -- Taken from warn-starving @@ -76,6 +79,8 @@ local function getIgnoredUnits() ignoresCache = {} for _, entry in ipairs(ignores) do + print(entry) + printall(ignoresCache) unit_id = entry.ints[1] ignoresCache[unit_id] = entry end @@ -109,10 +114,10 @@ local function toggleUnitIgnore(unit, refresh) if entry then entry:delete() - table.remove(ignoresCache, unit.id) + ignoresCache[unit.id] = nil return true else - entry = dfhack.persistent.save({key = scriptPrefix, ints = {unit.id}}) + entry = dfhack.persistent.save({key = scriptPrefix, ints = {unit.id}}, true) ignoresCache[unit.id] = entry return false end @@ -145,9 +150,10 @@ local function toggleGroup(groups, groupNumber) for _, unit in ipairs(group['units']) do local isIgnored = unitIgnored(unit) + if isIgnored then isIgnored = true else isIgnored = false end if allIgnored == isIgnored then - ignored = toggleUnitIgnore(unit) + toggleUnitIgnore(unit) end end @@ -166,7 +172,7 @@ WarningWindow.ATTRS{ autoarrange_subviews=true, } -function WarningWindow:init() +function WarningWindow:init(info) self:addviews{ widgets.List{ frame={h=15}, @@ -218,12 +224,11 @@ function WarningWindow:initListChoices() for groupIndex, group in ipairs(self.groups) do local groupDesignation = getGroupDesignation(group, groupIndex) - local ignoresCache = deserializeIgnoredUnits() for _, unit in ipairs(group['units']) do local text = '' - text = addIgnored(text, unit, ignoresCache) + text = addIgnored(text, unit) text = text..getUnitDescription(unit)..groupDesignation table.insert(choices, { text = text, data = {unit = unit, group = groupIndex} }) @@ -251,7 +256,7 @@ function WarningWindow:onIgnoreAll() end end - self:dismiss() + self:initListChoices() end function WarningWindow:onClear() @@ -278,7 +283,7 @@ end WarningScreen = defclass(WarningScreen, gui.ZScreenModal) function WarningScreen:init(info) - self:addviews{WarningWindow{info}} + self:addviews{WarningWindow{groups=info.groups}} end function WarningScreen:onDismiss() @@ -438,6 +443,16 @@ function doCheck() end end +-- Load ignores list on save game load +-- WARNING: This has to be above `dfhack_flags.module` or it will not work as intended on first game load +dfhack.onStateChange[scriptPrefix] = function(state_change) + if state_change ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + getIgnoredUnits() +end + if dfhack_flags.module then return end @@ -474,6 +489,8 @@ elseif positionals[1] == 'status' then end end + printall(dfhack.persistent.get_all(scriptPrefix)) + print(dfhack.persistent.get_all(scriptPrefix)) return true end @@ -539,7 +556,7 @@ elseif positionals[1] == 'unignore' then return false end - if unitIgnored(citizen) == false then + if not unitIgnored(citizen) then print('Unit '..parameter..' is not ignored. You may want to use the ignore command.') return false end @@ -560,12 +577,3 @@ elseif positionals[1] == 'unignoregroup' then else view = view and view:raise() or doCheck() end - --- Load ignores list on save game load -dfhack.onStateChange[scriptPrefix] = function(state_change) - if state_change ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return - end - - getIgnoredUnits() -end From 5d3de5c219bd017f1328ac18fa24328da7d1c6cb Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sat, 7 Oct 2023 00:40:37 -0500 Subject: [PATCH 589/732] Remove debug prints --- warn-stranded.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 7546d89846..80f5bff0d5 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -79,8 +79,6 @@ local function getIgnoredUnits() ignoresCache = {} for _, entry in ipairs(ignores) do - print(entry) - printall(ignoresCache) unit_id = entry.ints[1] ignoresCache[unit_id] = entry end @@ -489,8 +487,6 @@ elseif positionals[1] == 'status' then end end - printall(dfhack.persistent.get_all(scriptPrefix)) - print(dfhack.persistent.get_all(scriptPrefix)) return true end From 47b0576c9be1f59f2cb6b4bef01c7c5b09bb22c9 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 6 Oct 2023 23:06:11 -0700 Subject: [PATCH 590/732] Update autofish.lua --- autofish.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autofish.lua b/autofish.lua index dc7814a1e7..d5ce23d43c 100644 --- a/autofish.lua +++ b/autofish.lua @@ -73,7 +73,7 @@ end function toggle_fishing_labour(state) -- pass true to state to turn on, otherwise disable -- find all work details that have fishing enabled: - local work_details = df.global.plotinfo.labor_info.work_details + local work_details = df.global.plotinfo.hauling.labor_info.work_details for _,v in pairs(work_details) do if v.allowed_labors.FISH then -- set limited to true just in case a custom work detail is being From 728d902712655592ec4385e88fd36077641ccfb1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 00:20:44 -0700 Subject: [PATCH 591/732] use new work detail mode values --- autofish.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/autofish.lua b/autofish.lua index d5ce23d43c..8ceaeb3642 100644 --- a/autofish.lua +++ b/autofish.lua @@ -73,15 +73,15 @@ end function toggle_fishing_labour(state) -- pass true to state to turn on, otherwise disable -- find all work details that have fishing enabled: - local work_details = df.global.plotinfo.hauling.labor_info.work_details + local work_details = df.global.plotinfo.labor_info.work_details for _,v in pairs(work_details) do if v.allowed_labors.FISH then - -- set limited to true just in case a custom work detail is being - -- changed, to prevent *all* dwarves from fishing. - v.work_detail_flags.limited = true - v.work_detail_flags.enabled = state + v.work_detail_flags.mode = state and + df.work_detail_mode.OnlySelectedDoesThis or df.work_detail_mode.NobodyDoesThis - -- workaround to actually enable labours + -- since the work details are not actually applied unless a button + -- is clicked on the work details screen, we have to manually set + -- unit labours for _,v2 in ipairs(v.assigned_units) do -- find unit by ID and toggle fishing local unit = df.unit.find(v2) From 60a9a3e033e1a3da16e4216f50b443b0b3492e73 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sat, 7 Oct 2023 10:45:16 -0500 Subject: [PATCH 592/732] Apply easy fixes from code review Co-authored-by: Myk --- changelog.txt | 2 +- docs/warn-stranded.rst | 6 +++--- gui/control-panel.lua | 2 +- warn-stranded.lua | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index d247ec6801..07a731bb5a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,7 +31,7 @@ Template for new versions: ## New Features ## New Scripts -- `warn-stranded`: new repeatable maintenance script to check for stranded units, based off `warn-starving` +- `warn-stranded`: new repeatable maintenance script to check for stranded units, similar to `warn-starving` ## Fixes - `suspendmanager`: fix errors when constructing near the map edge diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 50bbeead40..10a92a8f2a 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -9,7 +9,7 @@ If any (live) groups of fort citizens are stranded from the main (largest) group the game will pause and you'll get a warning dialog telling you which citizens are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. -Each ciitizen will be put into a group with the other citizens stranded together. +Each citizen will be put into a group with the other citizens stranded together. There is a command line interface that can print status of citizens without pausing or bringing up a window. @@ -31,14 +31,14 @@ Examples -------- ``warn-stranded`` - Standard command that checks for standed citizens and causes a window to pop up with a warning if any are stranded. + Standard command that checks citizens and pops up a warning if any are stranded. Does nothing when there are no unignored stranded citizens. ``warn-stranded status`` List all stranded citizens and all ignored citizens. Includes citizen unit ids. ``warn-stranded clear`` - Clear(unignore) all ignored citizens. + Clear (unignore) all ignored citizens. ``warn-stranded ignore 1`` Ignore citizen with unit id 1. diff --git a/gui/control-panel.lua b/gui/control-panel.lua index c4cccc281d..4836fdc877 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -136,7 +136,7 @@ local REPEATS = { command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, ['warn-stranded']={ desc='Show a warning dialog when units are stranded from all others.', - command={'--time', '300', '--timeUnits', 'ticks', '--command', '[', 'warn-stranded', ']'}}, + command={'--time', '0.25', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, ['empty-wheelbarrows']={ desc='Empties wheelbarrows which have rocks stuck in them.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, diff --git a/warn-stranded.lua b/warn-stranded.lua index 80f5bff0d5..ee116910da 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,7 +8,7 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -scriptPrefix = 'warn-stranded' +local scriptPrefix = 'warn-stranded' ignoresCache = ignoresCache or {} -- =============================================== From ba04508e7308ed033c930f723d35dcd562a7e2fa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 19:02:13 -0700 Subject: [PATCH 593/732] adjust to new location of full text search var --- gui/control-panel.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 96050eb591..1d642f0402 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -91,8 +91,10 @@ local PREFERENCES = { desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.'}, SCROLL_DELAY_MS={label='Mouse scroll repeat delay (ms)', type='int', default=20, min=5, desc='The delay between events when holding the mouse button down on a scrollbar, in ms.'}, - FILTER_FULL_TEXT={label='DFHack list filters search full text', type='bool', default=false, - desc='Whether to search for a match in the full text (true) or just at the start of words (false).'}, + }, + ['utils']={ + FILTER_FULL_TEXT={label='DFHack searches full text', type='bool', default=false, + desc='When searching, choose whether to match anywhere in the text (true) or just at the start of words (false).'}, }, } local CPP_PREFERENCES = { From 481a57f483ac350044b8fc450ab7f21810ba838c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 19:21:33 -0700 Subject: [PATCH 594/732] clarify title of post-blueprint popup --- gui/quickfort.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/quickfort.lua b/gui/quickfort.lua index ca27ca6c51..5e0fe48693 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -626,7 +626,7 @@ function Quickfort:do_command(command, dry_run, post_fn) if command == 'run' then if #ctx.messages > 0 then self._dialog = dialogs.showMessage( - 'Attention', + 'Blueprint messages', table.concat(ctx.messages, '\n\n'):wrap(dialog_width), nil, post_fn) From 46f345d4a87f0a3654e204d4a1b4926bbe2ff988 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 19:25:19 -0700 Subject: [PATCH 595/732] add full syntax to set-personality docs --- docs/modtools/set-personality.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modtools/set-personality.rst b/docs/modtools/set-personality.rst index 3e34c7db0c..2761c700cf 100644 --- a/docs/modtools/set-personality.rst +++ b/docs/modtools/set-personality.rst @@ -13,7 +13,7 @@ Usage :: modtools/set-personality --list - modtools/set-personality [] + modtools/set-personality [] [] If no target option is given, the unit selected in the UI is used by default. From ffe13474affd1265170181796d31223825a89feb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 19:35:42 -0700 Subject: [PATCH 596/732] fix some doc formatting --- docs/devel/click-monitor.rst | 1 + docs/gui/color-schemes.rst | 2 +- docs/modtools/change-build-menu.rst | 6 ++++-- docs/modtools/create-tree.rst | 5 ++++- docs/modtools/create-unit.rst | 7 ++++++- docs/modtools/extra-gamelog.rst | 6 ++++-- docs/modtools/force.rst | 5 ++++- docs/modtools/if-entity.rst | 15 ++++++++------- docs/modtools/interaction-trigger.rst | 5 ++++- docs/modtools/projectile-trigger.rst | 7 ++++++- docs/modtools/reaction-product-trigger.rst | 7 ++++++- 11 files changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/devel/click-monitor.rst b/docs/devel/click-monitor.rst index 38bdfa4215..6dce54918e 100644 --- a/docs/devel/click-monitor.rst +++ b/docs/devel/click-monitor.rst @@ -12,4 +12,5 @@ Usage ----- :: + enable devel/click-monitor diff --git a/docs/gui/color-schemes.rst b/docs/gui/color-schemes.rst index 83e99a021b..d6a7f832d1 100644 --- a/docs/gui/color-schemes.rst +++ b/docs/gui/color-schemes.rst @@ -7,7 +7,7 @@ gui/color-schemes This is an in-game interface for `color-schemes`, which allows you to modify the colors in the Dwarf Fortress interface. This script must be called from either -the title screen (shown when you first start the Dwarf Fortress game) or a +the title screen (shown when you first start the Dwarf Fortress game) or the fortress main map screen. Usage diff --git a/docs/modtools/change-build-menu.rst b/docs/modtools/change-build-menu.rst index 2fffec91a7..349559b4e7 100644 --- a/docs/modtools/change-build-menu.rst +++ b/docs/modtools/change-build-menu.rst @@ -17,7 +17,8 @@ your changes each time the world loads. Just to be clear: You CANNOT use this script AT ALL if there is no world loaded! -**Usage:** +Usage +----- ``enable modtools/change-build-menu``: @@ -92,7 +93,8 @@ loaded! changes you no longer want/need. -**Module Usage:** +Module Usage +------------ To use this script as a module put the following somewhere in your own script: diff --git a/docs/modtools/create-tree.rst b/docs/modtools/create-tree.rst index e425c02229..e2a4a12d47 100644 --- a/docs/modtools/create-tree.rst +++ b/docs/modtools/create-tree.rst @@ -7,7 +7,10 @@ modtools/create-tree Spawns a tree. -Usage:: +Usage +----- + +:: -tree treeName specify the tree to be created diff --git a/docs/modtools/create-unit.rst b/docs/modtools/create-unit.rst index 9c338bde20..fc4f713d65 100644 --- a/docs/modtools/create-unit.rst +++ b/docs/modtools/create-unit.rst @@ -5,7 +5,12 @@ modtools/create-unit :summary: Create arbitrary units. :tags: unavailable dev -Creates a unit. Usage:: +Creates a unit. + +Usage +----- + +:: -race raceName (obligatory) diff --git a/docs/modtools/extra-gamelog.rst b/docs/modtools/extra-gamelog.rst index 2c79594771..f465fdeaa2 100644 --- a/docs/modtools/extra-gamelog.rst +++ b/docs/modtools/extra-gamelog.rst @@ -8,7 +8,9 @@ modtools/extra-gamelog This script writes extra information to the gamelog. This is useful for tools like :forums:`Soundsense <60287>`. -Usage:: +Usage +----- + +:: modtools/extra-gamelog enable - modtools/extra-gamelog disable diff --git a/docs/modtools/force.rst b/docs/modtools/force.rst index 01831201c1..9f71183fa1 100644 --- a/docs/modtools/force.rst +++ b/docs/modtools/force.rst @@ -7,7 +7,10 @@ modtools/force This tool triggers events like megabeasts, caravans, and migrants. -Usage:: +Usage +----- + +:: -eventType event specify the type of the event to trigger diff --git a/docs/modtools/if-entity.rst b/docs/modtools/if-entity.rst index 9846842b31..6b32c2f863 100644 --- a/docs/modtools/if-entity.rst +++ b/docs/modtools/if-entity.rst @@ -11,18 +11,19 @@ To use this script effectively it needs to be called from "raw/onload.init". Calling this from the main dfhack.init file will do nothing, as no world has been loaded yet. -Usage: +Usage +----- -- ``id``: +``id`` Specify the entity ID to match -- ``cmd [ commandStrs ]``: +``cmd [ commandStrs ]`` Specify the command to be run if the current entity matches the entity given via -id All arguments are required. -Example: +Example +------- -- Print a message if you load an elf fort, but not a dwarf, human, etc. fort:: - - if-entity -id "FOREST" -cmd [ lua "print('Dirty hippies.')" ] +``if-entity -id "FOREST" -cmd [ lua "print('Dirty hippies.')" ]`` + Print a message if you load an elf fort, but not a dwarf, human, etc. fort. diff --git a/docs/modtools/interaction-trigger.rst b/docs/modtools/interaction-trigger.rst index f1ea4b61df..547bc7784f 100644 --- a/docs/modtools/interaction-trigger.rst +++ b/docs/modtools/interaction-trigger.rst @@ -10,7 +10,10 @@ scanning the announcements for the correct attack verb, so the attack verb must be specified in the interaction. It includes an option to suppress this announcement after it finds it. -Usage:: +Usage +----- + +:: -clear unregisters all triggers diff --git a/docs/modtools/projectile-trigger.rst b/docs/modtools/projectile-trigger.rst index d50c434dec..987bc7e065 100644 --- a/docs/modtools/projectile-trigger.rst +++ b/docs/modtools/projectile-trigger.rst @@ -5,7 +5,12 @@ modtools/projectile-trigger :summary: Run DFHack commands when projectiles hit their targets. :tags: unavailable dev -This triggers dfhack commands when projectiles hit their targets. Usage:: +This triggers dfhack commands when projectiles hit their targets. + +Usage +----- + +:: -clear unregister all triggers diff --git a/docs/modtools/reaction-product-trigger.rst b/docs/modtools/reaction-product-trigger.rst index 32ede32949..b3b4e421b6 100644 --- a/docs/modtools/reaction-product-trigger.rst +++ b/docs/modtools/reaction-product-trigger.rst @@ -6,7 +6,12 @@ modtools/reaction-product-trigger :tags: unavailable dev This triggers dfhack commands when reaction products are produced, once per -product. Usage:: +product. + +Usage +----- + +:: -clear unregister all reaction hooks From 76af3fb101749c425a99c88ff97cbc13ef6961f2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 04:13:42 -0700 Subject: [PATCH 597/732] don't click too quickly for embark tutorial --- changelog.txt | 1 + hide-tutorials.lua | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index d03ae453cc..e830ab0c49 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: ## Fixes - `suspendmanager`: fix errors when constructing near the map edge - `gui/sandbox`: fix scrollbar moving double distance on click +- `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped ## Misc Improvements diff --git a/hide-tutorials.lua b/hide-tutorials.lua index ef855539e4..5ad2c112a7 100644 --- a/hide-tutorials.lua +++ b/hide-tutorials.lua @@ -22,8 +22,9 @@ local function close_help() help.open = false end -function skip_tutorial_prompt(scr) +function skip_tutorial_prompt() if not help.open then return end + local scr = dfhack.gui.getDFViewscreen(true) local mouse_y = 23 if help.context == df.help_context_type.EMBARK_TUTORIAL_CHOICE then help.context = df.help_context_type.EMBARK_MESSAGE @@ -55,7 +56,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if df.viewscreen_new_regionst:is_instance(scr) then close_help() elseif df.viewscreen_choose_start_sitest:is_instance(scr) then - skip_tutorial_prompt(scr) + dfhack.timeout(1, 'frames', skip_tutorial_prompt) end elseif sc == SC_MAP_LOADED and df.global.gamemode == df.game_mode.DWARF then hide_all_popups() From 5d11aa55a3bd656d693c86626708c0df9f959dae Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 9 Oct 2023 18:35:22 -0500 Subject: [PATCH 598/732] Update getIgnoredUnits and rename to loadIgnoredUnits --- warn-stranded.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index ee116910da..b1a65be863 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -72,12 +72,12 @@ end -- Uses persistent API. Low-level, gets all entries currently in our persistent table -- will return an empty array if needed. Clears and adds entries to our cache. -- Returns the new global ignoresCache value -local function getIgnoredUnits() +local function loadIgnoredUnits() local ignores = dfhack.persistent.get_all(scriptPrefix) - if ignores == nil then return {} end - ignoresCache = {} + if ignores == nil then return ignoresCache end + for _, entry in ipairs(ignores) do unit_id = entry.ints[1] ignoresCache[unit_id] = entry @@ -90,7 +90,7 @@ end -- instead of using our cache. -- Returns the persistent entry or nil local function unitIgnored(unit, refresh) - if refresh then getIgnoredUnits() end + if refresh then loadIgnoredUnits() end return ignoresCache[unit.id] end @@ -448,7 +448,7 @@ dfhack.onStateChange[scriptPrefix] = function(state_change) return end - getIgnoredUnits() + loadIgnoredUnits() end if dfhack_flags.module then From da145162c6286f76202b1592d7afa7239bdfaf5f Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 9 Oct 2023 23:54:42 -0500 Subject: [PATCH 599/732] Attempt at making a GUI layout that better uses space --- warn-stranded.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index b1a65be863..70a63b8b45 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -175,8 +175,6 @@ function WarningWindow:init(info) widgets.List{ frame={h=15}, view_id = 'list', - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, on_submit=self:callback('onIgnore'), on_select=self:callback('onZoom'), on_double_click=self:callback('onIgnore'), @@ -184,29 +182,39 @@ function WarningWindow:init(info) }, widgets.Panel{ frame={h=5}, - autoarrange_subviews=true, subviews = { widgets.HotkeyLabel{ + frame={b=3, l=0}, key='SELECT', label='Toggle ignore', + auto_width=true, }, widgets.HotkeyLabel{ + frame={b=3, l=21}, key='CUSTOM_G', label='Toggle group', on_activate = self:callback('onToggleGroup'), + auto_width=true, + }, widgets.HotkeyLabel{ + frame={b=3, l=37}, key = 'CUSTOM_SHIFT_I', label = 'Ignore all', on_activate = self:callback('onIgnoreAll'), + auto_width=true, + }, widgets.HotkeyLabel{ + frame={b=3, l=52}, key = 'CUSTOM_SHIFT_C', label = 'Clear all ignored', on_activate = self:callback('onClear'), + auto_width=true, + }, widgets.WrappedLabel{ - frame={b=0, l=0, r=0}, + frame={b=1, l=0}, text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', }, } From 7a20d0eb990d9488b25d1df450802d42a9369a18 Mon Sep 17 00:00:00 2001 From: Quinn Cypher Date: Tue, 10 Oct 2023 02:05:46 -0400 Subject: [PATCH 600/732] Initial update + docs --- burial.lua | 51 ++++++++++++++++++++++++++++--------------------- docs/burial.rst | 22 ++++++++++++--------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/burial.lua b/burial.lua index 3f75c50b46..144b1a2136 100644 --- a/burial.lua +++ b/burial.lua @@ -1,27 +1,34 @@ --- allows burial in unowned coffins --- by Putnam https://gist.github.com/Putnam3145/e7031588f4d9b24b9dda ---[====[ +-- Allows burial in unowned coffins. +-- Based on Putnam's work (https://gist.github.com/Putnam3145/e7031588f4d9b24b9dda) +local argparse = require('argparse') +local utils = require('utils') -burial -====== -Sets all unowned coffins to allow burial. ``burial -pets`` also allows burial -of pets. +local args = argparse.processArgs({...}, utils.invert{'d', 'p'}) -]====] - -local utils=require('utils') - -local validArgs = utils.invert({ - 'pets' -}) - -local args = utils.processArgs({...}, validArgs) - -for k,v in ipairs(df.global.world.buildings.other.COFFIN) do --as:df.building_coffinst - if v.owner_id==-1 then - v.burial_mode.allow_burial=true - if not args.pets then - v.burial_mode.no_pets=true +for i, c in pairs(df.global.world.buildings.other.COFFIN) do + -- Check for existing tomb + for i, z in pairs(c.relations) do + if z.type == df.civzone_type.Tomb then + goto skip end end + + dfhack.buildings.constructBuilding { + type = df.building_type.Civzone, + subtype = df.civzone_type.Tomb, + pos = xyz2pos(c.x1, c.y1, c.z), + abstract = true, + fields = { + is_active = 8, + zone_settings = { + tomb = { + no_pets = args.d and not args.p, + no_citizens = args.p and not args.d, + }, + }, + }, + } + + ::skip:: end + diff --git a/docs/burial.rst b/docs/burial.rst index 19e58b9b87..db7601e526 100644 --- a/docs/burial.rst +++ b/docs/burial.rst @@ -2,20 +2,24 @@ burial ====== .. dfhack-tool:: - :summary: Configures all unowned coffins to allow burial. - :tags: unavailable fort productivity buildings + :summary: Allows burial in unowned coffins. + :tags: fort productivity buildings + +Creates a 1x1 tomb zone for each built coffin that doesn't already have one. Usage ----- -:: + ``burial [-d] [-p]`` - burial [--pets] +Created tombs allow both dwarves and pets by default. By specifying ``-d`` or +``-p``, they can be restricted to dwarves or pets, respectively. -if the ``--pets`` option is passed, coffins will also allow burial of pets. +Options +------- -Examples --------- +``-d`` + Create dwarf-only tombs +``-p`` + Create pet-only tombs -``burial --pets`` - Configures all unowned coffins to allow burial, including pets. From 6b5e442fd17c28738020de309cbc24533784161f Mon Sep 17 00:00:00 2001 From: Quinn Cypher Date: Tue, 10 Oct 2023 02:23:43 -0400 Subject: [PATCH 601/732] Added burial update to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index d03ae453cc..798530521d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ Template for new versions: ## New Tools - `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) +- `burial`: (reinstated) allows burial in unowned coffins (now creates tomb zones for all built coffins) ## New Features - `drain-aquifer`: gained ability to drain just above or below a certain z-level From 0b1dbac90d84554f6854927baef8e6dd32341dd5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:21:50 +0000 Subject: [PATCH 602/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- burial.lua | 3 +-- docs/burial.rst | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/burial.lua b/burial.lua index 144b1a2136..2b49f9a1ab 100644 --- a/burial.lua +++ b/burial.lua @@ -12,7 +12,7 @@ for i, c in pairs(df.global.world.buildings.other.COFFIN) do goto skip end end - + dfhack.buildings.constructBuilding { type = df.building_type.Civzone, subtype = df.civzone_type.Tomb, @@ -31,4 +31,3 @@ for i, c in pairs(df.global.world.buildings.other.COFFIN) do ::skip:: end - diff --git a/docs/burial.rst b/docs/burial.rst index db7601e526..befd16ba1a 100644 --- a/docs/burial.rst +++ b/docs/burial.rst @@ -22,4 +22,3 @@ Options Create dwarf-only tombs ``-p`` Create pet-only tombs - From 973537cebf9999ce1326b2f41fa4b6a49167381d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 11 Oct 2023 17:51:30 -0700 Subject: [PATCH 603/732] make hide-tutorials more robust sometimes frames go by so quickly that the mouse click isn't detected on the first or second try --- hide-tutorials.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hide-tutorials.lua b/hide-tutorials.lua index 5ad2c112a7..2ca950e3bf 100644 --- a/hide-tutorials.lua +++ b/hide-tutorials.lua @@ -37,6 +37,10 @@ function skip_tutorial_prompt() df.global.gps.mouse_y = mouse_y gui.simulateInput(scr, '_MOUSE_L') end + if help.open then + -- retry later + help.context = df.help_context_type.EMBARK_TUTORIAL_CHOICE + end end local function hide_all_popups() @@ -56,7 +60,10 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if df.viewscreen_new_regionst:is_instance(scr) then close_help() elseif df.viewscreen_choose_start_sitest:is_instance(scr) then - dfhack.timeout(1, 'frames', skip_tutorial_prompt) + skip_tutorial_prompt() + dfhack.timeout(10, 'frames', skip_tutorial_prompt) + dfhack.timeout(100, 'frames', skip_tutorial_prompt) + dfhack.timeout(1000, 'frames', skip_tutorial_prompt) end elseif sc == SC_MAP_LOADED and df.global.gamemode == df.game_mode.DWARF then hide_all_popups() From 218cae668a6762cb59fc571e528e9a3484dbbae8 Mon Sep 17 00:00:00 2001 From: Quinn Cypher Date: Thu, 12 Oct 2023 14:43:02 -0400 Subject: [PATCH 604/732] Now using quickfort, added zlevel option --- burial.lua | 47 ++++++++++++++++++++++++++--------------------- docs/burial.rst | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/burial.lua b/burial.lua index 2b49f9a1ab..ea1924c422 100644 --- a/burial.lua +++ b/burial.lua @@ -1,33 +1,38 @@ -- Allows burial in unowned coffins. -- Based on Putnam's work (https://gist.github.com/Putnam3145/e7031588f4d9b24b9dda) local argparse = require('argparse') -local utils = require('utils') +local quickfort = reqscript('quickfort') -local args = argparse.processArgs({...}, utils.invert{'d', 'p'}) +local cur_zlevel, citizens, pets = false, true, true +argparse.processArgsGetopt({...}, { + {'z', 'cur-zlevel', handler=function() cur_zlevel = true end}, + {'c', 'citizens-only', handler=function() pets = false end}, + {'p', 'pets-only', handler=function() citizens = false end}, +}) +local tomb_blueprint = { + mode = 'zone', + pos = nil, + -- Don't pass properties with default values to avoid 'unhandled property' warning + data = ('T{%s %s}'):format(citizens and '' or 'citizens=false', pets and 'pets=true' or ''), +} -for i, c in pairs(df.global.world.buildings.other.COFFIN) do - -- Check for existing tomb - for i, z in pairs(c.relations) do - if z.type == df.civzone_type.Tomb then +local tomb_count = 0 +for _, coffin in pairs(df.global.world.buildings.other.COFFIN) do + + if cur_zlevel and not (coffin.z == df.global.window_z) then + goto skip + end + for _, zone in pairs(coffin.relations) do + if zone.type == df.civzone_type.Tomb then goto skip end end - dfhack.buildings.constructBuilding { - type = df.building_type.Civzone, - subtype = df.civzone_type.Tomb, - pos = xyz2pos(c.x1, c.y1, c.z), - abstract = true, - fields = { - is_active = 8, - zone_settings = { - tomb = { - no_pets = args.d and not args.p, - no_citizens = args.p and not args.d, - }, - }, - }, - } + tomb_blueprint.pos = xyz2pos(coffin.x1, coffin.y1, coffin.z) + quickfort.apply_blueprint(tomb_blueprint) + tomb_count = tomb_count + 1 ::skip:: end + +print(('Created %s tombs.'):format(tomb_count)) diff --git a/docs/burial.rst b/docs/burial.rst index befd16ba1a..abc85b6264 100644 --- a/docs/burial.rst +++ b/docs/burial.rst @@ -3,22 +3,42 @@ burial .. dfhack-tool:: :summary: Allows burial in unowned coffins. - :tags: fort productivity buildings + :tags: fort | productivity | buildings -Creates a 1x1 tomb zone for each built coffin that doesn't already have one. +Creates a 1x1 tomb zone for each built coffin that isn't already in a tomb. Usage ----- - ``burial [-d] [-p]`` + ``burial []`` -Created tombs allow both dwarves and pets by default. By specifying ``-d`` or -``-p``, they can be restricted to dwarves or pets, respectively. +Examples +-------- + +``burial`` + Create a tomb for every coffin on the map with automatic burial enabled. + +``burial -z`` + Create tombs only on the current zlevel. + +``burial -c`` + Create tombs designated for automatic burial of citizens only. + +``burial -p`` + Create tombs designated for automatic burial of pets only. + +``burial -cp`` + Create tombs with automatic burial disabled for both citizens and pets, + requiring manual assignment of deceased units to each tomb. Options ------- -``-d`` - Create dwarf-only tombs -``-p`` - Create pet-only tombs +``-z``, ``--cur-zlevel`` + Only create tombs on the current zlevel. + +``-c``, ``--citizens-only`` + Only automatically bury citizens. + +``-p``, ``--pets-only`` + Only automatically bury pets. From e83bb778ee9907d0bf289142cd4955c034ce00d6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Oct 2023 02:27:55 -0700 Subject: [PATCH 605/732] include the insane for reachability purposes --- unforbid.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unforbid.lua b/unforbid.lua index 1daf1e41e1..38667602c1 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -5,7 +5,7 @@ local argparse = require('argparse') local function unforbid_all(include_unreachable, quiet) if not quiet then print('Unforbidding all items...') end - local citizens = dfhack.units.getCitizens() + local citizens = dfhack.units.getCitizens(true) local count = 0 for _, item in pairs(df.global.world.items.all) do if item.flags.forbid then From 1c67dcd56955c4ee12d6492d0c38774d1797dadf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Oct 2023 02:28:43 -0700 Subject: [PATCH 606/732] include the insane for reachability purposes --- forbid.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forbid.lua b/forbid.lua index cfb62a1e98..b0be9536f9 100644 --- a/forbid.lua +++ b/forbid.lua @@ -70,7 +70,7 @@ end if positionals[1] == "unreachable" then print("Forbidding all unreachable items on the map...") - local citizens = dfhack.units.getCitizens() + local citizens = dfhack.units.getCitizens(true) local count = 0 for _, item in pairs(df.global.world.items.all) do From 3d4854e1b21b60ad8b0957f6de7cc27fbdbd12ee Mon Sep 17 00:00:00 2001 From: Quinn Cypher Date: Fri, 13 Oct 2023 11:21:52 -0400 Subject: [PATCH 607/732] Apply suggestions from code review Co-authored-by: Myk --- burial.lua | 2 +- docs/burial.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/burial.lua b/burial.lua index ea1924c422..caf01e0064 100644 --- a/burial.lua +++ b/burial.lua @@ -35,4 +35,4 @@ for _, coffin in pairs(df.global.world.buildings.other.COFFIN) do ::skip:: end -print(('Created %s tombs.'):format(tomb_count)) +print(('Created %s tomb(s).'):format(tomb_count)) diff --git a/docs/burial.rst b/docs/burial.rst index abc85b6264..506d1a8715 100644 --- a/docs/burial.rst +++ b/docs/burial.rst @@ -2,8 +2,8 @@ burial ====== .. dfhack-tool:: - :summary: Allows burial in unowned coffins. - :tags: fort | productivity | buildings + :summary: Create tomb zones for unzoned coffins. + :tags: fort productivity buildings Creates a 1x1 tomb zone for each built coffin that isn't already in a tomb. From 923f10d98d69efc2a59fcadab591d4bdf71e0031 Mon Sep 17 00:00:00 2001 From: Quinn Cypher Date: Fri, 13 Oct 2023 12:00:53 -0400 Subject: [PATCH 608/732] New burial options --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 798530521d..398cfb8747 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,9 +28,10 @@ Template for new versions: ## New Tools - `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) -- `burial`: (reinstated) allows burial in unowned coffins (now creates tomb zones for all built coffins) +- `burial`: (reinstated) create tomb zones for unzoned coffins ## New Features +- `burial`: new options to configure automatic burial and limit scope to the current z-level - `drain-aquifer`: gained ability to drain just above or below a certain z-level - `drain-aquifer`: new option to drain all layers except for the first N aquifer layers, in case you want some aquifer layers but not too many - `gui/control-panel`: ``drain-aquifer --top 2`` added as an autostart option From b9109af5050ecbe4ced061466d72d5eb60bc77b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branislav=20Setl=C3=A1k?= Date: Sat, 14 Oct 2023 04:49:03 +0200 Subject: [PATCH 609/732] corrupt jobs deleted from units --- fix/corrupt-jobs.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 fix/corrupt-jobs.lua diff --git a/fix/corrupt-jobs.lua b/fix/corrupt-jobs.lua new file mode 100644 index 0000000000..a158d9642b --- /dev/null +++ b/fix/corrupt-jobs.lua @@ -0,0 +1,21 @@ +-- Deletes corrupted jobs from global job list + +local utils = require("utils") + +local count = 0 + +functioning_job_list = {} + +for _, job in ipairs(df.global.job_list) do + if job.id != -1 then + functioning_job_list[#functioning_job_list+1] = job + end +end + +for _, v in ipairs(df.global.world.units.all) do + if v.job.current_job then + if utils.linear_index(functioning_job_list, v.job.current_job) == nil then + v.job.current_job = nil + end + end +end From 04f561cdad149dfd8e67204520a453e1233cbf18 Mon Sep 17 00:00:00 2001 From: Rose Date: Fri, 13 Oct 2023 23:19:54 -0700 Subject: [PATCH 610/732] Updated transform-unit.lua to try to select the currently selected unit if none is provided. It still can crash the game, however. --- modtools/transform-unit.lua | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/modtools/transform-unit.lua b/modtools/transform-unit.lua index 3d559d5ab3..8833420ee3 100644 --- a/modtools/transform-unit.lua +++ b/modtools/transform-unit.lua @@ -66,9 +66,17 @@ if args.clear then return end -if not args.unit then - error 'Specify a unit.' +local unit +if args.unit then + unit = df.unit.find(tonumber(args.unit)) +else + unit = dfhack.gui.getSelectedUnit(true) +end +if not unit then + error 'Select or specify a valid unit' + return end +local unit_id = unit.id if not args.duration then args.duration = 'forever' @@ -78,11 +86,10 @@ local raceIndex local race local caste if args.untransform then - local unit = df.unit.find(tonumber(args.unit)) - raceIndex = normalRace[args.unit].race + raceIndex = normalRace[unit_id].race race = df.creature_raw.find(raceIndex) - caste = normalRace[args.unit].caste - normalRace[args.unit] = nil + caste = normalRace[unit_id].caste + normalRace[unit_id] = nil else if not args.race or not args.caste then error 'Specficy a target form.' @@ -113,13 +120,12 @@ else end end -local unit = df.unit.find(tonumber(args.unit)) local oldRace = unit.enemy.normal_race local oldCaste = unit.enemy.normal_caste if args.setPrevRace then - normalRace[args.unit] = {} - normalRace[args.unit].race = oldRace - normalRace[args.unit].caste = oldCaste + normalRace[unit_id] = {} + normalRace[unit_id].race = oldRace + normalRace[unit_id].caste = oldCaste end transform(unit,raceIndex,caste,args.setPrevRace) From 5952f23a21896f986b641557147c8bf455635063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branislav=20Setl=C3=A1k?= Date: Sat, 14 Oct 2023 14:49:12 +0200 Subject: [PATCH 611/732] finished script and added it to control panel --- docs/fix/corrupt-jobs.rst | 15 +++++++++++++++ fix/corrupt-jobs.lua | 21 +++++++-------------- gui/control-panel.lua | 3 +++ 3 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 docs/fix/corrupt-jobs.rst diff --git a/docs/fix/corrupt-jobs.rst b/docs/fix/corrupt-jobs.rst new file mode 100644 index 0000000000..36c74d5dd3 --- /dev/null +++ b/docs/fix/corrupt-jobs.rst @@ -0,0 +1,15 @@ +fix/corrupt-jobs +================== + +.. dfhack-tool:: + :summary: Removes jobs with an id of -1 from units. + :tags: fort bugfix + +This fix ensures that no units have a job with an id of -1 set as their current job. + +Usage +----- + +:: + + fix/corrupt-jobs diff --git a/fix/corrupt-jobs.lua b/fix/corrupt-jobs.lua index a158d9642b..bb43cbff32 100644 --- a/fix/corrupt-jobs.lua +++ b/fix/corrupt-jobs.lua @@ -1,21 +1,14 @@ --- Deletes corrupted jobs from global job list - -local utils = require("utils") +-- Deletes corrupted jobs from affected units local count = 0 -functioning_job_list = {} - -for _, job in ipairs(df.global.job_list) do - if job.id != -1 then - functioning_job_list[#functioning_job_list+1] = job +for _, unit in ipairs(df.global.world.units.all) do + if unit.job.current_job and unit.job.current_job.id == -1 then + unit.job.current_job = nil + count = count + 1 end end -for _, v in ipairs(df.global.world.units.all) do - if v.job.current_job then - if utils.linear_index(functioning_job_list, v.job.current_job) == nil then - v.job.current_job = nil - end - end +if count > 0 then + print(('removed %d corrupted job(s) from affected units'):format(count)) end diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 1d642f0402..e8a731ee88 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -140,6 +140,9 @@ local REPEATS = { ['empty-wheelbarrows']={ desc='Empties wheelbarrows which have rocks stuck in them.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, + ['corrupt-jobs']={ + desc='Removes corrupt jobs from affected units.', + command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/corrupt-jobs', ']'}}, } local REPEATS_LIST = {} for k in pairs(REPEATS) do From e626d72a386e9a558f22530369487920a3cc971a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branislav=20Setl=C3=A1k?= Date: Sat, 14 Oct 2023 15:01:22 +0200 Subject: [PATCH 612/732] fixed trailing whitespace --- fix/corrupt-jobs.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fix/corrupt-jobs.lua b/fix/corrupt-jobs.lua index bb43cbff32..ffa1dd64e7 100644 --- a/fix/corrupt-jobs.lua +++ b/fix/corrupt-jobs.lua @@ -3,10 +3,10 @@ local count = 0 for _, unit in ipairs(df.global.world.units.all) do - if unit.job.current_job and unit.job.current_job.id == -1 then + if unit.job.current_job and unit.job.current_job.id == -1 then unit.job.current_job = nil count = count + 1 - end + end end if count > 0 then From 5e2dddd4e06e896cde18e0c0d76d8f374dd900e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branislav=20Setl=C3=A1k?= Date: Sat, 14 Oct 2023 20:41:41 +0200 Subject: [PATCH 613/732] fixed doc and made the script run on load --- docs/fix/corrupt-jobs.rst | 4 ++-- fix/corrupt-jobs.lua | 28 +++++++++++++++++++++------- gui/control-panel.lua | 3 --- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/fix/corrupt-jobs.rst b/docs/fix/corrupt-jobs.rst index 36c74d5dd3..1894a59857 100644 --- a/docs/fix/corrupt-jobs.rst +++ b/docs/fix/corrupt-jobs.rst @@ -1,11 +1,11 @@ fix/corrupt-jobs -================== +================ .. dfhack-tool:: :summary: Removes jobs with an id of -1 from units. :tags: fort bugfix -This fix ensures that no units have a job with an id of -1 set as their current job. +This fix cleans up corrupt jobs so they don't cause crashes. It runs automatically on fort load, so you don't have to run it manually. Usage ----- diff --git a/fix/corrupt-jobs.lua b/fix/corrupt-jobs.lua index ffa1dd64e7..afa6a82b09 100644 --- a/fix/corrupt-jobs.lua +++ b/fix/corrupt-jobs.lua @@ -1,14 +1,28 @@ -- Deletes corrupted jobs from affected units -local count = 0 +local GLOBAL_KEY = 'corrupt-jobs' -for _, unit in ipairs(df.global.world.units.all) do - if unit.job.current_job and unit.job.current_job.id == -1 then - unit.job.current_job = nil - count = count + 1 +function remove_bad_jobs() + local count = 0 + + for _, unit in ipairs(df.global.world.units.all) do + if unit.job.current_job and unit.job.current_job.id == -1 then + unit.job.current_job = nil + count = count + 1 + end + end + + if count > 0 then + print(('removed %d corrupted job(s) from affected units'):format(count)) end end -if count > 0 then - print(('removed %d corrupted job(s) from affected units'):format(count)) +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_LOADED then + remove_bad_jobs() + end end + +if dfhack_flags.module then + return +end \ No newline at end of file diff --git a/gui/control-panel.lua b/gui/control-panel.lua index e8a731ee88..1d642f0402 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -140,9 +140,6 @@ local REPEATS = { ['empty-wheelbarrows']={ desc='Empties wheelbarrows which have rocks stuck in them.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, - ['corrupt-jobs']={ - desc='Removes corrupt jobs from affected units.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/corrupt-jobs', ']'}}, } local REPEATS_LIST = {} for k in pairs(REPEATS) do From 0b02f6b7526a591e84dc3800c391c6ed4dc36b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branislav=20Setl=C3=A1k?= Date: Sat, 14 Oct 2023 20:50:41 +0200 Subject: [PATCH 614/732] fixed enf of file --- fix/corrupt-jobs.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix/corrupt-jobs.lua b/fix/corrupt-jobs.lua index afa6a82b09..9e998ad01a 100644 --- a/fix/corrupt-jobs.lua +++ b/fix/corrupt-jobs.lua @@ -25,4 +25,4 @@ end if dfhack_flags.module then return -end \ No newline at end of file +end From 62bf06f248c22ba335b97eaf3d7726a20e2dc580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branislav=20Setl=C3=A1k?= Date: Sat, 14 Oct 2023 20:57:24 +0200 Subject: [PATCH 615/732] added module bool --- fix/corrupt-jobs.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/fix/corrupt-jobs.lua b/fix/corrupt-jobs.lua index 9e998ad01a..b3c5965ccc 100644 --- a/fix/corrupt-jobs.lua +++ b/fix/corrupt-jobs.lua @@ -1,4 +1,5 @@ -- Deletes corrupted jobs from affected units +--@module = true local GLOBAL_KEY = 'corrupt-jobs' From 55ab19fa6ba6d1f0e8b8a43c7e61f641bf4023df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branislav=20Setl=C3=A1k?= Date: Sat, 14 Oct 2023 21:11:33 +0200 Subject: [PATCH 616/732] added changelog entry --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index d03ae453cc..04544ea87c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ Template for new versions: ## New Tools - `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) +- `corrupt-jobs`: removes corrupted jobs from units at world load ## New Features - `drain-aquifer`: gained ability to drain just above or below a certain z-level From 625923175034aed61aa0f9bea9c272358588120e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branislav=20Setl=C3=A1k?= Date: Sat, 14 Oct 2023 21:18:11 +0200 Subject: [PATCH 617/732] added correct path --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 04544ea87c..66b3b15d32 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,7 +28,7 @@ Template for new versions: ## New Tools - `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) -- `corrupt-jobs`: removes corrupted jobs from units at world load +- `fix/corrupt-jobs`: removes corrupted jobs from units at world load ## New Features - `drain-aquifer`: gained ability to drain just above or below a certain z-level From 22238c9707be9335dd18e92505dcf393d6267baa Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 14 Oct 2023 12:28:50 -0700 Subject: [PATCH 618/732] Update burial.lua --- burial.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/burial.lua b/burial.lua index caf01e0064..3bc7f1072c 100644 --- a/burial.lua +++ b/burial.lua @@ -19,7 +19,7 @@ local tomb_blueprint = { local tomb_count = 0 for _, coffin in pairs(df.global.world.buildings.other.COFFIN) do - if cur_zlevel and not (coffin.z == df.global.window_z) then + if cur_zlevel and coffin.z ~= df.global.window_z then goto skip end for _, zone in pairs(coffin.relations) do From 7f2c3e55c9698fc8687d3419fe5f8b2bf6032245 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Oct 2023 12:44:46 -0700 Subject: [PATCH 619/732] only create tombs for coffins not in a zone that is, don't create tomb zones for coffins that are already in other types of zones --- burial.lua | 11 ++--------- docs/burial.rst | 9 +++++---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/burial.lua b/burial.lua index 3bc7f1072c..d6ce884014 100644 --- a/burial.lua +++ b/burial.lua @@ -1,5 +1,6 @@ -- Allows burial in unowned coffins. -- Based on Putnam's work (https://gist.github.com/Putnam3145/e7031588f4d9b24b9dda) + local argparse = require('argparse') local quickfort = reqscript('quickfort') @@ -18,20 +19,12 @@ local tomb_blueprint = { local tomb_count = 0 for _, coffin in pairs(df.global.world.buildings.other.COFFIN) do - - if cur_zlevel and coffin.z ~= df.global.window_z then + if #coffin.relations > 0 or cur_zlevel and coffin.z ~= df.global.window_z then goto skip end - for _, zone in pairs(coffin.relations) do - if zone.type == df.civzone_type.Tomb then - goto skip - end - end - tomb_blueprint.pos = xyz2pos(coffin.x1, coffin.y1, coffin.z) quickfort.apply_blueprint(tomb_blueprint) tomb_count = tomb_count + 1 - ::skip:: end diff --git a/docs/burial.rst b/docs/burial.rst index 506d1a8715..1923384572 100644 --- a/docs/burial.rst +++ b/docs/burial.rst @@ -5,7 +5,8 @@ burial :summary: Create tomb zones for unzoned coffins. :tags: fort productivity buildings -Creates a 1x1 tomb zone for each built coffin that isn't already in a tomb. +Creates a 1x1 tomb zone for each built coffin that isn't already contained in a +zone. Usage ----- @@ -16,16 +17,16 @@ Examples -------- ``burial`` - Create a tomb for every coffin on the map with automatic burial enabled. + Create a general use tomb for every unzoned coffin on the map. ``burial -z`` Create tombs only on the current zlevel. ``burial -c`` - Create tombs designated for automatic burial of citizens only. + Create tombs designated for burial of citizens only. ``burial -p`` - Create tombs designated for automatic burial of pets only. + Create tombs designated for burial of pets only. ``burial -cp`` Create tombs with automatic burial disabled for both citizens and pets, From 9079e8536660a8c697194b5826d7f503be2718bc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Oct 2023 12:48:59 -0700 Subject: [PATCH 620/732] also allow script to be run manually --- fix/corrupt-jobs.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fix/corrupt-jobs.lua b/fix/corrupt-jobs.lua index b3c5965ccc..de6eed2e7b 100644 --- a/fix/corrupt-jobs.lua +++ b/fix/corrupt-jobs.lua @@ -27,3 +27,6 @@ end if dfhack_flags.module then return end + +-- allow the player to run it manually if they want to +remove_bad_jobs() From 1302d281b3e4efdb87cd79204b5a50ecc8a309a3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Oct 2023 13:17:58 -0700 Subject: [PATCH 621/732] remove other tags from unavailable tools so they don't clutter up the indices --- docs/adaptation.rst | 2 +- docs/add-thought.rst | 2 +- docs/adv-fix-sleepers.rst | 2 +- docs/adv-max-skills.rst | 2 +- docs/adv-rumors.rst | 2 +- docs/assign-profile.rst | 2 +- docs/autolabor-artisans.rst | 2 +- docs/binpatch.rst | 2 +- docs/bodyswap.rst | 2 +- docs/break-dance.rst | 2 +- docs/build-now.rst | 2 +- docs/cannibalism.rst | 2 +- docs/color-schemes.rst | 2 +- docs/combat-harden.rst | 2 +- docs/deteriorate.rst | 2 +- docs/devel/block-borders.rst | 2 +- docs/devel/cmptiles.rst | 2 +- docs/devel/find-offsets.rst | 2 +- docs/devel/find-twbt.rst | 2 +- docs/devel/inject-raws.rst | 2 +- docs/devel/kill-hf.rst | 2 +- docs/devel/light.rst | 2 +- docs/devel/list-filters.rst | 2 +- docs/devel/lua-example.rst | 2 +- docs/devel/luacov.rst | 2 +- docs/devel/nuke-items.rst | 2 +- docs/devel/prepare-save.rst | 2 +- docs/devel/print-event.rst | 2 +- docs/devel/test-perlin.rst | 2 +- docs/devel/unit-path.rst | 2 +- docs/devel/watch-minecarts.rst | 2 +- docs/do-job-now.rst | 2 +- docs/dwarf-op.rst | 2 +- docs/embark-skills.rst | 2 +- docs/fix-ster.rst | 2 +- docs/fix/corrupt-equipment.rst | 2 +- docs/fix/item-occupancy.rst | 2 +- docs/fix/population-cap.rst | 2 +- docs/fix/tile-occupancy.rst | 2 +- docs/fixnaked.rst | 2 +- docs/flashstep.rst | 2 +- docs/forget-dead-body.rst | 2 +- docs/forum-dwarves.rst | 2 +- docs/ghostly.rst | 2 +- docs/growcrops.rst | 2 +- docs/gui/advfort.rst | 2 +- docs/gui/autogems.rst | 2 +- docs/gui/choose-weapons.rst | 2 +- docs/gui/clone-uniform.rst | 2 +- docs/gui/color-schemes.rst | 2 +- docs/gui/companion-order.rst | 2 +- docs/gui/create-tree.rst | 2 +- docs/gui/dfstatus.rst | 2 +- docs/gui/extended-status.rst | 2 +- docs/gui/family-affairs.rst | 2 +- docs/gui/guide-path.rst | 2 +- docs/gui/kitchen-info.rst | 2 +- docs/gui/load-screen.rst | 2 +- docs/gui/manager-quantity.rst | 2 +- docs/gui/mechanisms.rst | 2 +- docs/gui/petitions.rst | 2 +- docs/gui/power-meter.rst | 2 +- docs/gui/quantum.rst | 2 +- docs/gui/rename.rst | 2 +- docs/gui/room-list.rst | 2 +- docs/gui/settings-manager.rst | 2 +- docs/gui/siege-engine.rst | 2 +- docs/gui/stamper.rst | 2 +- docs/gui/stockpiles.rst | 2 +- docs/gui/teleport.rst | 2 +- docs/gui/unit-info-viewer.rst | 2 +- docs/gui/workflow.rst | 2 +- docs/hotkey-notes.rst | 2 +- docs/launch.rst | 2 +- docs/linger.rst | 2 +- docs/list-waves.rst | 2 +- docs/load-save.rst | 2 +- docs/make-legendary.rst | 2 +- docs/markdown.rst | 2 +- docs/max-wave.rst | 2 +- docs/modtools/anonymous-script.rst | 2 +- docs/modtools/change-build-menu.rst | 2 +- docs/modtools/create-tree.rst | 2 +- docs/modtools/create-unit.rst | 2 +- docs/modtools/equip-item.rst | 2 +- docs/modtools/extra-gamelog.rst | 2 +- docs/modtools/fire-rate.rst | 2 +- docs/modtools/if-entity.rst | 2 +- docs/modtools/interaction-trigger.rst | 2 +- docs/modtools/invader-item-destroyer.rst | 2 +- docs/modtools/item-trigger.rst | 2 +- docs/modtools/moddable-gods.rst | 2 +- docs/modtools/outside-only.rst | 2 +- docs/modtools/pref-edit.rst | 2 +- docs/modtools/projectile-trigger.rst | 2 +- docs/modtools/random-trigger.rst | 2 +- docs/modtools/raw-lint.rst | 2 +- docs/modtools/reaction-product-trigger.rst | 2 +- docs/modtools/reaction-trigger-transition.rst | 2 +- docs/modtools/reaction-trigger.rst | 2 +- docs/modtools/set-belief.rst | 2 +- docs/modtools/set-need.rst | 2 +- docs/modtools/set-personality.rst | 2 +- docs/modtools/spawn-flow.rst | 2 +- docs/modtools/syndrome-trigger.rst | 2 +- docs/modtools/transform-unit.rst | 2 +- docs/names.rst | 2 +- docs/open-legends.rst | 2 +- docs/pop-control.rst | 2 +- docs/prefchange.rst | 2 +- docs/putontable.rst | 2 +- docs/questport.rst | 2 +- docs/resurrect-adv.rst | 2 +- docs/reveal-adv-map.rst | 2 +- docs/season-palette.rst | 2 +- docs/siren.rst | 2 +- docs/spawnunit.rst | 2 +- docs/tidlers.rst | 2 +- docs/timestream.rst | 2 +- docs/undump-buildings.rst | 2 +- docs/uniform-unstick.rst | 2 +- docs/unretire-anyone.rst | 2 +- docs/view-item-info.rst | 2 +- docs/view-unit-reports.rst | 2 +- docs/warn-stealers.rst | 2 +- 125 files changed, 125 insertions(+), 125 deletions(-) diff --git a/docs/adaptation.rst b/docs/adaptation.rst index 140fa81a21..9cf08ecd68 100644 --- a/docs/adaptation.rst +++ b/docs/adaptation.rst @@ -3,7 +3,7 @@ adaptation .. dfhack-tool:: :summary: Adjust a unit's cave adaptation level. - :tags: unavailable fort armok units + :tags: unavailable View or set level of cavern adaptation for the selected unit or the whole fort. diff --git a/docs/add-thought.rst b/docs/add-thought.rst index b2d9e5e5c8..9a1a24754d 100644 --- a/docs/add-thought.rst +++ b/docs/add-thought.rst @@ -3,7 +3,7 @@ add-thought .. dfhack-tool:: :summary: Adds a thought to the selected unit. - :tags: unavailable fort armok units + :tags: unavailable Usage ----- diff --git a/docs/adv-fix-sleepers.rst b/docs/adv-fix-sleepers.rst index befa194dc0..111a743f43 100644 --- a/docs/adv-fix-sleepers.rst +++ b/docs/adv-fix-sleepers.rst @@ -3,7 +3,7 @@ adv-fix-sleepers .. dfhack-tool:: :summary: Fix units who refuse to awaken in adventure mode. - :tags: unavailable adventure bugfix units + :tags: unavailable Use this tool if you encounter sleeping units who refuse to awaken regardless of talking to them, hitting them, or waiting so long you die of thirst diff --git a/docs/adv-max-skills.rst b/docs/adv-max-skills.rst index 34c7ad25c5..75c201ed7b 100644 --- a/docs/adv-max-skills.rst +++ b/docs/adv-max-skills.rst @@ -3,7 +3,7 @@ adv-max-skills .. dfhack-tool:: :summary: Raises adventurer stats to max. - :tags: unavailable adventure embark armok + :tags: unavailable When creating an adventurer, raises all changeable skills and attributes to their maximum level. diff --git a/docs/adv-rumors.rst b/docs/adv-rumors.rst index f07cb324d8..563c22df36 100644 --- a/docs/adv-rumors.rst +++ b/docs/adv-rumors.rst @@ -3,7 +3,7 @@ adv-rumors .. dfhack-tool:: :summary: Improves the rumors menu in adventure mode. - :tags: unavailable adventure interface + :tags: unavailable In adventure mode, start a conversation with someone and then run this tool to improve the "Bring up specific incident or rumor" menu. Specifically, this diff --git a/docs/assign-profile.rst b/docs/assign-profile.rst index c491428180..d7092360bf 100644 --- a/docs/assign-profile.rst +++ b/docs/assign-profile.rst @@ -3,7 +3,7 @@ assign-profile .. dfhack-tool:: :summary: Adjust characteristics of a unit according to saved profiles. - :tags: unavailable fort armok units + :tags: unavailable This tool can load a profile stored in a JSON file and apply the characteristics to a unit. diff --git a/docs/autolabor-artisans.rst b/docs/autolabor-artisans.rst index 7e1b662123..9c7b248b5b 100644 --- a/docs/autolabor-artisans.rst +++ b/docs/autolabor-artisans.rst @@ -3,7 +3,7 @@ autolabor-artisans .. dfhack-tool:: :summary: Configures autolabor to produce artisan dwarves. - :tags: unavailable fort labors + :tags: unavailable This script runs an `autolabor` command for all labors where skill level influences output quality (e.g. Carpentry, Stone detailing, Weaponsmithing, diff --git a/docs/binpatch.rst b/docs/binpatch.rst index 517af00afc..c5da27f4b8 100644 --- a/docs/binpatch.rst +++ b/docs/binpatch.rst @@ -3,7 +3,7 @@ binpatch .. dfhack-tool:: :summary: Applies or removes binary patches. - :tags: unavailable dev + :tags: unavailable See `binpatches` for more info. diff --git a/docs/bodyswap.rst b/docs/bodyswap.rst index b9b880eead..51c0fdcfd1 100644 --- a/docs/bodyswap.rst +++ b/docs/bodyswap.rst @@ -3,7 +3,7 @@ bodyswap .. dfhack-tool:: :summary: Take direct control of any visible unit. - :tags: unavailable adventure armok units + :tags: unavailable This script allows the player to take direct control of any unit present in adventure mode whilst giving up control of their current player character. diff --git a/docs/break-dance.rst b/docs/break-dance.rst index 9a58be4c73..048fa1a85f 100644 --- a/docs/break-dance.rst +++ b/docs/break-dance.rst @@ -3,7 +3,7 @@ break-dance .. dfhack-tool:: :summary: Fixes buggy tavern dances. - :tags: unavailable fort bugfix units + :tags: unavailable Sometimes when a unit can't find a dance partner, the dance becomes stuck and never stops. This tool can get them unstuck. diff --git a/docs/build-now.rst b/docs/build-now.rst index 3a3d472a5d..873ecabfd5 100644 --- a/docs/build-now.rst +++ b/docs/build-now.rst @@ -3,7 +3,7 @@ build-now .. dfhack-tool:: :summary: Instantly completes building construction jobs. - :tags: unavailable fort armok buildings + :tags: unavailable By default, all unsuspended buildings on the map are completed, but the area of effect is configurable. diff --git a/docs/cannibalism.rst b/docs/cannibalism.rst index 75a63df0ea..69d3973e24 100644 --- a/docs/cannibalism.rst +++ b/docs/cannibalism.rst @@ -3,7 +3,7 @@ cannibalism .. dfhack-tool:: :summary: Allows a player character to consume sapient corpses. - :tags: unavailable adventure gameplay + :tags: unavailable This tool clears the flag from items that mark them as being from a sapient creature. Use from an adventurer's inventory screen or an individual item's diff --git a/docs/color-schemes.rst b/docs/color-schemes.rst index 13f13ee145..07445b20fd 100644 --- a/docs/color-schemes.rst +++ b/docs/color-schemes.rst @@ -3,7 +3,7 @@ color-schemes .. dfhack-tool:: :summary: Modify the colors used by the DF UI. - :tags: unavailable fort gameplay graphics + :tags: unavailable This tool allows you to set exactly which shades of colors should be used in the DF interface color palette. diff --git a/docs/combat-harden.rst b/docs/combat-harden.rst index f77ff1f979..d8fa17ab84 100644 --- a/docs/combat-harden.rst +++ b/docs/combat-harden.rst @@ -3,7 +3,7 @@ combat-harden .. dfhack-tool:: :summary: Set the combat-hardened value on a unit. - :tags: unavailable fort armok military units + :tags: unavailable This tool can make a unit care more/less about seeing corpses. diff --git a/docs/deteriorate.rst b/docs/deteriorate.rst index 5135cd9ae3..583150afdb 100644 --- a/docs/deteriorate.rst +++ b/docs/deteriorate.rst @@ -3,7 +3,7 @@ deteriorate .. dfhack-tool:: :summary: Cause corpses, clothes, and/or food to rot away over time. - :tags: unavailable fort auto fps gameplay items plants + :tags: unavailable When enabled, this script will cause the specified item types to slowly rot away. By default, items disappear after a few months, but you can choose to slow diff --git a/docs/devel/block-borders.rst b/docs/devel/block-borders.rst index b13f5b074a..acc6f39877 100644 --- a/docs/devel/block-borders.rst +++ b/docs/devel/block-borders.rst @@ -3,7 +3,7 @@ devel/block-borders .. dfhack-tool:: :summary: Outline map blocks on the map screen. - :tags: unavailable dev map + :tags: unavailable This tool displays an overlay that highlights the borders of map blocks. See :doc:`/docs/api/Maps` for details on map blocks. diff --git a/docs/devel/cmptiles.rst b/docs/devel/cmptiles.rst index c10949e9e7..20fab2157e 100644 --- a/docs/devel/cmptiles.rst +++ b/docs/devel/cmptiles.rst @@ -3,7 +3,7 @@ devel/cmptiles .. dfhack-tool:: :summary: List or compare two tiletype material groups. - :tags: unavailable dev + :tags: unavailable Lists and/or compares two tiletype material groups. You can see the list of valid material groups by running:: diff --git a/docs/devel/find-offsets.rst b/docs/devel/find-offsets.rst index 160253ebf8..72d9c869f3 100644 --- a/docs/devel/find-offsets.rst +++ b/docs/devel/find-offsets.rst @@ -3,7 +3,7 @@ devel/find-offsets .. dfhack-tool:: :summary: Find memory offsets of DF data structures. - :tags: unavailable dev + :tags: unavailable .. warning:: diff --git a/docs/devel/find-twbt.rst b/docs/devel/find-twbt.rst index 1d10c12c1e..4a5e056945 100644 --- a/docs/devel/find-twbt.rst +++ b/docs/devel/find-twbt.rst @@ -3,7 +3,7 @@ devel/find-twbt .. dfhack-tool:: :summary: Display the memory offsets of some important TWBT functions. - :tags: unavailable dev + :tags: unavailable Finds some TWBT-related offsets - currently just ``twbt_render_map``. diff --git a/docs/devel/inject-raws.rst b/docs/devel/inject-raws.rst index 42dd30ce3a..dadd11904a 100644 --- a/docs/devel/inject-raws.rst +++ b/docs/devel/inject-raws.rst @@ -3,7 +3,7 @@ devel/inject-raws .. dfhack-tool:: :summary: Add objects and reactions into an existing world. - :tags: unavailable dev + :tags: unavailable WARNING: THIS SCRIPT CAN PERMANENTLY DAMAGE YOUR SAVE. diff --git a/docs/devel/kill-hf.rst b/docs/devel/kill-hf.rst index 9e9a29f2d2..3d3209335b 100644 --- a/docs/devel/kill-hf.rst +++ b/docs/devel/kill-hf.rst @@ -3,7 +3,7 @@ devel/kill-hf .. dfhack-tool:: :summary: Kill a historical figure. - :tags: unavailable dev + :tags: unavailable This tool can kill the specified historical figure, even if off-site, or terminate a pregnancy. Useful for working around :bug:`11549`. diff --git a/docs/devel/light.rst b/docs/devel/light.rst index ce175767cd..25a9c97ee6 100644 --- a/docs/devel/light.rst +++ b/docs/devel/light.rst @@ -3,7 +3,7 @@ devel/light .. dfhack-tool:: :summary: Experiment with lighting overlays. - :tags: unavailable dev graphics + :tags: unavailable This is an experimental lighting engine for DF, using the `rendermax` plugin. diff --git a/docs/devel/list-filters.rst b/docs/devel/list-filters.rst index 36642018f6..f8b761efb8 100644 --- a/docs/devel/list-filters.rst +++ b/docs/devel/list-filters.rst @@ -3,7 +3,7 @@ devel/list-filters .. dfhack-tool:: :summary: List input items for the selected building type. - :tags: unavailable dev + :tags: unavailable This tool lists input items for the building that is currently being built. You must be in build mode and have a building type selected for placement. This is diff --git a/docs/devel/lua-example.rst b/docs/devel/lua-example.rst index 63634a9931..60fb93bbaf 100644 --- a/docs/devel/lua-example.rst +++ b/docs/devel/lua-example.rst @@ -3,7 +3,7 @@ devel/lua-example .. dfhack-tool:: :summary: An example lua script. - :tags: unavailable dev + :tags: unavailable This is an example Lua script which just reports the number of times it has been called. Useful for testing environment persistence. diff --git a/docs/devel/luacov.rst b/docs/devel/luacov.rst index e3af3e7899..8b9667cfd4 100644 --- a/docs/devel/luacov.rst +++ b/docs/devel/luacov.rst @@ -3,7 +3,7 @@ devel/luacov .. dfhack-tool:: :summary: Lua script coverage report generator. - :tags: unavailable dev + :tags: unavailable This script generates a coverage report from collected statistics. By default it reports on every Lua file in all of DFHack. To filter filenames, specify one or diff --git a/docs/devel/nuke-items.rst b/docs/devel/nuke-items.rst index 26c6dd6a84..4961652276 100644 --- a/docs/devel/nuke-items.rst +++ b/docs/devel/nuke-items.rst @@ -3,7 +3,7 @@ devel/nuke-items .. dfhack-tool:: :summary: Deletes all free items in the game. - :tags: unavailable dev fps items + :tags: unavailable This tool deletes **ALL** items not referred to by units, buildings, or jobs. Intended solely for lag investigation. diff --git a/docs/devel/prepare-save.rst b/docs/devel/prepare-save.rst index 7498e92840..c11df0cd7f 100644 --- a/docs/devel/prepare-save.rst +++ b/docs/devel/prepare-save.rst @@ -3,7 +3,7 @@ devel/prepare-save .. dfhack-tool:: :summary: Set internal game state to known values for memory analysis. - :tags: unavailable dev + :tags: unavailable .. warning:: diff --git a/docs/devel/print-event.rst b/docs/devel/print-event.rst index 7d1bb97945..271e447cb8 100644 --- a/docs/devel/print-event.rst +++ b/docs/devel/print-event.rst @@ -3,7 +3,7 @@ devel/print-event .. dfhack-tool:: :summary: Show historical events. - :tags: unavailable dev + :tags: unavailable This tool displays the description of a historical event. diff --git a/docs/devel/test-perlin.rst b/docs/devel/test-perlin.rst index 5f8deceef1..f5e01568a8 100644 --- a/docs/devel/test-perlin.rst +++ b/docs/devel/test-perlin.rst @@ -3,7 +3,7 @@ devel/test-perlin .. dfhack-tool:: :summary: Generate an image based on perlin noise. - :tags: unavailable dev + :tags: unavailable Generates an image using multiple octaves of perlin noise. diff --git a/docs/devel/unit-path.rst b/docs/devel/unit-path.rst index 8bca45cc46..e3bf50db34 100644 --- a/docs/devel/unit-path.rst +++ b/docs/devel/unit-path.rst @@ -3,7 +3,7 @@ devel/unit-path .. dfhack-tool:: :summary: Inspect where a unit is going and how it's getting there. - :tags: unavailable dev + :tags: unavailable When run with a unit selected, the path that the unit is currently following is highlighted on the map. You can jump between the unit and the destination tile. diff --git a/docs/devel/watch-minecarts.rst b/docs/devel/watch-minecarts.rst index 6b915a5aa1..215c4d6500 100644 --- a/docs/devel/watch-minecarts.rst +++ b/docs/devel/watch-minecarts.rst @@ -3,7 +3,7 @@ devel/watch-minecarts .. dfhack-tool:: :summary: Inspect minecart coordinates and speeds. - :tags: unavailable dev + :tags: unavailable When running, this tool will log minecart coordinates and speeds to the console. diff --git a/docs/do-job-now.rst b/docs/do-job-now.rst index 291defd133..6f687b7400 100644 --- a/docs/do-job-now.rst +++ b/docs/do-job-now.rst @@ -3,7 +3,7 @@ do-job-now .. dfhack-tool:: :summary: Mark the job related to what you're looking at as high priority. - :tags: unavailable fort productivity jobs + :tags: unavailable The script will try its best to find a job related to the selected entity (which can be a job, dwarf, animal, item, building, plant or work order) and then mark diff --git a/docs/dwarf-op.rst b/docs/dwarf-op.rst index 4b60a88802..bf1dd75d0f 100644 --- a/docs/dwarf-op.rst +++ b/docs/dwarf-op.rst @@ -3,7 +3,7 @@ dwarf-op .. dfhack-tool:: :summary: Tune units to perform underrepresented job roles in your fortress. - :tags: unavailable fort armok units + :tags: unavailable ``dwarf-op`` examines the distribution of skills and attributes across the dwarves in your fortress and can rewrite the characteristics of a dwarf (or diff --git a/docs/embark-skills.rst b/docs/embark-skills.rst index a43c5b2672..cdf54aa246 100644 --- a/docs/embark-skills.rst +++ b/docs/embark-skills.rst @@ -3,7 +3,7 @@ embark-skills .. dfhack-tool:: :summary: Adjust dwarves' skills when embarking. - :tags: unavailable embark fort armok units + :tags: unavailable When selecting starting skills for your dwarves on the embark screen, this tool can manipulate the skill values or adjust the number of points you have diff --git a/docs/fix-ster.rst b/docs/fix-ster.rst index d21e62f725..767a9780bc 100644 --- a/docs/fix-ster.rst +++ b/docs/fix-ster.rst @@ -3,7 +3,7 @@ fix-ster .. dfhack-tool:: :summary: Toggle infertility for units. - :tags: unavailable fort armok animals + :tags: unavailable Now you can restore fertility to infertile creatures or inflict infertility on creatures that you do not want to breed. diff --git a/docs/fix/corrupt-equipment.rst b/docs/fix/corrupt-equipment.rst index e9cc0e16b9..26a2784039 100644 --- a/docs/fix/corrupt-equipment.rst +++ b/docs/fix/corrupt-equipment.rst @@ -3,7 +3,7 @@ fix/corrupt-equipment .. dfhack-tool:: :summary: Fixes some game crashes caused by corrupt military equipment. - :tags: unavailable fort bugfix military + :tags: unavailable This fix corrects some kinds of corruption that can occur in equipment lists, as in :bug:`11014`. Run this script at least every time a squad comes back from a diff --git a/docs/fix/item-occupancy.rst b/docs/fix/item-occupancy.rst index 193d3899aa..b039cc73f8 100644 --- a/docs/fix/item-occupancy.rst +++ b/docs/fix/item-occupancy.rst @@ -3,7 +3,7 @@ fix/item-occupancy .. dfhack-tool:: :summary: Fixes errors with phantom items occupying site. - :tags: unavailable fort bugfix map + :tags: unavailable This tool diagnoses and fixes issues with nonexistent 'items occupying site', usually caused by hacking mishaps with items being improperly moved about. diff --git a/docs/fix/population-cap.rst b/docs/fix/population-cap.rst index 59fcbdb2ea..e259a83044 100644 --- a/docs/fix/population-cap.rst +++ b/docs/fix/population-cap.rst @@ -3,7 +3,7 @@ fix/population-cap .. dfhack-tool:: :summary: Ensure the population cap is respected. - :tags: unavailable fort bugfix units + :tags: unavailable Run this after every migrant wave to ensure your population cap is not exceeded. diff --git a/docs/fix/tile-occupancy.rst b/docs/fix/tile-occupancy.rst index 25210f6121..fbcfaa5ca0 100644 --- a/docs/fix/tile-occupancy.rst +++ b/docs/fix/tile-occupancy.rst @@ -3,7 +3,7 @@ fix/tile-occupancy .. dfhack-tool:: :summary: Fix tile occupancy flags. - :tags: unavailable fort bugfix map + :tags: unavailable This tool clears bad occupancy flags at the selected tile. It is useful for getting rid of phantom "building present" messages when trying to build diff --git a/docs/fixnaked.rst b/docs/fixnaked.rst index c416c35694..a646930572 100644 --- a/docs/fixnaked.rst +++ b/docs/fixnaked.rst @@ -3,7 +3,7 @@ fixnaked .. dfhack-tool:: :summary: Removes all unhappy thoughts due to lack of clothing. - :tags: unavailable fort armok units + :tags: unavailable If you're having trouble keeping your dwarves properly clothed and the stress is mounting, this tool can help you calm things down. ``fixnaked`` will go through diff --git a/docs/flashstep.rst b/docs/flashstep.rst index 888ae36826..5dcf37fe62 100644 --- a/docs/flashstep.rst +++ b/docs/flashstep.rst @@ -3,7 +3,7 @@ flashstep .. dfhack-tool:: :summary: Teleport your adventurer to the cursor. - :tags: unavailable adventure armok + :tags: unavailable ``flashstep`` is a hotkey-friendly teleport that places your adventurer where your cursor is. diff --git a/docs/forget-dead-body.rst b/docs/forget-dead-body.rst index 494555c0e7..a203f6cfa9 100644 --- a/docs/forget-dead-body.rst +++ b/docs/forget-dead-body.rst @@ -3,7 +3,7 @@ forget-dead-body .. dfhack-tool:: :summary: Removes emotions associated with seeing a dead body. - :tags: unavailable fort armok units + :tags: unavailable This tool can help your dwarves recover from seeing a massacre. It removes all emotions associated with seeing a dead body. If your dwarves are traumatized and diff --git a/docs/forum-dwarves.rst b/docs/forum-dwarves.rst index fd3f34fcbc..2e4692854f 100644 --- a/docs/forum-dwarves.rst +++ b/docs/forum-dwarves.rst @@ -3,7 +3,7 @@ forum-dwarves .. dfhack-tool:: :summary: Exports the text you see on the screen for posting to the forums. - :tags: unavailable dfhack + :tags: unavailable This tool saves a copy of a text screen, formatted in BBcode for posting to the Bay12 Forums. Text color and layout is preserved. See `markdown` if you want to diff --git a/docs/ghostly.rst b/docs/ghostly.rst index 0afc2dd854..999125a28d 100644 --- a/docs/ghostly.rst +++ b/docs/ghostly.rst @@ -3,7 +3,7 @@ ghostly .. dfhack-tool:: :summary: Toggles an adventurer's ghost status. - :tags: unavailable adventure armok units + :tags: unavailable This is useful for walking through walls, avoiding attacks, or recovering after a death. diff --git a/docs/growcrops.rst b/docs/growcrops.rst index c53d4b5b67..8b100a72e8 100644 --- a/docs/growcrops.rst +++ b/docs/growcrops.rst @@ -3,7 +3,7 @@ growcrops .. dfhack-tool:: :summary: Instantly grow planted seeds into crops. - :tags: unavailable fort armok plants + :tags: unavailable With no parameters, this command lists the seed types currently planted in your farming plots. With a seed type, the script will grow those seeds, ready to be diff --git a/docs/gui/advfort.rst b/docs/gui/advfort.rst index 7575213b5b..0397a55295 100644 --- a/docs/gui/advfort.rst +++ b/docs/gui/advfort.rst @@ -3,7 +3,7 @@ gui/advfort .. dfhack-tool:: :summary: Perform fort-like jobs in adventure mode. - :tags: unavailable adventure gameplay + :tags: unavailable This script allows performing jobs in adventure mode. For interactive help, press :kbd:`?` while the script is running. diff --git a/docs/gui/autogems.rst b/docs/gui/autogems.rst index 64f61c0fad..82759baa13 100644 --- a/docs/gui/autogems.rst +++ b/docs/gui/autogems.rst @@ -4,7 +4,7 @@ gui/autogems .. dfhack-tool:: :summary: Automatically cut rough gems. - :tags: unavailable fort auto workorders + :tags: unavailable This is a frontend for the `autogems` plugin that allows interactively configuring the gem types that you want to be cut. diff --git a/docs/gui/choose-weapons.rst b/docs/gui/choose-weapons.rst index acd197fb29..e8d81dcec8 100644 --- a/docs/gui/choose-weapons.rst +++ b/docs/gui/choose-weapons.rst @@ -3,7 +3,7 @@ gui/choose-weapons .. dfhack-tool:: :summary: Ensure military dwarves choose appropriate weapons. - :tags: unavailable fort productivity military + :tags: unavailable Activate in the :guilabel:`Equip->View/Customize` page of the military screen. diff --git a/docs/gui/clone-uniform.rst b/docs/gui/clone-uniform.rst index f3bbe94da4..d7e9daa887 100644 --- a/docs/gui/clone-uniform.rst +++ b/docs/gui/clone-uniform.rst @@ -3,7 +3,7 @@ gui/clone-uniform .. dfhack-tool:: :summary: Duplicate an existing military uniform. - :tags: unavailable fort productivity military + :tags: unavailable When invoked, this tool duplicates the currently selected uniform template and selects the newly created copy. Activate in the :guilabel:`Uniforms` page of the diff --git a/docs/gui/color-schemes.rst b/docs/gui/color-schemes.rst index d6a7f832d1..f4187a1495 100644 --- a/docs/gui/color-schemes.rst +++ b/docs/gui/color-schemes.rst @@ -3,7 +3,7 @@ gui/color-schemes .. dfhack-tool:: :summary: Modify the colors in the DF UI. - :tags: unavailable graphics + :tags: unavailable This is an in-game interface for `color-schemes`, which allows you to modify the colors in the Dwarf Fortress interface. This script must be called from either diff --git a/docs/gui/companion-order.rst b/docs/gui/companion-order.rst index c55ebf038b..71488c1b66 100644 --- a/docs/gui/companion-order.rst +++ b/docs/gui/companion-order.rst @@ -3,7 +3,7 @@ gui/companion-order .. dfhack-tool:: :summary: Issue orders to companions. - :tags: unavailable adventure interface + :tags: unavailable This tool allows you to issue orders to your adventurer's companions. Select which companions to issue orders to with lower case letters (green when diff --git a/docs/gui/create-tree.rst b/docs/gui/create-tree.rst index 78ef616998..c1a3819522 100644 --- a/docs/gui/create-tree.rst +++ b/docs/gui/create-tree.rst @@ -3,7 +3,7 @@ gui/create-tree .. dfhack-tool:: :summary: Create a tree. - :tags: unavailable fort armok plants + :tags: unavailable This tool provides a graphical interface for creating trees. diff --git a/docs/gui/dfstatus.rst b/docs/gui/dfstatus.rst index be45390ca7..6c7e44369a 100644 --- a/docs/gui/dfstatus.rst +++ b/docs/gui/dfstatus.rst @@ -3,7 +3,7 @@ gui/dfstatus .. dfhack-tool:: :summary: Show a quick overview of critical stock quantities. - :tags: unavailable fort inspection + :tags: unavailable This tool show a quick overview of stock quantities for: diff --git a/docs/gui/extended-status.rst b/docs/gui/extended-status.rst index 52e2a086c5..f170bc7611 100644 --- a/docs/gui/extended-status.rst +++ b/docs/gui/extended-status.rst @@ -3,7 +3,7 @@ gui/extended-status .. dfhack-tool:: :summary: Add information on beds and bedrooms to the status screen. - :tags: unavailable fort inspection interface + :tags: unavailable Adds an additional page to the ``z`` status screen where you can see information about beds, bedrooms, and whether your dwarves have bedrooms of their own. diff --git a/docs/gui/family-affairs.rst b/docs/gui/family-affairs.rst index 170e2a1a9d..7a1e0a4bfc 100644 --- a/docs/gui/family-affairs.rst +++ b/docs/gui/family-affairs.rst @@ -3,7 +3,7 @@ gui/family-affairs .. dfhack-tool:: :summary: Inspect or meddle with romantic relationships. - :tags: unavailable fort armok inspection units + :tags: unavailable This tool provides a user-friendly interface to view romantic relationships, with the ability to add, remove, or otherwise change them at your whim - diff --git a/docs/gui/guide-path.rst b/docs/gui/guide-path.rst index 5d76993cb8..703d84e22b 100644 --- a/docs/gui/guide-path.rst +++ b/docs/gui/guide-path.rst @@ -3,7 +3,7 @@ gui/guide-path .. dfhack-tool:: :summary: Visualize minecart guide paths. - :tags: unavailable fort inspection map + :tags: unavailable This tool displays the cached path that will be used by the minecart guide order. The game computes this path when the order is executed for the first diff --git a/docs/gui/kitchen-info.rst b/docs/gui/kitchen-info.rst index 86584a88b7..faf1114eda 100644 --- a/docs/gui/kitchen-info.rst +++ b/docs/gui/kitchen-info.rst @@ -3,7 +3,7 @@ gui/kitchen-info .. dfhack-tool:: :summary: Show food item uses in the kitchen status screen. - :tags: unavailable fort inspection + :tags: unavailable This tool is an overlay that adds more info to the Kitchen screen, such as the potential alternate uses of the items that you could mark for cooking. diff --git a/docs/gui/load-screen.rst b/docs/gui/load-screen.rst index 1301ab09f3..ef4bb0e12a 100644 --- a/docs/gui/load-screen.rst +++ b/docs/gui/load-screen.rst @@ -3,7 +3,7 @@ gui/load-screen .. dfhack-tool:: :summary: Replace DF's continue game screen with a searchable list. - :tags: unavailable dfhack + :tags: unavailable If you tend to have many ongoing games, this tool can make it much easier to load the one you're looking for. It replaces DF's "continue game" screen with diff --git a/docs/gui/manager-quantity.rst b/docs/gui/manager-quantity.rst index 5eb9a44c36..4005565e09 100644 --- a/docs/gui/manager-quantity.rst +++ b/docs/gui/manager-quantity.rst @@ -3,7 +3,7 @@ gui/manager-quantity .. dfhack-tool:: :summary: Set the quantity of the selected manager workorder. - :tags: unavailable fort workorders + :tags: unavailable There is no way in the base DF game to change the quantity for an existing manager workorder. Select a workorder on the j-m or u-m screens and run this diff --git a/docs/gui/mechanisms.rst b/docs/gui/mechanisms.rst index 266fb280d2..9de336e84b 100644 --- a/docs/gui/mechanisms.rst +++ b/docs/gui/mechanisms.rst @@ -3,7 +3,7 @@ gui/mechanisms .. dfhack-tool:: :summary: List mechanisms and links connected to a building. - :tags: unavailable fort inspection buildings + :tags: unavailable This convenient tool lists the mechanisms connected to the building and the buildings linked via the mechanisms. Navigating the list centers the view on the diff --git a/docs/gui/petitions.rst b/docs/gui/petitions.rst index 0f48bc90a4..84c5f5e18b 100644 --- a/docs/gui/petitions.rst +++ b/docs/gui/petitions.rst @@ -3,7 +3,7 @@ gui/petitions .. dfhack-tool:: :summary: Show information about your fort's petitions. - :tags: unavailable fort inspection + :tags: unavailable Show your fort's petitions, both pending and fulfilled. diff --git a/docs/gui/power-meter.rst b/docs/gui/power-meter.rst index fc7e59bff1..02ea9f27e9 100644 --- a/docs/gui/power-meter.rst +++ b/docs/gui/power-meter.rst @@ -3,7 +3,7 @@ gui/power-meter .. dfhack-tool:: :summary: Allow pressure plates to measure power. - :tags: unavailable fort gameplay buildings + :tags: unavailable If you run this tool after selecting :guilabel:`Pressure Plate` in the build menu, you will build a power meter building instead of a regular pressure plate. diff --git a/docs/gui/quantum.rst b/docs/gui/quantum.rst index 534f35fcbd..b3d4071716 100644 --- a/docs/gui/quantum.rst +++ b/docs/gui/quantum.rst @@ -3,7 +3,7 @@ gui/quantum .. dfhack-tool:: :summary: Quickly and easily create quantum stockpiles. - :tags: unavailable fort productivity stockpiles + :tags: unavailable This tool provides a visual, interactive interface for creating quantum stockpiles. diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index b23aa8c783..5688354bb7 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -3,7 +3,7 @@ gui/rename .. dfhack-tool:: :summary: Give buildings and units new names, optionally with special chars. - :tags: unavailable fort productivity buildings stockpiles units + :tags: unavailable Once you select a target on the game map, this tool allows you to rename it. It is more powerful than the in-game rename functionality since it allows you to diff --git a/docs/gui/room-list.rst b/docs/gui/room-list.rst index 6a5749215e..e0c5c2ce54 100644 --- a/docs/gui/room-list.rst +++ b/docs/gui/room-list.rst @@ -3,7 +3,7 @@ gui/room-list .. dfhack-tool:: :summary: Manage rooms owned by a dwarf. - :tags: unavailable fort inspection + :tags: unavailable When invoked in :kbd:`q` mode with the cursor over an owned room, this tool lists other rooms owned by the same owner, or by the unit selected in the assign diff --git a/docs/gui/settings-manager.rst b/docs/gui/settings-manager.rst index 6ea2a623fe..e0cdc355d4 100644 --- a/docs/gui/settings-manager.rst +++ b/docs/gui/settings-manager.rst @@ -3,7 +3,7 @@ gui/settings-manager .. dfhack-tool:: :summary: Dynamically adjust global DF settings. - :tags: unavailable dfhack + :tags: unavailable This tool is an in-game editor for settings defined in :file:`data/init/init.txt` and :file:`data/init/d_init.txt`. Changes are written diff --git a/docs/gui/siege-engine.rst b/docs/gui/siege-engine.rst index 704fd8c931..2ca43a64f5 100644 --- a/docs/gui/siege-engine.rst +++ b/docs/gui/siege-engine.rst @@ -3,7 +3,7 @@ gui/siege-engine .. dfhack-tool:: :summary: Extend the functionality and usability of siege engines. - :tags: unavailable fort gameplay buildings + :tags: unavailable This tool is an in-game interface for `siege-engine`, which allows you to link siege engines to stockpiles, restrict operation to certain dwarves, fire a diff --git a/docs/gui/stamper.rst b/docs/gui/stamper.rst index 779e57b6e2..930dab73a0 100644 --- a/docs/gui/stamper.rst +++ b/docs/gui/stamper.rst @@ -3,7 +3,7 @@ gui/stamper .. dfhack-tool:: :summary: Copy, paste, and transform dig designations. - :tags: unavailable fort design map + :tags: unavailable This tool allows you to copy and paste blocks of dig designations. You can also transform what you have copied by shifting it, reflecting it, rotating it, diff --git a/docs/gui/stockpiles.rst b/docs/gui/stockpiles.rst index 80405f6c37..6d28f7c8dd 100644 --- a/docs/gui/stockpiles.rst +++ b/docs/gui/stockpiles.rst @@ -3,7 +3,7 @@ gui/stockpiles .. dfhack-tool:: :summary: Import and export stockpile settings. - :tags: unavailable fort design stockpiles + :tags: unavailable With a stockpile selected in :kbd:`q` mode, you can use this tool to load stockpile settings from a file or save them to a file for later loading, in diff --git a/docs/gui/teleport.rst b/docs/gui/teleport.rst index e2cd9229e4..82d15abeb0 100644 --- a/docs/gui/teleport.rst +++ b/docs/gui/teleport.rst @@ -3,7 +3,7 @@ gui/teleport .. dfhack-tool:: :summary: Teleport a unit anywhere. - :tags: unavailable fort armok units + :tags: unavailable This tool is a front-end for the `teleport` tool. It allows you to interactively choose a unit to teleport and a destination tile using the in-game cursor. diff --git a/docs/gui/unit-info-viewer.rst b/docs/gui/unit-info-viewer.rst index dac54e87e5..856a93257e 100644 --- a/docs/gui/unit-info-viewer.rst +++ b/docs/gui/unit-info-viewer.rst @@ -3,7 +3,7 @@ gui/unit-info-viewer .. dfhack-tool:: :summary: Display detailed information about a unit. - :tags: unavailable fort inspection units + :tags: unavailable Displays information about age, birth, maxage, shearing, milking, grazing, egg laying, body size, and death for the selected unit. diff --git a/docs/gui/workflow.rst b/docs/gui/workflow.rst index d4724efb9a..06ddf0bfbb 100644 --- a/docs/gui/workflow.rst +++ b/docs/gui/workflow.rst @@ -3,7 +3,7 @@ gui/workflow .. dfhack-tool:: :summary: Manage automated item production rules. - :tags: unavailable fort auto jobs + :tags: unavailable This tool provides a simple interface to item production constraints managed by `workflow`. When a workshop job is selected in :kbd:`q` mode and this tool is diff --git a/docs/hotkey-notes.rst b/docs/hotkey-notes.rst index b0f2be8184..85c8582a8b 100644 --- a/docs/hotkey-notes.rst +++ b/docs/hotkey-notes.rst @@ -3,7 +3,7 @@ hotkey-notes .. dfhack-tool:: :summary: Show info on DF map location hotkeys. - :tags: unavailable fort inspection + :tags: unavailable This command lists the key (e.g. :kbd:`F1`), name, and jump position of the map location hotkeys you set in the :kbd:`H` menu. diff --git a/docs/launch.rst b/docs/launch.rst index d6d0f88096..6b884fcfb7 100644 --- a/docs/launch.rst +++ b/docs/launch.rst @@ -3,7 +3,7 @@ launch .. dfhack-tool:: :summary: Thrash your enemies with a flying suplex. - :tags: unavailable adventure armok units + :tags: unavailable Attack another unit and then run this command to grab them and fly in a glorious parabolic arc to where you have placed the cursor. You'll land safely and your diff --git a/docs/linger.rst b/docs/linger.rst index c985ac95a0..19d1bb6d9b 100644 --- a/docs/linger.rst +++ b/docs/linger.rst @@ -3,7 +3,7 @@ linger .. dfhack-tool:: :summary: Take control of your adventurer's killer. - :tags: unavailable adventure armok + :tags: unavailable Run this script after being presented with the "You are deceased." message to abandon your dead adventurer and take control of your adventurer's killer. diff --git a/docs/list-waves.rst b/docs/list-waves.rst index 574315bbb8..784fca4a4d 100644 --- a/docs/list-waves.rst +++ b/docs/list-waves.rst @@ -3,7 +3,7 @@ list-waves .. dfhack-tool:: :summary: Show migration wave information for your dwarves. - :tags: unavailable fort inspection units + :tags: unavailable This script displays information about migration waves or identifies which wave a particular dwarf came from. diff --git a/docs/load-save.rst b/docs/load-save.rst index 5d3e281273..c5f60fe510 100644 --- a/docs/load-save.rst +++ b/docs/load-save.rst @@ -3,7 +3,7 @@ load-save .. dfhack-tool:: :summary: Load a savegame. - :tags: unavailable dfhack + :tags: unavailable When run on the Dwarf Fortress title screen or "load game" screen, this script will load the save with the given folder name without requiring interaction. diff --git a/docs/make-legendary.rst b/docs/make-legendary.rst index 4964947aff..c4c431b8cf 100644 --- a/docs/make-legendary.rst +++ b/docs/make-legendary.rst @@ -3,7 +3,7 @@ make-legendary .. dfhack-tool:: :summary: Boost skills of the selected dwarf. - :tags: unavailable fort armok units + :tags: unavailable This tool can make the selected dwarf legendary in one skill, a group of skills, or all skills. diff --git a/docs/markdown.rst b/docs/markdown.rst index ae4be55a94..bffc5cfba6 100644 --- a/docs/markdown.rst +++ b/docs/markdown.rst @@ -3,7 +3,7 @@ markdown .. dfhack-tool:: :summary: Exports the text you see on the screen for posting online. - :tags: unavailable dfhack + :tags: unavailable This tool saves a copy of a text screen, formatted in markdown, for posting to Reddit (among other places). See `forum-dwarves` if you want to export BBCode diff --git a/docs/max-wave.rst b/docs/max-wave.rst index 6b53e7c2b1..386453647e 100644 --- a/docs/max-wave.rst +++ b/docs/max-wave.rst @@ -3,7 +3,7 @@ max-wave .. dfhack-tool:: :summary: Dynamically limit the next immigration wave. - :tags: unavailable fort gameplay + :tags: unavailable Limit the number of migrants that can arrive in the next wave by overriding the population cap value from data/init/d_init.txt. diff --git a/docs/modtools/anonymous-script.rst b/docs/modtools/anonymous-script.rst index 9c2074fcb4..2f653d6b30 100644 --- a/docs/modtools/anonymous-script.rst +++ b/docs/modtools/anonymous-script.rst @@ -3,7 +3,7 @@ modtools/anonymous-script .. dfhack-tool:: :summary: Run dynamically generated script code. - :tags: unavailable dev + :tags: unavailable This allows running a short simple Lua script passed as an argument instead of running a script from a file. This is useful when you want to do something too diff --git a/docs/modtools/change-build-menu.rst b/docs/modtools/change-build-menu.rst index 349559b4e7..5b7176f5f1 100644 --- a/docs/modtools/change-build-menu.rst +++ b/docs/modtools/change-build-menu.rst @@ -3,7 +3,7 @@ modtools/change-build-menu .. dfhack-tool:: :summary: Add or remove items from the build sidebar menus. - :tags: unavailable dev + :tags: unavailable Change the build sidebar menus. diff --git a/docs/modtools/create-tree.rst b/docs/modtools/create-tree.rst index e2a4a12d47..b33fb48563 100644 --- a/docs/modtools/create-tree.rst +++ b/docs/modtools/create-tree.rst @@ -3,7 +3,7 @@ modtools/create-tree .. dfhack-tool:: :summary: Spawn trees. - :tags: unavailable dev + :tags: unavailable Spawns a tree. diff --git a/docs/modtools/create-unit.rst b/docs/modtools/create-unit.rst index fc4f713d65..7e674178a0 100644 --- a/docs/modtools/create-unit.rst +++ b/docs/modtools/create-unit.rst @@ -3,7 +3,7 @@ modtools/create-unit .. dfhack-tool:: :summary: Create arbitrary units. - :tags: unavailable dev + :tags: unavailable Creates a unit. diff --git a/docs/modtools/equip-item.rst b/docs/modtools/equip-item.rst index f590b4a4ad..9535093eb7 100644 --- a/docs/modtools/equip-item.rst +++ b/docs/modtools/equip-item.rst @@ -3,7 +3,7 @@ modtools/equip-item .. dfhack-tool:: :summary: Force a unit to equip an item. - :tags: unavailable dev + :tags: unavailable Force a unit to equip an item with a particular body part; useful in conjunction with the ``create`` scripts above. See also `forceequip`. diff --git a/docs/modtools/extra-gamelog.rst b/docs/modtools/extra-gamelog.rst index f465fdeaa2..944de0a7b1 100644 --- a/docs/modtools/extra-gamelog.rst +++ b/docs/modtools/extra-gamelog.rst @@ -3,7 +3,7 @@ modtools/extra-gamelog .. dfhack-tool:: :summary: Write info to the gamelog for Soundsense. - :tags: unavailable dev + :tags: unavailable This script writes extra information to the gamelog. This is useful for tools like :forums:`Soundsense <60287>`. diff --git a/docs/modtools/fire-rate.rst b/docs/modtools/fire-rate.rst index 35310d873d..0807f11036 100644 --- a/docs/modtools/fire-rate.rst +++ b/docs/modtools/fire-rate.rst @@ -3,7 +3,7 @@ modtools/fire-rate .. dfhack-tool:: :summary: Alter the fire rate of ranged weapons. - :tags: unavailable dev + :tags: unavailable Allows altering the fire rates of ranged weapons. Each are defined on a per-item basis. As this is done in an on-world basis, commands for this should be placed diff --git a/docs/modtools/if-entity.rst b/docs/modtools/if-entity.rst index 6b32c2f863..5f54620828 100644 --- a/docs/modtools/if-entity.rst +++ b/docs/modtools/if-entity.rst @@ -3,7 +3,7 @@ modtools/if-entity .. dfhack-tool:: :summary: Run DFHack commands based on current civ id. - :tags: unavailable dev + :tags: unavailable Run a command if the current entity matches a given ID. diff --git a/docs/modtools/interaction-trigger.rst b/docs/modtools/interaction-trigger.rst index 547bc7784f..e7b5e0feae 100644 --- a/docs/modtools/interaction-trigger.rst +++ b/docs/modtools/interaction-trigger.rst @@ -3,7 +3,7 @@ modtools/interaction-trigger .. dfhack-tool:: :summary: Run DFHack commands when a unit attacks or defends. - :tags: unavailable dev + :tags: unavailable This triggers events when a unit uses an interaction on another. It works by scanning the announcements for the correct attack verb, so the attack verb diff --git a/docs/modtools/invader-item-destroyer.rst b/docs/modtools/invader-item-destroyer.rst index 20f300109d..23d21f1651 100644 --- a/docs/modtools/invader-item-destroyer.rst +++ b/docs/modtools/invader-item-destroyer.rst @@ -3,7 +3,7 @@ modtools/invader-item-destroyer .. dfhack-tool:: :summary: Destroy invader items when they die. - :tags: unavailable dev + :tags: unavailable This tool can destroy invader items to prevent clutter or to prevent the player from getting tools exclusive to certain races. diff --git a/docs/modtools/item-trigger.rst b/docs/modtools/item-trigger.rst index 8db00cef7d..3eacd5924d 100644 --- a/docs/modtools/item-trigger.rst +++ b/docs/modtools/item-trigger.rst @@ -3,7 +3,7 @@ modtools/item-trigger .. dfhack-tool:: :summary: Run DFHack commands when a unit uses an item. - :tags: unavailable dev + :tags: unavailable This powerful tool triggers DFHack commands when a unit equips, unequips, or attacks another unit with specified item types, specified item materials, or diff --git a/docs/modtools/moddable-gods.rst b/docs/modtools/moddable-gods.rst index 763082762b..1a575c8325 100644 --- a/docs/modtools/moddable-gods.rst +++ b/docs/modtools/moddable-gods.rst @@ -3,7 +3,7 @@ modtools/moddable-gods .. dfhack-tool:: :summary: Create deities. - :tags: unavailable dev + :tags: unavailable This is a standardized version of Putnam's moddableGods script. It allows you to create gods on the command-line. diff --git a/docs/modtools/outside-only.rst b/docs/modtools/outside-only.rst index 15c9751c61..2d1ba6ab44 100644 --- a/docs/modtools/outside-only.rst +++ b/docs/modtools/outside-only.rst @@ -3,7 +3,7 @@ modtools/outside-only .. dfhack-tool:: :summary: Set building inside/outside restrictions. - :tags: unavailable dev + :tags: unavailable This allows you to specify certain custom buildings as outside only, or inside only. If the player attempts to build a building in an inappropriate location, diff --git a/docs/modtools/pref-edit.rst b/docs/modtools/pref-edit.rst index 220ebf70b8..c305c97a0a 100644 --- a/docs/modtools/pref-edit.rst +++ b/docs/modtools/pref-edit.rst @@ -3,7 +3,7 @@ modtools/pref-edit .. dfhack-tool:: :summary: Modify unit preferences. - :tags: unavailable dev + :tags: unavailable Add, remove, or edit the preferences of a unit. Requires a modifier, a unit argument, and filters. diff --git a/docs/modtools/projectile-trigger.rst b/docs/modtools/projectile-trigger.rst index 987bc7e065..9456cb7ace 100644 --- a/docs/modtools/projectile-trigger.rst +++ b/docs/modtools/projectile-trigger.rst @@ -3,7 +3,7 @@ modtools/projectile-trigger .. dfhack-tool:: :summary: Run DFHack commands when projectiles hit their targets. - :tags: unavailable dev + :tags: unavailable This triggers dfhack commands when projectiles hit their targets. diff --git a/docs/modtools/random-trigger.rst b/docs/modtools/random-trigger.rst index 56b6c21868..3f87c7906c 100644 --- a/docs/modtools/random-trigger.rst +++ b/docs/modtools/random-trigger.rst @@ -3,7 +3,7 @@ modtools/random-trigger .. dfhack-tool:: :summary: Randomly select DFHack scripts to run. - :tags: unavailable dev + :tags: unavailable Trigger random dfhack commands with specified probabilities. Register a few scripts, then tell it to "go" and it will pick one diff --git a/docs/modtools/raw-lint.rst b/docs/modtools/raw-lint.rst index 13311b1f06..6aed4c1af2 100644 --- a/docs/modtools/raw-lint.rst +++ b/docs/modtools/raw-lint.rst @@ -3,6 +3,6 @@ modtools/raw-lint .. dfhack-tool:: :summary: Check for errors in raw files. - :tags: unavailable dev + :tags: unavailable Checks for simple issues with raw files. Can be run automatically. diff --git a/docs/modtools/reaction-product-trigger.rst b/docs/modtools/reaction-product-trigger.rst index b3b4e421b6..de240107c9 100644 --- a/docs/modtools/reaction-product-trigger.rst +++ b/docs/modtools/reaction-product-trigger.rst @@ -3,7 +3,7 @@ modtools/reaction-product-trigger .. dfhack-tool:: :summary: Call DFHack commands when reaction products are produced. - :tags: unavailable dev + :tags: unavailable This triggers dfhack commands when reaction products are produced, once per product. diff --git a/docs/modtools/reaction-trigger-transition.rst b/docs/modtools/reaction-trigger-transition.rst index bc3c3e45c3..9831f18801 100644 --- a/docs/modtools/reaction-trigger-transition.rst +++ b/docs/modtools/reaction-trigger-transition.rst @@ -3,7 +3,7 @@ modtools/reaction-trigger-transition .. dfhack-tool:: :summary: Help create reaction triggers. - :tags: unavailable dev + :tags: unavailable Prints useful things to the console and a file to help modders transition from ``autoSyndrome`` to `modtools/reaction-trigger`. diff --git a/docs/modtools/reaction-trigger.rst b/docs/modtools/reaction-trigger.rst index bf35bcf0aa..90c28e2f40 100644 --- a/docs/modtools/reaction-trigger.rst +++ b/docs/modtools/reaction-trigger.rst @@ -3,7 +3,7 @@ modtools/reaction-trigger .. dfhack-tool:: :summary: Run DFHack commands when custom reactions complete. - :tags: unavailable dev + :tags: unavailable Triggers dfhack commands when custom reactions complete, regardless of whether it produced anything, once per completion. Arguments:: diff --git a/docs/modtools/set-belief.rst b/docs/modtools/set-belief.rst index 4d010a0cd5..09fdd3ab14 100644 --- a/docs/modtools/set-belief.rst +++ b/docs/modtools/set-belief.rst @@ -3,7 +3,7 @@ modtools/set-belief .. dfhack-tool:: :summary: Change the beliefs/values of a unit. - :tags: unavailable dev + :tags: unavailable Changes the beliefs (values) of units. Requires a belief, modifier, and a target. diff --git a/docs/modtools/set-need.rst b/docs/modtools/set-need.rst index b4beea169b..e2199553c3 100644 --- a/docs/modtools/set-need.rst +++ b/docs/modtools/set-need.rst @@ -3,7 +3,7 @@ modtools/set-need .. dfhack-tool:: :summary: Change the needs of a unit. - :tags: unavailable dev + :tags: unavailable Sets and edits unit needs. diff --git a/docs/modtools/set-personality.rst b/docs/modtools/set-personality.rst index 2761c700cf..9c8d1d45fe 100644 --- a/docs/modtools/set-personality.rst +++ b/docs/modtools/set-personality.rst @@ -3,7 +3,7 @@ modtools/set-personality .. dfhack-tool:: :summary: Change a unit's personality. - :tags: unavailable dev + :tags: unavailable Changes the personality of units. diff --git a/docs/modtools/spawn-flow.rst b/docs/modtools/spawn-flow.rst index 1565e75a6d..48f90ddd1c 100644 --- a/docs/modtools/spawn-flow.rst +++ b/docs/modtools/spawn-flow.rst @@ -3,7 +3,7 @@ modtools/spawn-flow .. dfhack-tool:: :summary: Creates flows at the specified location. - :tags: unavailable dev + :tags: unavailable Creates flows at the specified location. diff --git a/docs/modtools/syndrome-trigger.rst b/docs/modtools/syndrome-trigger.rst index 24e15ef8f5..7e915fda1a 100644 --- a/docs/modtools/syndrome-trigger.rst +++ b/docs/modtools/syndrome-trigger.rst @@ -3,7 +3,7 @@ modtools/syndrome-trigger .. dfhack-tool:: :summary: Trigger DFHack commands when units acquire syndromes. - :tags: unavailable dev + :tags: unavailable This script helps you set up commands that trigger when syndromes are applied to units. diff --git a/docs/modtools/transform-unit.rst b/docs/modtools/transform-unit.rst index 9fe68975c3..d5aa41bd0a 100644 --- a/docs/modtools/transform-unit.rst +++ b/docs/modtools/transform-unit.rst @@ -3,7 +3,7 @@ modtools/transform-unit .. dfhack-tool:: :summary: Transform a unit into another unit type. - :tags: unavailable dev + :tags: unavailable This tool transforms a unit into another unit type, either temporarily or permanently. diff --git a/docs/names.rst b/docs/names.rst index 40b9ed38b3..5cfea6a7a6 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -3,7 +3,7 @@ names .. dfhack-tool:: :summary: Rename units or items with the DF name generator. - :tags: unavailable fort productivity units + :tags: unavailable This tool allows you to rename the selected unit or item (including artifacts) with the native Dwarf Fortress name generation interface. diff --git a/docs/open-legends.rst b/docs/open-legends.rst index b0c0395d6e..d1ecc310b6 100644 --- a/docs/open-legends.rst +++ b/docs/open-legends.rst @@ -3,7 +3,7 @@ open-legends .. dfhack-tool:: :summary: Open a legends screen from fort or adventure mode. - :tags: unavailable legends inspection + :tags: unavailable You can use this tool to open legends mode from a world loaded in fortress or adventure mode. You can browse around, or even run `exportlegends` while you're diff --git a/docs/pop-control.rst b/docs/pop-control.rst index d648451241..3d894688a0 100644 --- a/docs/pop-control.rst +++ b/docs/pop-control.rst @@ -3,7 +3,7 @@ pop-control .. dfhack-tool:: :summary: Controls population and migration caps persistently per-fort. - :tags: unavailable fort auto gameplay + :tags: unavailable This script controls `hermit` and the various population caps per-fortress. It is intended to be run from ``dfhack-config/init/onMapLoad.init`` as diff --git a/docs/prefchange.rst b/docs/prefchange.rst index 11eebd6efd..8da2ba6bf5 100644 --- a/docs/prefchange.rst +++ b/docs/prefchange.rst @@ -3,7 +3,7 @@ prefchange .. dfhack-tool:: :summary: Set strange mood preferences. - :tags: unavailable fort armok units + :tags: unavailable This tool sets preferences for strange moods to include a weapon type, equipment type, and material. If you also wish to trigger a mood, see `strangemood`. diff --git a/docs/putontable.rst b/docs/putontable.rst index 99c3f3778b..e880ac896d 100644 --- a/docs/putontable.rst +++ b/docs/putontable.rst @@ -3,7 +3,7 @@ putontable .. dfhack-tool:: :summary: Make an item appear on a table. - :tags: unavailable fort armok items + :tags: unavailable To use this tool, move an item to the ground on the same tile as a built table. Then, place the cursor over the table and item and run this command. The item diff --git a/docs/questport.rst b/docs/questport.rst index c2c2712e1a..6d937895e5 100644 --- a/docs/questport.rst +++ b/docs/questport.rst @@ -3,7 +3,7 @@ questport .. dfhack-tool:: :summary: Teleport to your quest log map cursor. - :tags: unavailable adventure armok + :tags: unavailable If you open the quest log map and move the cursor to your target location, you can run this command to teleport straight there. This can be done both within diff --git a/docs/resurrect-adv.rst b/docs/resurrect-adv.rst index 6bd10393af..d125502dbb 100644 --- a/docs/resurrect-adv.rst +++ b/docs/resurrect-adv.rst @@ -3,7 +3,7 @@ resurrect-adv .. dfhack-tool:: :summary: Bring a dead adventurer back to life. - :tags: unavailable adventure armok + :tags: unavailable Have you ever died, but wish you hadn't? This tool can help : ) When you see the "You are deceased" message, run this command to be resurrected and fully healed. diff --git a/docs/reveal-adv-map.rst b/docs/reveal-adv-map.rst index af92f06b39..e710255f3a 100644 --- a/docs/reveal-adv-map.rst +++ b/docs/reveal-adv-map.rst @@ -3,7 +3,7 @@ reveal-adv-map .. dfhack-tool:: :summary: Reveal or hide the world map. - :tags: unavailable adventure armok map + :tags: unavailable This tool can be used to either reveal or hide all tiles on the world map in adventure mode (visible when viewing the quest log or fast traveling). diff --git a/docs/season-palette.rst b/docs/season-palette.rst index 5b7dcc000d..a1d22e9125 100644 --- a/docs/season-palette.rst +++ b/docs/season-palette.rst @@ -3,7 +3,7 @@ season-palette .. dfhack-tool:: :summary: Swap color palettes when the seasons change. - :tags: unavailable fort auto graphics + :tags: unavailable For this tool to work you need to add *at least* one color palette file to your save raw directory. These files must be in the same format as diff --git a/docs/siren.rst b/docs/siren.rst index c62668a2cb..da9fe01b07 100644 --- a/docs/siren.rst +++ b/docs/siren.rst @@ -3,7 +3,7 @@ siren .. dfhack-tool:: :summary: Wake up sleeping units and stop parties. - :tags: unavailable fort armok units + :tags: unavailable Sound the alarm! This tool can shake your sleeping units awake and knock some sense into your party animal military dwarves so they can address a siege. diff --git a/docs/spawnunit.rst b/docs/spawnunit.rst index 42a0687108..e3695388eb 100644 --- a/docs/spawnunit.rst +++ b/docs/spawnunit.rst @@ -3,7 +3,7 @@ spawnunit .. dfhack-tool:: :summary: Create a unit. - :tags: unavailable fort armok units + :tags: unavailable This tool allows you to easily spawn a unit of your choice. It is a simplified interface to `modtools/create-unit`, which this tool uses to actually create diff --git a/docs/tidlers.rst b/docs/tidlers.rst index 295bd8d998..40588c8529 100644 --- a/docs/tidlers.rst +++ b/docs/tidlers.rst @@ -3,7 +3,7 @@ tidlers .. dfhack-tool:: :summary: Change where the idlers count is displayed. - :tags: unavailable interface + :tags: unavailable This tool simply cycles the idlers count among the possible positions where the idlers count can be placed, including making it disappear entirely. diff --git a/docs/timestream.rst b/docs/timestream.rst index 07e359278c..aa15ea5ff0 100644 --- a/docs/timestream.rst +++ b/docs/timestream.rst @@ -3,7 +3,7 @@ timestream .. dfhack-tool:: :summary: Fix FPS death. - :tags: unavailable fort auto fps + :tags: unavailable Do you remember when you first start a new fort, your initial 7 dwarves zip around the screen and get things done so quickly? As a player, you never had diff --git a/docs/undump-buildings.rst b/docs/undump-buildings.rst index cf76ca7234..8848442734 100644 --- a/docs/undump-buildings.rst +++ b/docs/undump-buildings.rst @@ -3,7 +3,7 @@ undump-buildings .. dfhack-tool:: :summary: Undesignate building base materials for dumping. - :tags: unavailable fort productivity buildings + :tags: unavailable If you designate a bunch of tiles in dump mode, all the items on those tiles will be marked for dumping. Unfortunately, if there are buildings on any of diff --git a/docs/uniform-unstick.rst b/docs/uniform-unstick.rst index d9b08d7a1a..3e877aae2d 100644 --- a/docs/uniform-unstick.rst +++ b/docs/uniform-unstick.rst @@ -3,7 +3,7 @@ uniform-unstick .. dfhack-tool:: :summary: Make military units reevaluate their uniforms. - :tags: unavailable fort bugfix military + :tags: unavailable This tool prompts military units to reevaluate their uniform, making them remove and drop potentially conflicting worn items. diff --git a/docs/unretire-anyone.rst b/docs/unretire-anyone.rst index 9adce5ad0d..47879d8d14 100644 --- a/docs/unretire-anyone.rst +++ b/docs/unretire-anyone.rst @@ -3,7 +3,7 @@ unretire-anyone .. dfhack-tool:: :summary: Adventure as any living historical figure. - :tags: unavailable adventure embark armok + :tags: unavailable This tool allows you to play as any living (or undead) historical figure (except for deities) in adventure mode. diff --git a/docs/view-item-info.rst b/docs/view-item-info.rst index 84b6eb47a3..468761b3cd 100644 --- a/docs/view-item-info.rst +++ b/docs/view-item-info.rst @@ -3,7 +3,7 @@ view-item-info .. dfhack-tool:: :summary: Extend item and unit descriptions with more information. - :tags: unavailable adventure fort interface + :tags: unavailable This tool extends the item or unit description viewscreen with additional information, including a custom description of each item (when available), and diff --git a/docs/view-unit-reports.rst b/docs/view-unit-reports.rst index b649038ded..5987c3aa0c 100644 --- a/docs/view-unit-reports.rst +++ b/docs/view-unit-reports.rst @@ -3,7 +3,7 @@ view-unit-reports .. dfhack-tool:: :summary: Show combat reports for a unit. - :tags: unavailable fort inspection military + :tags: unavailable Show combat reports specifically for the selected unit. You can select a unit with the cursor in :kbd:`v` mode, from the list in :kbd:`u` mode, or from the diff --git a/docs/warn-stealers.rst b/docs/warn-stealers.rst index bf81f0b602..4c7bdd6ce9 100644 --- a/docs/warn-stealers.rst +++ b/docs/warn-stealers.rst @@ -3,7 +3,7 @@ warn-stealers .. dfhack-tool:: :summary: Watch for and warn about units that like to steal your stuff. - :tags: unavailable fort armok auto units + :tags: unavailable This script will watch for new units entering the map and will make a zoomable announcement whenever a creature that can eat food, guzzle drinks, or steal From 2c86c21a4213a384855f96d298ddf931fd3ce740 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 15 Oct 2023 14:19:46 -0700 Subject: [PATCH 622/732] refuse to automatically prioritize dig jobs --- changelog.txt | 1 + prioritize.lua | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/changelog.txt b/changelog.txt index ac7490dc00..4c8c70b2a7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -43,6 +43,7 @@ Template for new versions: - `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped ## Misc Improvements +- `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities ## Removed diff --git a/prioritize.lua b/prioritize.lua index 188c7e8dc4..ce284dc1f3 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -6,6 +6,7 @@ local argparse = require('argparse') local json = require('json') local eventful = require('plugins.eventful') local persist = require('persist-table') +local utils = require('utils') local GLOBAL_KEY = 'prioritize' -- used for state change hooks and persistence @@ -269,6 +270,27 @@ local function boost_and_watch_special(job_type, job_matcher, end end +local JOB_TYPES_DENYLIST = utils.invert{ + df.job_type.CarveFortification, + df.job_type.SmoothWall, + df.job_type.SmoothFloor, + df.job_type.DetailWall, + df.job_type.DetailFloor, + df.job_type.Dig, + df.job_type.CarveUpwardStaircase, + df.job_type.CarveDownwardStaircase, + df.job_type.CarveUpDownStaircase, + df.job_type.CarveRamp, + df.job_type.DigChannel, +} + +local DIG_SMOOTH_WARNING = { + 'Priortizing current pending jobs, but skipping automatic boosting of dig and', + 'smooth/engrave job types. Automatic priority boosting of these types of jobs', + 'will overwhelm the DF job scheduler. Instead, consider specializing units for', + 'mining and related work details, and using vanilla designation priorities.', +} + local function boost_and_watch(job_matchers, opts) local quiet = opts.quiet boost(job_matchers, opts) @@ -284,6 +306,10 @@ local function boost_and_watch(job_matchers, opts) function(jm) return jm.reaction_matchers end, function(jm) jm.reaction_matchers = nil end, get_annotation_str, quiet) + elseif JOB_TYPES_DENYLIST[job_type] then + for _,msg in ipairs(DIG_SMOOTH_WARNING) do + dfhack.printerr(msg) + end elseif watched_job_matchers[job_type] then if not quiet then print_skip_add_message(job_type) From 4afd2f7c715d3aa060a386676deb0f506c86ff46 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 15 Oct 2023 16:45:43 -0700 Subject: [PATCH 623/732] allow constructions to be built on top of constructions but skip floors being built on top of floors and ramps on top of ramps. this allows blueprints to continue to be idempotent --- changelog.txt | 1 + docs/gui/quickfort.rst | 11 ++++++++--- internal/quickfort/build.lua | 20 +++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4c8c70b2a7..79b55859be 100644 --- a/changelog.txt +++ b/changelog.txt @@ -44,6 +44,7 @@ Template for new versions: ## Misc Improvements - `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities +- `quickfort`: now allows constructions to be built on top of constructed floors and ramps, just like vanilla. however, to allow blueprints to be safely reapplied to the same area, for example to fill in buildings whose constructions were canceled due to lost items, floors will not be rebuilt on top of floors and ramps will not be rebuilt on top of ramps ## Removed diff --git a/docs/gui/quickfort.rst b/docs/gui/quickfort.rst index 39e322c131..467339e710 100644 --- a/docs/gui/quickfort.rst +++ b/docs/gui/quickfort.rst @@ -9,9 +9,14 @@ This is the graphical interface for the `quickfort` script. Once you load a blueprint, you will see a highlight over the tiles that will be modified. You can use the mouse cursor to reposition the blueprint and the hotkeys to rotate and repeat the blueprint up or down z-levels. Once you are satisfied, -click the mouse or hit :kbd:`Enter` to apply the blueprint to the map. You can -apply the blueprint as many times as you wish to different spots on the map. -Right click or hit :kbd:`Esc` to stop. +click the mouse or hit :kbd:`Enter` to apply the blueprint to the map. + +You can apply the blueprint as many times as you wish to different spots on the +map. If a blueprint that you designated was only partially applied (due to job +cancellations, incomplete dig area, or any other reason) you can apply the +blueprint a second time to fill in any gaps. Any part of the blueprint that has +already been completed will be harmlessly skipped. Right click or hit +:kbd:`Esc` to close the `gui/quickfort` UI. Usage ----- diff --git a/internal/quickfort/build.lua b/internal/quickfort/build.lua index 750c6f050b..3d09c0f53f 100644 --- a/internal/quickfort/build.lua +++ b/internal/quickfort/build.lua @@ -114,12 +114,26 @@ local function is_valid_tile_bridge(pos, db_entry, b) return is_valid_tile_has_space_or_is_ramp(pos) end -local function is_valid_tile_construction(pos) +-- although vanilla allows constructions to be built on top of constructed +-- floors or ramps, we want to offer an idempotency guarantee for quickfort. +-- this means that the user should be able to apply the same blueprint to the +-- same area more than once to complete any bits that failed on the first attempt. +-- therefore, we check that we're not building a construction on top of an +-- existing construction of the same shape +local function is_valid_tile_construction(pos, db_entry) + if not is_valid_tile_has_space_or_is_ramp(pos) then return false end local tileattrs = df.tiletype.attrs[dfhack.maps.getTileType(pos)] local shape = tileattrs.shape local mat = tileattrs.material - return is_valid_tile_has_space_or_is_ramp(pos) and - mat ~= df.tiletype_material.CONSTRUCTION + if mat == df.tiletype_material.CONSTRUCTION and + ( + (shape == df.tiletype_shape.FLOOR and db_entry.subtype == df.construction_type.Floor) or + (shape == df.tiletype_shape.RAMP and db_entry.subtype == df.construction_type.Ramp) + ) + then + return false + end + return true end local function is_shape_at(pos, allowed_shapes) From 860a9a45078b02a783320b6f4dd363d3153d2cdf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 15 Oct 2023 17:24:41 -0700 Subject: [PATCH 624/732] document that quickfort will use buildingplan settings --- docs/gui/quickfort.rst | 5 +++++ gui/quickfort.lua | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/gui/quickfort.rst b/docs/gui/quickfort.rst index 467339e710..1ca58304e5 100644 --- a/docs/gui/quickfort.rst +++ b/docs/gui/quickfort.rst @@ -18,6 +18,11 @@ blueprint a second time to fill in any gaps. Any part of the blueprint that has already been completed will be harmlessly skipped. Right click or hit :kbd:`Esc` to close the `gui/quickfort` UI. +Note that `quickfort` blueprints will use the DFHack building planner +(`buildingplan`) material filter settings. If you want specific materials to be +used, use the building planner UI to set the appropriate filters before +applying a blueprint. + Usage ----- diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 5e0fe48693..cdeda369b0 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -237,7 +237,7 @@ end Quickfort = defclass(Quickfort, widgets.Window) Quickfort.ATTRS { frame_title='Quickfort', - frame={w=34, h=30, r=2, t=18}, + frame={w=34, h=32, r=2, t=18}, resizable=true, resize_min={h=26}, autoarrange_subviews=true, @@ -358,6 +358,9 @@ function Quickfort:init() active=function() return self.blueprint_name end, enabled=function() return self.blueprint_name end, on_activate=self:callback('do_command', 'undo')}, + widgets.WrappedLabel{ + text_to_wrap='Blueprints will use DFHack building planner material filter settings.', + }, } end From d3ea291ea1aca3995cf53e3b7ed1763df0439a24 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 15 Oct 2023 17:27:04 -0700 Subject: [PATCH 625/732] ensure dialog is modal --- gui/quickfort.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/quickfort.lua b/gui/quickfort.lua index cdeda369b0..60b4b0b4aa 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -225,6 +225,7 @@ function BlueprintDialog:onInput(keys) end return true end + return true end -- From 370ab02904ae69d2e1cc443a5d3ca0054ffc795d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 16 Oct 2023 00:06:59 -0700 Subject: [PATCH 626/732] destroy the corpse like autodump-destroy Items::remove may not work properly --- changelog.txt | 1 + full-heal.lua | 99 ++++++++++++++++++--------------------------------- 2 files changed, 36 insertions(+), 64 deletions(-) diff --git a/changelog.txt b/changelog.txt index 79b55859be..471f57b8fd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -41,6 +41,7 @@ Template for new versions: - `suspendmanager`: fix errors when constructing near the map edge - `gui/sandbox`: fix scrollbar moving double distance on click - `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped +- `full-heal`: fix removal of corpse after resurrection ## Misc Improvements - `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities diff --git a/full-heal.lua b/full-heal.lua index 63276976c3..2dc71c3dca 100644 --- a/full-heal.lua +++ b/full-heal.lua @@ -1,35 +1,6 @@ -- Attempts to fully heal the selected unit --author Kurik Amudnil, Urist DaVinci --edited by expwnent and AtomicChicken - ---[====[ - -full-heal -========= -Attempts to fully heal the selected unit from anything, optionally -including death. Usage: - -:full-heal: - Completely heal the currently selected unit. -:full-heal -unit [unitId]: - Apply command to the unit with the given ID, instead of selected unit. -:full-heal -r [-keep_corpse]: - Heal the unit, raising from the dead if needed. - Add ``-keep_corpse`` to avoid removing their corpse. - The unit can be targeted by selecting its corpse on the UI. -:full-heal -all [-r] [-keep_corpse]: - Heal all units on the map. -:full-heal -all_citizens [-r] [-keep_corpse]: - Heal all fortress citizens on the map. Does not include pets. -:full-heal -all_civ [-r] [-keep_corpse]: - Heal all units belonging to your parent civilisation, including pets and visitors. - -For example, ``full-heal -r -keep_corpse -unit ID_NUM`` will fully heal -unit ID_NUM. If this unit was dead, it will be resurrected without deleting -the corpse - creepy! - -]====] - --@ module = true local utils = require('utils') @@ -121,7 +92,9 @@ function heal(unit,resurrect,keep_corpse) for i = #corpses-1,0,-1 do local corpse = corpses[i] --as:df.item_body_component if corpse.unit_id == unit.id then - dfhack.items.remove(corpse) + corpse.flags.garbage_collect = true + corpse.flags.forbid = true + corpse.flags.hidden = true end end end @@ -278,49 +251,47 @@ function heal(unit,resurrect,keep_corpse) end end -if not dfhack_flags.module then +if dfhack_flags.module then + return +end - if args.all then - for _,unit in ipairs(df.global.world.units.active) do +if args.all then + for _,unit in ipairs(df.global.world.units.active) do + heal(unit,args.r,args.keep_corpse) + end +elseif args.all_citizens then + for _,unit in ipairs(df.global.world.units.active) do + if isCitizen(unit) then heal(unit,args.r,args.keep_corpse) end - - elseif args.all_citizens then - for _,unit in ipairs(df.global.world.units.active) do - if isCitizen(unit) then - heal(unit,args.r,args.keep_corpse) - end + end +elseif args.all_civ then + for _,unit in ipairs(df.global.world.units.active) do + if isFortCivMember(unit) then + heal(unit,args.r,args.keep_corpse) end - - elseif args.all_civ then - for _,unit in ipairs(df.global.world.units.active) do - if isFortCivMember(unit) then - heal(unit,args.r,args.keep_corpse) - end + end +else + local unit + if args.unit then + unit = df.unit.find(tonumber(args.unit)) + if not unit then + qerror('Invalid unit ID: ' .. args.unit) end - else - local unit - if args.unit then - unit = df.unit.find(tonumber(args.unit)) + local item = dfhack.gui.getSelectedItem(true) + if item and df.item_corpsest:is_instance(item) then + unit = df.unit.find(item.unit_id) if not unit then - qerror('Invalid unit ID: ' .. args.unit) + qerror('This corpse can no longer be resurrected.') -- unit has been offloaded end + unit.pos:assign(xyz2pos(dfhack.items.getPosition(item))) -- to make the unit resurrect at the location of the corpse, rather than the location of death else - local item = dfhack.gui.getSelectedItem(true) - if item and df.item_corpsest:is_instance(item) then - unit = df.unit.find(item.unit_id) - if not unit then - qerror('This corpse can no longer be resurrected.') -- unit has been offloaded - end - unit.pos:assign(xyz2pos(dfhack.items.getPosition(item))) -- to make the unit resurrect at the location of the corpse, rather than the location of death - else - unit = dfhack.gui.getSelectedUnit() - end + unit = dfhack.gui.getSelectedUnit() end - if not unit then - qerror('Please select a unit or corpse, or specify its ID via the -unit argument.') - end - heal(unit,args.r,args.keep_corpse) end + if not unit then + qerror('Please select a unit or corpse, or specify its ID via the -unit argument.') + end + heal(unit,args.r,args.keep_corpse) end From 493d50e953c3fb7ab53e80c97ce52371b95615a8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 16 Oct 2023 03:08:26 -0700 Subject: [PATCH 627/732] filter overlays by context, not just by screen there are so many overlays now so we need to be more selective --- changelog.txt | 1 + gui/overlay.lua | 28 ++++++++++++++++------------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index 471f57b8fd..c668587008 100644 --- a/changelog.txt +++ b/changelog.txt @@ -45,6 +45,7 @@ Template for new versions: ## Misc Improvements - `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities +- `gui/overlay`: filter overlays by current context so there are fewer on the screen at once and you can more easily click on the one you want to reposition - `quickfort`: now allows constructions to be built on top of constructed floors and ramps, just like vanilla. however, to allow blueprints to be safely reapplied to the same area, for example to fill in buildings whose constructions were canceled due to lost items, floors will not be rebuilt on top of floors and ramps will not be rebuilt on top of ramps ## Removed diff --git a/gui/overlay.lua b/gui/overlay.lua index a0c5719495..2a671a0b2c 100644 --- a/gui/overlay.lua +++ b/gui/overlay.lua @@ -111,8 +111,7 @@ function OverlayConfig:init() -- prevent hotspot widgets from reacting overlay.register_trigger_lock_screen(self) - self.scr_name = overlay.simplify_viewscreen_name( - getmetatable(dfhack.gui.getDFViewscreen(true))) + local contexts = dfhack.gui.getFocusStrings(dfhack.gui.getDFViewscreen(true)) local main_panel = widgets.Window{ frame={w=DIALOG_WIDTH, h=LIST_HEIGHT+15}, @@ -123,13 +122,16 @@ function OverlayConfig:init() main_panel:addviews{ widgets.Label{ frame={t=0, l=0}, - text={'Current screen: ', {text=self.scr_name, pen=COLOR_CYAN}}}, + text={ + 'Current contexts: ', + {text=table.concat(contexts, ', '), pen=COLOR_CYAN} + }}, widgets.CycleHotkeyLabel{ view_id='filter', frame={t=2, l=0}, key='CUSTOM_CTRL_O', label='Showing:', - options={{label='overlays for the current screen', value='cur'}, + options={{label='overlays for the current contexts', value='cur'}, {label='all overlays', value='all'}}, on_change=self:callback('refresh_list')}, widgets.FilteredList{ @@ -173,6 +175,7 @@ end function OverlayConfig:refresh_list(filter) local choices = {} + local scr = dfhack.gui.getDFViewscreen(true) local state = overlay.get_state() local list = self.subviews.list local make_on_click_fn = function(idx) @@ -182,16 +185,15 @@ function OverlayConfig:refresh_list(filter) local db_entry = state.db[name] local widget = db_entry.widget if widget.overlay_only then goto continue end - if not widget.hotspot and filter ~= 'all' then - local matched = false - for _,scr in ipairs(overlay.normalize_list(widget.viewscreens)) do - if overlay.simplify_viewscreen_name(scr):startswith(self.scr_name) then - matched = true - break + if (not widget.hotspot or #widget.viewscreens > 0) and filter ~= 'all' then + for _,vs in ipairs(overlay.normalize_list(widget.viewscreens)) do + if dfhack.gui.matchFocusString(overlay.simplify_viewscreen_name(vs), scr) then + goto matched end end - if not matched then goto continue end + goto continue end + ::matched:: local panel = nil panel = DraggablePanel{ frame=make_highlight_frame(widget.frame), @@ -290,12 +292,14 @@ function OverlayConfig:onInput(keys) return true end end + if self:inputToSubviews(keys) then + return true + end for _,choice in ipairs(self.subviews.list:getVisibleChoices()) do if choice.panel and choice.panel:onInput(keys) then return true end end - return self:inputToSubviews(keys) end function OverlayConfig:onRenderFrame(dc, rect) From ccaf1fb4af9194d03724dd8c4c897b08777402f3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Oct 2023 23:26:37 -0700 Subject: [PATCH 628/732] move from click to edit to double click to edit --- changelog.txt | 1 + docs/gui/gm-editor.rst | 16 +++++++++------- gui/gm-editor.lua | 11 ++++++----- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index c668587008..0d06a038a8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -47,6 +47,7 @@ Template for new versions: - `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities - `gui/overlay`: filter overlays by current context so there are fewer on the screen at once and you can more easily click on the one you want to reposition - `quickfort`: now allows constructions to be built on top of constructed floors and ramps, just like vanilla. however, to allow blueprints to be safely reapplied to the same area, for example to fill in buildings whose constructions were canceled due to lost items, floors will not be rebuilt on top of floors and ramps will not be rebuilt on top of ramps +- `gui/gm-editor`: change from click to edit to click to select, double-click to edit. this should help prevent accidental modifications to the data and make zoom and hyperlink hotkeys easier to use ## Removed diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index d81c014c2d..df3f616546 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -8,14 +8,16 @@ gui/gm-editor This editor allows you to inspect or modify almost anything in DF. Press :kbd:`?` for in-game help. -If you just want to browse without fear of accidentally changing anything, hit -:kbd:`Ctrl`:kbd:`D` to toggle read-only mode. +Select a field and hit :kbd:`Enter` or double click to edit, or, for structured +fields, to inspect their contents. Right click or hit :kbd:`Esc` to go back to +the previous structure you were inspecting. Right clicking when viewing the +structure you started with will exit the tool. Hold down :kbd:`Shift` and right +click to exit, even if you are inspecting a substructure, no matter how deep. -Click on fields to edit them or, for structured fields, to inspect their -contents. Right click or hit :kbd:`Esc` to go back to the previous structure -you were inspecting. Right clicking when viewing the structure you started with -will exit the tool. Hold down :kbd:`Shift` and right click to exit, even if you -are inspecting a substructure. +If you just want to browse without fear of accidentally changing anything, hit +:kbd:`Ctrl`:kbd:`D` to toggle read-only mode. If you want `gui/gm-editor` to +automatically pick up changes to game data in realtime, hit :kbd:`Alt`:kbd:`A` +to switch to auto update mode. Usage ----- diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 380e9cdd3c..90de0e9567 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -142,9 +142,8 @@ function GmEditorUi:init(args) local helpPage=widgets.Panel{ subviews={widgets.Label{text=helptext,frame = {l=1,t=1,yalign=0}}}} - local mainList=widgets.List{view_id="list_main",choices={},frame = {l=1,t=3,yalign=0},on_submit=self:callback("editSelected"), - on_submit2=self:callback("editSelectedRaw"), - text_pen=COLOR_GREY, cursor_pen=COLOR_YELLOW} + local mainList=widgets.List{view_id="list_main",choices={},frame = {l=1,t=3,yalign=0},on_double_click=self:callback("editSelected"), + on_double_click2=self:callback("editSelectedRaw"), text_pen=COLOR_GREY, cursor_pen=COLOR_YELLOW} local mainPage=widgets.Panel{ subviews={ mainList, @@ -443,7 +442,7 @@ end function GmEditorUi:editSelectedRaw(index,choice) self:editSelected(index, choice, {raw=true}) end -function GmEditorUi:editSelected(index,choice,opts) +function GmEditorUi:editSelected(index,_,opts) if not self:verifyStack() then self:updateTarget() return @@ -530,7 +529,9 @@ function GmEditorUi:onInput(keys) return false end - if keys[keybindings.toggle_ro.key] then + if keys.SELECT then + self:editSelected(self.subviews.list_main:getSelected()) + elseif keys[keybindings.toggle_ro.key] then self.read_only = not self.read_only self:updateTitles() return true From a336f5ccd050e8c695dd25962bff0119737227c3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 16 Oct 2023 01:13:54 -0700 Subject: [PATCH 629/732] still recurse into substructure on single click --- docs/gui/gm-editor.rst | 9 +++++---- gui/gm-editor.lua | 12 +++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index df3f616546..080c49b98e 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -9,10 +9,11 @@ This editor allows you to inspect or modify almost anything in DF. Press :kbd:`?` for in-game help. Select a field and hit :kbd:`Enter` or double click to edit, or, for structured -fields, to inspect their contents. Right click or hit :kbd:`Esc` to go back to -the previous structure you were inspecting. Right clicking when viewing the -structure you started with will exit the tool. Hold down :kbd:`Shift` and right -click to exit, even if you are inspecting a substructure, no matter how deep. +fields, hit :kbd:`Enter` or single click to inspect their contents. Right click +or hit :kbd:`Esc` to go back to the previous structure you were inspecting. +Right clicking when viewing the structure you started with will exit the tool. +Hold down :kbd:`Shift` and right click to exit, even if you are inspecting a +substructure, no matter how deep. If you just want to browse without fear of accidentally changing anything, hit :kbd:`Ctrl`:kbd:`D` to toggle read-only mode. If you want `gui/gm-editor` to diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 90de0e9567..86cf6c3879 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -511,7 +511,17 @@ function GmEditorUi:set(key,input) self:updateTarget(true) end function GmEditorUi:onInput(keys) - if GmEditorUi.super.onInput(self, keys) then return true end + if GmEditorUi.super.onInput(self, keys) then + local index = self.subviews.list_main:getIdxUnderMouse() + if keys._MOUSE_L and index then + local trg = self:currentTarget() + local trg_type = type(trg.target[trg.keys[index]]) + if trg_type == 'userdata' or trg_type == 'table' then + self:editSelected(index) + end + end + return true + end if keys.LEAVESCREEN or keys._MOUSE_R then if dfhack.internal.getModifiers().shift then From 890378345003cca73f6570d043bdb1d0eea8dc48 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 16 Oct 2023 01:15:47 -0700 Subject: [PATCH 630/732] update changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 0d06a038a8..3a846051aa 100644 --- a/changelog.txt +++ b/changelog.txt @@ -47,7 +47,7 @@ Template for new versions: - `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities - `gui/overlay`: filter overlays by current context so there are fewer on the screen at once and you can more easily click on the one you want to reposition - `quickfort`: now allows constructions to be built on top of constructed floors and ramps, just like vanilla. however, to allow blueprints to be safely reapplied to the same area, for example to fill in buildings whose constructions were canceled due to lost items, floors will not be rebuilt on top of floors and ramps will not be rebuilt on top of ramps -- `gui/gm-editor`: change from click to edit to click to select, double-click to edit. this should help prevent accidental modifications to the data and make zoom and hyperlink hotkeys easier to use +- `gui/gm-editor`: for fields with primitive types, change from click to edit to click to select, double-click to edit. this should help prevent accidental modifications to the data and make hotkeys easier to use (since you have to click on a data item to use a hotkey on it) ## Removed From 5dbbcf5d57daec4e869cb842c606c09aa51afd7f Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 16 Oct 2023 12:30:08 -0700 Subject: [PATCH 631/732] Apply suggestions from code review --- warn-stranded.lua | 84 ++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 70a63b8b45..cd190e3c1f 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -163,62 +163,53 @@ end -- =============================================================== WarningWindow = defclass(WarningWindow, widgets.Window) WarningWindow.ATTRS{ - frame={w=80, h=25}, - min_size={w=60, h=25}, - frame_title='Stranded Citizen Warning', + frame={w=60, h=25, r=2, t=18}, + resize_min={w=50, h=15}, + frame_title='Stranded citizen warning', resizable=true, - autoarrange_subviews=true, } function WarningWindow:init(info) self:addviews{ widgets.List{ - frame={h=15}, + frame={l=0, r=0, t=0, b=6}, view_id = 'list', - on_submit=self:callback('onIgnore'), on_select=self:callback('onZoom'), on_double_click=self:callback('onIgnore'), on_double_click2=self:callback('onToggleGroup'), }, - widgets.Panel{ - frame={h=5}, - subviews = { - widgets.HotkeyLabel{ - frame={b=3, l=0}, - key='SELECT', - label='Toggle ignore', - auto_width=true, - }, - widgets.HotkeyLabel{ - frame={b=3, l=21}, - key='CUSTOM_G', - label='Toggle group', - on_activate = self:callback('onToggleGroup'), - auto_width=true, - - }, - widgets.HotkeyLabel{ - frame={b=3, l=37}, - key = 'CUSTOM_SHIFT_I', - label = 'Ignore all', - on_activate = self:callback('onIgnoreAll'), - auto_width=true, - - }, - widgets.HotkeyLabel{ - frame={b=3, l=52}, - key = 'CUSTOM_SHIFT_C', - label = 'Clear all ignored', - on_activate = self:callback('onClear'), - auto_width=true, - - }, - widgets.WrappedLabel{ - frame={b=1, l=0}, - text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', - }, - } + widgets.WrappedLabel{ + frame={b=3, l=0}, + text_to_wrap='Double click to toggle unit ignore. Shift double click to toggle a group.', }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='SELECT', + label='Toggle ignore', + on_activate=self:callback('onIgnore'), + auto_width=true, + }, + widgets.HotkeyLabel{ + frame={b=1, l=23}, + key='CUSTOM_G', + label='Toggle group', + on_activate = self:callback('onToggleGroup'), + auto_width=true, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key = 'CUSTOM_SHIFT_I', + label = 'Ignore all', + on_activate = self:callback('onIgnoreAll'), + auto_width=true, + + }, + widgets.HotkeyLabel{ + frame={b=0, l=23}, + key = 'CUSTOM_SHIFT_C', + label = 'Clear all ignored', + on_activate = self:callback('onClear'), + auto_width=true, } self.groups = info.groups @@ -246,6 +237,9 @@ function WarningWindow:initListChoices() end function WarningWindow:onIgnore(_, choice) + if not choice then + _, choice = self.subviews.list:getSelected() + end local unit = choice.data['unit'] toggleUnitIgnore(unit) @@ -307,7 +301,7 @@ end local function getStrandedUnits() local groupCount = 0 local grouped = {} - local citizens = dfhack.units.getCitizens() + local citizens = dfhack.units.getCitizens(true) -- Don't use ignored units to determine if there are any stranded units -- but keep them to display later From d141a512f470d12f62d994fa455810874ebb28e4 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 16 Oct 2023 12:33:22 -0700 Subject: [PATCH 632/732] Update warn-stranded.lua --- warn-stranded.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/warn-stranded.lua b/warn-stranded.lua index cd190e3c1f..81bdef5de4 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -210,6 +210,7 @@ function WarningWindow:init(info) label = 'Clear all ignored', on_activate = self:callback('onClear'), auto_width=true, + }, } self.groups = info.groups From 1fcc83838e471fd8ef50cc6d6cb9259b69f1d847 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 17 Oct 2023 01:19:42 -0700 Subject: [PATCH 633/732] bump to 50.11-r2 --- changelog.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 93de0c8fbf..67bde5550a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 50.11-r2 + ## New Tools - `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) - `fix/corrupt-jobs`: prevents crashes by automatically removing corrupted jobs @@ -52,8 +64,6 @@ Template for new versions: - `quickfort`: now allows constructions to be built on top of constructed floors and ramps, just like vanilla. however, to allow blueprints to be safely reapplied to the same area, for example to fill in buildings whose constructions were canceled due to lost items, floors will not be rebuilt on top of floors and ramps will not be rebuilt on top of ramps - `gui/gm-editor`: for fields with primitive types, change from click to edit to click to select, double-click to edit. this should help prevent accidental modifications to the data and make hotkeys easier to use (since you have to click on a data item to use a hotkey on it) -## Removed - # 50.11-r1 ## New Tools From 1ed1aed8ddb075b0b26d52d5eba8456514bcee44 Mon Sep 17 00:00:00 2001 From: Petter Jennison Date: Wed, 18 Oct 2023 11:57:35 +0100 Subject: [PATCH 634/732] Update unit-syndromes.lua added missing return in the getSyndromeName function --- gui/unit-syndromes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/unit-syndromes.lua b/gui/unit-syndromes.lua index 994f967c08..9454a586c8 100644 --- a/gui/unit-syndromes.lua +++ b/gui/unit-syndromes.lua @@ -245,7 +245,7 @@ local function getSyndromeName(syndrome_raw) end if syndrome_raw.syn_name ~= "" then - syndrome_raw.syn_name:gsub("^%l", string.upper) + return syndrome_raw.syn_name:gsub("^%l", string.upper) elseif is_transformation then return "Body transformation" end From ecda07937d02493055d7d0e66e48ed18b21a8f6d Mon Sep 17 00:00:00 2001 From: Petter Jennison Date: Wed, 18 Oct 2023 13:56:21 +0100 Subject: [PATCH 635/732] Update changelog.txt for unit-syndromes fix --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 67bde5550a..7c06a5a50b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: - `gui/sandbox`: fix scrollbar moving double distance on click - `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped - `full-heal`: fix removal of corpse after resurrection +- `gui/unit-syndromes`: the syndrome names should now be visible ## Misc Improvements - `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities From eb10f740a284b6062ef7eed5a0400e60a8721559 Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 25 Oct 2023 00:02:15 -0700 Subject: [PATCH 636/732] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 7c06a5a50b..4dbea9f8bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/unit-syndromes`: show the syndrome names properly in the UI ## Misc Improvements @@ -57,7 +58,6 @@ Template for new versions: - `gui/sandbox`: fix scrollbar moving double distance on click - `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped - `full-heal`: fix removal of corpse after resurrection -- `gui/unit-syndromes`: the syndrome names should now be visible ## Misc Improvements - `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities From fb768c9a8cc90b181ea95afcade26eff403c74ce Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Oct 2023 09:07:40 -0700 Subject: [PATCH 637/732] clean up installation paths remove unused lines, don't install empty docs dir --- CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3631626adc..e9e5234b0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,8 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DESTINATION ${DFHACK_DATA_DESTINATION} FILES_MATCHING PATTERN "*.lua" - PATTERN "*.rb" PATTERN "*.json" - PATTERN "3rdparty" EXCLUDE + PATTERN "scripts/docs" EXCLUDE PATTERN "scripts/test" EXCLUDE ) From d842e90244e99351db62e670eaa857f89ba50e79 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Oct 2023 19:15:05 -0700 Subject: [PATCH 638/732] don't warn for units on unwalkable tiles if an adjacent tile is walkable. this avoids warning for, for example, units walking under waterfalls or through variable depth water --- changelog.txt | 1 + warn-stranded.lua | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4dbea9f8bb..ba1435d045 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: - `gui/unit-syndromes`: show the syndrome names properly in the UI ## Misc Improvements +- `warn-stranded`: don't warn for units that are temporarily on unwalkable tiles (e.g. as they pass under a waterfall) ## Removed diff --git a/warn-stranded.lua b/warn-stranded.lua index 81bdef5de4..24235093f5 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -4,7 +4,6 @@ --@module = true local gui = require 'gui' -local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} @@ -266,7 +265,7 @@ function WarningWindow:onClear() end function WarningWindow:onZoom() - local index, choice = self.subviews.list:getSelected() + local _, choice = self.subviews.list:getSelected() local unit = choice.data['unit'] local target = xyz2pos(dfhack.units.getPosition(unit)) @@ -299,6 +298,11 @@ local function compareGroups(group_one, group_two) return #group_one['units'] < #group_two['units'] end +local function getWalkGroup(pos) + local block = dfhack.maps.getTileBlock(pos) + return block and block.walkable[pos.x % 16][pos.y % 16] +end + local function getStrandedUnits() local groupCount = 0 local grouped = {} @@ -310,9 +314,22 @@ local function getStrandedUnits() -- Pathability group calculation is from gui/pathable for _, unit in ipairs(citizens) do - local target = xyz2pos(dfhack.units.getPosition(unit)) - local block = dfhack.maps.getTileBlock(target) - local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 + local unitPos = xyz2pos(dfhack.units.getPosition(unit)) + local walkGroup = getWalkGroup(unitPos) or 0 + + -- if on an unpathable tile, use the walkGroup of an adjacent tile. this prevents + -- warnings for units that are walking under falling water, which sometimes makes + -- a tile unwalkable while the unit is standing on it + if walkGroup == 0 then + walkGroup = getWalkGroup(xyz2pos(unitPos.x-1, unitPos.y-1, unitPos.z)) + or getWalkGroup(xyz2pos(unitPos.x, unitPos.y-1, unitPos.z)) + or getWalkGroup(xyz2pos(unitPos.x+1, unitPos.y-1, unitPos.z)) + or getWalkGroup(xyz2pos(unitPos.x-1, unitPos.y, unitPos.z)) + or getWalkGroup(xyz2pos(unitPos.x+1, unitPos.y, unitPos.z)) + or getWalkGroup(xyz2pos(unitPos.x-1, unitPos.y+1, unitPos.z)) + or getWalkGroup(xyz2pos(unitPos.x, unitPos.y+1, unitPos.z)) + or getWalkGroup(xyz2pos(unitPos.x+1, unitPos.y+1, unitPos.z)) + end if unitIgnored(unit) then table.insert(ensure_key(ignoredGroup, walkGroup), unit) From d41fc4b6c7db5d29c6449cfb1366bed89dda9201 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 27 Oct 2023 03:28:00 -0700 Subject: [PATCH 639/732] calculate walkGroup properly when current tile is not walkable --- warn-stranded.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 24235093f5..9ebc2efacd 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -179,7 +179,7 @@ function WarningWindow:init(info) }, widgets.WrappedLabel{ frame={b=3, l=0}, - text_to_wrap='Double click to toggle unit ignore. Shift double click to toggle a group.', + text_to_wrap='Select to zoom to unit. Double click to toggle unit ignore. Shift double click to toggle a group.', }, widgets.HotkeyLabel{ frame={b=1, l=0}, @@ -300,7 +300,9 @@ end local function getWalkGroup(pos) local block = dfhack.maps.getTileBlock(pos) - return block and block.walkable[pos.x % 16][pos.y % 16] + if not block then return end + local walkGroup = block.walkable[pos.x % 16][pos.y % 16] + return walkGroup ~= 0 and walkGroup or nil end local function getStrandedUnits() @@ -329,6 +331,7 @@ local function getStrandedUnits() or getWalkGroup(xyz2pos(unitPos.x-1, unitPos.y+1, unitPos.z)) or getWalkGroup(xyz2pos(unitPos.x, unitPos.y+1, unitPos.z)) or getWalkGroup(xyz2pos(unitPos.x+1, unitPos.y+1, unitPos.z)) + or 0 end if unitIgnored(unit) then From 54fb79912bc6d333afee6d4a48471b6dbb868a37 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 27 Oct 2023 03:47:11 -0700 Subject: [PATCH 640/732] take advantage of new highlight parameter --- gui/gm-editor.lua | 5 +---- warn-stranded.lua | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 86cf6c3879..1c0fe7a345 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -433,10 +433,7 @@ function GmEditorUi:gotoPos() end end if pos then - dfhack.gui.revealInDwarfmodeMap(pos,true) - df.global.game.main_interface.recenter_indicator_m.x = pos.x - df.global.game.main_interface.recenter_indicator_m.y = pos.y - df.global.game.main_interface.recenter_indicator_m.z = pos.z + dfhack.gui.revealInDwarfmodeMap(pos,true,true) end end function GmEditorUi:editSelectedRaw(index,choice) diff --git a/warn-stranded.lua b/warn-stranded.lua index 9ebc2efacd..0933a7364b 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -269,7 +269,7 @@ function WarningWindow:onZoom() local unit = choice.data['unit'] local target = xyz2pos(dfhack.units.getPosition(unit)) - dfhack.gui.revealInDwarfmodeMap(target, true) + dfhack.gui.revealInDwarfmodeMap(target, false, true) end function WarningWindow:onToggleGroup() From 31dd3ee5cc473c22ee2ad9a72a1789ce8e5d2c2e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 29 Oct 2023 16:37:49 -0700 Subject: [PATCH 641/732] use new walkability group api --- devel/unit-path.lua | 11 +---------- gui/pathable.lua | 3 +-- warn-stranded.lua | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/devel/unit-path.lua b/devel/unit-path.lua index 707ecf8e94..459ff92159 100644 --- a/devel/unit-path.lua +++ b/devel/unit-path.lua @@ -47,15 +47,6 @@ local function getTileType(cursor) end end -local function getTileWalkable(cursor) - local block = dfhack.maps.getTileBlock(cursor) - if block then - return block.walkable[cursor.x%16][cursor.y%16] - else - return 0 - end -end - local function paintMapTile(dc, vp, cursor, pos, ...) if not same_xyz(cursor, pos) then local stile = vp:tileToScreen(pos) @@ -115,7 +106,7 @@ function UnitPathUI:renderPath(dc,vp,cursor) end end local color = COLOR_LIGHTGREEN - if getTileWalkable(pt) == 0 then color = COLOR_LIGHTRED end + if dfhack.maps.getWalkableGroup(pt) == 0 then color = COLOR_LIGHTRED end paintMapTile(dc, vp, cursor, pt, char, color) end end diff --git a/gui/pathable.lua b/gui/pathable.lua index 863e625ece..868ce5d7f6 100644 --- a/gui/pathable.lua +++ b/gui/pathable.lua @@ -73,8 +73,7 @@ function Pathable:onRenderBody() return end - local block = dfhack.maps.getTileBlock(target) - local walk_group = block and block.walkable[target.x % 16][target.y % 16] or 0 + local walk_group = dfhack.maps.getWalkableGroup(target) group:setText(walk_group == 0 and 'None' or tostring(walk_group)) if self.subviews.draw:getOptionValue() then diff --git a/warn-stranded.lua b/warn-stranded.lua index 0933a7364b..b68f80ead8 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -301,7 +301,7 @@ end local function getWalkGroup(pos) local block = dfhack.maps.getTileBlock(pos) if not block then return end - local walkGroup = block.walkable[pos.x % 16][pos.y % 16] + local walkGroup = dfhack.maps.getWalkableGroup(pos) return walkGroup ~= 0 and walkGroup or nil end From 89e0c9c3c2f80e54cc40e35c69f68a6219a9c432 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 30 Oct 2023 02:00:23 -0700 Subject: [PATCH 642/732] add sync-windmills --- changelog.txt | 1 + docs/sync-windmills.rst | 32 ++++++++++++++++++++++++++++++++ sync-windmills.lua | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 docs/sync-windmills.rst create mode 100644 sync-windmills.lua diff --git a/changelog.txt b/changelog.txt index ba1435d045..6bcec1b704 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `sync-windmills`: synchronize or randomize movement of active windmills ## New Features diff --git a/docs/sync-windmills.rst b/docs/sync-windmills.rst new file mode 100644 index 0000000000..5696b82694 --- /dev/null +++ b/docs/sync-windmills.rst @@ -0,0 +1,32 @@ +sync-windmills +============== + +.. dfhack-tool:: + :summary: Synchronize or randomize windmill movement. + :tags: fort buildings + +This tool can adjust the appearance of running windmills so that they are +either all in synchronization or are all completely randomized. + +Usage +----- + +:: + + sync-windmills [] + +Examples +-------- + +``sync-windmills`` + Make all active windmills synchronize their turning. +``sync-windmills -r`` + Randomize the movement of all active windmills. + +Options +------- + +``-q``, ``--quiet`` + Suppress non-error console output. +``-r``, ``--randomize`` + Randomize windmill state. diff --git a/sync-windmills.lua b/sync-windmills.lua new file mode 100644 index 0000000000..a50e6fdbe1 --- /dev/null +++ b/sync-windmills.lua @@ -0,0 +1,32 @@ +local argparse = require('argparse') + +local function process_windmills(rotate_fn, timer_fn) + for _, bld in ipairs(df.global.world.buildings.other.WINDMILL) do + if bld.is_working ~= 0 then + bld.visual_rotated = rotate_fn() + bld.rotate_timer = timer_fn() + end + end +end + +local opts = {} +argparse.processArgsGetopt({...}, { + { 'h', 'help', handler = function() opts.help = true end }, + { 'q', 'quiet', handler = function() opts.quiet = true end }, + { 'r', 'randomize', handler = function() opts.randomize = true end }, +}) + +if opts.help then + print(dfhack.script_help()) + return +end + +process_windmills( + opts.randomize and function() return math.random(1, 2) == 1 end or function() return false end, + opts.randomize and function() return math.random(0, 74) end or function() return 0 end) + +if not opts.quiet then + print(('%d windmills %s'):format( + #df.global.world.buildings.other.WINDMILL, + opts.randomize and 'randomized' or 'synchronized')) +end From 61599905d062dc888e0d0714ed0f858c4a7f851b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 30 Oct 2023 13:05:13 -0700 Subject: [PATCH 643/732] add --timing-only option for synchronizing timing but not polarity --- docs/sync-windmills.rst | 17 +++++++++++++---- sync-windmills.lua | 9 +++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/sync-windmills.rst b/docs/sync-windmills.rst index 5696b82694..21bf44e156 100644 --- a/docs/sync-windmills.rst +++ b/docs/sync-windmills.rst @@ -5,8 +5,15 @@ sync-windmills :summary: Synchronize or randomize windmill movement. :tags: fort buildings -This tool can adjust the appearance of running windmills so that they are -either all in synchronization or are all completely randomized. +Windmills cycle between two graphical states to simulate movement. This is the +polarity of the appearance. Each windmill also has a timer that controls when +the windmill switches polarity. Each windmill's timer starts from zero at the +instant that it is built, so two different windmills will rarely have exactly +the same state. This tool can adjust the alignment of polarity and timers +across your active windmills to your preference. + +Note that this tool will not affect windmills that have just been activated and +are still rotating to adjust to the regional wind direction. Usage ----- @@ -19,7 +26,7 @@ Examples -------- ``sync-windmills`` - Make all active windmills synchronize their turning. + Synchronize movement of all active windmills. ``sync-windmills -r`` Randomize the movement of all active windmills. @@ -29,4 +36,6 @@ Options ``-q``, ``--quiet`` Suppress non-error console output. ``-r``, ``--randomize`` - Randomize windmill state. + Randomize the polarity and timer value for all windmills. +``-t``, ``--timing-only`` + Randomize windmill polarity, but synchronize windmill timers. diff --git a/sync-windmills.lua b/sync-windmills.lua index a50e6fdbe1..cc25703395 100644 --- a/sync-windmills.lua +++ b/sync-windmills.lua @@ -14,6 +14,7 @@ argparse.processArgsGetopt({...}, { { 'h', 'help', handler = function() opts.help = true end }, { 'q', 'quiet', handler = function() opts.quiet = true end }, { 'r', 'randomize', handler = function() opts.randomize = true end }, + { 't', 'timing-only', handler = function() opts.timing = true end }, }) if opts.help then @@ -22,8 +23,12 @@ if opts.help then end process_windmills( - opts.randomize and function() return math.random(1, 2) == 1 end or function() return false end, - opts.randomize and function() return math.random(0, 74) end or function() return 0 end) + (opts.randomize or opts.timing) and + function() return math.random(1, 2) == 1 end or + function() return false end, + opts.randomize and not opts.timing and + function() return math.random(0, 74) end or + function() return 0 end) if not opts.quiet then print(('%d windmills %s'):format( From 339387850daf6495504157ce411a8fd4cd165d11 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 30 Oct 2023 23:28:04 -0700 Subject: [PATCH 644/732] add floating label indicating selected dimensions --- changelog.txt | 1 + docs/gui/design.rst | 7 +++++ gui/design.lua | 64 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index ba1435d045..bc27dedd21 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging ## Fixes - `gui/unit-syndromes`: show the syndrome names properly in the UI diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 2c2e2feb8b..139ef7a7c6 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -15,3 +15,10 @@ Usage :: gui/design + +Overlay +------- + +This script provides an overlay that shows the selected dimensions when +designating something with vanilla tools, for example when painting a burrow or +designating digging. diff --git a/gui/design.lua b/gui/design.lua index a0a215d4ad..72f592e6b3 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1,5 +1,5 @@ -- A GUI front-end for creating designs ---@ module = false +--@ module = true -- TODOS ==================== @@ -38,6 +38,7 @@ local gui = require("gui") local textures = require("gui.textures") local guidm = require("gui.dwarfmode") local widgets = require("gui.widgets") +local overlay = require('plugins.overlay') local quickfort = reqscript("quickfort") local shapes = reqscript("internal/design/shapes") local util = reqscript("internal/design/util") @@ -1771,6 +1772,67 @@ function DesignScreen:onDismiss() view = nil end +-- ----------------- -- +-- DimensionsOverlay -- +-- ----------------- -- + +local DIMENSION_LABEL_WIDTH = 15 +local DIMENSION_LABEL_HEIGHT = 1 + +DimensionsOverlay = defclass(DimensionsOverlay, overlay.OverlayWidget) +DimensionsOverlay.ATTRS{ + default_pos={x=1,y=1}, + default_enabled=true, + viewscreens={ + 'dwarfmode/Designate', + 'dwarfmode/Burrow/Paint', + 'dwarfmode/Stockpile/Paint', + }, + frame={w=DIMENSION_LABEL_WIDTH, h=DIMENSION_LABEL_HEIGHT}, +} + +local selection_rect = df.global.selection_rect + +local function is_choosing_area() + return selection_rect.start_x >= 0 and dfhack.gui.getMousePos() +end + +local function get_cur_area_dims() + local pos1 = dfhack.gui.getMousePos() + local pos2 = xyz2pos(selection_rect.start_x, selection_rect.start_y, selection_rect.start_z) + return math.abs(pos1.x - pos2.x) + 1, + math.abs(pos1.y - pos2.y) + 1, + math.abs(pos1.z - pos2.z) + 1 +end + +function DimensionsOverlay:init() + self:addviews{ + widgets.Label{ + view_id='label', + frame={t=0, l=0, h=DIMENSION_LABEL_HEIGHT}, + text={ + {text=function() return ('%dx%dx%d'):format(get_cur_area_dims()) end}, + }, + visible=is_choosing_area, + }, + } +end + +function DimensionsOverlay:onRenderFrame(dc, rect) + DimensionsOverlay.super.onRenderFrame(self, dc, rect) + local sw, sh = dfhack.screen.getWindowSize() + local x, y = dfhack.screen.getMousePos() + x = math.min(x + 3, sw - DIMENSION_LABEL_WIDTH) + y = math.min(y + 3, sh - DIMENSION_LABEL_HEIGHT) + self.frame.w = x + DIMENSION_LABEL_WIDTH + self.frame.h = y + DIMENSION_LABEL_HEIGHT + self.subviews.label.frame = {t=y, l=x} +end + +OVERLAY_WIDGETS = { + dimensions=DimensionsOverlay, +} + if dfhack_flags.module then return end if not dfhack.isMapLoaded() then From 80487f5ad68c83083d67fcf85e6c27066dc5ec66 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 31 Oct 2023 11:49:38 -0700 Subject: [PATCH 645/732] add frame around dimensions tooltip and fix processing when selection starts from out of bounds --- gui/design.lua | 67 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 72f592e6b3..a33a0a364d 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1776,8 +1776,8 @@ end -- DimensionsOverlay -- -- ----------------- -- -local DIMENSION_LABEL_WIDTH = 15 -local DIMENSION_LABEL_HEIGHT = 1 +local DEFAULT_DIMENSION_TOOLTIP_WIDTH = 17 +local DIMENSION_TOOLTIP_HEIGHT = 3 DimensionsOverlay = defclass(DimensionsOverlay, overlay.OverlayWidget) DimensionsOverlay.ATTRS{ @@ -1788,7 +1788,7 @@ DimensionsOverlay.ATTRS{ 'dwarfmode/Burrow/Paint', 'dwarfmode/Stockpile/Paint', }, - frame={w=DIMENSION_LABEL_WIDTH, h=DIMENSION_LABEL_HEIGHT}, + frame={w=DEFAULT_DIMENSION_TOOLTIP_WIDTH, h=DIMENSION_TOOLTIP_HEIGHT}, } local selection_rect = df.global.selection_rect @@ -1799,34 +1799,65 @@ end local function get_cur_area_dims() local pos1 = dfhack.gui.getMousePos() - local pos2 = xyz2pos(selection_rect.start_x, selection_rect.start_y, selection_rect.start_z) + if not pos1 or selection_rect.start_x < 0 then return 1, 1, 1 end + + -- clamp to map edges (since you can start selection out of bounds) + local pos2 = xyz2pos( + math.max(0, math.min(df.global.world.map.x_count-1, selection_rect.start_x)), + math.max(0, math.min(df.global.world.map.y_count-1, selection_rect.start_y)), + math.max(0, math.min(df.global.world.map.z_count-1, selection_rect.start_z))) + return math.abs(pos1.x - pos2.x) + 1, math.abs(pos1.y - pos2.y) + 1, math.abs(pos1.z - pos2.z) + 1 end +local function format_dims() + return ('%dx%dx%d'):format(get_cur_area_dims()) +end + function DimensionsOverlay:init() self:addviews{ - widgets.Label{ - view_id='label', - frame={t=0, l=0, h=DIMENSION_LABEL_HEIGHT}, - text={ - {text=function() return ('%dx%dx%d'):format(get_cur_area_dims()) end}, - }, + widgets.ResizingPanel{ + view_id='tooltip', + frame={b=0, r=0, w=DEFAULT_DIMENSION_TOOLTIP_WIDTH, h=DIMENSION_TOOLTIP_HEIGHT}, + frame_style=gui.FRAME_INTERIOR, + auto_width=true, visible=is_choosing_area, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + auto_width=true, + text={{text=format_dims}}, + }, + }, }, } end -function DimensionsOverlay:onRenderFrame(dc, rect) - DimensionsOverlay.super.onRenderFrame(self, dc, rect) - local sw, sh = dfhack.screen.getWindowSize() +-- don't imply that stockpiles will be 3d +local main_interface = df.global.game.main_interface +local function check_stockpile_dims() + if main_interface.bottom_mode_selected == df.main_bottom_mode_type.STOCKPILE_PAINT then + selection_rect.start_z = df.global.window_z + end +end + +function DimensionsOverlay:render(dc) + check_stockpile_dims() local x, y = dfhack.screen.getMousePos() - x = math.min(x + 3, sw - DIMENSION_LABEL_WIDTH) - y = math.min(y + 3, sh - DIMENSION_LABEL_HEIGHT) - self.frame.w = x + DIMENSION_LABEL_WIDTH - self.frame.h = y + DIMENSION_LABEL_HEIGHT - self.subviews.label.frame = {t=y, l=x} + if not x then return end + local sw, sh = dfhack.screen.getWindowSize() + local tooltip_width = #format_dims() + 2 + if tooltip_width ~= self.prev_tooltip_width then + self:updateLayout() + self.prev_tooltip_width = tooltip_width + end + x = math.min(x + 3, sw - tooltip_width) + y = math.min(y + 3, sh - DIMENSION_TOOLTIP_HEIGHT) + self.frame.w = x + tooltip_width + self.frame.h = y + DIMENSION_TOOLTIP_HEIGHT + DimensionsOverlay.super.render(self, dc) end OVERLAY_WIDGETS = { From 887e44324b4b55c9687fdb5737f6d2ef2b00b19a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 31 Oct 2023 11:54:52 -0700 Subject: [PATCH 646/732] update docs --- docs/gui/design.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 139ef7a7c6..29d46dfebb 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -21,4 +21,5 @@ Overlay This script provides an overlay that shows the selected dimensions when designating something with vanilla tools, for example when painting a burrow or -designating digging. +designating digging. The dimensions show up in a tooltip that follows the mouse +cursor. From d56aac6bcd6628d22420d57b4a96987490e5a11f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 1 Nov 2023 18:01:57 -0700 Subject: [PATCH 647/732] add DFHack signature to dims tooltip --- gui/design.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index a33a0a364d..d44f690d60 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1777,7 +1777,7 @@ end -- ----------------- -- local DEFAULT_DIMENSION_TOOLTIP_WIDTH = 17 -local DIMENSION_TOOLTIP_HEIGHT = 3 +local DIMENSION_TOOLTIP_HEIGHT = 4 DimensionsOverlay = defclass(DimensionsOverlay, overlay.OverlayWidget) DimensionsOverlay.ATTRS{ @@ -1821,12 +1821,17 @@ function DimensionsOverlay:init() widgets.ResizingPanel{ view_id='tooltip', frame={b=0, r=0, w=DEFAULT_DIMENSION_TOOLTIP_WIDTH, h=DIMENSION_TOOLTIP_HEIGHT}, - frame_style=gui.FRAME_INTERIOR, + frame_style=gui.FRAME_THIN, + frame_background=gui.CLEAR_PEN, auto_width=true, visible=is_choosing_area, subviews={ + widgets.Panel{ + -- set minimum size for tooltip frame so DFHack label fits + frame={t=0, l=0, w=7, h=2}, + }, widgets.Label{ - frame={t=0, l=0}, + frame={t=0}, auto_width=true, text={{text=format_dims}}, }, @@ -1848,7 +1853,7 @@ function DimensionsOverlay:render(dc) local x, y = dfhack.screen.getMousePos() if not x then return end local sw, sh = dfhack.screen.getWindowSize() - local tooltip_width = #format_dims() + 2 + local tooltip_width = math.max(9, #format_dims() + 2) if tooltip_width ~= self.prev_tooltip_width then self:updateLayout() self.prev_tooltip_width = tooltip_width From 24fd513e61c14de927b46a119ab41e155fc8985e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Nov 2023 15:56:12 -0700 Subject: [PATCH 648/732] remove gui/mechanisms functionality has been added to vanilla UI --- docs/gui/mechanisms.rst | 27 -------- gui/mechanisms.lua | 144 ---------------------------------------- 2 files changed, 171 deletions(-) delete mode 100644 docs/gui/mechanisms.rst delete mode 100644 gui/mechanisms.lua diff --git a/docs/gui/mechanisms.rst b/docs/gui/mechanisms.rst deleted file mode 100644 index 9de336e84b..0000000000 --- a/docs/gui/mechanisms.rst +++ /dev/null @@ -1,27 +0,0 @@ -gui/mechanisms -============== - -.. dfhack-tool:: - :summary: List mechanisms and links connected to a building. - :tags: unavailable - -This convenient tool lists the mechanisms connected to the building and the -buildings linked via the mechanisms. Navigating the list centers the view on the -relevant linked building. - -To exit, press :kbd:`Esc` or :kbd:`Enter`; :kbd:`Esc` recenters on the original -building, while :kbd:`Enter` leaves focus on the current one. -:kbd:`Shift`:kbd:`Enter` has an effect equivalent to pressing :kbd:`Enter`, and -then re-entering the mechanisms UI. - -Usage ------ - -:: - - gui/mechanisms - -Screenshot ----------- - -.. image:: /docs/images/mechanisms.png diff --git a/gui/mechanisms.lua b/gui/mechanisms.lua deleted file mode 100644 index 32a85436c9..0000000000 --- a/gui/mechanisms.lua +++ /dev/null @@ -1,144 +0,0 @@ --- Shows mechanisms linked to the current building. ---[====[ - -gui/mechanisms -============== -Lists mechanisms connected to the building, and their links. Navigating -the list centers the view on the relevant linked buildings. - -.. image:: /docs/images/mechanisms.png - -To exit, press :kbd:`Esc` or :kbd:`Enter`; :kbd:`Esc` recenters on -the original building, while :kbd:`Enter` leaves focus on the current -one. :kbd:`Shift`:kbd:`Enter` has an effect equivalent to pressing -:kbd:`Enter`, and then re-entering the mechanisms UI. - -]====] -local utils = require 'utils' -local gui = require 'gui' -local guidm = require 'gui.dwarfmode' - -function listMechanismLinks(building) - local lst = {} - local function push(item, mode) - if item then - lst[#lst+1] = { - obj = item, mode = mode, - name = utils.getBuildingName(item) - } - end - end - - push(building, 'self') - - if not df.building_actual:is_instance(building) then - return lst - end - - local item, tref, tgt - for _,v in ipairs(building.contained_items) do - item = v.item - if df.item_trappartsst:is_instance(item) then - tref = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_TRIGGER) - if tref then - push(tref:getBuilding(), 'trigger') - end - tref = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_TRIGGERTARGET) - if tref then - push(tref:getBuilding(), 'target') - end - end - end - - return lst -end - -MechanismList = defclass(MechanismList, guidm.MenuOverlay) - -MechanismList.focus_path = 'mechanisms' - -function MechanismList:init(info) - self:assign{ - links = {}, selected = 1 - } - self:fillList(info.building) -end - -function MechanismList:fillList(building) - local links = listMechanismLinks(building) - - self.old_viewport = self:getViewport() - self.old_cursor = guidm.getCursorPos() - - if #links <= 1 then - links[1].mode = 'none' - end - - self.links = links - self.selected = 1 -end - -local colors = { - self = COLOR_CYAN, none = COLOR_CYAN, - trigger = COLOR_GREEN, target = COLOR_GREEN -} -local icons = { - self = 128, none = 63, trigger = 27, target = 26 -} - -function MechanismList:onRenderBody(dc) - dc:clear() - dc:seek(1,1):string("Mechanism Links", COLOR_WHITE):newline() - - for i,v in ipairs(self.links) do - local pen = { fg=colors[v.mode], bold = (i == self.selected) } - dc:newline(1):pen(pen):char(icons[v.mode]) - dc:advance(1):string(v.name) - end - - local nlinks = #self.links - - if nlinks <= 1 then - dc:newline():newline(1):string("This building has no links", COLOR_LIGHTRED) - end - - dc:newline():newline(1):pen(COLOR_WHITE) - dc:key('LEAVESCREEN'):string(": Back, ") - dc:key('SELECT'):string(": Switch"):newline(1) - dc:key_string('LEAVESCREEN_ALL', "Exit to map") -end - -function MechanismList:changeSelected(delta) - if #self.links <= 1 then return end - self.selected = 1 + (self.selected + delta - 1) % #self.links - self:selectBuilding(self.links[self.selected].obj) -end - -function MechanismList:onInput(keys) - if keys.SECONDSCROLL_UP then - self:changeSelected(-1) - elseif keys.SECONDSCROLL_DOWN then - self:changeSelected(1) - elseif keys.LEAVESCREEN or keys.LEAVESCREEN_ALL then - self:dismiss() - if self.selected ~= 1 and not keys.LEAVESCREEN_ALL then - self:selectBuilding(self.links[1].obj, self.old_cursor, self.old_viewport) - end - elseif keys.SELECT_ALL then - if self.selected > 1 then - self:fillList(self.links[self.selected].obj) - end - elseif keys.SELECT then - self:dismiss() - elseif self:simulateViewScroll(keys) then - return - end -end - -if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then - qerror("This script requires a mechanism-linked building to be selected in 'q' mode") -end - -local list = MechanismList{ building = df.global.world.selected_building } -list:show() -list:changeSelected(1) From f7d6869a60f0bec0ab9f8b3383827c2ab7a1ef0c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 3 Nov 2023 02:27:45 -0700 Subject: [PATCH 649/732] don't allow dims overlay to be repositioned --- gui/design.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/design.lua b/gui/design.lua index d44f690d60..7c27aaf1fd 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1783,6 +1783,7 @@ DimensionsOverlay = defclass(DimensionsOverlay, overlay.OverlayWidget) DimensionsOverlay.ATTRS{ default_pos={x=1,y=1}, default_enabled=true, + overlay_only=true, -- not player-repositionable viewscreens={ 'dwarfmode/Designate', 'dwarfmode/Burrow/Paint', From e0dd34dcb89aaa3f37a9a52bcac4491c75c8b11a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 3 Nov 2023 02:29:01 -0700 Subject: [PATCH 650/732] implement basic burrow mode functionality --- changelog.txt | 1 + internal/quickfort/burrow.lua | 167 ++++++++++++++++++++++++++++++++++ internal/quickfort/parse.lua | 1 + quickfort.lua | 1 + 4 files changed, 170 insertions(+) create mode 100644 internal/quickfort/burrow.lua diff --git a/changelog.txt b/changelog.txt index 15fe6d24c7..c40e9e4ce5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging +- `quickfort`: new blueprint mode for designating burrows ## Fixes - `gui/unit-syndromes`: show the syndrome names properly in the UI diff --git a/internal/quickfort/burrow.lua b/internal/quickfort/burrow.lua new file mode 100644 index 0000000000..7157b59ee6 --- /dev/null +++ b/internal/quickfort/burrow.lua @@ -0,0 +1,167 @@ +-- burrow-related data and logic for the quickfort script +--@ module = true + +if not dfhack_flags.module then + qerror('this script cannot be called directly') +end + +local quickfort_common = reqscript('internal/quickfort/common') +local quickfort_map = reqscript('internal/quickfort/map') +local quickfort_parse = reqscript('internal/quickfort/parse') +local quickfort_preview = reqscript('internal/quickfort/preview') +local utils = require('utils') + +local log = quickfort_common.log +local logfn = quickfort_common.logfn + +local burrow_db = { + a={label='Add', add=true}, + e={label='Erase', add=false}, +} + +local function custom_burrow(_, keys) + local token_and_label, props_start_pos = quickfort_parse.parse_token_and_label(keys, 1, '%w') + if not token_and_label or not rawget(burrow_db, token_and_label.token) then return nil end + local db_entry = copyall(burrow_db[token_and_label.token]) + local props, next_token_pos = quickfort_parse.parse_properties(keys, props_start_pos) + if props.name then + db_entry.name = props.name + props.name = nil + end + if db_entry.add and props.create == 'true' then + db_entry.create = true + props.create = nil + end + + for k,v in pairs(props) do + dfhack.printerr(('unhandled property for symbol "%s": "%s"="%s"'):format( + token_and_label.token, k, v)) + end + + return db_entry +end + +setmetatable(burrow_db, {__index=custom_burrow}) + +local burrows = df.global.plotinfo.burrows + +local function do_burrow(ctx, db_entry, pos) + local stats = ctx.stats + local b + if db_entry.name then + b = dfhack.burrows.findByName(db_entry.name, true) + end + if not b and db_entry.add and db_entry.create then + b = df.burrow:new() + b.id = burrows.next_id + burrows.next_id = burrows.next_id + 1 + if db_entry.name then + b.name = db_entry.name + end + b.symbol_index = math.random(0, 22) + b.texture_r = math.random(0, 255) + b.texture_g = math.random(0, 255) + b.texture_b = math.random(0, 255) + b.texture_br = 255 - b.texture_r + b.texture_bg = 255 - b.texture_g + b.texture_bb = 255 - b.texture_b + burrows.list:insert('#', b) + stats.burrow_created.value = stats.burrow_created.value + 1 + end + if not b and db_entry.add then + log('could not find burrow to add to') + return + end + if b then + dfhack.burrows.setAssignedTile(b, pos, db_entry.add) + stats['burrow_tiles_'..(db_entry.add and 'added' or 'removed')].value = + stats['burrow_tiles_'..(db_entry.add and 'added' or 'removed')].value + 1 + if not db_entry.add and db_entry.create and #dfhack.burrows.listBlocks(b) == 0 then + dfhack.burrows.clearTiles(b) + local _, _, idx = utils.binsearch(burrows.list, b.id, 'id') + if idx then + burrows.list:erase(idx) + b:delete() + stats.burrow_destroyed.value = stats.burrow_destroyed.value + 1 + end + end + elseif not db_entry.add then + for _,b in ipairs(burrows.list) do + dfhack.burrows.setAssignedTile(b, pos, false) + end + stats.burrow_tiles_removed.value = stats.burrow_tiles_removed.value + 1 + end +end + +function do_run_impl(zlevel, grid, ctx, invert) + local stats = ctx.stats + stats.burrow_created = stats.burrow_created or + {label='Burrows created', value=0} + stats.burrow_destroyed = stats.burrow_destroyed or + {label='Burrows destroyed', value=0} + stats.burrow_tiles_added = stats.burrow_tiles_added or + {label='Burrow tiles added', value=0} + stats.burrow_tiles_removed = stats.burrow_tiles_removed or + {label='Burrow tiles removed', value=0} + + ctx.bounds = ctx.bounds or quickfort_map.MapBoundsChecker{} + for y, row in pairs(grid) do + for x, cell_and_text in pairs(row) do + local cell, text = cell_and_text.cell, cell_and_text.text + local pos = xyz2pos(x, y, zlevel) + log('applying spreadsheet cell %s with text "%s" to map' .. + ' coordinates (%d, %d, %d)', cell, text, pos.x, pos.y, pos.z) + local db_entry = nil + local keys, extent = quickfort_parse.parse_cell(ctx, text) + if keys then db_entry = burrow_db[keys] end + if not db_entry then + dfhack.printerr(('invalid key sequence: "%s" in cell %s') + :format(text, cell)) + stats.invalid_keys.value = stats.invalid_keys.value + 1 + goto continue + end + if invert then + db_entry = copyall(db_entry) + db_entry.add = not db_entry.add + end + if extent.specified then + -- shift pos to the upper left corner of the extent and convert + -- the extent dimensions to positive, simplifying the logic below + pos.x = math.min(pos.x, pos.x + extent.width + 1) + pos.y = math.min(pos.y, pos.y + extent.height + 1) + end + for extent_x=1,math.abs(extent.width) do + for extent_y=1,math.abs(extent.height) do + local extent_pos = xyz2pos( + pos.x+extent_x-1, + pos.y+extent_y-1, + pos.z) + if not ctx.bounds:is_on_map(extent_pos) then + log('coordinates out of bounds; skipping (%d, %d, %d)', + extent_pos.x, extent_pos.y, extent_pos.z) + stats.out_of_bounds.value = + stats.out_of_bounds.value + 1 + else + quickfort_preview.set_preview_tile(ctx, extent_pos, true) + if not ctx.dry_run then + do_burrow(ctx, db_entry, extent_pos) + end + end + end + end + ::continue:: + end + end +end + +function do_run(zlevel, grid, ctx) + do_run_impl(zlevel, grid, ctx, false) +end + +function do_orders() + log('nothing to do for blueprints in mode: burrow') +end + +function do_undo(zlevel, grid, ctx) + do_run_impl(zlevel, grid, ctx, true) +end diff --git a/internal/quickfort/parse.lua b/internal/quickfort/parse.lua index 1e29010e7f..4cf10482d4 100644 --- a/internal/quickfort/parse.lua +++ b/internal/quickfort/parse.lua @@ -15,6 +15,7 @@ valid_modes = utils.invert({ 'build', 'place', 'zone', + 'burrow', 'meta', 'notes', 'ignore', diff --git a/quickfort.lua b/quickfort.lua index 0835689318..2e2c1ac03d 100644 --- a/quickfort.lua +++ b/quickfort.lua @@ -19,6 +19,7 @@ function refresh_scripts() reqscript('internal/quickfort/api') reqscript('internal/quickfort/build') reqscript('internal/quickfort/building') + reqscript('internal/quickfort/burrow') reqscript('internal/quickfort/command') reqscript('internal/quickfort/common') reqscript('internal/quickfort/dig') From 25db4b6837d2ed37bfe7494624b464b30769320a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 3 Nov 2023 02:50:42 -0700 Subject: [PATCH 651/732] support setting the civalert burrow from quickfort --- gui/civ-alert.lua | 15 +++++++++++++++ internal/quickfort/burrow.lua | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index f043f4c222..1c6cb17519 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -35,6 +35,21 @@ local function clear_alarm() df.global.plotinfo.alerts.civ_alert_idx = 0 end +function set_civalert_burrow_if_unset(burrow) + local burrows = get_civ_alert().burrows + if #burrows == 0 then + burrows:insert('#', burrow.id) + end +end + +function unset_civalert_burrow_if_set(burrow) + local burrows = get_civ_alert().burrows + if #burrows > 0 and burrows[0] == burrow.id then + burrows:resize(0) + clear_alarm() + end +end + local function toggle_civalert_burrow(id) local burrows = get_civ_alert().burrows if #burrows == 0 then diff --git a/internal/quickfort/burrow.lua b/internal/quickfort/burrow.lua index 7157b59ee6..2091e831c1 100644 --- a/internal/quickfort/burrow.lua +++ b/internal/quickfort/burrow.lua @@ -5,6 +5,7 @@ if not dfhack_flags.module then qerror('this script cannot be called directly') end +local civalert = reqscript('gui/civ-alert') local quickfort_common = reqscript('internal/quickfort/common') local quickfort_map = reqscript('internal/quickfort/map') local quickfort_parse = reqscript('internal/quickfort/parse') @@ -32,6 +33,10 @@ local function custom_burrow(_, keys) db_entry.create = true props.create = nil end + if db_entry.add and props.civalert == 'true' then + db_entry.civalert = true + props.civalert = nil + end for k,v in pairs(props) do dfhack.printerr(('unhandled property for symbol "%s": "%s"="%s"'):format( @@ -76,6 +81,13 @@ local function do_burrow(ctx, db_entry, pos) dfhack.burrows.setAssignedTile(b, pos, db_entry.add) stats['burrow_tiles_'..(db_entry.add and 'added' or 'removed')].value = stats['burrow_tiles_'..(db_entry.add and 'added' or 'removed')].value + 1 + if db_entry.civalert then + if db_entry.add then + civalert.set_civalert_burrow_if_unset(b) + else + civalert.unset_civalert_burrow_if_set(b) + end + end if not db_entry.add and db_entry.create and #dfhack.burrows.listBlocks(b) == 0 then dfhack.burrows.clearTiles(b) local _, _, idx = utils.binsearch(burrows.list, b.id, 'id') From 5f4ee323600274c87614a676f5d43773fbe77a67 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 3 Nov 2023 11:34:26 -0700 Subject: [PATCH 652/732] support autochop integration for burrows blueprints --- internal/quickfort/burrow.lua | 66 +++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/internal/quickfort/burrow.lua b/internal/quickfort/burrow.lua index 2091e831c1..bfb7e0958b 100644 --- a/internal/quickfort/burrow.lua +++ b/internal/quickfort/burrow.lua @@ -24,7 +24,7 @@ local function custom_burrow(_, keys) local token_and_label, props_start_pos = quickfort_parse.parse_token_and_label(keys, 1, '%w') if not token_and_label or not rawget(burrow_db, token_and_label.token) then return nil end local db_entry = copyall(burrow_db[token_and_label.token]) - local props, next_token_pos = quickfort_parse.parse_properties(keys, props_start_pos) + local props = quickfort_parse.parse_properties(keys, props_start_pos) if props.name then db_entry.name = props.name props.name = nil @@ -37,6 +37,14 @@ local function custom_burrow(_, keys) db_entry.civalert = true props.civalert = nil end + if db_entry.add and props.autochop_clear == 'true' then + db_entry.autochop_clear = true + props.autochop_clear = nil + end + if db_entry.add and props.autochop_chop == 'true' then + db_entry.autochop_chop = true + props.autochop_chop = nil + end for k,v in pairs(props) do dfhack.printerr(('unhandled property for symbol "%s": "%s"="%s"'):format( @@ -50,32 +58,38 @@ setmetatable(burrow_db, {__index=custom_burrow}) local burrows = df.global.plotinfo.burrows +local function create_burrow(name) + local b = df.burrow:new() + b.id = burrows.next_id + burrows.next_id = burrows.next_id + 1 + if name then + b.name = name + end + b.symbol_index = math.random(0, 22) + b.texture_r = math.random(0, 255) + b.texture_g = math.random(0, 255) + b.texture_b = math.random(0, 255) + b.texture_br = 255 - b.texture_r + b.texture_bg = 255 - b.texture_g + b.texture_bb = 255 - b.texture_b + burrows.list:insert('#', b) + return b +end + local function do_burrow(ctx, db_entry, pos) local stats = ctx.stats local b if db_entry.name then b = dfhack.burrows.findByName(db_entry.name, true) end - if not b and db_entry.add and db_entry.create then - b = df.burrow:new() - b.id = burrows.next_id - burrows.next_id = burrows.next_id + 1 - if db_entry.name then - b.name = db_entry.name - end - b.symbol_index = math.random(0, 22) - b.texture_r = math.random(0, 255) - b.texture_g = math.random(0, 255) - b.texture_b = math.random(0, 255) - b.texture_br = 255 - b.texture_r - b.texture_bg = 255 - b.texture_g - b.texture_bb = 255 - b.texture_b - burrows.list:insert('#', b) - stats.burrow_created.value = stats.burrow_created.value + 1 - end if not b and db_entry.add then - log('could not find burrow to add to') - return + if db_entry.create then + b = create_burrow(db_entry.name) + stats.burrow_created.value = stats.burrow_created.value + 1 + else + log('could not find burrow to add to') + return + end end if b then dfhack.burrows.setAssignedTile(b, pos, db_entry.add) @@ -88,6 +102,14 @@ local function do_burrow(ctx, db_entry, pos) civalert.unset_civalert_burrow_if_set(b) end end + if db_entry.autochop_clear or db_entry.autochop_chop then + if db_entry.autochop_chop then + dfhack.run_command('autochop', (db_entry.add and '' or 'no')..'chop', tostring(b.id)) + end + if db_entry.autochop_clear then + dfhack.run_command('autochop', (db_entry.add and '' or 'no')..'clear', tostring(b.id)) + end + end if not db_entry.add and db_entry.create and #dfhack.burrows.listBlocks(b) == 0 then dfhack.burrows.clearTiles(b) local _, _, idx = utils.binsearch(burrows.list, b.id, 'id') @@ -98,8 +120,8 @@ local function do_burrow(ctx, db_entry, pos) end end elseif not db_entry.add then - for _,b in ipairs(burrows.list) do - dfhack.burrows.setAssignedTile(b, pos, false) + for _,burrow in ipairs(burrows.list) do + dfhack.burrows.setAssignedTile(burrow, pos, false) end stats.burrow_tiles_removed.value = stats.burrow_tiles_removed.value + 1 end From c284e6e73d7f9dc6f0dd85bd71337385c022734d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Vuchener?= Date: Sun, 5 Nov 2023 10:39:02 +0100 Subject: [PATCH 653/732] export-dt-ini: add new equipement update addresses --- devel/export-dt-ini.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index 1b5913ca20..49d46c4bd7 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -130,6 +130,7 @@ address('world_site_type',df.world_site,'type') address('active_sites_vector',df.world_data,'active_site') address('gview',globals,'gview') address('external_flag',globals,'game','external_flag') +address('global_equipment_update',globals,'plotinfo','equipment','update') vtable('viewscreen_setupdwarfgame_vtable','viewscreen_setupdwarfgamest') header('offsets') @@ -449,6 +450,7 @@ address('shield_vector',df.squad_position,'uniform','shield') address('weapon_vector',df.squad_position,'uniform','weapon') address('uniform_item_filter',df.squad_uniform_spec,'item_filter') address('uniform_indiv_choice',df.squad_uniform_spec,'indiv_choice') +address('equipment_update',df.squad,'ammo','update') header('activity_offsets') address('activity_type',df.activity_entry,'type') From 7d184a9ca2443ff5685c9e0a7b36604357157273 Mon Sep 17 00:00:00 2001 From: Robob27 Date: Sun, 5 Nov 2023 20:19:33 -0500 Subject: [PATCH 654/732] Add trackstop overlay --- changelog.txt | 1 + docs/trackstop.rst | 9 +++ trackstop.lua | 160 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 docs/trackstop.rst create mode 100644 trackstop.lua diff --git a/changelog.txt b/changelog.txt index c40e9e4ce5..79a8bcefca 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ Template for new versions: ## New Tools - `sync-windmills`: synchronize or randomize movement of active windmills +- `trackstop`: new overlay to allow changing track stop dump direction and friction after construction ## New Features - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging diff --git a/docs/trackstop.rst b/docs/trackstop.rst new file mode 100644 index 0000000000..cabaabb8f7 --- /dev/null +++ b/docs/trackstop.rst @@ -0,0 +1,9 @@ +trackstop +========= + +.. dfhack-tool:: + :summary: Overlay to allow changing track stop friction and dump direction after construction + :tags: fort gameplay buildings interface + +This script provides an overlay that is managed by the `overlay` framework. +The overlay allows the player to change the friction and dump direction of a selected track stop after it has been constructed. diff --git a/trackstop.lua b/trackstop.lua new file mode 100644 index 0000000000..b7b5ed0b1b --- /dev/null +++ b/trackstop.lua @@ -0,0 +1,160 @@ +-- Overlay to allow changing track stop friction and dump direction after construction +--@ module = true +local gui = require('gui') +local widgets = require('gui.widgets') +local overlay = require('plugins.overlay') + +local NORTH = 'North' +local EAST = 'East' +local SOUTH = 'South' +local WEST = 'West' + +local LOW = 'Low' +local MEDIUM = 'Medium' +local HIGH = 'High' +local MAX = 'Max' + +local NONE = 'None' + +local FRICTION_MAP = { + [NONE] = 10, + [LOW] = 50, + [MEDIUM] = 500, + [HIGH] = 10000, + [MAX] = 50000, +} + +local FRICTION_MAP_REVERSE = {} +for k, v in pairs(FRICTION_MAP) do + FRICTION_MAP_REVERSE[v] = k +end + +TrackStopOverlay = defclass(TrackStopOverlay, overlay.OverlayWidget) +TrackStopOverlay.ATTRS{ + default_pos={x=-71, y=29}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING/Trap', + frame={w=27, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function TrackStopOverlay:getFriction() + return dfhack.gui.getSelectedBuilding().friction +end + +function TrackStopOverlay:setFriction(friction) + local building = dfhack.gui.getSelectedBuilding() + + building.friction = FRICTION_MAP[friction] +end + +function TrackStopOverlay:getDumpDirection() + local building = dfhack.gui.getSelectedBuilding() + local use_dump = building.use_dump + local dump_x_shift = building.dump_x_shift + local dump_y_shift = building.dump_y_shift + + if use_dump == 0 then + return NONE + else + if dump_x_shift == 0 and dump_y_shift == -1 then + return NORTH + elseif dump_x_shift == 1 and dump_y_shift == 0 then + return EAST + elseif dump_x_shift == 0 and dump_y_shift == 1 then + return SOUTH + elseif dump_x_shift == -1 and dump_y_shift == 0 then + return WEST + end + end +end + +function TrackStopOverlay:setDumpDirection(direction) + local building = dfhack.gui.getSelectedBuilding() + + if direction == NONE then + building.use_dump = 0 + building.dump_x_shift = 0 + building.dump_y_shift = 0 + elseif direction == NORTH then + building.use_dump = 1 + building.dump_x_shift = 0 + building.dump_y_shift = -1 + elseif direction == EAST then + building.use_dump = 1 + building.dump_x_shift = 1 + building.dump_y_shift = 0 + elseif direction == SOUTH then + building.use_dump = 1 + building.dump_x_shift = 0 + building.dump_y_shift = 1 + elseif direction == WEST then + building.use_dump = 1 + building.dump_x_shift = -1 + building.dump_y_shift = 0 + end +end + +function TrackStopOverlay:render(dc) + if not self:shouldRender() then + return + end + + local building = dfhack.gui.getSelectedBuilding() + local friction = building.friction + local friction_cycle = self.subviews.friction + + friction_cycle:setOption(FRICTION_MAP_REVERSE[friction]) + + self.subviews.dump_direction:setOption(self:getDumpDirection()) + + TrackStopOverlay.super.render(self, dc) +end + +function TrackStopOverlay:shouldRender() + local building = dfhack.gui.getSelectedBuilding() + return building and building.trap_type == df.trap_type.TrackStop +end + +function TrackStopOverlay:onInput(keys) + if not self:shouldRender() then + return + end + TrackStopOverlay.super.onInput(self, keys) +end + +function TrackStopOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Dump', + }, + widgets.CycleHotkeyLabel{ + frame={t=0, l=9}, + key='CUSTOM_CTRL_X', + options={NONE, NORTH, EAST, SOUTH, WEST}, + view_id='dump_direction', + on_change=function(val) self:setDumpDirection(val) end, + }, + widgets.Label{ + frame={t=1, l=0}, + text='Friction', + }, + widgets.CycleHotkeyLabel{ + frame={t=1, l=9}, + key='CUSTOM_CTRL_F', + options={NONE, LOW, MEDIUM, HIGH, MAX}, + view_id='friction', + on_change=function(val) self:setFriction(val) end, + }, + } +end + +OVERLAY_WIDGETS = { + trackstop=TrackStopOverlay +} + +if not dfhack_flags.module then + main{...} +end From c7dd669a56416eec94a47b8c09ba7a6a89992330 Mon Sep 17 00:00:00 2001 From: Robob27 Date: Sun, 5 Nov 2023 22:04:04 -0500 Subject: [PATCH 655/732] Add rollers overlay --- changelog.txt | 2 +- docs/trackstop.rst | 7 ++-- trackstop.lua | 101 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 79a8bcefca..f7e41acd3b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,7 +28,7 @@ Template for new versions: ## New Tools - `sync-windmills`: synchronize or randomize movement of active windmills -- `trackstop`: new overlay to allow changing track stop dump direction and friction after construction +- `trackstop`: new overlay to allow changing track stop dump direction and friction and roller direction and speed after construction ## New Features - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging diff --git a/docs/trackstop.rst b/docs/trackstop.rst index cabaabb8f7..d91bb01932 100644 --- a/docs/trackstop.rst +++ b/docs/trackstop.rst @@ -2,8 +2,9 @@ trackstop ========= .. dfhack-tool:: - :summary: Overlay to allow changing track stop friction and dump direction after construction + :summary: Overlay to allow changing track stop friction and dump direction and roller direction and speed after construction. :tags: fort gameplay buildings interface -This script provides an overlay that is managed by the `overlay` framework. -The overlay allows the player to change the friction and dump direction of a selected track stop after it has been constructed. +This script provides 2 overlays that are managed by the `overlay` framework. +The trackstop overlay allows the player to change the friction and dump direction of a selected track stop after it has been constructed. +The rollers overlay allows the player to change the roller direction and speed of a selected track stop after it has been constructed. diff --git a/trackstop.lua b/trackstop.lua index b7b5ed0b1b..03a2252857 100644 --- a/trackstop.lua +++ b/trackstop.lua @@ -12,6 +12,7 @@ local WEST = 'West' local LOW = 'Low' local MEDIUM = 'Medium' local HIGH = 'High' +local HIGHER = 'Higher' local MAX = 'Max' local NONE = 'None' @@ -29,6 +30,31 @@ for k, v in pairs(FRICTION_MAP) do FRICTION_MAP_REVERSE[v] = k end +local SPEED_MAP = { + [LOW] = 10000, + [MEDIUM] = 20000, + [HIGH] = 30000, + [HIGHER] = 40000, + [MAX] = 50000, +} + +local SPEED_MAP_REVERSE = {} +for k, v in pairs(SPEED_MAP) do + SPEED_MAP_REVERSE[v] = k +end + +local DIRECTION_MAP = { + [NORTH] = df.screw_pump_direction.FromSouth, + [EAST] = df.screw_pump_direction.FromWest, + [SOUTH] = df.screw_pump_direction.FromNorth, + [WEST] = df.screw_pump_direction.FromEast, +} + +local DIRECTION_MAP_REVERSE = {} +for k, v in pairs(DIRECTION_MAP) do + DIRECTION_MAP_REVERSE[v] = k +end + TrackStopOverlay = defclass(TrackStopOverlay, overlay.OverlayWidget) TrackStopOverlay.ATTRS{ default_pos={x=-71, y=29}, @@ -151,8 +177,81 @@ function TrackStopOverlay:init() } end +RollerOverlay = defclass(RollerOverlay, overlay.OverlayWidget) +RollerOverlay.ATTRS{ + default_pos={x=-71, y=29}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING/Rollers', + frame={w=27, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function RollerOverlay:getDirection() + local building = dfhack.gui.getSelectedBuilding() + local direction = building.direction + + return DIRECTION_MAP_REVERSE[direction] +end + +function RollerOverlay:setDirection(direction) + local building = dfhack.gui.getSelectedBuilding() + + building.direction = DIRECTION_MAP[direction] +end + +function RollerOverlay:getSpeed() + local building = dfhack.gui.getSelectedBuilding() + local speed = building.speed + + return SPEED_MAP_REVERSE[speed] +end + +function RollerOverlay:setSpeed(speed) + local building = dfhack.gui.getSelectedBuilding() + + building.speed = SPEED_MAP[speed] +end + +function RollerOverlay:render(dc) + local building = dfhack.gui.getSelectedBuilding() + + self.subviews.direction:setOption(DIRECTION_MAP_REVERSE[building.direction]) + self.subviews.speed:setOption(SPEED_MAP_REVERSE[building.speed]) + + TrackStopOverlay.super.render(self, dc) +end + +function RollerOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Direction', + }, + widgets.CycleHotkeyLabel{ + frame={t=0, l=10}, + key='CUSTOM_CTRL_X', + options={NORTH, EAST, SOUTH, WEST}, + view_id='direction', + on_change=function(val) self:setDirection(val) end, + }, + widgets.Label{ + frame={t=1, l=0}, + text='Speed', + }, + widgets.CycleHotkeyLabel{ + frame={t=1, l=10}, + key='CUSTOM_CTRL_F', + options={LOW, MEDIUM, HIGH, HIGHER, MAX}, + view_id='speed', + on_change=function(val) self:setSpeed(val) end, + }, + } +end + OVERLAY_WIDGETS = { - trackstop=TrackStopOverlay + trackstop=TrackStopOverlay, + rollers=RollerOverlay, } if not dfhack_flags.module then From b9c1db08dc1f05f9034968bc5c50b7c733a36131 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 6 Nov 2023 06:03:23 -0800 Subject: [PATCH 656/732] show tooltip even when mouse is outside of map --- gui/design.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 7c27aaf1fd..0005263944 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1795,14 +1795,18 @@ DimensionsOverlay.ATTRS{ local selection_rect = df.global.selection_rect local function is_choosing_area() - return selection_rect.start_x >= 0 and dfhack.gui.getMousePos() + return selection_rect.start_z >= 0 and dfhack.gui.getMousePos(true) end local function get_cur_area_dims() - local pos1 = dfhack.gui.getMousePos() - if not pos1 or selection_rect.start_x < 0 then return 1, 1, 1 end + local pos1 = dfhack.gui.getMousePos(true) + if not pos1 or selection_rect.start_z < 0 then return 1, 1, 1 end -- clamp to map edges (since you can start selection out of bounds) + pos1 = xyz2pos( + math.max(0, math.min(df.global.world.map.x_count-1, pos1.x)), + math.max(0, math.min(df.global.world.map.y_count-1, pos1.y)), + math.max(0, math.min(df.global.world.map.z_count-1, pos1.z))) local pos2 = xyz2pos( math.max(0, math.min(df.global.world.map.x_count-1, selection_rect.start_x)), math.max(0, math.min(df.global.world.map.y_count-1, selection_rect.start_y)), From 49d0ef505ae8268116b127e732a87a50336218c6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 19:28:11 +0000 Subject: [PATCH 657/732] [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: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) - [github.com/python-jsonschema/check-jsonschema: 0.27.0 → 0.27.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.0...0.27.1) - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.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 ac959adc36..e3dd5636d3 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: v4.4.0 + rev: v4.5.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.27.0 + rev: 0.27.1 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: v4.4.0 + rev: v4.5.0 hooks: - id: forbid-new-submodules From eba5ea9dcf4727192e4c34def7a521b999c60e8c Mon Sep 17 00:00:00 2001 From: Robob27 Date: Mon, 6 Nov 2023 21:15:32 -0500 Subject: [PATCH 658/732] Feedback --- docs/trackstop.rst | 8 ++--- trackstop.lua | 89 +++++++++++++++++++++++----------------------- 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/docs/trackstop.rst b/docs/trackstop.rst index d91bb01932..88579b783f 100644 --- a/docs/trackstop.rst +++ b/docs/trackstop.rst @@ -2,9 +2,9 @@ trackstop ========= .. dfhack-tool:: - :summary: Overlay to allow changing track stop friction and dump direction and roller direction and speed after construction. - :tags: fort gameplay buildings interface + :summary: Add dynamic configuration options for track stops. + :tags: fort buildings interface -This script provides 2 overlays that are managed by the `overlay` framework. +This script provides 2 overlays that are managed by the `overlay` framework. The script does nothing when executed. The trackstop overlay allows the player to change the friction and dump direction of a selected track stop after it has been constructed. -The rollers overlay allows the player to change the roller direction and speed of a selected track stop after it has been constructed. +The rollers overlay allows the player to change the roller direction and speed of a selected roller after it has been constructed. diff --git a/trackstop.lua b/trackstop.lua index 03a2252857..4a68b3c35f 100644 --- a/trackstop.lua +++ b/trackstop.lua @@ -1,13 +1,19 @@ -- Overlay to allow changing track stop friction and dump direction after construction --@ module = true + +if not dfhack_flags.module then + qerror('trackstop cannot be called directly') +end + local gui = require('gui') local widgets = require('gui.widgets') local overlay = require('plugins.overlay') +local utils = require('utils') -local NORTH = 'North' -local EAST = 'East' -local SOUTH = 'South' -local WEST = 'West' +local NORTH = 'North ^' +local EAST = 'East >' +local SOUTH = 'South v' +local WEST = 'West <' local LOW = 'Low' local MEDIUM = 'Medium' @@ -25,10 +31,7 @@ local FRICTION_MAP = { [MAX] = 50000, } -local FRICTION_MAP_REVERSE = {} -for k, v in pairs(FRICTION_MAP) do - FRICTION_MAP_REVERSE[v] = k -end +local FRICTION_MAP_REVERSE = utils.invert(FRICTION_MAP) local SPEED_MAP = { [LOW] = 10000, @@ -38,10 +41,7 @@ local SPEED_MAP = { [MAX] = 50000, } -local SPEED_MAP_REVERSE = {} -for k, v in pairs(SPEED_MAP) do - SPEED_MAP_REVERSE[v] = k -end +local SPEED_MAP_REVERSE = utils.invert(SPEED_MAP) local DIRECTION_MAP = { [NORTH] = df.screw_pump_direction.FromSouth, @@ -50,17 +50,14 @@ local DIRECTION_MAP = { [WEST] = df.screw_pump_direction.FromEast, } -local DIRECTION_MAP_REVERSE = {} -for k, v in pairs(DIRECTION_MAP) do - DIRECTION_MAP_REVERSE[v] = k -end +local DIRECTION_MAP_REVERSE = utils.invert(DIRECTION_MAP) TrackStopOverlay = defclass(TrackStopOverlay, overlay.OverlayWidget) TrackStopOverlay.ATTRS{ - default_pos={x=-71, y=29}, + default_pos={x=-73, y=29}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING/Trap', - frame={w=27, h=4}, + frame={w=25, h=4}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -152,25 +149,31 @@ end function TrackStopOverlay:init() self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text='Dump', - }, widgets.CycleHotkeyLabel{ - frame={t=0, l=9}, + frame={t=0, l=0}, + label='Dump', key='CUSTOM_CTRL_X', - options={NONE, NORTH, EAST, SOUTH, WEST}, + options={ + {label=NONE, value=NONE, pen=COLOR_BLUE}, + NORTH, + EAST, + SOUTH, + WEST, + }, view_id='dump_direction', on_change=function(val) self:setDumpDirection(val) end, }, - widgets.Label{ - frame={t=1, l=0}, - text='Friction', - }, widgets.CycleHotkeyLabel{ - frame={t=1, l=9}, + label='Friction', + frame={t=1, l=0}, key='CUSTOM_CTRL_F', - options={NONE, LOW, MEDIUM, HIGH, MAX}, + options={ + {label=NONE, value=NONE, pen=COLOR_BLUE}, + {label=LOW, value=LOW, pen=COLOR_GREEN}, + {label=MEDIUM, value=MEDIUM, pen=COLOR_YELLOW}, + {label=HIGH, value=HIGH, pen=COLOR_LIGHTRED}, + {label=MAX, value=MAX, pen=COLOR_RED}, + }, view_id='friction', on_change=function(val) self:setFriction(val) end, }, @@ -224,25 +227,25 @@ end function RollerOverlay:init() self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text='Direction', - }, widgets.CycleHotkeyLabel{ - frame={t=0, l=10}, + label='Direction', + frame={t=0, l=0}, key='CUSTOM_CTRL_X', options={NORTH, EAST, SOUTH, WEST}, view_id='direction', on_change=function(val) self:setDirection(val) end, }, - widgets.Label{ - frame={t=1, l=0}, - text='Speed', - }, widgets.CycleHotkeyLabel{ - frame={t=1, l=10}, + label='Speed', + frame={t=1, l=0}, key='CUSTOM_CTRL_F', - options={LOW, MEDIUM, HIGH, HIGHER, MAX}, + options={ + {label=LOW, value=LOW, pen=COLOR_BLUE}, + {label=MEDIUM, value=MEDIUM, pen=COLOR_GREEN}, + {label=HIGH, value=HIGH, pen=COLOR_YELLOW}, + {label=HIGHER, value=HIGHER, pen=COLOR_LIGHTRED}, + {label=MAX, value=MAX, pen=COLOR_RED}, + }, view_id='speed', on_change=function(val) self:setSpeed(val) end, }, @@ -253,7 +256,3 @@ OVERLAY_WIDGETS = { trackstop=TrackStopOverlay, rollers=RollerOverlay, } - -if not dfhack_flags.module then - main{...} -end From ac3584b8e1a8b18a7c8727badd7d9a9093495de3 Mon Sep 17 00:00:00 2001 From: Robob27 Date: Mon, 6 Nov 2023 23:16:42 -0500 Subject: [PATCH 659/732] Remove shouldRender, use better focus string --- trackstop.lua | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/trackstop.lua b/trackstop.lua index 4a68b3c35f..6657209b08 100644 --- a/trackstop.lua +++ b/trackstop.lua @@ -56,7 +56,7 @@ TrackStopOverlay = defclass(TrackStopOverlay, overlay.OverlayWidget) TrackStopOverlay.ATTRS{ default_pos={x=-73, y=29}, default_enabled=true, - viewscreens='dwarfmode/ViewSheets/BUILDING/Trap', + viewscreens='dwarfmode/ViewSheets/BUILDING/Trap/TrackStop', frame={w=25, h=4}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, @@ -120,10 +120,6 @@ function TrackStopOverlay:setDumpDirection(direction) end function TrackStopOverlay:render(dc) - if not self:shouldRender() then - return - end - local building = dfhack.gui.getSelectedBuilding() local friction = building.friction local friction_cycle = self.subviews.friction @@ -135,18 +131,6 @@ function TrackStopOverlay:render(dc) TrackStopOverlay.super.render(self, dc) end -function TrackStopOverlay:shouldRender() - local building = dfhack.gui.getSelectedBuilding() - return building and building.trap_type == df.trap_type.TrackStop -end - -function TrackStopOverlay:onInput(keys) - if not self:shouldRender() then - return - end - TrackStopOverlay.super.onInput(self, keys) -end - function TrackStopOverlay:init() self:addviews{ widgets.CycleHotkeyLabel{ From b9caabf019823909fdbeb0dcf8c89f568140fd3d Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Thu, 9 Nov 2023 19:59:21 -0700 Subject: [PATCH 660/732] unforbid: ignore tattered items by default also adds option to disable ignoring tattered items. --- unforbid.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/unforbid.lua b/unforbid.lua index 38667602c1..e59edcfa28 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -5,7 +5,7 @@ local argparse = require('argparse') local function unforbid_all(include_unreachable, quiet) if not quiet then print('Unforbidding all items...') end - local citizens = dfhack.units.getCitizens(true) + local citizens = dfhack.units.getCitizens() local count = 0 for _, item in pairs(df.global.world.items.all) do if item.flags.forbid then @@ -22,6 +22,11 @@ local function unforbid_all(include_unreachable, quiet) if not quiet then print((' unreachable: %s (skipping)'):format(item)) end goto skipitem end + + if ((not options.include_tattered) and item.wear >= 3) then + if not quiet then print((' tattered: %s (skipping)'):format(item)) end + goto skipitem + end end if not quiet then print((' unforbid: %s'):format(item)) end @@ -40,11 +45,13 @@ local options, args = { help = false, quiet = false, include_unreachable = false, -}, { ... } + include_tattered = false +}, {...} local positionals = argparse.processArgsGetopt(args, { { 'h', 'help', handler = function() options.help = true end }, { 'q', 'quiet', handler = function() options.quiet = true end }, + { 'X', 'include-tattered', handler = function() options.include_tattered = true end}, { 'u', 'include-unreachable', handler = function() options.include_unreachable = true end }, }) @@ -54,5 +61,5 @@ if positionals[1] == nil or positionals[1] == 'help' or options.help then end if positionals[1] == 'all' then - unforbid_all(options.include_unreachable, options.quiet) + unforbid_all(options.include_unreachable, options.include_tattered, options.quiet) end From b97e1b1e674d0c14b1ed107a019bcb9e63282eae Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:04:10 -0700 Subject: [PATCH 661/732] oops, don't forget the argument --- unforbid.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unforbid.lua b/unforbid.lua index e59edcfa28..a57fefbc24 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -2,7 +2,7 @@ local argparse = require('argparse') -local function unforbid_all(include_unreachable, quiet) +local function unforbid_all(include_unreachable, include_tattered, quiet) if not quiet then print('Unforbidding all items...') end local citizens = dfhack.units.getCitizens() From fb3ba652ff430620580c8f47fb3b069dddbdb6a3 Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:02:57 -0700 Subject: [PATCH 662/732] change wear threshold to 2. --- unforbid.lua | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/unforbid.lua b/unforbid.lua index a57fefbc24..9d775107c9 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -5,7 +5,7 @@ local argparse = require('argparse') local function unforbid_all(include_unreachable, include_tattered, quiet) if not quiet then print('Unforbidding all items...') end - local citizens = dfhack.units.getCitizens() + local citizens = dfhack.units.getCitizens(true) local count = 0 for _, item in pairs(df.global.world.items.all) do if item.flags.forbid then @@ -23,8 +23,8 @@ local function unforbid_all(include_unreachable, include_tattered, quiet) goto skipitem end - if ((not options.include_tattered) and item.wear >= 3) then - if not quiet then print((' tattered: %s (skipping)'):format(item)) end + if ((not options.include_worn) and item.wear >= 2) then + if not quiet then print((' worn: %s (skipping)'):format(item)) end goto skipitem end end @@ -45,13 +45,14 @@ local options, args = { help = false, quiet = false, include_unreachable = false, - include_tattered = false + include_worn = false }, {...} local positionals = argparse.processArgsGetopt(args, { { 'h', 'help', handler = function() options.help = true end }, { 'q', 'quiet', handler = function() options.quiet = true end }, - { 'X', 'include-tattered', handler = function() options.include_tattered = true end}, + { 'X', 'include-tattered', handler = function() options.include_worn + = true end}, { 'u', 'include-unreachable', handler = function() options.include_unreachable = true end }, }) @@ -61,5 +62,5 @@ if positionals[1] == nil or positionals[1] == 'help' or options.help then end if positionals[1] == 'all' then - unforbid_all(options.include_unreachable, options.include_tattered, options.quiet) + unforbid_all(options.include_unreachable, options.include_worn, options.quiet) end From 56d17555ea067bc38c562c9d00b8aa831ae7c01b Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:03:22 -0700 Subject: [PATCH 663/732] typo fix --- unforbid.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/unforbid.lua b/unforbid.lua index 9d775107c9..24f90d9bd4 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -51,8 +51,7 @@ local options, args = { local positionals = argparse.processArgsGetopt(args, { { 'h', 'help', handler = function() options.help = true end }, { 'q', 'quiet', handler = function() options.quiet = true end }, - { 'X', 'include-tattered', handler = function() options.include_worn - = true end}, + { 'X', 'include-tattered', handler = function() options.include_worn = true end}, { 'u', 'include-unreachable', handler = function() options.include_unreachable = true end }, }) From b950abef765e83a5b34a6147cda8848e0a486abe Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:25:11 -0700 Subject: [PATCH 664/732] remove repeated if quiet then reorganize main function parameters for backwards compat --- unforbid.lua | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/unforbid.lua b/unforbid.lua index 24f90d9bd4..1177e3148f 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -2,8 +2,11 @@ local argparse = require('argparse') -local function unforbid_all(include_unreachable, include_tattered, quiet) - if not quiet then print('Unforbidding all items...') end +local function unforbid_all(include_unreachable, quiet, include_worn) + local p + if quiet then p=function(s) return end; else p=function(s) return print(s) end; end + + p('Unforbidding all items...') local citizens = dfhack.units.getCitizens(true) local count = 0 @@ -19,17 +22,17 @@ local function unforbid_all(include_unreachable, include_tattered, quiet) end if not reachable then - if not quiet then print((' unreachable: %s (skipping)'):format(item)) end + p((' unreachable: %s (skipping)'):format(item)) goto skipitem end + end - if ((not options.include_worn) and item.wear >= 2) then - if not quiet then print((' worn: %s (skipping)'):format(item)) end - goto skipitem - end + if ((not include_worn) and item.wear >= 2) then + p((' worn: %s (skipping)'):format(item)) + goto skipitem end - if not quiet then print((' unforbid: %s'):format(item)) end + p((' unforbid: %s'):format(item)) item.flags.forbid = false count = count + 1 @@ -37,7 +40,7 @@ local function unforbid_all(include_unreachable, include_tattered, quiet) end end - if not quiet then print(('%d items unforbidden'):format(count)) end + p(('%d items unforbidden'):format(count)) end end -- let the common --help parameter work, even though it's undocumented @@ -61,5 +64,5 @@ if positionals[1] == nil or positionals[1] == 'help' or options.help then end if positionals[1] == 'all' then - unforbid_all(options.include_unreachable, options.include_worn, options.quiet) + unforbid_all(options.include_unreachable, options.quiet, options.include_worn) end From 84fe195a41f5960a7d6c5811d96893dc38cff37f Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:33:47 -0700 Subject: [PATCH 665/732] fixed syntax error --- unforbid.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/unforbid.lua b/unforbid.lua index 1177e3148f..6089dd4549 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -4,8 +4,11 @@ local argparse = require('argparse') local function unforbid_all(include_unreachable, quiet, include_worn) local p - if quiet then p=function(s) return end; else p=function(s) return print(s) end; end - + if quiet then + p=function(s) return end; + else + p=function(s) return print(s) end; + end p('Unforbidding all items...') local citizens = dfhack.units.getCitizens(true) @@ -40,7 +43,7 @@ local function unforbid_all(include_unreachable, quiet, include_worn) end end - p(('%d items unforbidden'):format(count)) end + p(('%d items unforbidden'):format(count)) end -- let the common --help parameter work, even though it's undocumented From c24a0cbc966fadac1e722fbfb355350ea4f98126 Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:38:40 -0700 Subject: [PATCH 666/732] add docs --- docs/unforbid.rst | 3 +++ unforbid.lua | 8 ++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/unforbid.rst b/docs/unforbid.rst index 4160b7b25e..bedaeb4b7a 100644 --- a/docs/unforbid.rst +++ b/docs/unforbid.rst @@ -24,3 +24,6 @@ Options ``-q``, ``--quiet`` Suppress non-error console output. + +``-x``, ``-include-worn`` + Include worn (X) and tattered (XX) items when unforbidding. diff --git a/unforbid.lua b/unforbid.lua index 6089dd4549..12b7430c36 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -4,11 +4,7 @@ local argparse = require('argparse') local function unforbid_all(include_unreachable, quiet, include_worn) local p - if quiet then - p=function(s) return end; - else - p=function(s) return print(s) end; - end + if quiet then p=function(s) return end; else p=function(s) return print(s) end; end p('Unforbidding all items...') local citizens = dfhack.units.getCitizens(true) @@ -57,7 +53,7 @@ local options, args = { local positionals = argparse.processArgsGetopt(args, { { 'h', 'help', handler = function() options.help = true end }, { 'q', 'quiet', handler = function() options.quiet = true end }, - { 'X', 'include-tattered', handler = function() options.include_worn = true end}, + { 'X', 'include-worn', handler = function() options.include_worn = true end}, { 'u', 'include-unreachable', handler = function() options.include_unreachable = true end }, }) From 761ec41b12f4c5dfe683a167cabad17e5a80be5a Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:41:55 -0700 Subject: [PATCH 667/732] add changelog entry --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index f7e41acd3b..5acf82d8ae 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## New Features - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging - `quickfort`: new blueprint mode for designating burrows +- `unforbid`: now ignores worn and tattered items by default (X/XX), use -x to bypass ## Fixes - `gui/unit-syndromes`: show the syndrome names properly in the UI From 0affdac32a1b06a543e2e5a4780306b200c47b7d Mon Sep 17 00:00:00 2001 From: Andriel Chaoti <3628387+AndrielChaoti@users.noreply.github.com> Date: Sat, 11 Nov 2023 22:33:31 -0700 Subject: [PATCH 668/732] Fix typos in docs --- docs/unforbid.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/unforbid.rst b/docs/unforbid.rst index bedaeb4b7a..03615492ad 100644 --- a/docs/unforbid.rst +++ b/docs/unforbid.rst @@ -25,5 +25,5 @@ Options ``-q``, ``--quiet`` Suppress non-error console output. -``-x``, ``-include-worn`` +``-X``, ``--include-worn`` Include worn (X) and tattered (XX) items when unforbidding. From 4b95139518259635b953246e16e5e00b1e0636b2 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 11 Nov 2023 22:21:22 -0800 Subject: [PATCH 669/732] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 5acf82d8ae..6b64a26fc9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,7 +33,7 @@ Template for new versions: ## New Features - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging - `quickfort`: new blueprint mode for designating burrows -- `unforbid`: now ignores worn and tattered items by default (X/XX), use -x to bypass +- `unforbid`: now ignores worn and tattered items by default (X/XX), use -X to bypass ## Fixes - `gui/unit-syndromes`: show the syndrome names properly in the UI From 722a0835e822cfea8fb9fd7dbe1e4b8c23d43cd5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Nov 2023 01:38:15 -0800 Subject: [PATCH 670/732] fix structure path to work details --- changelog.txt | 1 + emigration.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 6b64a26fc9..623aa150f0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: ## Fixes - `gui/unit-syndromes`: show the syndrome names properly in the UI +- `emigration`: fix clearing of work details assigned to units that leave the fort ## Misc Improvements - `warn-stranded`: don't warn for units that are temporarily on unwalkable tiles (e.g. as they pass under a waterfall) diff --git a/emigration.lua b/emigration.lua index 742e1a147e..22dc4cee39 100644 --- a/emigration.lua +++ b/emigration.lua @@ -75,7 +75,7 @@ function desert(u,method,civ) end -- disassociate from work details - for _, detail in ipairs(df.global.plotinfo.hauling.work_details) do + for _, detail in ipairs(df.global.plotinfo.labor_info.work_details) do for k, v in ipairs(detail.assigned_units) do if v == u.id then detail.assigned_units:erase(k) From 02d625ffe2e012198181bc5290cb8786333e9824 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Nov 2023 16:59:08 -0800 Subject: [PATCH 671/732] remove some tools from the lists --- changelog.txt | 2 ++ gui/control-panel.lua | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 623aa150f0..4e8970c180 100644 --- a/changelog.txt +++ b/changelog.txt @@ -43,6 +43,8 @@ Template for new versions: - `warn-stranded`: don't warn for units that are temporarily on unwalkable tiles (e.g. as they pass under a waterfall) ## Removed +- `gui/control-panel`: removed always-on system services from the ``System`` tab: `buildingplan`, `confirm`, `logistics`, and `overlay`. The base services should not be turned off by the player. Individual confirmation prompts can be managed via `gui/confirm`, and overlays (including those for `buildingplan` and `logistics`) are managed on the control panel ``Overlays`` tab. +- `gui/control-panel`: removed `autolabor` from the ``Fort`` and ``Autostart`` tabs. The tool does not function correctly with the new labor types, and is causing confusion. You can still enable `autolabor` from the commandline with ``enable autolabor`` if you understand and accept its limitations. # 50.11-r2 diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 38de219e1b..9836dd79c0 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -20,7 +20,6 @@ local FORT_SERVICES = { 'autoclothing', 'autofarm', 'autofish', - 'autolabor', 'autonestbox', 'autoslab', 'dwarfvet', @@ -57,10 +56,6 @@ table.sort(FORT_AUTOSTART) -- these are re-enabled by the default DFHack init scripts local SYSTEM_SERVICES = { - 'buildingplan', - 'confirm', - 'logistics', - 'overlay', } -- these are fully controlled by the user local SYSTEM_USER_SERVICES = { From d9bca7a3828e058be5c73e6c9bef05752b1ea5d3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Nov 2023 17:43:36 -0800 Subject: [PATCH 672/732] add option for scrubbing dead units from burrows --- changelog.txt | 1 + docs/fix/dead-units.rst | 19 ++++++++++++++- fix/dead-units.lua | 53 +++++++++++++++++++++++++++++------------ gui/control-panel.lua | 15 +++++++----- 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4e8970c180..fb31369137 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging - `quickfort`: new blueprint mode for designating burrows - `unforbid`: now ignores worn and tattered items by default (X/XX), use -X to bypass +- `fix/dead-units`: gained ability to scrub dead units from burrow membership lists ## Fixes - `gui/unit-syndromes`: show the syndrome names properly in the UI diff --git a/docs/fix/dead-units.rst b/docs/fix/dead-units.rst index 04e5e53ed2..b6d5f33cd6 100644 --- a/docs/fix/dead-units.rst +++ b/docs/fix/dead-units.rst @@ -10,9 +10,26 @@ If so many units have died at your fort that your dead units list exceeds about (like slaughtered animals and nameless goblins) from the unit list, allowing migrants to start coming again. +It also supports scanning burrows and cleaning out dead units from burrow +assignments. The vanilla UI doesn't provide any way to remove dead units, and +the dead units artificially increase the reported count of units that are +assigned to the burrow. + Usage ----- :: - fix/dead-units + fix/dead-units [--active] [-q] + fix/dead-units --burrow [-q] + +Options +------- + +``--active`` + Scrub dead units from the ``active`` vector so they don't show up in the + dead units list. This is the default if no option is specified. +``--burrow`` + Scrub dead units from burrow membership lists. +``-q``, ``--quiet`` + Surpress console output (final status update is still printed if at least one item was affected). diff --git a/fix/dead-units.lua b/fix/dead-units.lua index 31660f93de..ab794b4589 100644 --- a/fix/dead-units.lua +++ b/fix/dead-units.lua @@ -1,28 +1,24 @@ --- Remove uninteresting dead units from the unit list. ---[====[ +local argparse = require('argparse') -fix/dead-units -============== -Removes uninteresting dead units from the unit list. Doesn't seem to give any -noticeable performance gain, but migrants normally stop if the unit list grows -to around 3000 units, and this script reduces it back. - -]====] local units = df.global.world.units.active -local count = 0 local MONTH = 1200 * 28 local YEAR = MONTH * 12 -for i=#units-1,0,-1 do - local unit = units[i] - if dfhack.units.isDead(unit) and not dfhack.units.isOwnRace(unit) then +local count = 0 + +local function scrub_active() + for i=#units-1,0,-1 do + local unit = units[i] + if not dfhack.units.isDead(unit) or dfhack.units.isOwnRace(unit) then + goto continue + end local remove = false if dfhack.units.isMarkedForSlaughter(unit) then remove = true elseif unit.hist_figure_id == -1 then remove = true elseif not dfhack.units.isOwnCiv(unit) and - not (dfhack.units.isMerchant(unit) or dfhack.units.isDiplomat(unit)) then + not (dfhack.units.isMerchant(unit) or dfhack.units.isDiplomat(unit)) then remove = true end if remove and unit.counters.death_id ~= -1 then @@ -44,7 +40,34 @@ for i=#units-1,0,-1 do count = count + 1 units:erase(i) end + ::continue:: + end +end + +local function scrub_burrows() + for _, burrow in ipairs(df.global.plotinfo.burrows.list) do + for _, unit_id in ipairs(burrow.units) do + local unit = df.unit.find(unit_id) + if unit and dfhack.units.isDead(unit) then + count = count + 1 + dfhack.burrows.setAssignedUnit(burrow, unit, false) + end + end end end -print('Units removed from active: '..count) +local args = {...} +if not args[1] then args[1] = '--active' end + +local quiet = false + +argparse.processArgsGetopt(args, { + {nil, 'active', handler=scrub_active}, + {nil, 'burrow', handler=scrub_burrows}, + {nil, 'burrows', handler=scrub_burrows}, + {'q', 'quiet', handler=function() quiet = true end}, +}) + +if count > 0 or not quiet then + print('Dead units scrubbed: ' .. count) +end diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 9836dd79c0..e92b004a84 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -117,9 +117,12 @@ local REPEATS = { ['combine']={ desc='Combine partial stacks in stockpiles into full stacks.', command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, - ['stuck-instruments']={ - desc='Fix activity references on stuck instruments to make them usable again.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, + ['dead-units-burrow']={ + desc='Fix units still being assigned to burrows after death.', + command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, + ['empty-wheelbarrows']={ + desc='Empties wheelbarrows which have rocks stuck in them.', + command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, ['general-strike']={ desc='Prevent dwarves from getting stuck and refusing to work.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, @@ -129,15 +132,15 @@ local REPEATS = { ['orders-reevaluate']={ desc='Invalidates work orders once a month, allowing conditions to be rechecked.', command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, + ['stuck-instruments']={ + desc='Fix activity references on stuck instruments to make them usable again.', + command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, ['warn-stranded']={ desc='Show a warning dialog when units are stranded from all others.', command={'--time', '0.25', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, - ['empty-wheelbarrows']={ - desc='Empties wheelbarrows which have rocks stuck in them.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, } local REPEATS_LIST = {} for k in pairs(REPEATS) do From 4c794d1f34438562e6be53f29a463859a7fad38d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Nov 2023 17:46:18 -0800 Subject: [PATCH 673/732] document that units must be dead for a month before being culled --- docs/fix/dead-units.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/fix/dead-units.rst b/docs/fix/dead-units.rst index b6d5f33cd6..49156be5f7 100644 --- a/docs/fix/dead-units.rst +++ b/docs/fix/dead-units.rst @@ -27,8 +27,8 @@ Options ------- ``--active`` - Scrub dead units from the ``active`` vector so they don't show up in the - dead units list. This is the default if no option is specified. + Scrub units that have been dead for more than a month from the ``active`` + vector. This is the default if no option is specified. ``--burrow`` Scrub dead units from burrow membership lists. ``-q``, ``--quiet`` From 829da5c65c396cc82e97d343873755fed8fffec2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Nov 2023 21:46:34 -0800 Subject: [PATCH 674/732] reword trackstop changelog entry --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index fb31369137..1279997138 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,7 +28,7 @@ Template for new versions: ## New Tools - `sync-windmills`: synchronize or randomize movement of active windmills -- `trackstop`: new overlay to allow changing track stop dump direction and friction and roller direction and speed after construction +- `trackstop`: (reimplemented) integrated overlay for changing track stop and roller settings after construction ## New Features - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging From f78395c4af414ba2866afb9955793b9336044f0a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Nov 2023 19:20:32 -0800 Subject: [PATCH 675/732] sync tag sheet to docs --- docs/emigration.rst | 2 +- docs/starvingdead.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/emigration.rst b/docs/emigration.rst index d53345b117..58a3d623e7 100644 --- a/docs/emigration.rst +++ b/docs/emigration.rst @@ -3,7 +3,7 @@ emigration .. dfhack-tool:: :summary: Allow dwarves to emigrate from the fortress when stressed. - :tags: fort auto gameplay units + :tags: fort gameplay units If a dwarf is spiraling downward and is unable to cope in your fort, this tool will give them the choice to leave the fortress (and the map). diff --git a/docs/starvingdead.rst b/docs/starvingdead.rst index 1e0dc938df..12f7efa14c 100644 --- a/docs/starvingdead.rst +++ b/docs/starvingdead.rst @@ -3,7 +3,7 @@ starvingdead .. dfhack-tool:: :summary: Prevent infinite accumulation of roaming undead. - :tags: fort auto fps gameplay units + :tags: fort fps gameplay units With this tool running, all undead that have been on the map for one month gradually decay, losing strength, speed, and toughness. After six months, From 9b8c43e0874c6030794926a38fd34ef8bc9040e2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Nov 2023 19:21:42 -0800 Subject: [PATCH 676/732] update changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 1279997138..161ed4b72b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -32,7 +32,7 @@ Template for new versions: ## New Features - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging -- `quickfort`: new blueprint mode for designating burrows +- `quickfort`: new ``burrow`` blueprint mode for designating or manipulating burrows - `unforbid`: now ignores worn and tattered items by default (X/XX), use -X to bypass - `fix/dead-units`: gained ability to scrub dead units from burrow membership lists From 8fe7c969cc0cead09ded191c1a3c7aaf53f8e4be Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Nov 2023 19:39:31 -0800 Subject: [PATCH 677/732] clear cursor position when disabling cursor --- changelog.txt | 1 + toggle-kbd-cursor.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 161ed4b72b..f52c018a40 100644 --- a/changelog.txt +++ b/changelog.txt @@ -68,6 +68,7 @@ Template for new versions: - `gui/sandbox`: fix scrollbar moving double distance on click - `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped - `full-heal`: fix removal of corpse after resurrection +- `toggle-kbd-cursor`: clear the cursor position when disabling, preventing the game from sometimes jumping the viewport around when cursor keys are hit ## Misc Improvements - `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities diff --git a/toggle-kbd-cursor.lua b/toggle-kbd-cursor.lua index b8bfd1ec37..ecc72bba68 100644 --- a/toggle-kbd-cursor.lua +++ b/toggle-kbd-cursor.lua @@ -4,6 +4,7 @@ local flags4 = df.global.d_init.flags4 if flags4.KEYBOARD_CURSOR then flags4.KEYBOARD_CURSOR = false + guidm.setCursorPos(xyz2pos(-30000, -30000, -30000)) print('Keyboard cursor disabled.') else guidm.setCursorPos(guidm.Viewport.get():getCenter()) From 6bfd4422b8434c6a0146047010b1ffa8e1542d3d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 14 Nov 2023 11:06:27 -0800 Subject: [PATCH 678/732] bump changelog to 50.11-r3 --- changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/changelog.txt b/changelog.txt index f52c018a40..84329fa0c4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 50.11-r3 + ## New Tools - `sync-windmills`: synchronize or randomize movement of active windmills - `trackstop`: (reimplemented) integrated overlay for changing track stop and roller settings after construction From 03c71869c1697b8c252073b5ad24f924d5e1bf87 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 14 Nov 2023 21:44:03 -0800 Subject: [PATCH 679/732] ensure label is resized when dimensions change even if the mouse cursor doesn't move --- gui/design.lua | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 0005263944..4ccb85697e 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1779,6 +1779,9 @@ end local DEFAULT_DIMENSION_TOOLTIP_WIDTH = 17 local DIMENSION_TOOLTIP_HEIGHT = 4 +local DIMENSION_TOOLTIP_X_OFFSET = 3 +local DIMENSION_TOOLTIP_Y_OFFSET = 3 + DimensionsOverlay = defclass(DimensionsOverlay, overlay.OverlayWidget) DimensionsOverlay.ATTRS{ default_pos={x=1,y=1}, @@ -1836,6 +1839,7 @@ function DimensionsOverlay:init() frame={t=0, l=0, w=7, h=2}, }, widgets.Label{ + view_id='label', frame={t=0}, auto_width=true, text={{text=format_dims}}, @@ -1858,14 +1862,11 @@ function DimensionsOverlay:render(dc) local x, y = dfhack.screen.getMousePos() if not x then return end local sw, sh = dfhack.screen.getWindowSize() - local tooltip_width = math.max(9, #format_dims() + 2) - if tooltip_width ~= self.prev_tooltip_width then - self:updateLayout() - self.prev_tooltip_width = tooltip_width - end - x = math.min(x + 3, sw - tooltip_width) - y = math.min(y + 3, sh - DIMENSION_TOOLTIP_HEIGHT) - self.frame.w = x + tooltip_width + local frame_width = math.max(9, self.subviews.label:getTextWidth() + 2) + self:updateLayout() + x = math.min(x + DIMENSION_TOOLTIP_X_OFFSET, sw - frame_width) + y = math.min(y + DIMENSION_TOOLTIP_Y_OFFSET, sh - DIMENSION_TOOLTIP_HEIGHT) + self.frame.w = x + frame_width self.frame.h = y + DIMENSION_TOOLTIP_HEIGHT DimensionsOverlay.super.render(self, dc) end From 73ee9ccb3dd3c0c0180251f9334234289d8d813c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Nov 2023 16:23:44 -0800 Subject: [PATCH 680/732] reinstate build-now, using DF-native completeBuild --- build-now.lua | 181 +++------------------------------------------ changelog.txt | 1 + docs/build-now.rst | 6 +- 3 files changed, 14 insertions(+), 174 deletions(-) diff --git a/build-now.lua b/build-now.lua index f14e0d1bdc..e533faea5d 100644 --- a/build-now.lua +++ b/build-now.lua @@ -1,9 +1,7 @@ -- instantly completes unsuspended building construction jobs local argparse = require('argparse') -local dig_now = require('plugins.dig-now') local gui = require('gui') -local tiletypes = require('plugins.tiletypes') local utils = require('utils') local ok, buildingplan = pcall(require, 'plugins.buildingplan') @@ -22,16 +20,12 @@ local function parse_commandline(args) local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, {'q', 'quiet', handler=function() opts.quiet = true end}, - {nil, 'really', handler=function() opts.really = true end}, + {'z', 'zlevel', handler=function() opts.zlevel = true end}, }) if positionals[1] == 'help' then opts.help = true end if opts.help then return opts end - if not opts.really then - qerror('This script is known to cause corruption and crashes with some building types, and the DFHack team is still looking into solutions. To bypass this message, pass the "--really" option to the script.') - end - if #positionals >= 1 then opts.start = argparse.coords(positionals[1]) if #positionals >= 2 then @@ -48,6 +42,10 @@ local function parse_commandline(args) local x, y, z = dfhack.maps.getTileSize() opts['end'] = xyz2pos(x-1, y-1, z-1) end + if opts.zlevel then + opts.start.z = df.global.window_z + opts['end'].z = df.global.window_z + end return opts end @@ -84,7 +82,7 @@ local function get_jobs(opts) goto continue end - -- accept building if if any part is within the processing area + -- accept building if any part is within the processing area if bld.z < opts.start.z or bld.z > opts['end'].z or bld.x2 < opts.start.x or bld.x1 > opts['end'].x or bld.y2 < opts.start.y or bld.y1 > opts['end'].y then @@ -101,7 +99,7 @@ local function get_jobs(opts) :format(num_suspended, num_suspended ~= 1 and 's' or '')) end if num_incomplete > 0 then - print(('Skipped %d building%s with missing items') + print(('Skipped %d building%s with pending items') :format(num_incomplete, num_incomplete ~= 1 and 's' or '')) end if num_clipped > 0 then @@ -318,139 +316,6 @@ local function attach_items(bld, items) return true end --- from observation of vectors sorted by the DF, pos sorting seems to be by x, --- then by y, then by z -local function pos_cmp(a, b) - local xcmp = utils.compare(a.x, b.x) - if xcmp ~= 0 then return xcmp end - local ycmp = utils.compare(a.y, b.y) - if ycmp ~= 0 then return ycmp end - return utils.compare(a.z, b.z) -end - -local function get_original_tiletype(pos) - -- TODO: this is not always exactly the existing tile type. for example, - -- tracks are ignored - return dfhack.maps.getTileType(pos) -end - -local function reuse_construction(construction, item) - construction.item_type = item:getType() - construction.item_subtype = item:getSubtype() - construction.mat_type = item:getMaterial() - construction.mat_index = item:getMaterialIndex() - construction.flags.top_of_wall = false - construction.flags.no_build_item = true -end - -local function create_and_link_construction(pos, item, top_of_wall) - local construction = df.construction:new() - utils.assign(construction.pos, pos) - construction.item_type = item:getType() - construction.item_subtype = item:getSubtype() - construction.mat_type = item:getMaterial() - construction.mat_index = item:getMaterialIndex() - construction.flags.top_of_wall = top_of_wall - construction.flags.no_build_item = not top_of_wall - construction.original_tile = get_original_tiletype(pos) - utils.insert_sorted(df.global.world.constructions, construction, - 'pos', pos_cmp) -end - --- maps construction_type to the resulting tiletype -local const_to_tile = { - [df.construction_type.Fortification] = df.tiletype.ConstructedFortification, - [df.construction_type.Wall] = df.tiletype.ConstructedPillar, - [df.construction_type.Floor] = df.tiletype.ConstructedFloor, - [df.construction_type.UpStair] = df.tiletype.ConstructedStairU, - [df.construction_type.DownStair] = df.tiletype.ConstructedStairD, - [df.construction_type.UpDownStair] = df.tiletype.ConstructedStairUD, - [df.construction_type.Ramp] = df.tiletype.ConstructedRamp, -} --- fill in all the track mappings, which have nice consistent naming conventions -for i,v in ipairs(df.construction_type) do - if type(v) ~= 'string' then goto continue end - local _, _, base, dir = v:find('^(TrackR?a?m?p?)([NSEW]+)') - if base == 'Track' then - const_to_tile[i] = df.tiletype['ConstructedFloorTrack'..dir] - elseif base == 'TrackRamp' then - const_to_tile[i] = df.tiletype['ConstructedRampTrack'..dir] - end - ::continue:: -end - -local function set_tiletype(pos, tt) - local block = dfhack.maps.ensureTileBlock(pos) - block.tiletype[pos.x%16][pos.y%16] = tt - if tt == df.tiletype.ConstructedPillar then - block.designation[pos.x%16][pos.y%16].outside = 0 - end - -- all tiles below this one are now "inside" - for z = pos.z-1,0,-1 do - block = dfhack.maps.ensureTileBlock(pos.x, pos.y, z) - if not block or block.designation[pos.x%16][pos.y%16].outside == 0 then - return - end - block.designation[pos.x%16][pos.y%16].outside = 0 - end -end - -local function adjust_tile_above(pos_above, item, construction_type) - if not dfhack.maps.ensureTileBlock(pos_above) then return end - local tt_above = dfhack.maps.getTileType(pos_above) - local shape_above = df.tiletype.attrs[tt_above].shape - if shape_above ~= df.tiletype_shape.EMPTY - and shape_above ~= df.tiletype_shape.RAMP_TOP then - return - end - if construction_type == df.construction_type.Wall then - create_and_link_construction(pos_above, item, true) - set_tiletype(pos_above, df.tiletype.ConstructedFloor) - elseif df.construction_type[construction_type]:find('Ramp') then - set_tiletype(pos_above, df.tiletype.RampTop) - end -end - --- add new construction to the world list and manage tiletype conversion -local function build_construction(bld) - -- remember required metadata and get rid of building used for designation - local item = bld.contained_items[0].item - local pos = copyall(item.pos) - local construction_type = bld.type - dfhack.buildings.deconstruct(bld) - - -- check if we're building on a construction (i.e. building a construction on top of a wall) - local tiletype = dfhack.maps.getTileType(pos) - local tileattrs = df.tiletype.attrs[tiletype] - if tileattrs.material == df.tiletype_material.CONSTRUCTION then - -- modify the construction to the new type - local construction, found = utils.binsearch(df.global.world.constructions, pos, 'pos', pos_cmp) - if not found then - error('Could not find construction entry for construction tile at ' .. pos.x .. ', ' .. pos.y .. ', ' .. pos.z) - end - reuse_construction(construction, item) - else - -- add entry to df.global.world.constructions - create_and_link_construction(pos, item, false) - end - -- adjust tiletypes for the construction itself - set_tiletype(pos, const_to_tile[construction_type]) - if construction_type == df.construction_type.Wall then - dig_now.link_adjacent_smooth_walls(pos) - end - - -- for walls and ramps with empty space above, adjust the tile above - if construction_type == df.construction_type.Wall - or df.construction_type[construction_type]:find('Ramp') then - adjust_tile_above(xyz2pos(pos.x, pos.y, pos.z+1), item, - construction_type) - end - - -- a duplicate item will get created on deconstruction due to the - -- no_build_item flag set in create_and_link_construction; destroy this item - dfhack.items.remove(item) -end - -- complete architecture, if required, and perform the adjustments the game -- normally does when a building is built. this logic is reverse engineered from -- observing game behavior and may be incomplete. @@ -465,31 +330,7 @@ local function build_building(bld) design.max_hitpoints = 80640 end bld:setBuildStage(bld:getMaxBuildStage()) - bld.flags.exists = true - -- update occupancy flags and build dirt roads (they don't build themselves) - local bld_type = bld:getType() - local is_dirt_road = bld_type == df.building_type.RoadDirt - for x = bld.x1,bld.x2 do - for y = bld.y1,bld.y2 do - bld:updateOccupancy(x, y) - if is_dirt_road and dfhack.buildings.containsTile(bld, x, y) then - -- note that this does not clear shrubs. we need to figure out - -- how to do that - if not tiletypes.tiletypes_setTile(xyz2pos(x, y, bld.z), - -1, df.tiletype_material.SOIL, df.tiletype_special.NORMAL, -1) then - dfhack.printerr('failed to build tile of dirt road') - end - end - end - end - -- all buildings call this, though it only appears to have an effect for - -- farm plots - bld:initFarmSeasons() - -- doors and floodgates link to adjacent smooth walls - if bld_type == df.building_type.Door or - bld_type == df.building_type.Floodgate then - dig_now.link_adjacent_smooth_walls(bld.centerx, bld.centery, bld.z) - end + dfhack.buildings.completeBuild(bld) end local function throw(bld, msg) @@ -559,11 +400,7 @@ for _,job in ipairs(get_jobs(opts)) do 'failed to attach items to building; state may be inconsistent') end - if bld_type == df.building_type.Construction then - build_construction(bld) - else - build_building(bld) - end + build_building(bld) num_jobs = num_jobs + 1 ::continue:: diff --git a/changelog.txt b/changelog.txt index 84329fa0c4..a48d7f6475 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `build-now`: (reinstated) instantly complete unsuspended buildings that are ready to be built ## New Features diff --git a/docs/build-now.rst b/docs/build-now.rst index 873ecabfd5..63bdb6a3e9 100644 --- a/docs/build-now.rst +++ b/docs/build-now.rst @@ -3,7 +3,7 @@ build-now .. dfhack-tool:: :summary: Instantly completes building construction jobs. - :tags: unavailable + :tags: fort armok buildings By default, all unsuspended buildings on the map are completed, but the area of effect is configurable. @@ -25,7 +25,7 @@ the building at that coordinate is built. The ```` parameters can either be an ``,,`` triple (e.g. ``35,12,150``) or the string ``here``, which means the position of the active -game cursor. +keyboard game cursor. Examples -------- @@ -40,3 +40,5 @@ Options ``-q``, ``--quiet`` Suppress informational output (error messages are still printed). +``-z``, ``--zlevel`` + Restrict operation to the currently visible z-level From db1bb2cc1e9200c972cb16b5e59a86c0d44837e1 Mon Sep 17 00:00:00 2001 From: vallode <18506096+vallode@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:28:32 +0100 Subject: [PATCH 681/732] Change items.all to items.other.IN_PLAY for forbid scripts --- forbid.lua | 6 +++--- unforbid.lua | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/forbid.lua b/forbid.lua index b0be9536f9..4a09732703 100644 --- a/forbid.lua +++ b/forbid.lua @@ -5,7 +5,7 @@ local argparse = require('argparse') local function getForbiddenItems() local items = {} - for _, item in pairs(df.global.world.items.all) do + for _, item in pairs(df.global.world.items.other.IN_PLAY) do if item.flags.forbid then local item_type = df.item_type[item:getType()] @@ -59,7 +59,7 @@ if positionals[1] == "all" then print("Forbidding all items on the map...") local count = 0 - for _, item in pairs(df.global.world.items.all) do + for _, item in pairs(df.global.world.items.other.IN_PLAY) do item.flags.forbid = true count = count + 1 end @@ -73,7 +73,7 @@ if positionals[1] == "unreachable" then local citizens = dfhack.units.getCitizens(true) local count = 0 - for _, item in pairs(df.global.world.items.all) do + for _, item in pairs(df.global.world.items.other.IN_PLAY) do if item.flags.construction or item.flags.in_building or item.flags.artifact then goto skipitem end diff --git a/unforbid.lua b/unforbid.lua index 12b7430c36..286218a211 100644 --- a/unforbid.lua +++ b/unforbid.lua @@ -9,7 +9,7 @@ local function unforbid_all(include_unreachable, quiet, include_worn) local citizens = dfhack.units.getCitizens(true) local count = 0 - for _, item in pairs(df.global.world.items.all) do + for _, item in pairs(df.global.world.items.other.IN_PLAY) do if item.flags.forbid then if not include_unreachable then local reachable = false From 773d9e3e9ef829c2fac95e0af3740fe504880403 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sun, 19 Nov 2023 12:34:53 +1100 Subject: [PATCH 682/732] realistic limits for combine --- combine.lua | 97 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/combine.lua b/combine.lua index fa9b299ea6..98318744b6 100644 --- a/combine.lua +++ b/combine.lua @@ -10,12 +10,11 @@ local opts, args = { dry_run = false, types = nil, quiet = false, + unlimited_stack = false, verbose = 0, }, {...} -- default max stack size of 30 -local MAX_ITEM_STACK=30 -local MAX_AMMO_STACK=25 local MAX_CONT_ITEMS=30 local MAX_MAT_AMT=30 @@ -25,24 +24,23 @@ local typesThatUseMaterial=utils.invert{'CORPSEPIECE'} -- list of valid item types for merging -- Notes: 1. mergeable stacks are ones with the same type_id+race+caste or type_id+mat_type+mat_index --- 2. the maximum stack size is calcuated at run time: the highest value of MAX_ITEM_STACK or largest current stack size. --- 3. even though powders are specified, sand and plaster types items are excluded from merging. --- 4. seeds cannot be combined in stacks > 1. +-- 2. even though powders are specified, sand and plaster types items are excluded from merging. +-- 3. seeds cannot be combined in stacks > 1. local valid_types_map = { ['all'] = { }, - ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_size=MAX_AMMO_STACK}}, + ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_size=25}}, ['parts'] = {[df.item_type.CORPSEPIECE] ={type_id=df.item_type.CORPSEPIECE, max_size=1}}, - ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=MAX_ITEM_STACK}}, - ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=MAX_ITEM_STACK}, - [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=MAX_ITEM_STACK}}, - ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=MAX_ITEM_STACK}, - [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=MAX_ITEM_STACK}, - [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=MAX_ITEM_STACK}}, - ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=MAX_ITEM_STACK}}, - ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=MAX_ITEM_STACK}}, - ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=MAX_ITEM_STACK}, - [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=MAX_ITEM_STACK}}, - ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=MAX_ITEM_STACK}}, + ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=math.huge}}, + ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=5}, + [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=5}}, + ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=5}, + [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=5}, + [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=5}}, + ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=20}}, + ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=5}}, + ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=5}, + [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=5}}, + ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=10}}, ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_size=1}}, } @@ -110,9 +108,9 @@ local function comp_item_add_item(stockpile, stack_type, comp_item, item, contai comp_item.before_stacks = comp_item.before_stacks + 1 comp_item.description = utils.getItemDescription(item, 1) - if item.stack_size > comp_item.max_size then - comp_item.max_size = item.stack_size - end +-- if item.stack_size > comp_item.max_size then +-- comp_item.max_size = item.stack_size +-- end local new_item = {} new_item.item = item @@ -213,9 +211,9 @@ local function stacks_add_item(stockpile, stacks, stack_type, item, container) stacks.item_qty = stacks.item_qty + item.stack_size stacks.material_amt = stacks.material_amt + new_comp_item_item.before_mat_amt.Qty - if item.stack_size > stack_type.max_size then - stack_type.max_size = item.stack_size - end +-- if item.stack_size > stack_type.max_size then +-- stack_type.max_size = item.stack_size +-- end -- item is in a container if container then @@ -318,19 +316,19 @@ local function print_stacks_details(stacks, quiet) if #stacks.containers > 0 then log(1, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do - log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size)) + log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d cap:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size, cont.capacity)) end end if stacks.item_qty > 0 then log(1, ('Items: #Qty: %6d sizes: bef:%5d aft:%5d Mat amt:%6d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks, stacks.material_amt)) for key, stack_type in pairs(stacks.stack_types) do if stack_type.item_qty > 0 then - log(1, (' Type: %12s <%d> #Qty:%6d sizes: max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) + log(1, (' Type: %12s <%d> #Qty:%6d sizes: max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do if comp_item.item_qty > 0 then - log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) + log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) for _, item in sorted_items_qty(comp_item.items) do - log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6d Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) + log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6.0f Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) log(3, ('\n')) end @@ -406,7 +404,7 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) -- item type in list of included types? if stack_type and not item:isSand() and not item:isPlaster() and isValidPart(item) then - if not isRestrictedItem(item) then + if not isRestrictedItem(item) and item.stack_size < stack_type.max_size then stacks_add_item(stockpile, stacks, stack_type, item, container) @@ -424,7 +422,7 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) else -- restricted; such as marked for action or dump. - log(4, (' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) + log(5, (' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) end -- add contained items @@ -435,7 +433,8 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) stacks.containers[item.id].container = item stacks.containers[item.id].before_size = #contained_items stacks.containers[item.id].description = utils.getItemDescription(item, 1) - log(4, (' %sContainer:%s <%6d> #items:%5d\n'):format(ind, utils.getItemDescription(item), item.id, count, item:isSandBearing())) + stacks.containers[item.id].capacity = dfhack.items.getCapacity(item) + log(4, (' %sContainer:%s <%6d> #items:%5d #capacity:%5d\n'):format(ind, utils.getItemDescription(item), item.id, count, dfhack.items.getCapacity(item))) stacks_add_items(stockpile, stacks, contained_items, item, ind .. ' ') -- excluded item types @@ -445,6 +444,20 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) end end +local function unlimited_stacks(types) + log(4, 'Unlimited stacks\n') + + if opts.unlimited_stack then + for type_id, type_vals in pairs(types) do + print(type_id, type_vals ) + if types[type_id].max_size > 1 then + types[type_id].max_size = math.huge + end + print(type_id, type_vals) + end + end +end + local function populate_stacks(stacks, stockpiles, types) -- 1. loop through the specified types and add them to the stacks table. stacks[type_id] -- 2. loop through the table of stockpiles, get each item in the stockpile, then add them to stacks if the type_id matches @@ -452,13 +465,15 @@ local function populate_stacks(stacks, stockpiles, types) -- comp_key is a compound key comprised of type_id+race+caste or type_id+mat_type+mat_index log(4, 'Populating phase\n') + unlimited_stacks(types) + -- iterate across the types log(4, 'stack types\n') for type_id, type_vals in pairs(types) do if not stacks.stack_types[type_id] then stacks.stack_types[type_id] = stack_type_new(type_vals) local stack_type = stacks.stack_types[type_id] - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end @@ -483,10 +498,10 @@ local function preview_stacks(stacks) log(4, '\nPreview phase\n') for _, stack_type in pairs(stacks.stack_types) do - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) for comp_key, comp_item in pairs(stack_type.comp_items) do - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) -- item qty used? if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then @@ -496,13 +511,16 @@ local function preview_stacks(stacks) comp_item.max_size = stack_type.max_size end - -- how many stacks are needed? + -- how many stacks are needed? For math.huge, this will be 0. local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) - -- how many items are left over after the max stacks are allocated? - local stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + local stack_remainder = comp_item.item_qty + if comp_item.max_size < math.huge then + stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + end if stack_remainder > 0 then + -- if have remainder items, then need an additional stack. comp_item.after_stacks = stacks_needed + 1 else comp_item.after_stacks = stacks_needed @@ -510,6 +528,8 @@ local function preview_stacks(stacks) stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks + log(4, (' comp item:%40s <%12s> stacks needed:%.0f stacks remainder: %.0f after stacks: %.0f \n'):format(comp_item.description, comp_item.comp_key, stacks_needed, stack_remainder, stacks.after_stacks)) + -- Update the after stack sizes. for _, item in sorted_items_qty(comp_item.items) do @@ -619,9 +639,9 @@ local function preview_stacks(stacks) before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 end end - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) end - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end @@ -738,6 +758,7 @@ local function parse_commandline(opts, args) {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, {'d', 'dry-run', handler=function() opts.dry_run = true end}, {'q', 'quiet', handler=function() opts.quiet = true end}, + {'u','unlimited-stack',handler=function() opts.unlimited_stack = true end}, {'v', 'verbose', hasArg=true, handler=function(optarg) opts.verbose = math.tointeger(optarg) or 0 end}, }) From 9add898902e9ae09413fece522c4abbd8cf288fb Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sun, 19 Nov 2023 12:34:53 +1100 Subject: [PATCH 683/732] realistic limits for combine --- combine.lua | 93 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/combine.lua b/combine.lua index fa9b299ea6..1f0cf388d4 100644 --- a/combine.lua +++ b/combine.lua @@ -10,12 +10,11 @@ local opts, args = { dry_run = false, types = nil, quiet = false, + unlimited_stack = false, verbose = 0, }, {...} -- default max stack size of 30 -local MAX_ITEM_STACK=30 -local MAX_AMMO_STACK=25 local MAX_CONT_ITEMS=30 local MAX_MAT_AMT=30 @@ -25,24 +24,23 @@ local typesThatUseMaterial=utils.invert{'CORPSEPIECE'} -- list of valid item types for merging -- Notes: 1. mergeable stacks are ones with the same type_id+race+caste or type_id+mat_type+mat_index --- 2. the maximum stack size is calcuated at run time: the highest value of MAX_ITEM_STACK or largest current stack size. --- 3. even though powders are specified, sand and plaster types items are excluded from merging. --- 4. seeds cannot be combined in stacks > 1. +-- 2. even though powders are specified, sand and plaster types items are excluded from merging. +-- 3. seeds cannot be combined in stacks > 1. local valid_types_map = { ['all'] = { }, - ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_size=MAX_AMMO_STACK}}, + ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_size=25}}, ['parts'] = {[df.item_type.CORPSEPIECE] ={type_id=df.item_type.CORPSEPIECE, max_size=1}}, - ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=MAX_ITEM_STACK}}, - ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=MAX_ITEM_STACK}, - [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=MAX_ITEM_STACK}}, - ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=MAX_ITEM_STACK}, - [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=MAX_ITEM_STACK}, - [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=MAX_ITEM_STACK}}, - ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=MAX_ITEM_STACK}}, - ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=MAX_ITEM_STACK}}, - ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=MAX_ITEM_STACK}, - [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=MAX_ITEM_STACK}}, - ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=MAX_ITEM_STACK}}, + ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=math.huge}}, + ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=5}, + [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=5}}, + ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=5}, + [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=5}, + [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=5}}, + ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=20}}, + ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=5}}, + ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=5}, + [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=5}}, + ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=10}}, ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_size=1}}, } @@ -110,10 +108,6 @@ local function comp_item_add_item(stockpile, stack_type, comp_item, item, contai comp_item.before_stacks = comp_item.before_stacks + 1 comp_item.description = utils.getItemDescription(item, 1) - if item.stack_size > comp_item.max_size then - comp_item.max_size = item.stack_size - end - local new_item = {} new_item.item = item new_item.before_size = item.stack_size @@ -213,10 +207,6 @@ local function stacks_add_item(stockpile, stacks, stack_type, item, container) stacks.item_qty = stacks.item_qty + item.stack_size stacks.material_amt = stacks.material_amt + new_comp_item_item.before_mat_amt.Qty - if item.stack_size > stack_type.max_size then - stack_type.max_size = item.stack_size - end - -- item is in a container if container then @@ -318,19 +308,19 @@ local function print_stacks_details(stacks, quiet) if #stacks.containers > 0 then log(1, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do - log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size)) + log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d cap:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size, cont.capacity)) end end if stacks.item_qty > 0 then log(1, ('Items: #Qty: %6d sizes: bef:%5d aft:%5d Mat amt:%6d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks, stacks.material_amt)) for key, stack_type in pairs(stacks.stack_types) do if stack_type.item_qty > 0 then - log(1, (' Type: %12s <%d> #Qty:%6d sizes: max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) + log(1, (' Type: %12s <%d> #Qty:%6d sizes: max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do if comp_item.item_qty > 0 then - log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) + log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) for _, item in sorted_items_qty(comp_item.items) do - log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6d Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) + log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6.0f Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) log(3, ('\n')) end @@ -406,7 +396,7 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) -- item type in list of included types? if stack_type and not item:isSand() and not item:isPlaster() and isValidPart(item) then - if not isRestrictedItem(item) then + if not isRestrictedItem(item) and item.stack_size < stack_type.max_size then stacks_add_item(stockpile, stacks, stack_type, item, container) @@ -424,7 +414,7 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) else -- restricted; such as marked for action or dump. - log(4, (' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) + log(5, (' %sitem:%40s <%6d> is restricted\n'):format(ind, utils.getItemDescription(item), item.id)) end -- add contained items @@ -435,7 +425,8 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) stacks.containers[item.id].container = item stacks.containers[item.id].before_size = #contained_items stacks.containers[item.id].description = utils.getItemDescription(item, 1) - log(4, (' %sContainer:%s <%6d> #items:%5d\n'):format(ind, utils.getItemDescription(item), item.id, count, item:isSandBearing())) + stacks.containers[item.id].capacity = dfhack.items.getCapacity(item) + log(4, (' %sContainer:%s <%6d> #items:%5d #capacity:%5d\n'):format(ind, utils.getItemDescription(item), item.id, count, dfhack.items.getCapacity(item))) stacks_add_items(stockpile, stacks, contained_items, item, ind .. ' ') -- excluded item types @@ -445,6 +436,20 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) end end +local function unlimited_stacks(types) + log(4, 'Unlimited stacks\n') + + if opts.unlimited_stack then + for type_id, type_vals in pairs(types) do + print(type_id, type_vals ) + if types[type_id].max_size > 1 then + types[type_id].max_size = math.huge + end + print(type_id, type_vals) + end + end +end + local function populate_stacks(stacks, stockpiles, types) -- 1. loop through the specified types and add them to the stacks table. stacks[type_id] -- 2. loop through the table of stockpiles, get each item in the stockpile, then add them to stacks if the type_id matches @@ -452,13 +457,15 @@ local function populate_stacks(stacks, stockpiles, types) -- comp_key is a compound key comprised of type_id+race+caste or type_id+mat_type+mat_index log(4, 'Populating phase\n') + unlimited_stacks(types) + -- iterate across the types log(4, 'stack types\n') for type_id, type_vals in pairs(types) do if not stacks.stack_types[type_id] then stacks.stack_types[type_id] = stack_type_new(type_vals) local stack_type = stacks.stack_types[type_id] - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end @@ -483,10 +490,10 @@ local function preview_stacks(stacks) log(4, '\nPreview phase\n') for _, stack_type in pairs(stacks.stack_types) do - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) for comp_key, comp_item in pairs(stack_type.comp_items) do - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) -- item qty used? if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then @@ -496,13 +503,16 @@ local function preview_stacks(stacks) comp_item.max_size = stack_type.max_size end - -- how many stacks are needed? + -- how many stacks are needed? For math.huge, this will be 0. local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) - -- how many items are left over after the max stacks are allocated? - local stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + local stack_remainder = comp_item.item_qty + if comp_item.max_size < math.huge then + stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + end if stack_remainder > 0 then + -- if have remainder items, then need an additional stack. comp_item.after_stacks = stacks_needed + 1 else comp_item.after_stacks = stacks_needed @@ -510,6 +520,8 @@ local function preview_stacks(stacks) stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks + log(4, (' comp item:%40s <%12s> stacks needed:%.0f stacks remainder: %.0f after stacks: %.0f \n'):format(comp_item.description, comp_item.comp_key, stacks_needed, stack_remainder, stacks.after_stacks)) + -- Update the after stack sizes. for _, item in sorted_items_qty(comp_item.items) do @@ -619,9 +631,9 @@ local function preview_stacks(stacks) before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 end end - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) end - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) end end @@ -738,6 +750,7 @@ local function parse_commandline(opts, args) {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, {'d', 'dry-run', handler=function() opts.dry_run = true end}, {'q', 'quiet', handler=function() opts.quiet = true end}, + {'u','unlimited-stack',handler=function() opts.unlimited_stack = true end}, {'v', 'verbose', hasArg=true, handler=function(optarg) opts.verbose = math.tointeger(optarg) or 0 end}, }) From db6d0059996fb7c135c48de1a714c8eda8331e33 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sun, 19 Nov 2023 22:19:23 +1100 Subject: [PATCH 684/732] review changes and started vol change for cont --- combine.lua | 184 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 107 insertions(+), 77 deletions(-) diff --git a/combine.lua b/combine.lua index c557522c83..71639bab07 100644 --- a/combine.lua +++ b/combine.lua @@ -28,20 +28,20 @@ local typesThatUseMaterial=utils.invert{'CORPSEPIECE'} -- 3. seeds cannot be combined in stacks > 1. local valid_types_map = { ['all'] = { }, - ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_size=25}}, - ['parts'] = {[df.item_type.CORPSEPIECE] ={type_id=df.item_type.CORPSEPIECE, max_size=1}}, - ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=math.huge}}, - ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=5}, - [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=5}}, - ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=5}, - [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=5}, - [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=5}}, - ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=20}}, - ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=5}}, - ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=5}, - [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=5}}, - ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=10}}, - ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_size=1}}, + ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_stack_qty=25}}, + ['parts'] = {[df.item_type.CORPSEPIECE] ={type_id=df.item_type.CORPSEPIECE, max_stack_qty=1}}, + ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_stack_qty=math.huge}}, + ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_stack_qty=5}, + [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_stack_qty=5}}, + ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_stack_qty=5}, + [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_stack_qty=5}, + [df.item_type.EGG] ={type_id=df.item_type.EGG, max_stack_qty=5}}, + ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_stack_qty=20}}, + ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_stack_qty=5}}, + ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_stack_qty=5}, + [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_stack_qty=5}}, + ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_stack_qty=10}}, + ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_stack_qty=1}}, } @@ -75,43 +75,61 @@ function CList:new(o) return o end -local function comp_item_new(comp_key, max_size) +local function comp_item_new(comp_key, max_stack_qty) -- create a new comp_item entry to be added to a comp_items table. local comp_item = {} if not comp_key then qerror('new_comp_item: comp_key is nil') end comp_item.comp_key = comp_key -- key used to index comparable items for merging comp_item.description = '' -- description of the comp item for output - comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack + comp_item.max_stack_qty = max_stack_qty or 0 -- how many of a comp item can be in one stack -- item info comp_item.items = CList:new(nil) -- key:item.id, - -- val:{item, - -- before_size, after_size, before_cont_id, after_cont_id, + -- val:{item, base_weight, density, + -- before_stack_qty, after_stack_qty, + -- before_cont_id, after_cont_id, + -- before_volume, after_volume, -- stockpile_id, stockpile_name, -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} -- } comp_item.item_qty = 0 -- total quantity of items + comp_item.base_weight = 0 -- base weight of an item + comp_item.density = 0 -- density of an item + comp_item.volume = 0 -- volume = qty * base weight + comp_item.weight = 0 -- weight = density * volume * 10 / 1,000,000 comp_item.material_amt = 0 -- total amount of materials comp_item.max_mat_amt = MAX_MAT_AMT -- max amount of materials in one stack comp_item.before_stacks = 0 -- the number of stacks of the items before... comp_item.after_stacks = 0 -- ...and after the merge --container info - comp_item.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id - comp_item.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id + comp_item.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id + comp_item.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id return comp_item end local function comp_item_add_item(stockpile, stack_type, comp_item, item, container) -- add an item into the comp_items table, setting the comp_item attributes. if not comp_item.items[item.id] then - comp_item.item_qty = comp_item.item_qty + item.stack_size + local item_qty = item.stack_size + local item_base_weight = item:getBaseWeight() + local item_volume = item:getVolume() + local item_density = item:getSolidDensity() + + comp_item.item_qty = comp_item.item_qty + item_qty comp_item.before_stacks = comp_item.before_stacks + 1 comp_item.description = utils.getItemDescription(item, 1) + comp_item.base_weight = item_base_weight + comp_item.volume = item_qty * item_base_weight + comp_item.density = item_density local new_item = {} new_item.item = item - new_item.before_size = item.stack_size + new_item.base_weight = item_base_weight + new_item.density = item_density + + new_item.before_stack_qty = item_qty + new_item.before_volume = item_qty * item_base_weight new_item.stockpile_id = stockpile.id new_item.stockpile_name = stockpile.name @@ -131,10 +149,16 @@ local function comp_item_add_item(stockpile, stack_type, comp_item, item, contai new_item.before_mat_amt.Horn = item.material_amount.Horn new_item.before_mat_amt.HairWool = item.material_amount.HairWool new_item.before_mat_amt.Yarn = item.material_amount.Yarn - for _, v in pairs(new_item.before_mat_amt) do if new_item.before_mat_amt.Qty < v then new_item.before_mat_amt.Qty = v end end + for _, v in pairs(new_item.before_mat_amt) do + if new_item.before_mat_amt.Qty < v then + new_item.before_mat_amt.Qty = v + end + end comp_item.material_amt = comp_item.material_amt + new_item.before_mat_amt.Qty - if new_item.before_mat_amt.Qty > comp_item.max_mat_amt then comp_item.max_mat_amt = new_item.before_mat_amt.Qty end + if new_item.before_mat_amt.Qty > comp_item.max_mat_amt then + comp_item.max_mat_amt = new_item.before_mat_amt.Qty + end end -- item is in a container @@ -162,15 +186,15 @@ local function stack_type_new(type_vals) end -- item info - stack_type.comp_items = CList:new(nil) -- key:comp_key, val:comp_item + stack_type.comp_items = CList:new(nil) -- key:comp_key, val:comp_item stack_type.item_qty = 0 -- total quantity of items types stack_type.material_amt = 0 -- total amount of materials stack_type.before_stacks = 0 -- the number of stacks of the item types before ... stack_type.after_stacks = 0 -- ...and after the merge --container info - stack_type.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id - stack_type.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id + stack_type.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id + stack_type.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id return stack_type end @@ -195,7 +219,7 @@ local function stacks_add_item(stockpile, stacks, stack_type, item, container) end if not stack_type.comp_items[comp_key] then - stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_size) + stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_stack_qty) end local new_comp_item_item = comp_item_add_item(stockpile, stack_type, stack_type.comp_items[comp_key], item, container) @@ -221,17 +245,17 @@ local function stacks_add_item(stockpile, stacks, stack_type, item, container) end local function sorted_items_qty(tab) - -- used to sort the comp_items by contained, then size. Important for combining containers. + -- used to sort the comp_items by contained, then qty. Important for combining containers. local tmp = {} for id, val in pairs(tab) do - local val = {id=id, before_cont_id=val.before_cont_id, before_size=val.before_size} + local val = {id=id, before_cont_id=val.before_cont_id, before_stack_qty=val.before_stack_qty} table.insert(tmp, val) end table.sort(tmp, function(a, b) if not a.before_cont_id and not b.before_cont_id or a.before_cont_id and b.before_cont_id then - return a.before_size > b.before_size + return a.before_stack_qty > b.before_stack_qty else return a.before_cont_id and not b.before_cont_id end @@ -309,19 +333,20 @@ local function print_stacks_details(stacks, quiet) if #stacks.containers > 0 then log(1, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do - log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d cap:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size, cont.capacity)) + log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d cap:%5d\n'):format(cont.description, cont_id, cont.before_stack_qty, cont.after_stack_qty, cont.capacity)) end end if stacks.item_qty > 0 then - log(1, ('Items: #Qty: %6d sizes: bef:%5d aft:%5d Mat amt:%6d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks, stacks.material_amt)) + log(1, ('Items: #Qty: %6d Stacks: bef:%5d aft:%5d Mat amt:%6d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks, stacks.material_amt)) for key, stack_type in pairs(stacks.stack_types) do if stack_type.item_qty > 0 then - log(1, (' Type: %12s <%d> #Qty:%6d sizes: max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) + log(1, (' Type: %12s <%d> #Qty:%6d Stacks: max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do if comp_item.item_qty > 0 then - log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) + log(2, ('G Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) for _, item in sorted_items_qty(comp_item.items) do - log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6.0f Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) + log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6.0f Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_stack_qty or 0, item.after_stack_qty or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) + log(2, (' Item:%40s <%6d> Qty: bef:%6d aft:%6.0f Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d sp:%s den: %6d s size: %6d liq:%s b wt:%6d vol:%6d\n'):format(utils.getItemDescription(item.item), item.item.id, item.before_stack_qty or 0, item.after_stack_qty or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name, item.item:getSolidDensity(), item.item:getStackSize(), item.item:isLiquid(), item.item:getBaseWeight(), item.item:getVolume())) log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) log(3, ('\n')) end @@ -353,7 +378,7 @@ local function stacks_new() local stacks = {} stacks.stack_types = CList:new(nil) -- key=type_id, val=stack_type - stacks.containers = CList:new(nil) -- key=container.id, val={container, description, before_size, after_size} + stacks.containers = CList:new(nil) -- key=container.id, val={container, description, before_stack_qty, after_stack_qty} stacks.before_cont_ids = CList:new(nil) -- key=container.id, val=container.id stacks.after_cont_ids = CList:new(nil) -- key=container.id, val=container.id stacks.item_qty = 0 @@ -394,10 +419,11 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) local type_id = item:getType() local subtype_id = item:getSubtype() local stack_type = stacks.stack_types[type_id] + local item_qty = item.stack_size -- stack size is actually a qty in this context. -- item type in list of included types? if stack_type and not item:isSand() and not item:isPlaster() and isValidPart(item) then - if not isRestrictedItem(item) and item.stack_size < stack_type.max_size then + if not isRestrictedItem(item) and item_qty < stack_type.max_stack_qty then stacks_add_item(stockpile, stacks, stack_type, item, container) @@ -424,7 +450,7 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) local count = #contained_items stacks.containers[item.id] = {} stacks.containers[item.id].container = item - stacks.containers[item.id].before_size = #contained_items + stacks.containers[item.id].before_stack_qty = #contained_items stacks.containers[item.id].description = utils.getItemDescription(item, 1) stacks.containers[item.id].capacity = dfhack.items.getCapacity(item) log(4, (' %sContainer:%s <%6d> #items:%5d #capacity:%5d\n'):format(ind, utils.getItemDescription(item), item.id, count, dfhack.items.getCapacity(item))) @@ -438,15 +464,16 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) end local function unlimited_stacks(types) - log(4, 'Unlimited stacks\n') + -- Override the realistic limits of stack sizes. This is armok behaviour for those that want it. + -- Note: large stack sizes causes bottle neck effects for production of constructed items. + -- Need to determine how this interacts with container capacities. + -- There are certain types such as seeds that don't like being in stacks + -- greater than 1, so exclude these ones. + log(4, 'Unlimited stacks applied\n') - if opts.unlimited_stack then - for type_id, type_vals in pairs(types) do - print(type_id, type_vals ) - if types[type_id].max_size > 1 then - types[type_id].max_size = math.huge - end - print(type_id, type_vals) + for type_id, type_vals in pairs(types) do + if types[type_id].max_stack_qty > 1 then + types[type_id].max_stack_qty = math.huge end end end @@ -458,7 +485,10 @@ local function populate_stacks(stacks, stockpiles, types) -- comp_key is a compound key comprised of type_id+race+caste or type_id+mat_type+mat_index log(4, 'Populating phase\n') - unlimited_stacks(types) + -- unlimited stack sizes option enabled = Armok. Note that this is disabled by default. + if opts.unlimited_stack then + unlimited_stacks(types) + end -- iterate across the types log(4, 'stack types\n') @@ -466,7 +496,7 @@ local function populate_stacks(stacks, stockpiles, types) if not stacks.stack_types[type_id] then stacks.stack_types[type_id] = stack_type_new(type_vals) local stack_type = stacks.stack_types[type_id] - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, stack_type.after_stacks)) end end @@ -491,25 +521,25 @@ local function preview_stacks(stacks) log(4, '\nPreview phase\n') for _, stack_type in pairs(stacks.stack_types) do - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, stack_type.after_stacks)) for comp_key, comp_item in pairs(stack_type.comp_items) do - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) -- item qty used? if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then -- max size comparison - if stack_type.max_size > comp_item.max_size then - comp_item.max_size = stack_type.max_size + if stack_type.max_stack_qty > comp_item.max_stack_qty then + comp_item.max_stack_qty = stack_type.max_stack_qty end -- how many stacks are needed? For math.huge, this will be 0. - local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) + local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_stack_qty) local stack_remainder = comp_item.item_qty - if comp_item.max_size < math.huge then - stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + if comp_item.max_stack_qty < math.huge then + stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_stack_qty end if stack_remainder > 0 then @@ -528,12 +558,12 @@ local function preview_stacks(stacks) for _, item in sorted_items_qty(comp_item.items) do if stacks_needed > 0 then stacks_needed = stacks_needed - 1 - item.after_size = comp_item.max_size + item.after_stack_qty = comp_item.max_stack_qty elseif stack_remainder > 0 then - item.after_size = stack_remainder + item.after_stack_qty = stack_remainder stack_remainder = 0 else - item.after_size = 0 + item.after_stack_qty = 0 end end @@ -555,7 +585,7 @@ local function preview_stacks(stacks) item.after_mat_amt = {} if stacks_needed > 0 then stacks_needed = stacks_needed - 1 - item.after_size = item.before_size + item.after_stack_qty = item.before_stack_qty for k2, v in pairs(item.before_mat_amt) do if v > 0 then item.after_mat_amt[k2] = comp_item.max_mat_amt @@ -564,7 +594,7 @@ local function preview_stacks(stacks) end end elseif stack_remainder > 0 then - item.after_size = item.before_size + item.after_stack_qty = item.before_stack_qty for k2, v in pairs(item.before_mat_amt) do if v > 0 then item.after_mat_amt[k2] = stack_remainder @@ -577,7 +607,7 @@ local function preview_stacks(stacks) for k2, v in pairs(item.before_mat_amt) do item.after_mat_amt[k2] = 0 end - item.after_size = 0 + item.after_stack_qty = 0 end end end @@ -589,7 +619,7 @@ local function preview_stacks(stacks) for item_id, item in sorted_items_qty(comp_item.items) do -- non-zero quantity? - if item.after_size > 0 then + if item.after_stack_qty > 0 then -- in a container before merge? if item.before_cont_id then @@ -600,7 +630,7 @@ local function preview_stacks(stacks) if not curr_cont or curr_size >= MAX_CONT_ITEMS then curr_cont = before_cont - curr_size = curr_cont.before_size + curr_size = curr_cont.before_stack_qty stacks.after_cont_ids[item.before_cont_id] = item.before_cont_id stack_type.after_cont_ids[item.before_cont_id] = item.before_cont_id comp_item.after_cont_ids[item.before_cont_id] = item.before_cont_id @@ -608,17 +638,17 @@ local function preview_stacks(stacks) -- enough room in current container else curr_size = curr_size + 1 - before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 + before_cont.after_stack_qty = (before_cont.after_stack_qty or before_cont.before_stack_qty) - 1 end - curr_cont.after_size = curr_size + curr_cont.after_stack_qty = curr_size item.after_cont_id = curr_cont.container.id -- not in a container before merge, container exists, and has space elseif curr_cont and curr_size < MAX_CONT_ITEMS then curr_size = curr_size + 1 - curr_cont.after_size = curr_size + curr_cont.after_stack_qty = curr_size item.after_cont_id = curr_cont.container.id -- not in a container, no container exists or no space in container @@ -629,34 +659,34 @@ local function preview_stacks(stacks) -- zero after size, reduce the number of stacks in the container elseif item.before_cont_id then local before_cont = stacks.containers[item.before_cont_id] - before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 + before_cont.after_stack_qty = (before_cont.after_stack_qty or before_cont.before_stack_qty) - 1 end end - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) end - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, stack_type.after_stacks)) end end local function merge_stacks(stacks) - -- apply the stack size changes in the after_item_stack_size - -- if the after_item_stack_size is zero, then remove the item + -- apply the stack size changes in the item.after_stack_qty + -- if the item.after_stack_qty is zero, then remove the item log(4, 'Merge phase\n') for _, stack_type in pairs(stacks.stack_types) do for comp_key, comp_item in pairs(stack_type.comp_items) do for item_id, item in pairs(comp_item.items) do - log(4, (' item amt:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d> mat: bef:%5d aft:%5d '):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0)) + log(4, (' item amt:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d> mat: bef:%5d aft:%5d '):format(comp_item.description, item.item.id, item.before_stack_qty or 0, item.after_stack_qty or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0)) -- no items left in stack? - if item.after_size == 0 then + if item.after_stack_qty == 0 then log(4, ' removing\n') dfhack.items.remove(item.item) -- some items left in stack - elseif not typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_size ~= item.after_size then + elseif not typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_stack_qty ~= item.after_stack_qty then log(4, ' updating qty\n') - item.item.stack_size = item.after_size + item.item.stack_size = item.after_stack_qty elseif typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_mat_amt.Qty ~= item.after_mat_amt.Qty then log(4, ' updating material\n') @@ -674,7 +704,7 @@ local function merge_stacks(stacks) -- move to a container? if item.after_cont_id then if (item.before_cont_id or 0) ~= item.after_cont_id then - log(4, (' moving item:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d>\n'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + log(4, (' moving item:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d>\n'):format(comp_item.description, item.item.id, item.before_stack_qty or 0, item.after_stack_qty or 0, item.before_cont_id or 0, item.after_cont_id or 0)) dfhack.items.moveToContainer(item.item, stacks.containers[item.after_cont_id].container) end end From 809845f6b591e7ff8f7e46e580718249195b03b7 Mon Sep 17 00:00:00 2001 From: silverflyone Date: Sun, 19 Nov 2023 22:19:23 +1100 Subject: [PATCH 685/732] review comments + cont capacity started --- combine.lua | 184 +++++++++++++++++++++++++++-------------------- docs/combine.rst | 28 ++++---- 2 files changed, 121 insertions(+), 91 deletions(-) diff --git a/combine.lua b/combine.lua index c557522c83..71639bab07 100644 --- a/combine.lua +++ b/combine.lua @@ -28,20 +28,20 @@ local typesThatUseMaterial=utils.invert{'CORPSEPIECE'} -- 3. seeds cannot be combined in stacks > 1. local valid_types_map = { ['all'] = { }, - ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_size=25}}, - ['parts'] = {[df.item_type.CORPSEPIECE] ={type_id=df.item_type.CORPSEPIECE, max_size=1}}, - ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_size=math.huge}}, - ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_size=5}, - [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_size=5}}, - ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_size=5}, - [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_size=5}, - [df.item_type.EGG] ={type_id=df.item_type.EGG, max_size=5}}, - ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_size=20}}, - ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_size=5}}, - ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_size=5}, - [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_size=5}}, - ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_size=10}}, - ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_size=1}}, + ['ammo'] = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_stack_qty=25}}, + ['parts'] = {[df.item_type.CORPSEPIECE] ={type_id=df.item_type.CORPSEPIECE, max_stack_qty=1}}, + ['drink'] = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_stack_qty=math.huge}}, + ['fat'] = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_stack_qty=5}, + [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_stack_qty=5}}, + ['fish'] = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_stack_qty=5}, + [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_stack_qty=5}, + [df.item_type.EGG] ={type_id=df.item_type.EGG, max_stack_qty=5}}, + ['food'] = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_stack_qty=20}}, + ['meat'] = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_stack_qty=5}}, + ['plant'] = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_stack_qty=5}, + [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_stack_qty=5}}, + ['powder'] = {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_stack_qty=10}}, + ['seed'] = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_stack_qty=1}}, } @@ -75,43 +75,61 @@ function CList:new(o) return o end -local function comp_item_new(comp_key, max_size) +local function comp_item_new(comp_key, max_stack_qty) -- create a new comp_item entry to be added to a comp_items table. local comp_item = {} if not comp_key then qerror('new_comp_item: comp_key is nil') end comp_item.comp_key = comp_key -- key used to index comparable items for merging comp_item.description = '' -- description of the comp item for output - comp_item.max_size = max_size or 0 -- how many of a comp item can be in one stack + comp_item.max_stack_qty = max_stack_qty or 0 -- how many of a comp item can be in one stack -- item info comp_item.items = CList:new(nil) -- key:item.id, - -- val:{item, - -- before_size, after_size, before_cont_id, after_cont_id, + -- val:{item, base_weight, density, + -- before_stack_qty, after_stack_qty, + -- before_cont_id, after_cont_id, + -- before_volume, after_volume, -- stockpile_id, stockpile_name, -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} -- } comp_item.item_qty = 0 -- total quantity of items + comp_item.base_weight = 0 -- base weight of an item + comp_item.density = 0 -- density of an item + comp_item.volume = 0 -- volume = qty * base weight + comp_item.weight = 0 -- weight = density * volume * 10 / 1,000,000 comp_item.material_amt = 0 -- total amount of materials comp_item.max_mat_amt = MAX_MAT_AMT -- max amount of materials in one stack comp_item.before_stacks = 0 -- the number of stacks of the items before... comp_item.after_stacks = 0 -- ...and after the merge --container info - comp_item.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id - comp_item.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id + comp_item.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id + comp_item.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id return comp_item end local function comp_item_add_item(stockpile, stack_type, comp_item, item, container) -- add an item into the comp_items table, setting the comp_item attributes. if not comp_item.items[item.id] then - comp_item.item_qty = comp_item.item_qty + item.stack_size + local item_qty = item.stack_size + local item_base_weight = item:getBaseWeight() + local item_volume = item:getVolume() + local item_density = item:getSolidDensity() + + comp_item.item_qty = comp_item.item_qty + item_qty comp_item.before_stacks = comp_item.before_stacks + 1 comp_item.description = utils.getItemDescription(item, 1) + comp_item.base_weight = item_base_weight + comp_item.volume = item_qty * item_base_weight + comp_item.density = item_density local new_item = {} new_item.item = item - new_item.before_size = item.stack_size + new_item.base_weight = item_base_weight + new_item.density = item_density + + new_item.before_stack_qty = item_qty + new_item.before_volume = item_qty * item_base_weight new_item.stockpile_id = stockpile.id new_item.stockpile_name = stockpile.name @@ -131,10 +149,16 @@ local function comp_item_add_item(stockpile, stack_type, comp_item, item, contai new_item.before_mat_amt.Horn = item.material_amount.Horn new_item.before_mat_amt.HairWool = item.material_amount.HairWool new_item.before_mat_amt.Yarn = item.material_amount.Yarn - for _, v in pairs(new_item.before_mat_amt) do if new_item.before_mat_amt.Qty < v then new_item.before_mat_amt.Qty = v end end + for _, v in pairs(new_item.before_mat_amt) do + if new_item.before_mat_amt.Qty < v then + new_item.before_mat_amt.Qty = v + end + end comp_item.material_amt = comp_item.material_amt + new_item.before_mat_amt.Qty - if new_item.before_mat_amt.Qty > comp_item.max_mat_amt then comp_item.max_mat_amt = new_item.before_mat_amt.Qty end + if new_item.before_mat_amt.Qty > comp_item.max_mat_amt then + comp_item.max_mat_amt = new_item.before_mat_amt.Qty + end end -- item is in a container @@ -162,15 +186,15 @@ local function stack_type_new(type_vals) end -- item info - stack_type.comp_items = CList:new(nil) -- key:comp_key, val:comp_item + stack_type.comp_items = CList:new(nil) -- key:comp_key, val:comp_item stack_type.item_qty = 0 -- total quantity of items types stack_type.material_amt = 0 -- total amount of materials stack_type.before_stacks = 0 -- the number of stacks of the item types before ... stack_type.after_stacks = 0 -- ...and after the merge --container info - stack_type.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id - stack_type.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id + stack_type.before_cont_ids = CList:new(nil) -- key:container.id, val:container.id + stack_type.after_cont_ids = CList:new(nil) -- key:container.id, val:container.id return stack_type end @@ -195,7 +219,7 @@ local function stacks_add_item(stockpile, stacks, stack_type, item, container) end if not stack_type.comp_items[comp_key] then - stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_size) + stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type.max_stack_qty) end local new_comp_item_item = comp_item_add_item(stockpile, stack_type, stack_type.comp_items[comp_key], item, container) @@ -221,17 +245,17 @@ local function stacks_add_item(stockpile, stacks, stack_type, item, container) end local function sorted_items_qty(tab) - -- used to sort the comp_items by contained, then size. Important for combining containers. + -- used to sort the comp_items by contained, then qty. Important for combining containers. local tmp = {} for id, val in pairs(tab) do - local val = {id=id, before_cont_id=val.before_cont_id, before_size=val.before_size} + local val = {id=id, before_cont_id=val.before_cont_id, before_stack_qty=val.before_stack_qty} table.insert(tmp, val) end table.sort(tmp, function(a, b) if not a.before_cont_id and not b.before_cont_id or a.before_cont_id and b.before_cont_id then - return a.before_size > b.before_size + return a.before_stack_qty > b.before_stack_qty else return a.before_cont_id and not b.before_cont_id end @@ -309,19 +333,20 @@ local function print_stacks_details(stacks, quiet) if #stacks.containers > 0 then log(1, 'Summary:\nContainers:%5d before:%5d after:%5d\n', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do - log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d cap:%5d\n'):format(cont.description, cont_id, cont.before_size, cont.after_size, cont.capacity)) + log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d cap:%5d\n'):format(cont.description, cont_id, cont.before_stack_qty, cont.after_stack_qty, cont.capacity)) end end if stacks.item_qty > 0 then - log(1, ('Items: #Qty: %6d sizes: bef:%5d aft:%5d Mat amt:%6d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks, stacks.material_amt)) + log(1, ('Items: #Qty: %6d Stacks: bef:%5d aft:%5d Mat amt:%6d\n'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks, stacks.material_amt)) for key, stack_type in pairs(stacks.stack_types) do if stack_type.item_qty > 0 then - log(1, (' Type: %12s <%d> #Qty:%6d sizes: max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) + log(1, (' Type: %12s <%d> #Qty:%6d Stacks: max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do if comp_item.item_qty > 0 then - log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) + log(2, ('G Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%.0f bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) for _, item in sorted_items_qty(comp_item.items) do - log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6.0f Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) + log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6.0f Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format(utils.getItemDescription(item.item), item.item.id, item.before_stack_qty or 0, item.after_stack_qty or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) + log(2, (' Item:%40s <%6d> Qty: bef:%6d aft:%6.0f Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d sp:%s den: %6d s size: %6d liq:%s b wt:%6d vol:%6d\n'):format(utils.getItemDescription(item.item), item.item.id, item.before_stack_qty or 0, item.after_stack_qty or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name, item.item:getSolidDensity(), item.item:getStackSize(), item.item:isLiquid(), item.item:getBaseWeight(), item.item:getVolume())) log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) log(3, ('\n')) end @@ -353,7 +378,7 @@ local function stacks_new() local stacks = {} stacks.stack_types = CList:new(nil) -- key=type_id, val=stack_type - stacks.containers = CList:new(nil) -- key=container.id, val={container, description, before_size, after_size} + stacks.containers = CList:new(nil) -- key=container.id, val={container, description, before_stack_qty, after_stack_qty} stacks.before_cont_ids = CList:new(nil) -- key=container.id, val=container.id stacks.after_cont_ids = CList:new(nil) -- key=container.id, val=container.id stacks.item_qty = 0 @@ -394,10 +419,11 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) local type_id = item:getType() local subtype_id = item:getSubtype() local stack_type = stacks.stack_types[type_id] + local item_qty = item.stack_size -- stack size is actually a qty in this context. -- item type in list of included types? if stack_type and not item:isSand() and not item:isPlaster() and isValidPart(item) then - if not isRestrictedItem(item) and item.stack_size < stack_type.max_size then + if not isRestrictedItem(item) and item_qty < stack_type.max_stack_qty then stacks_add_item(stockpile, stacks, stack_type, item, container) @@ -424,7 +450,7 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) local count = #contained_items stacks.containers[item.id] = {} stacks.containers[item.id].container = item - stacks.containers[item.id].before_size = #contained_items + stacks.containers[item.id].before_stack_qty = #contained_items stacks.containers[item.id].description = utils.getItemDescription(item, 1) stacks.containers[item.id].capacity = dfhack.items.getCapacity(item) log(4, (' %sContainer:%s <%6d> #items:%5d #capacity:%5d\n'):format(ind, utils.getItemDescription(item), item.id, count, dfhack.items.getCapacity(item))) @@ -438,15 +464,16 @@ local function stacks_add_items(stockpile, stacks, items, container, ind) end local function unlimited_stacks(types) - log(4, 'Unlimited stacks\n') + -- Override the realistic limits of stack sizes. This is armok behaviour for those that want it. + -- Note: large stack sizes causes bottle neck effects for production of constructed items. + -- Need to determine how this interacts with container capacities. + -- There are certain types such as seeds that don't like being in stacks + -- greater than 1, so exclude these ones. + log(4, 'Unlimited stacks applied\n') - if opts.unlimited_stack then - for type_id, type_vals in pairs(types) do - print(type_id, type_vals ) - if types[type_id].max_size > 1 then - types[type_id].max_size = math.huge - end - print(type_id, type_vals) + for type_id, type_vals in pairs(types) do + if types[type_id].max_stack_qty > 1 then + types[type_id].max_stack_qty = math.huge end end end @@ -458,7 +485,10 @@ local function populate_stacks(stacks, stockpiles, types) -- comp_key is a compound key comprised of type_id+race+caste or type_id+mat_type+mat_index log(4, 'Populating phase\n') - unlimited_stacks(types) + -- unlimited stack sizes option enabled = Armok. Note that this is disabled by default. + if opts.unlimited_stack then + unlimited_stacks(types) + end -- iterate across the types log(4, 'stack types\n') @@ -466,7 +496,7 @@ local function populate_stacks(stacks, stockpiles, types) if not stacks.stack_types[type_id] then stacks.stack_types[type_id] = stack_type_new(type_vals) local stack_type = stacks.stack_types[type_id] - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, stack_type.after_stacks)) end end @@ -491,25 +521,25 @@ local function preview_stacks(stacks) log(4, '\nPreview phase\n') for _, stack_type in pairs(stacks.stack_types) do - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, stack_type.after_stacks)) for comp_key, comp_item in pairs(stack_type.comp_items) do - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d Cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) -- item qty used? if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then -- max size comparison - if stack_type.max_size > comp_item.max_size then - comp_item.max_size = stack_type.max_size + if stack_type.max_stack_qty > comp_item.max_stack_qty then + comp_item.max_stack_qty = stack_type.max_stack_qty end -- how many stacks are needed? For math.huge, this will be 0. - local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_size) + local stacks_needed = math.floor(comp_item.item_qty / comp_item.max_stack_qty) local stack_remainder = comp_item.item_qty - if comp_item.max_size < math.huge then - stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_size + if comp_item.max_stack_qty < math.huge then + stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_stack_qty end if stack_remainder > 0 then @@ -528,12 +558,12 @@ local function preview_stacks(stacks) for _, item in sorted_items_qty(comp_item.items) do if stacks_needed > 0 then stacks_needed = stacks_needed - 1 - item.after_size = comp_item.max_size + item.after_stack_qty = comp_item.max_stack_qty elseif stack_remainder > 0 then - item.after_size = stack_remainder + item.after_stack_qty = stack_remainder stack_remainder = 0 else - item.after_size = 0 + item.after_stack_qty = 0 end end @@ -555,7 +585,7 @@ local function preview_stacks(stacks) item.after_mat_amt = {} if stacks_needed > 0 then stacks_needed = stacks_needed - 1 - item.after_size = item.before_size + item.after_stack_qty = item.before_stack_qty for k2, v in pairs(item.before_mat_amt) do if v > 0 then item.after_mat_amt[k2] = comp_item.max_mat_amt @@ -564,7 +594,7 @@ local function preview_stacks(stacks) end end elseif stack_remainder > 0 then - item.after_size = item.before_size + item.after_stack_qty = item.before_stack_qty for k2, v in pairs(item.before_mat_amt) do if v > 0 then item.after_mat_amt[k2] = stack_remainder @@ -577,7 +607,7 @@ local function preview_stacks(stacks) for k2, v in pairs(item.before_mat_amt) do item.after_mat_amt[k2] = 0 end - item.after_size = 0 + item.after_stack_qty = 0 end end end @@ -589,7 +619,7 @@ local function preview_stacks(stacks) for item_id, item in sorted_items_qty(comp_item.items) do -- non-zero quantity? - if item.after_size > 0 then + if item.after_stack_qty > 0 then -- in a container before merge? if item.before_cont_id then @@ -600,7 +630,7 @@ local function preview_stacks(stacks) if not curr_cont or curr_size >= MAX_CONT_ITEMS then curr_cont = before_cont - curr_size = curr_cont.before_size + curr_size = curr_cont.before_stack_qty stacks.after_cont_ids[item.before_cont_id] = item.before_cont_id stack_type.after_cont_ids[item.before_cont_id] = item.before_cont_id comp_item.after_cont_ids[item.before_cont_id] = item.before_cont_id @@ -608,17 +638,17 @@ local function preview_stacks(stacks) -- enough room in current container else curr_size = curr_size + 1 - before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 + before_cont.after_stack_qty = (before_cont.after_stack_qty or before_cont.before_stack_qty) - 1 end - curr_cont.after_size = curr_size + curr_cont.after_stack_qty = curr_size item.after_cont_id = curr_cont.container.id -- not in a container before merge, container exists, and has space elseif curr_cont and curr_size < MAX_CONT_ITEMS then curr_size = curr_size + 1 - curr_cont.after_size = curr_size + curr_cont.after_stack_qty = curr_size item.after_cont_id = curr_cont.container.id -- not in a container, no container exists or no space in container @@ -629,34 +659,34 @@ local function preview_stacks(stacks) -- zero after size, reduce the number of stacks in the container elseif item.before_cont_id then local before_cont = stacks.containers[item.before_cont_id] - before_cont.after_size = (before_cont.after_size or before_cont.before_size) - 1 + before_cont.after_stack_qty = (before_cont.after_stack_qty or before_cont.before_stack_qty) - 1 end end - log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_size, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max: %.0f bef:%5d aft:%5d cont: bef:%5d aft:%5d\n'):format(comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) end - log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_size, stack_type.before_stacks, stack_type.after_stacks)) + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %.0f bef:%5d aft:%5d\n'):format(df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, stack_type.after_stacks)) end end local function merge_stacks(stacks) - -- apply the stack size changes in the after_item_stack_size - -- if the after_item_stack_size is zero, then remove the item + -- apply the stack size changes in the item.after_stack_qty + -- if the item.after_stack_qty is zero, then remove the item log(4, 'Merge phase\n') for _, stack_type in pairs(stacks.stack_types) do for comp_key, comp_item in pairs(stack_type.comp_items) do for item_id, item in pairs(comp_item.items) do - log(4, (' item amt:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d> mat: bef:%5d aft:%5d '):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0)) + log(4, (' item amt:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d> mat: bef:%5d aft:%5d '):format(comp_item.description, item.item.id, item.before_stack_qty or 0, item.after_stack_qty or 0, item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0)) -- no items left in stack? - if item.after_size == 0 then + if item.after_stack_qty == 0 then log(4, ' removing\n') dfhack.items.remove(item.item) -- some items left in stack - elseif not typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_size ~= item.after_size then + elseif not typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_stack_qty ~= item.after_stack_qty then log(4, ' updating qty\n') - item.item.stack_size = item.after_size + item.item.stack_size = item.after_stack_qty elseif typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_mat_amt.Qty ~= item.after_mat_amt.Qty then log(4, ' updating material\n') @@ -674,7 +704,7 @@ local function merge_stacks(stacks) -- move to a container? if item.after_cont_id then if (item.before_cont_id or 0) ~= item.after_cont_id then - log(4, (' moving item:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d>\n'):format(comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, item.after_cont_id or 0)) + log(4, (' moving item:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d>\n'):format(comp_item.description, item.item.id, item.before_stack_qty or 0, item.after_stack_qty or 0, item.before_cont_id or 0, item.after_cont_id or 0)) dfhack.items.moveToContainer(item.item, stacks.containers[item.after_cont_id].container) end end diff --git a/docs/combine.rst b/docs/combine.rst index 8f8889090a..67cb16100e 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -45,29 +45,32 @@ Options ``all``: all of the types listed here. - ``ammo``: AMMO + ``ammo``: AMMO. Max 25. - ``drink``: DRINK + ``drink``: DRINK. No Max. - ``fat``: GLOB and CHEESE + ``fat``: GLOB and CHEESE. Max 5. - ``fish``: FISH, FISH_RAW and EGG + ``fish``: FISH, FISH_RAW and EGG. Max 5. - ``food``: FOOD + ``food``: FOOD. Max 20. - ``meat``: MEAT + ``meat``: MEAT. Max 5. - ``parts``: CORPSEPIECE + ``parts``: CORPSEPIECE. Max 1. - ``plant``: PLANT and PLANT_GROWTH + ``plant``: PLANT and PLANT_GROWTH. Max 5. - ``powders``: POWDERS_MISC + ``powders``: POWDERS_MISC. Max 10. - ``seed``: SEEDS + ``seed``: SEEDS. Max 1. ``-q``, ``--quiet`` Only print changes instead of a summary of all processed stockpiles. +``-u``, ``--unlimited-stack`` + Use unlimited stack size (Armok). Default false. + ``-v``, ``--verbose n`` Print verbose output, n from 1 to 4. @@ -84,7 +87,4 @@ The following categories are defined: 2. Items that have an associated race/caste, grouped by item type, race, and caste 3. Ammo, grouped by ammo type, material, and quality. If the ammo is a masterwork, it is also grouped by who created it. 4. Anything else, grouped by item type and material - -Each category has a default stack size of 30 unless a larger stack already -exists "naturally" in your fort. In that case the largest existing stack size -is used. + \ No newline at end of file From 1105e29104b7527445520c569d29144faf66a6d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 19 Nov 2023 11:30:19 +0000 Subject: [PATCH 686/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- combine.lua | 4 ++-- docs/combine.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/combine.lua b/combine.lua index 71639bab07..376f54cd56 100644 --- a/combine.lua +++ b/combine.lua @@ -85,7 +85,7 @@ local function comp_item_new(comp_key, max_stack_qty) -- item info comp_item.items = CList:new(nil) -- key:item.id, -- val:{item, base_weight, density, - -- before_stack_qty, after_stack_qty, + -- before_stack_qty, after_stack_qty, -- before_cont_id, after_cont_id, -- before_volume, after_volume, -- stockpile_id, stockpile_name, @@ -467,7 +467,7 @@ local function unlimited_stacks(types) -- Override the realistic limits of stack sizes. This is armok behaviour for those that want it. -- Note: large stack sizes causes bottle neck effects for production of constructed items. -- Need to determine how this interacts with container capacities. - -- There are certain types such as seeds that don't like being in stacks + -- There are certain types such as seeds that don't like being in stacks -- greater than 1, so exclude these ones. log(4, 'Unlimited stacks applied\n') diff --git a/docs/combine.rst b/docs/combine.rst index 67cb16100e..843f1b0dbb 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -87,4 +87,4 @@ The following categories are defined: 2. Items that have an associated race/caste, grouped by item type, race, and caste 3. Ammo, grouped by ammo type, material, and quality. If the ammo is a masterwork, it is also grouped by who created it. 4. Anything else, grouped by item type and material - \ No newline at end of file + From 1b28493bc2c641163e9ada7a212a825b78aacc98 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:44:36 +0000 Subject: [PATCH 687/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/combine.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/combine.rst b/docs/combine.rst index 594463d058..928a9611ed 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -82,4 +82,4 @@ The following categories are defined: 2. Items that have an associated race/caste, grouped by item type, race, and caste 3. Ammo, grouped by ammo type, material, and quality. If the ammo is a masterwork, it is also grouped by who created it. 4. Anything else, grouped by item type and material. - \ No newline at end of file + From 6ad7b44c82a8ea7093e617a90007cb4e15801861 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:46:12 +0000 Subject: [PATCH 688/732] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/combine.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/combine.rst b/docs/combine.rst index 928a9611ed..32d5036d2e 100644 --- a/docs/combine.rst +++ b/docs/combine.rst @@ -82,4 +82,3 @@ The following categories are defined: 2. Items that have an associated race/caste, grouped by item type, race, and caste 3. Ammo, grouped by ammo type, material, and quality. If the ammo is a masterwork, it is also grouped by who created it. 4. Anything else, grouped by item type and material. - From da5f81a0eeee209e1341798c2470628f542cb2f4 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 20 Nov 2023 15:42:23 -0800 Subject: [PATCH 689/732] Update combine.lua --- combine.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/combine.lua b/combine.lua index f45acf660a..cee95fcd47 100644 --- a/combine.lua +++ b/combine.lua @@ -17,7 +17,7 @@ local opts, args = { local MAX_CONT_ITEMS=30 -- TODO: --- 1. Combine plantable seeds only, currently removed seeds as option. +-- 1. Combine non-plantable seeds only, currently removed seeds as option. -- 2. Quality for food, currently ignoring. -- 3. Obey capacity limits for containers as default. Container quality? -- 4. Override stack size; armok option. From 9931992f102de553d75e1c9b3d99f6d18994479b Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 20 Nov 2023 15:44:24 -0800 Subject: [PATCH 690/732] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 84329fa0c4..3626793bae 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `combine`: prevent stack sizes from growing beyond quantities that you would normally see in vanilla gameplay ## Removed From 759ef349788d65087ea1b39dc391d70bd55ed808 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 20 Nov 2023 15:55:08 -0800 Subject: [PATCH 691/732] allow searching within containers in trade screen --- internal/caravan/common.lua | 9 +++++++++ internal/caravan/trade.lua | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 88527c3a1f..dcd652365d 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -25,6 +25,15 @@ function make_search_key(str) return table.concat(words, ' ') end +function make_container_search_key(item, desc) + local words = {} + add_words(words, desc) + for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do + add_words(words, get_item_description(contained_item)) + end + return table.concat(words, ' ') +end + local function get_broker_skill() local broker = dfhack.units.getUnitByNobleRole('broker') if not broker then return 0 end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 6d5f3a8d89..ded4a7c716 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -398,13 +398,19 @@ function Trade:cache_choices(list_idx, trade_bins) parent_data.has_requested = parent_data.has_requested or is_requested parent_data.ethical = parent_data.ethical and is_ethical end + local is_container = df.item_binst:is_instance(item) + local search_key + if (trade_bins and is_container) or item:isFoodStorage() then + search_key = common.make_container_search_key(item, desc) + else + search_key = common.make_search_key(desc) + end local choice = { - search_key=common.make_search_key(desc), + search_key=search_key, icon=curry(get_entry_icon, data), data=data, text=make_choice_text(data.value, desc), } - local is_container = df.item_binst:is_instance(item) if not data.update_container_fn then table.insert(trade_bins_choices, choice) end From 633cd7eb3bafc8acba00057c1691c6a617ffb744 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 20 Nov 2023 16:21:06 -0800 Subject: [PATCH 692/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 20931ce2d3..46a791c4a9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Misc Improvements - `combine`: prevent stack sizes from growing beyond quantities that you would normally see in vanilla gameplay +- `caravan`: enable searching within containers in trade screen when in "trade bin with contents" mode ## Removed From eb73ccaa7d336a5bd18b67d37efa6d1aca5368b3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Nov 2023 11:20:32 -0800 Subject: [PATCH 693/732] bump changelog to 50.11-r4 --- changelog.txt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 46a791c4a9..57a4eaca74 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,18 +27,26 @@ Template for new versions: # Future ## New Tools -- `build-now`: (reinstated) instantly complete unsuspended buildings that are ready to be built ## New Features ## Fixes ## Misc Improvements -- `combine`: prevent stack sizes from growing beyond quantities that you would normally see in vanilla gameplay -- `caravan`: enable searching within containers in trade screen when in "trade bin with contents" mode ## Removed +# 50.11-r4 + +## New Tools +- `build-now`: (reinstated) instantly complete unsuspended buildings that are ready to be built + +## Fixes +- `combine`: prevent stack sizes from growing beyond quantities that you would normally see in vanilla gameplay + +## Misc Improvements +- `caravan`: enable searching within containers in trade screen when in "trade bin with contents" mode + # 50.11-r3 ## New Tools From 7b12b1fcaa540b5dcb354fe00ac6f9cc10dcbc95 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sun, 26 Nov 2023 00:31:03 -0600 Subject: [PATCH 694/732] Ignore all fruit gatherers when checking stranded --- warn-stranded.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index b68f80ead8..36113730f2 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -333,8 +333,9 @@ local function getStrandedUnits() or getWalkGroup(xyz2pos(unitPos.x+1, unitPos.y+1, unitPos.z)) or 0 end - - if unitIgnored(unit) then + + -- Ignore units who are gathering plants to avoid errors with stepladders + if unitIgnored(unit) or unit.job.current_job.job_type == df.job_type.GatherPlants then table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) From 29fec9086dc081e00371214f41d41bdc7d002a6d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Nov 2023 03:11:10 -0800 Subject: [PATCH 695/732] allow map movement keys to work --- gui/pathable.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/pathable.lua b/gui/pathable.lua index 868ce5d7f6..2e31583053 100644 --- a/gui/pathable.lua +++ b/gui/pathable.lua @@ -8,6 +8,7 @@ local widgets = require('gui.widgets') Pathable = defclass(Pathable, gui.ZScreen) Pathable.ATTRS{ focus_path='pathable', + pass_movement_keys=true, } function Pathable:init() From dbe79480fd048751335ab2e52257f683aa541c52 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Nov 2023 09:18:45 -0800 Subject: [PATCH 696/732] reduce frequency of warn-stranded check --- changelog.txt | 1 + gui/control-panel.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 57a4eaca74..ce9ac65c9e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `gui/control-panel`: reduce frequency for `warn-stranded` check to once every 2 days ## Removed diff --git a/gui/control-panel.lua b/gui/control-panel.lua index e92b004a84..d90c617c7b 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -140,7 +140,7 @@ local REPEATS = { command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, ['warn-stranded']={ desc='Show a warning dialog when units are stranded from all others.', - command={'--time', '0.25', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, + command={'--time', '2', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, } local REPEATS_LIST = {} for k in pairs(REPEATS) do From 2405d542025ec37419f8f468d3e1df865c4bd9ae Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 27 Nov 2023 23:15:13 -0600 Subject: [PATCH 697/732] Fix null error when unit has no job --- warn-stranded.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 36113730f2..89286be508 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -333,9 +333,9 @@ local function getStrandedUnits() or getWalkGroup(xyz2pos(unitPos.x+1, unitPos.y+1, unitPos.z)) or 0 end - + -- Ignore units who are gathering plants to avoid errors with stepladders - if unitIgnored(unit) or unit.job.current_job.job_type == df.job_type.GatherPlants then + if unitIgnored(unit) or (unit.job.current_job and unit.job.current_job.job_type == df.job_type.GatherPlants) then table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) From f60bf85a01bf68b71f810173924dff464af3b655 Mon Sep 17 00:00:00 2001 From: Droseran <97368320+Droseran@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:36:18 -0500 Subject: [PATCH 698/732] Update ban-cooking.lua The creature ID is currently NIL due to being an uninitialized variable, causing ban-cooking to error if a creature alcohol has the [EDIBLE_COOKED] token. This replaces the uninitialized variable with the one containing the creature ID. --- ban-cooking.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ban-cooking.lua b/ban-cooking.lua index eca6b63c57..0a672834ba 100644 --- a/ban-cooking.lua +++ b/ban-cooking.lua @@ -90,7 +90,7 @@ funcs.booze = function() for _, c in ipairs(df.global.world.raws.creatures.all) do for _, m in ipairs(c.material) do if m.flags.ALCOHOL and m.flags.EDIBLE_COOKED then - local matinfo = dfhack.matinfo.find(creature_id.id, m.id) + local matinfo = dfhack.matinfo.find(c.creature_id, m.id) ban_cooking(c.name[2] .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.DRINK, -1) end end From 378156be0c4b043816d0036d9d6d048e573fbc0b Mon Sep 17 00:00:00 2001 From: Droseran <97368320+Droseran@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:46:39 -0500 Subject: [PATCH 699/732] Update changelog.txt Added ban-cooking fix to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index ce9ac65c9e..2909fae9a3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `ban-cooking`: fix banning creature alcohols resulting in error ## Misc Improvements - `gui/control-panel`: reduce frequency for `warn-stranded` check to once every 2 days From dafeb3596488f8ae05cda4b41504222143e7d8fd Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 28 Nov 2023 23:28:31 -0600 Subject: [PATCH 700/732] Add fix to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 57a4eaca74..528ccc668c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `warn-stranded`: Automatically ignore citizens who are gathering plants to avoid issues with gathering fruit via stepladders ## Misc Improvements From a5a99bbeebddd42e4d460a7207b033b32ef67e44 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Wed, 29 Nov 2023 23:41:09 -0600 Subject: [PATCH 701/732] Add digging to list of auto ignored jobs --- warn-stranded.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 89286be508..c98573ee08 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -334,8 +334,10 @@ local function getStrandedUnits() or 0 end - -- Ignore units who are gathering plants to avoid errors with stepladders - if unitIgnored(unit) or (unit.job.current_job and unit.job.current_job.job_type == df.job_type.GatherPlants) then + -- Ignore units who are gathering plants or digging to avoid errors with stepladders and weird digging things + if unitIgnored(unit) or (unit.job.current_job and + (unit.job.current_job.job_type == df.job_type.GatherPlants or + df.job_type.attrs[unit.job.current_job.job_type].type == 'Digging')) then table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) From 96471b8f53d06a52f0737d447dcebce5e4b75833 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Wed, 29 Nov 2023 23:44:03 -0600 Subject: [PATCH 702/732] Change onZoom to pass boolean to center --- changelog.txt | 3 ++- warn-stranded.lua | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 528ccc668c..24c1816190 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,7 +31,8 @@ Template for new versions: ## New Features ## Fixes -- `warn-stranded`: Automatically ignore citizens who are gathering plants to avoid issues with gathering fruit via stepladders +- `warn-stranded`: Automatically ignore citizens who are gathering plants or digging to avoid issues with gathering fruit via stepladders and weird issues with digging +- `warn-stranded`: Update onZoom to use df's centering functionality ## Misc Improvements diff --git a/warn-stranded.lua b/warn-stranded.lua index c98573ee08..1b6e8c438f 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -269,7 +269,7 @@ function WarningWindow:onZoom() local unit = choice.data['unit'] local target = xyz2pos(dfhack.units.getPosition(unit)) - dfhack.gui.revealInDwarfmodeMap(target, false, true) + dfhack.gui.revealInDwarfmodeMap(target, true, true) end function WarningWindow:onToggleGroup() From b17acef82aca2128c184799d50445fc29b26044a Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Wed, 29 Nov 2023 23:56:01 -0600 Subject: [PATCH 703/732] Run pre-commit --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 1b6e8c438f..eb6da3c997 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -335,7 +335,7 @@ local function getStrandedUnits() end -- Ignore units who are gathering plants or digging to avoid errors with stepladders and weird digging things - if unitIgnored(unit) or (unit.job.current_job and + if unitIgnored(unit) or (unit.job.current_job and (unit.job.current_job.job_type == df.job_type.GatherPlants or df.job_type.attrs[unit.job.current_job.job_type].type == 'Digging')) then table.insert(ensure_key(ignoredGroup, walkGroup), unit) From 606d4be52806fc78445c824bc36d2bebc56e1026 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:28:11 +0000 Subject: [PATCH 704/732] [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.27.1 → 0.27.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.1...0.27.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 e3dd5636d3..73f2198492 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.27.1 + rev: 0.27.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 818b52c02457160b0a15050deff20cba70d83671 Mon Sep 17 00:00:00 2001 From: Christian Doczkal Date: Wed, 6 Dec 2023 17:40:47 +0100 Subject: [PATCH 705/732] [gui/blueprint] resolve duplicate key assignment --- gui/blueprint.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/blueprint.lua b/gui/blueprint.lua index f05d4bada5..8e2a58d4f4 100644 --- a/gui/blueprint.lua +++ b/gui/blueprint.lua @@ -241,7 +241,7 @@ function StartPosPanel:init() self:addviews{ widgets.CycleHotkeyLabel{ view_id='startpos', - key='CUSTOM_P', + key='CUSTOM_S', label='playback start', options={'Unset', 'Setting', 'Set'}, initial_option=self.start_pos and 'Set' or 'Unset', From 6c110aad9ea0899e1a94ac615a058061ad2d04d8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 29 Dec 2023 16:35:28 -0800 Subject: [PATCH 706/732] fix typo in protect-nicks docs --- docs/fix/protect-nicks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fix/protect-nicks.rst b/docs/fix/protect-nicks.rst index 0b00e94e7c..ead51cc42a 100644 --- a/docs/fix/protect-nicks.rst +++ b/docs/fix/protect-nicks.rst @@ -2,7 +2,7 @@ fix/protect-nicks ================= .. dfhack-tool:: - :summary: Fix nicknames being erased or not displayed + :summary: Fix nicknames being erased or not displayed. :tags: fort bugfix units Due to a bug, units nicknames are not displayed everywhere and are occasionally From b27ad1b9042bddf13642b7844acfc1075ba33612 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 10 Dec 2023 19:46:40 -0800 Subject: [PATCH 707/732] create registry of commands --- gui/control-panel.lua | 471 ++++++++++++++++++++++++------------------ 1 file changed, 268 insertions(+), 203 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index d90c617c7b..e4a7812509 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -1,9 +1,9 @@ local dialogs = require('gui.dialogs') local gui = require('gui') -local textures = require('gui.textures') local helpdb = require('helpdb') local overlay = require('plugins.overlay') local repeatUtil = require('repeat-util') +local textures = require('gui.textures') local utils = require('utils') local widgets = require('gui.widgets') @@ -13,60 +13,82 @@ local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferenc local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' --- service and command lists -local FORT_SERVICES = { - 'autobutcher', - 'autochop', - 'autoclothing', - 'autofarm', - 'autofish', - 'autonestbox', - 'autoslab', - 'dwarfvet', - 'emigration', - 'fastdwarf', - 'fix/protect-nicks', - 'hermit', - 'misery', - 'nestboxes', - 'preserve-tombs', - 'prioritize', - 'seedwatch', - 'starvingdead', - 'suspendmanager', - 'tailor', -} - -local FORT_AUTOSTART = { - 'autobutcher target 10 10 14 2 BIRD_GOOSE', - 'autobutcher target 10 10 14 2 BIRD_TURKEY', - 'autobutcher target 10 10 14 2 BIRD_CHICKEN', - 'autofarm threshold 150 grass_tail_pig', - 'ban-cooking all', - 'buildingplan set boulders false', - 'buildingplan set logs false', - 'drain-aquifer --top 2', - 'fix/blood-del fort', - 'light-aquifers-only fort', -} -for _,v in ipairs(FORT_SERVICES) do - table.insert(FORT_AUTOSTART, v) -end -table.sort(FORT_AUTOSTART) +local REGISTRY = { + -- automation tools + {command='autobutcher', tab='automation', mode='enable'}, + {command='autobutcher target 10 10 14 2 BIRD_GOOSE', tab='automation', mode='run'}, + {command='autobutcher target 10 10 14 2 BIRD_TURKEY', tab='automation', mode='run'}, + {command='autobutcher target 10 10 14 2 BIRD_CHICKEN', tab='automation', mode='run'}, + {command='autochop', tab='automation', mode='enable'}, + {command='autoclothing', tab='automation', mode='enable'}, + {command='autofarm', tab='automation', mode='enable'}, + {command='autofarm threshold 150 grass_tail_pig', tab='automation', mode='run'}, + {command='autofish', tab='automation', mode='enable'}, + --{command='autolabor', tab='automation', mode='enable'}, -- hide until it works better + {command='automilk', tab='automation', mode='repeat', + desc='Automatically milk creatures that are ready for milking.', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', '"{\\"job\\":\\"MilkCreature\\",\\"item_conditions\\":[{\\"condition\\":\\"AtLeast\\",\\"value\\":2,\\"flags\\":[\\"empty\\"],\\"item_type\\":\\"BUCKET\\"}]}"', ']'}}, + {command='autonestbox', tab='automation', mode='enable'}, + {command='autoshear', tab='automation', mode='repeat', + desc='Automatically shear creatures that are ready for shearing.', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, + {command='autoslab', tab='automation', mode='enable'}, + {command='ban-cooking all', tab='automation', mode='run'}, + {command='buildingplan set boulders false', tab='automation', mode='run'}, + {command='buildingplan set logs false', tab='automation', mode='run'}, + {command='cleanowned', tab='automation', mode='repeat', + desc='Encourage dwarves to drop tattered clothing and grab new ones.', + params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, + {command='nestboxes', tab='automation', mode='enable'}, + {command='orders-sort', tab='automation', mode='repeat', + desc='Sort manager orders by repeat frequency so one-time orders can be completed.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, + {command='prioritize', tab='automation', mode='enable'}, + {command='seedwatch', tab='automation', mode='enable'}, + {command='suspendmanager', tab='automation', mode='enable'}, + {command='tailor', tab='automation', mode='enable'}, + {command='work-now', tab='automation', mode='enable'}, + + -- bugfix tools + {command='dead-units-burrow', tab='bugfix', mode='repeat', default=true, + desc='Fix units still being assigned to burrows after death.', + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, + {command='fix/blood-del', tab='bugfix', mode='run', default=true}, + {command='fix/empty-wheelbarrows', tab='bugfix', mode='repeat', default=true, + desc='Make abandoned full wheelbarrows usable again.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, + {command='fix/general-strike', tab='bugfix', mode='repeat', default=true, + desc='Prevent dwarves from getting stuck and refusing to work.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, + {command='fix/protect-nicks', tab='bugfix', mode='enable', default=true}, + {command='fix/stuck-instruments', tab='bugfix', mode='repeat', default=true, + desc='Fix activity references on stuck instruments to make them usable again.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, + {command='preserve-tombs', tab='bugfix', mode='enable', default=true}, --- these are re-enabled by the default DFHack init scripts -local SYSTEM_SERVICES = { -} --- these are fully controlled by the user -local SYSTEM_USER_SERVICES = { - 'faststart', - 'hide-tutorials', - 'work-now', + -- gameplay tools + {command='combine', tab='gameplay', mode='repeat', + desc='Combine partial stacks in stockpiles into full stacks.', + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, + {command='drain-aquifer --top 2', tab='gameplay', mode='run'}, + {command='dwarfvet', tab='gameplay', mode='enable'}, + {command='emigration', tab='gameplay', mode='enable'}, + {command='fastdwarf', tab='gameplay', mode='enable'}, + {command='hermit', tab='gameplay', mode='enable'}, + {command='hide-tutorials', tab='gameplay', mode='system_enable'}, + {command='light-aquifers-only', tab='gameplay', mode='run'}, + {command='misery', tab='gameplay', mode='enable'}, + {command='orders-reevaluate', tab='gameplay', mode='repeat', + desc='Invalidates work orders once a month, allowing conditions to be rechecked.', + params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, + {command='starvingdead', tab='gameplay', mode='enable'}, + {command='warn-starving', tab='gameplay', mode='repeat', + desc='Show a warning dialog when units are starving or dehydrated.', + params={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, + {command='warn-stranded', tab='gameplay', mode='repeat', + desc='Show a warning dialog when units are stranded from all others.', + params={'--time', '2', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, } -for _,v in ipairs(SYSTEM_USER_SERVICES) do - table.insert(SYSTEM_SERVICES, v) -end -table.sort(SYSTEM_SERVICES) local PREFERENCES = { ['dfhack']={ @@ -104,49 +126,25 @@ local CPP_PREFERENCES = { }, } -local REPEATS = { - ['autoMilkCreature']={ - desc='Automatically milk creatures that are ready for milking.', - command={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', '"{\\"job\\":\\"MilkCreature\\",\\"item_conditions\\":[{\\"condition\\":\\"AtLeast\\",\\"value\\":2,\\"flags\\":[\\"empty\\"],\\"item_type\\":\\"BUCKET\\"}]}"', ']'}}, - ['autoShearCreature']={ - desc='Automatically shear creatures that are ready for shearing.', - command={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, - ['cleanowned']={ - desc='Encourage dwarves to drop tattered clothing and grab new ones.', - command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, - ['combine']={ - desc='Combine partial stacks in stockpiles into full stacks.', - command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, - ['dead-units-burrow']={ - desc='Fix units still being assigned to burrows after death.', - command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, - ['empty-wheelbarrows']={ - desc='Empties wheelbarrows which have rocks stuck in them.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, - ['general-strike']={ - desc='Prevent dwarves from getting stuck and refusing to work.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, - ['orders-sort']={ - desc='Sort manager orders by repeat frequency so one-time orders can be completed.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, - ['orders-reevaluate']={ - desc='Invalidates work orders once a month, allowing conditions to be rechecked.', - command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, - ['stuck-instruments']={ - desc='Fix activity references on stuck instruments to make them usable again.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, - ['warn-starving']={ - desc='Show a warning dialog when units are starving or dehydrated.', - command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, - ['warn-stranded']={ - desc='Show a warning dialog when units are stranded from all others.', - command={'--time', '2', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, -} -local REPEATS_LIST = {} -for k in pairs(REPEATS) do - table.insert(REPEATS_LIST, k) +local function read_init_file(fname, config_map, matchers) + local ok, f = pcall(io.open, SYSTEM_INIT_FILE) + if not ok or not f then return end + for line in f:lines() do + line = line:trim() + if #line == 0 or (line:startswith('#') and not line:startswith('##')) then + goto continue + end + local negate, service + for _, matcher in ipairs(matchers) do + negate, service = line:match(matcher) + if service then + config_map[service] = #negate == 0 + break + end + end + ::continue:: + end end -table.sort(REPEATS_LIST) -- save_fn takes the file as a param and should call f:write() to write data local function save_file(path, save_fn) @@ -157,11 +155,75 @@ local function save_file(path, save_fn) return end f:write('# DO NOT EDIT THIS FILE\n') - f:write('# Please use gui/control-panel to edit this file\n\n') + f:write('# Please use gui/control-panel to modify the contents of this file\n\n') save_fn(f) f:close() end +local function write_init_files(config_map) + save_file(SYSTEM_INIT_FILE, function(f) + for _,data in ipairs(REGISTRY) do + if data.mode ~= 'system_enable' then goto continue end + local command = data.command + local prefix = config_map[command] and '' or '##' + f:write(('%senable %s\n'):format(prefix, command)) + ::continue:: + end + end) + save_file(AUTOSTART_FILE, function(f) + for _,data in ipairs(REGISTRY) do + if data.mode == 'system_enable' or data.mode == 'repeat' then + goto continue + end + local command = data.command + local prefix = config_map[command] and '' or '##' + if data.mode == 'run' then + f:write(('%son-new-fortress %s\n'):format(prefix, command)) + elseif data.mode == 'enable' + f:write(('%son-new-fortress enable %s\n'):format(prefix, command)) + else + error('unhandled mode: '.. data.mode) + end + ::continue:: + end + end) + save_file(REPEATS_FILE, function(f) + for _,data in ipairs(REGISTRY) do + if data.mode ~= 'repeat' then goto continue end + local command = data.command + local prefix = config_map[command] and '' or '##' + local command_str = ('%srepeat --name %s %s\n'): + format(prefix, command, table.concat(data.params, ' ')) + f:write(command_str) + ::continue:: + end + end) +end + +local function init_config_state() + local config_map = {} + read_init_file(SYSTEM_INIT_FILE, config_map, { + '^(#?#?)enable ([%S]+)$', + }) + read_init_file(AUTOSTART_FILE, config_map, { + '^(#?#?)on%-new%-fortress enable ([%S]+)$', + '^(#?#?)on%-new%-fortress (.+)', + }) + read_init_file(REPEATS_FILE, config_map, { + '^(#?#?)repeat %-%-name ([%S]+)', + }) + + for _, data in ipairs(REGISTRY) do + if data.default and config_map[data.command] == nil then + config_map[data.command] = true + end + end + + write_init_files(config_map) + + return config_map +end + local function get_icon_pens() local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} @@ -508,6 +570,102 @@ function SystemServices:on_submit() save_file(SYSTEM_INIT_FILE, save_fn) end +-- +-- RepeatAutostart +-- + +RepeatAutostart = defclass(RepeatAutostart, ConfigPanel) +RepeatAutostart.ATTRS{ + title='Periodic', + is_enableable=true, + is_configurable=false, + intro_text='Tools that can run periodically to fix bugs or warn you of'.. + ' dangers that are otherwise difficult to detect (like'.. + ' starving caged animals).', +} + +function RepeatAutostart:init() + self.subviews.show_help_label.visible = false + self.subviews.launch.visible = false + local enabled_map = {} + local ok, f = pcall(io.open, REPEATS_FILE) + if ok and f then + for line in f:lines() do + line = line:trim() + if #line == 0 or line:startswith('#') then goto continue end + local service = line:match('^repeat %-%-name ([%S]+)') + if service then + enabled_map[service] = true + end + ::continue:: + end + end + self.enabled_map = enabled_map +end + +function RepeatAutostart:onInput(keys) + -- call grandparent's onInput since we don't want ConfigPanel's processing + local handled = RepeatAutostart.super.super.onInput(self, keys) + if keys._MOUSE_L then + local list = self.subviews.list.list + local idx = list:getIdxUnderMouse() + if idx then + local x = list:getMousePos() + if x <= 2 then + self:on_submit() + end + end + end + return handled +end + +function RepeatAutostart:refresh() + local choices = {} + for _,name in ipairs(REPEATS_LIST) do + local enabled = self.enabled_map[name] + local text = { + {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, + {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, + {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, + ' ', + name, + } + table.insert(choices, + {text=text, desc=REPEATS[name].desc, search_key=name, + name=name, enabled=enabled}) + end + local list = self.subviews.list + local filter = list:getFilter() + local selected = list:getSelected() + list:setChoices(choices) + list:setFilter(filter, selected) + list.edit:setFocus(true) +end + +function RepeatAutostart:on_submit() + _,choice = self.subviews.list:getSelected() + if not choice then return end + self.enabled_map[choice.name] = not choice.enabled + local run_commands = dfhack.isMapLoaded() + + local save_fn = function(f) + for name,enabled in pairs(self.enabled_map) do + if enabled then + local command_str = ('repeat --name %s %s\n'): + format(name, table.concat(REPEATS[name].command, ' ')) + f:write(command_str) + if run_commands then + dfhack.run_command(command_str) -- actually start it up too + end + elseif run_commands then + repeatUtil.cancel(name) + end + end + end + save_file(REPEATS_FILE, save_fn) + self:refresh() +end + -- -- Overlays -- @@ -518,7 +676,7 @@ Overlays.ATTRS{ is_enableable=true, is_configurable=false, intro_text='These are DFHack overlays that add information and'.. - ' functionality to various DF screens.', + ' functionality to vanilla screens.', } function Overlays:init() @@ -756,103 +914,7 @@ function Preferences:restore_defaults() end os.remove(PREFERENCES_INIT_FILE) self:refresh() - dialogs.showMessage('Success', 'Default preferences restored.') -end - --- --- RepeatAutostart --- - -RepeatAutostart = defclass(RepeatAutostart, ConfigPanel) -RepeatAutostart.ATTRS{ - title='Periodic', - is_enableable=true, - is_configurable=false, - intro_text='Tools that can run periodically to fix bugs or warn you of'.. - ' dangers that are otherwise difficult to detect (like'.. - ' starving caged animals).', -} - -function RepeatAutostart:init() - self.subviews.show_help_label.visible = false - self.subviews.launch.visible = false - local enabled_map = {} - local ok, f = pcall(io.open, REPEATS_FILE) - if ok and f then - for line in f:lines() do - line = line:trim() - if #line == 0 or line:startswith('#') then goto continue end - local service = line:match('^repeat %-%-name ([%S]+)') - if service then - enabled_map[service] = true - end - ::continue:: - end - end - self.enabled_map = enabled_map -end - -function RepeatAutostart:onInput(keys) - -- call grandparent's onInput since we don't want ConfigPanel's processing - local handled = RepeatAutostart.super.super.onInput(self, keys) - if keys._MOUSE_L then - local list = self.subviews.list.list - local idx = list:getIdxUnderMouse() - if idx then - local x = list:getMousePos() - if x <= 2 then - self:on_submit() - end - end - end - return handled -end - -function RepeatAutostart:refresh() - local choices = {} - for _,name in ipairs(REPEATS_LIST) do - local enabled = self.enabled_map[name] - local text = { - {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, - {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, - {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, - ' ', - name, - } - table.insert(choices, - {text=text, desc=REPEATS[name].desc, search_key=name, - name=name, enabled=enabled}) - end - local list = self.subviews.list - local filter = list:getFilter() - local selected = list:getSelected() - list:setChoices(choices) - list:setFilter(filter, selected) - list.edit:setFocus(true) -end - -function RepeatAutostart:on_submit() - _,choice = self.subviews.list:getSelected() - if not choice then return end - self.enabled_map[choice.name] = not choice.enabled - local run_commands = dfhack.isMapLoaded() - - local save_fn = function(f) - for name,enabled in pairs(self.enabled_map) do - if enabled then - local command_str = ('repeat --name %s %s\n'): - format(name, table.concat(REPEATS[name].command, ' ')) - f:write(command_str) - if run_commands then - dfhack.run_command(command_str) -- actually start it up too - end - elseif run_commands then - repeatUtil.cancel(name) - end - end - end - save_file(REPEATS_FILE, save_fn) - self:refresh() + dialogs.showMessage('Success', 'Default preference settings restored.') end -- @@ -874,12 +936,11 @@ function ControlPanel:init() widgets.TabBar{ frame={t=0}, labels={ - 'Fort', - 'Maintenance', - 'System', - 'Overlays', + 'Automation', + 'Bugfixes', + 'Gameplay', + 'UI Overlays', 'Preferences', - 'Autostart', }, on_select=self:callback('set_page'), get_cur_page=function() return self.subviews.pages:getSelected() end, @@ -888,12 +949,11 @@ function ControlPanel:init() view_id='pages', frame={t=5, l=0, b=0, r=0}, subviews={ - FortServices{}, - RepeatAutostart{}, - SystemServices{}, + Automation{}, + Bugfixes{}, + Gameplay{}, Overlays{}, Preferences{}, - FortServicesAutostart{}, }, }, } @@ -928,4 +988,9 @@ function ControlPanelScreen:onDismiss() view = nil end +if not view and ({...})[1] == '--check-defaults' then + init_config_state() + return +end + view = view and view:raise() or ControlPanelScreen{}:show() From 08fb6dcd28321805cd3f0fef7de7e025143d9602 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 19 Dec 2023 18:15:46 -0800 Subject: [PATCH 708/732] alphabetize --- gui/control-panel.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index e4a7812509..39e838f7df 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -50,10 +50,10 @@ local REGISTRY = { {command='work-now', tab='automation', mode='enable'}, -- bugfix tools - {command='dead-units-burrow', tab='bugfix', mode='repeat', default=true, + {command='fix/blood-del', tab='bugfix', mode='run', default=true}, + {command='fix/dead-units', tab='bugfix', mode='repeat', default=true, desc='Fix units still being assigned to burrows after death.', params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, - {command='fix/blood-del', tab='bugfix', mode='run', default=true}, {command='fix/empty-wheelbarrows', tab='bugfix', mode='repeat', default=true, desc='Make abandoned full wheelbarrows usable again.', params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, @@ -179,7 +179,7 @@ local function write_init_files(config_map) local prefix = config_map[command] and '' or '##' if data.mode == 'run' then f:write(('%son-new-fortress %s\n'):format(prefix, command)) - elseif data.mode == 'enable' + elseif data.mode == 'enable' then f:write(('%son-new-fortress enable %s\n'):format(prefix, command)) else error('unhandled mode: '.. data.mode) From 13ed3b12af1f927c7d2f29bb7d43ce86320a139a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 29 Dec 2023 16:34:44 -0800 Subject: [PATCH 709/732] implement control panel cli --- control-panel.lua | 273 +++++++++++++++++++++++++++ docs/control-panel.rst | 69 +++++++ gui/control-panel.lua | 5 - internal/control-panel/common.lua | 97 ++++++++++ internal/control-panel/migration.lua | 15 ++ internal/control-panel/registry.lua | 172 +++++++++++++++++ 6 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 control-panel.lua create mode 100644 docs/control-panel.rst create mode 100644 internal/control-panel/common.lua create mode 100644 internal/control-panel/migration.lua create mode 100644 internal/control-panel/registry.lua diff --git a/control-panel.lua b/control-panel.lua new file mode 100644 index 0000000000..436db1c397 --- /dev/null +++ b/control-panel.lua @@ -0,0 +1,273 @@ +--@module = true + +local argparse = require('argparse') +local common = reqscript('internal/control-panel/common') +local helpdb = require('helpdb') +local json = require('json') +local persist = require('persist-table') +local registry = reqscript('internal/control-panel/registry') +local utils = require('utils') + +local GLOBAL_KEY = 'control-panel' + +-- state change hooks + +local function apply_system_config() + local enabled_map =common.get_enabled_map() + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'system_enable' then + common.apply(data, enabled_map) + end + end + for _, data in ipairs(registry.PREFERENCES_BY_IDX) do + local value = safe_index(config.data.preferences, data.name, 'val') + if value ~= nil then + data.set_fn(value) + end + end +end + +local function apply_autostart_config() + local enabled_map =common.get_enabled_map() + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'enable' or data.mode == 'run' then + common.apply(data, enabled_map) + end + end +end + +local function apply_fort_loaded_config() + if not safe_index(json.decode(persist.GlobalTable[GLOBAL_KEY] or ''), 'autostart_done') then + apply_autostart_config() + persist.GlobalTable[GLOBAL_KEY] = json.encode({autostart_done=true}) + end + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'repeat' then + common.apply(data) + end + end +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_CORE_INITIALIZED then + apply_system_config() + elseif sc == SC_MAP_LOADED and dfhack.world.isFortressMode() then + apply_fort_loaded_config() + end +end + + +-- CLI + +local function print_header(header) + print() + print(header) + print(('-'):rep(#header)) +end + +local function get_first_word(str) + local word = str:trim():split(' +')[1] + if word:startswith(':') then word = word:sub(2) end + return word +end + +local function command_passes_filters(data, target_group, first_word, filter_strs) + if data.group ~= target_group then + return false + end + if dfhack.getHideArmokTools() and helpdb.is_entry(first_word) + and helpdb.get_entry_tags(first_word).armok + then + return false + end + if not utils.search_text(data.command, filter_strs) then + return false + end + return true +end + +local function list_command_group(group, filter_strs, enabled_map) + local header = ('Group: %s'):format(group) + for idx, data in ipairs(registry.COMMANDS_BY_IDX) do + local first_word = get_first_word(data.command) + if not command_passes_filters(data, group, first_word, filter_strs) then + goto continue + end + if header then + print_header(header) + ---@diagnostic disable-next-line: cast-local-type + header = nil + end + local extra = '' + if data.mode == 'system_enable' then + extra = ' (global)' + end + print(('%d) %s%s'):format(idx, data.command, extra)) + local desc = data.desc or + (helpdb.is_entry(first_word) and helpdb.get_entry_short_help(first_word)) + if desc then + print((' %s'):format(desc)) + end + local default_value = not not data.default + local current_value = safe_index(config.data.commands, data.command, 'autostart') + if current_value == nil then + current_value = default_value + end + print((' autostart enabled: %s (default: %s)'):format(current_value, default_value)) + if enabled_map[data.command] ~= nil then + print((' currently enabled: %s'):format(enabled_map[data.command])) + end + print() + ::continue:: + end + if not header then + end +end + +local function list_preferences(filter_strs) + local header = 'Preferences' + for _, data in ipairs(registry.PREFERENCES_BY_IDX) do + local search_key = ('%s %s %s'):format(data.name, data.label, data.desc) + if not utils.search_text(search_key, filter_strs) then goto continue end + if header then + print_header(header) + ---@diagnostic disable-next-line: cast-local-type + header = nil + end + print(('%s) %s'):format(data.name, data.label)) + print((' %s'):format(data.desc)) + print((' current: %s (default: %s)'):format(data.get_fn(), data.default)) + if data.min then + print((' minimum: %s'):format(data.min)) + end + print() + ::continue:: + end +end + +local function do_list(filter_strs) + local enabled_map =common.get_enabled_map() + list_command_group('automation', filter_strs, enabled_map) + list_command_group('bugfix', filter_strs, enabled_map) + list_command_group('gameplay', filter_strs, enabled_map) + list_preferences(filter_strs) +end + +local function get_command_data(name_or_idx) + if type(name_or_idx) == 'number' then + return registry.COMMANDS_BY_IDX[name_or_idx] + end + return registry.COMMANDS_BY_NAME[name_or_idx] +end + +local function do_enable_disable(which, entries) + local enabled_map =common.get_enabled_map() + for _, entry in ipairs(entries) do + local data = get_command_data(entry) + if common.apply(data, enabled_map, which == 'en') then + print(('%sabled %s'):format(which, entry)) + end + end +end + +local function do_enable(entries) + do_enable_disable('en', entries) +end + +local function do_disable(entries) + do_enable_disable('dis', entries) +end + +local function do_autostart_noautostart(which, entries) + for _, entry in ipairs(entries) do + local data = get_command_data(entry) + if not data then + qerror(('autostart command or index not found: "%s"'):format(entry)) + else + local enabled = which == 'en' + if enabled ~= not not data.default then + config.data.commands[entry].autostart = enabled + else + config.data.commands[entry] = nil + end + print(('%sabled autostart for: %s'):format(which, entry)) + end + end +end + +local function do_autostart(entries) + do_autostart_noautostart('en', entries) +end + +local function do_noautostart(entries) + do_autostart_noautostart('dis', entries) +end + +local function do_set(params) + local name, value = params[1], params[2] + local data = registry.PREFERENCES_BY_NAME[name] + if not data then + qerror(('preference name not found: "%s"'):format(name)) + end + local expected_type = type(data.default) + if expected_type == 'boolean' then + value = argparse.boolean(value) + end + local actual_type = type(value) + if actual_type ~= expected_type then + qerror(('"%s" has an unexpected value type: got: %s; expected: %s'):format( + params[2], actual_type, expected_type)) + end + if data.min and data.min > value then + qerror(('value too small: got: %s; minimum: %s'):format(value, data.min)) + end + data.set_fn(value) + if data.default ~= safe_index(config.data.preferences, name, 'val') then + config.data.preferences[name] = { + val=value, + version=data.version, + } + else + config.data.preferences[name] = nil + end +end + +local function do_reset(params) + local name = params[1] + local data = registry.PREFERENCES_BY_NAME[name] + if not data then + qerror(('preference name not found: "%s"'):format(name)) + end + data.set_fn(data.default) + config.data.preferences[name] = nil +end + +local command_switch = { + list=do_list, + enable=do_enable, + disable=do_disable, + autostart=do_autostart, + noautostart=do_noautostart, + set=do_set, + reset=do_reset, +} + +local function main(args) + local help = false + + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() help = true end}, + }) + + local command = table.remove(positionals, 1) + if help or not command or not command_switch[command] then + print(dfhack.script_help()) + return + end + + command_switch[command](positionals) +end + +if not dfhack_flags.module then + main{...} +end diff --git a/docs/control-panel.rst b/docs/control-panel.rst new file mode 100644 index 0000000000..5d73663ff9 --- /dev/null +++ b/docs/control-panel.rst @@ -0,0 +1,69 @@ +control-panel +============= + +.. dfhack-tool:: + :summary: Configure DFHack and manage active DFHack tools. + :tags: dfhack + +This is the commandline interface for configuring DFHack behavior, toggling +which functionality is enabled right now, and setting up which tools are +enabled/run when starting new fortress games. For an in-game +graphical interface, please use `gui/control-panel`. For a commandline +interface for configuring which overlays are enabled, please use `overlay`. + +This interface controls three kinds of configuration: + +1. Tools that are enabled right now. These are DFHack tools that run in the +background, like `autofarm`, or tools that DFHack can run on a repeating +schedule, like the "autoMilk" functionality of `workorder`. Most tools that can +be enabled are saved with your fort, so you can have different tools enabled +for different forts. If a tool is marked "global", however, like +`hide-tutorials`, then enabling it will make it take effect for all games. + +2. Tools or commands that should be auto-enabled or auto-run when you start a +new fortress. In addition to tools that can be "enabled", this includes +commands that you might want to run once just after you embark, such as +commands to configure `autobutcher` or to drain portions of excessively deep +aquifers. + +3. DFHack system preferences, such as whether "Armok" (god-mode) tools are +shown in DFHack lists (including the lists of commands shown by the control +panel) or mouse configuration like how fast you have to click for it to count +as a double click (for example, when maximizing DFHack tool windows). +Preferences are "global" in that they apply to all games. + +Run ``control-panel list`` to see the current settings and what tools and +preferences are available for configuration. + +Usage +----- + +:: + + control-panel list + control-panel enable|disable + control-panel autostart|noautostart + control-panel set + control-panel reset + +Examples +-------- +``control-panel list butcher`` + Shows the current configuration of all commands related to `autobutcher` + (and anything else that includes the text "butcher" in it). +``control-panel enable fix/empty-wheelbarrows`` or ``control-panel enable 25`` + Starts to run `fix/empty-wheelbarrows` periodically to maintain the + usability of your wheelbarrows. In the second version of this command, the + number "25" is used as an example. You'll have to run + ``control-panel list`` to see what number this command is actually listed + as. +``control-panel autostart autofarm`` + Configures `autofarm` to become automatically enabled when you start a new + fort. +``control-panel autostart fix/blood-del`` + Configures `fix/blood-del` to run once when you start a new fort. +``control-panel set HIDE_ARMOK_TOOLS true`` + Enable "mortal mode" and hide "armok" tools in the DFHack UIs. Note that + this will also remove some entries from the ``control-panel list`` output. + Run ``control-panel list`` to see all preference options and their + descriptions. diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 39e838f7df..dbd89d3b8a 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -7,11 +7,6 @@ local textures = require('gui.textures') local utils = require('utils') local widgets = require('gui.widgets') --- init files -local SYSTEM_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-system.init' -local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init' -local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' -local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' local REGISTRY = { -- automation tools diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua new file mode 100644 index 0000000000..6eff5053f1 --- /dev/null +++ b/internal/control-panel/common.lua @@ -0,0 +1,97 @@ +--@module = true + +local migration = reqscript('internal/control-panel/migration') +local registry = reqscript('internal/control-panel/registry') +local repeatUtil = require('repeat-util') + +local CONFIG_FILE = 'dfhack-config/control-panel.json' + +local function get_config() + local f = json.open(CONFIG_FILE) + local updated = false + if f.exists then + -- ensure proper structure + ensure_key(f.data, 'commands') + ensure_key(f.data, 'preferences') + -- remove unknown or out of date entries from the loaded config + for k, v in pairs(f.data) do + if k ~= 'commands' and k ~= 'preferences' then + updated = true + f.data[k] = nil + end + end + for name, config_command_data in pairs(f.data.commands) do + local data = registry.COMMANDS_BY_NAME[name] + if not data or config_command_data.version ~= data.version then + updated = true + f.data.commands[name] = nil + end + end + for name, config_pref_data in pairs(f.data.preferences) do + local data = registry.PREFERENCES_BY_NAME[name] + if not data or config_pref_data.version ~= data.version then + updated = true + f.data.preferences[name] = nil + end + end + else + -- migrate any data from old configs + migration.migrate(f.data) + updated = next(f.data.commands) or next(f.data.preferences) + end + if updated then + f:write() + end + return f +end + +config = config or get_config() + +function get_enabled_map() + local enabled_map = {} + local output = dfhack.run_command_silent('enable'):split('\n+') + for _,line in ipairs(output) do + local _,_,command,enabled_str = line:find('%s*(%S+):%s+(%S+)') + if enabled_str then + enabled_map[command] = enabled_str == 'on' + end + end + -- repeat entries override tool names for control-panel + for name in pairs(repeatUtil.repeating) do + enabled_map[name] = true + end + return enabled_map +end + +function apply(data, enabled_map, enabled) + enabled_map = enabled_map or {} + if enabled == nil then + enabled = safe_index(config.data.commands, data.command, 'autostart') + enabled = enabled or (enabled == nil and data.default) + if not enabled then return end + end + if data.mode == 'enable' or data.mode == 'system_enable' then + if enabled_map[data.command] == nil then + dfhack.printerr(('tool not enableable: "%s"'):format(data.command)) + return false + else + dfhack.run_command({enabled and 'enable' or 'disable', data.command}) + end + elseif data.mode == 'repeat' then + if enabled then + local command_str = ('repeat --name %s %s\n'): + format(data.command, table.concat(data.params, ' ')) + dfhack.run_command(command_str) + else + repeatUtil.cancel(data.command) + end + elseif data.mode == 'run' then + if enabled then + dfhack.run_command(data.command) + end + else + dfhack.printerr(('unhandled command: "%s"'):format(data.command)) + return false + end + return true +end diff --git a/internal/control-panel/migration.lua b/internal/control-panel/migration.lua new file mode 100644 index 0000000000..021fcc04d2 --- /dev/null +++ b/internal/control-panel/migration.lua @@ -0,0 +1,15 @@ +-- migrate configuration from 50.11-r4 and prior to new format +--@module = true + +-- init files +local SYSTEM_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-system.init' +local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init' +local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' +local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' + +function migrate(config_data) + -- read old files, add converted data to config_data, overwrite old files with + -- a message that says they are deprecated and can be deleted with the proper procedure + -- we can't delete them outright since steam may just restore them due to Steam Cloud + -- we *could* delete them if we know that we've been started from Steam as DFHack and not as DF +end diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua new file mode 100644 index 0000000000..ea4deaad79 --- /dev/null +++ b/internal/control-panel/registry.lua @@ -0,0 +1,172 @@ +--@module = true + +local gui = require('gui') +local widgets = require('gui.widgets') +local utils = require('utils') + +-- please keep in alphabetical order per group +-- add a 'version' attribute if we want to reset existing configs for a command to the default +COMMANDS_BY_IDX = { + -- automation tools + {command='autobutcher', group='automation', mode='enable'}, + {command='autobutcher target 10 10 14 2 BIRD_GOOSE', group='automation', mode='run', + desc='Set to autostart if you usually want to raise geese.'}, + {command='autobutcher target 10 10 14 2 BIRD_TURKEY', group='automation', mode='run', + desc='Set to autostart if you usually want to raise turkeys.'}, + {command='autobutcher target 10 10 14 2 BIRD_CHICKEN', group='automation', mode='run', + desc='Set to autostart if you usually want to raise chickens.'}, + {command='autochop', group='automation', mode='enable'}, + {command='autoclothing', group='automation', mode='enable'}, + {command='autofarm', group='automation', mode='enable'}, + {command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run', + desc='Set to autostart if you usually farm pig tails for the clothing industry.'}, + {command='autofish', group='automation', mode='enable'}, + --{command='autolabor', group='automation', mode='enable'}, -- hide until it works better + {command='automilk', group='automation', mode='repeat', + desc='Automatically milk creatures that are ready for milking.', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', '"{\\"job\\":\\"MilkCreature\\",\\"item_conditions\\":[{\\"condition\\":\\"AtLeast\\",\\"value\\":2,\\"flags\\":[\\"empty\\"],\\"item_type\\":\\"BUCKET\\"}]}"', ']'}}, + {command='autonestbox', group='automation', mode='enable'}, + {command='autoshear', group='automation', mode='repeat', + desc='Automatically shear creatures that are ready for shearing.', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, + {command='autoslab', group='automation', mode='enable'}, + {command='ban-cooking all', group='automation', mode='run'}, + {command='buildingplan set boulders false', group='automation', mode='run', + desc='Set to autostart if you usually don\'t want to use boulders for construction.'}, + {command='buildingplan set logs false', group='automation', mode='run', + desc='Set to autostart if you usually don\'t want to use logs for construction.'}, + {command='cleanowned', group='automation', mode='repeat', + desc='Encourage dwarves to drop tattered clothing and grab new ones.', + params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, + {command='nestboxes', group='automation', mode='enable'}, + {command='orders-sort', group='automation', mode='repeat', + desc='Sort manager orders by repeat frequency so one-time orders can be completed.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, + {command='prioritize', group='automation', mode='enable'}, + {command='seedwatch', group='automation', mode='enable'}, + {command='suspendmanager', group='automation', mode='enable'}, + {command='tailor', group='automation', mode='enable'}, + {command='work-now', group='automation', mode='enable'}, + + -- bugfix tools + {command='fix/blood-del', group='bugfix', mode='run', default=true}, + {command='fix/dead-units', group='bugfix', mode='repeat', default=true, + desc='Fix units still being assigned to burrows after death.', + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, + {command='fix/empty-wheelbarrows', group='bugfix', mode='repeat', default=true, + desc='Make abandoned full wheelbarrows usable again.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, + {command='fix/general-strike', group='bugfix', mode='repeat', default=true, + desc='Prevent dwarves from getting stuck and refusing to work.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, + {command='fix/protect-nicks', group='bugfix', mode='enable', default=true}, + {command='fix/stuck-instruments', group='bugfix', mode='repeat', default=true, + desc='Fix activity references on stuck instruments to make them usable again.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, + {command='preserve-tombs', group='bugfix', mode='enable', default=true}, + + -- gameplay tools + {command='combine', group='gameplay', mode='repeat', + desc='Combine partial stacks in stockpiles into full stacks.', + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, + {command='drain-aquifer --top 2', group='gameplay', mode='run', + desc='Set to autostart to ensure that your maps have no more than 2 layers of aquifer.'}, + {command='dwarfvet', group='gameplay', mode='enable'}, + {command='emigration', group='gameplay', mode='enable'}, + {command='fastdwarf', group='gameplay', mode='enable'}, + {command='hermit', group='gameplay', mode='enable'}, + {command='hide-tutorials', group='gameplay', mode='system_enable'}, + {command='light-aquifers-only', group='gameplay', mode='run'}, + {command='misery', group='gameplay', mode='enable'}, + {command='orders-reevaluate', group='gameplay', mode='repeat', + desc='Invalidates all work orders once a month, allowing conditions to be rechecked.', + params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, + {command='starvingdead', group='gameplay', mode='enable'}, + {command='warn-starving', group='gameplay', mode='repeat', + desc='Show a warning dialog when units are starving or dehydrated.', + params={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, + {command='warn-stranded', group='gameplay', mode='repeat', + desc='Show a warning dialog when units are stranded from all others.', + params={'--time', '2', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, +} + +COMMANDS_BY_NAME = {} +for _,data in ipairs(COMMANDS_BY_IDX) do + COMMANDS_BY_NAME[data.command] = data +end + +-- keep in desired display order +PREFERENCES_BY_IDX = { + { + name='HIDE_ARMOK_TOOLS', + label='Mortal mode: hide "armok" tools', + desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.', + default=false, + get_fn=function() return dfhack.HIDE_ARMOK_TOOLS end, + set_fn=function(val) dfhack.HIDE_ARMOK_TOOLS = val end, + }, + { + name='FILTER_FULL_TEXT', + label='DFHack searches full text', + desc='When searching, whether to match anywhere in the text (true) or just at the start of words (false).', + default=false, + get_fn=function() return utils.FILTER_FULL_TEXT end, + set_fn=function(val) utils.FILTER_FULL_TEXT = val end, + }, + { + name='HIDE_CONSOLE_ON_STARTUP', + label='Hide console on startup (MS Windows only)', + desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.', + default=true, + get_fn=function() return dfhack.HIDE_CONSOLE_ON_STARTUP end, + set_fn=function(val) dfhack.HIDE_CONSOLE_ON_STARTUP = val end, + }, + { + name='DEFAULT_INITIAL_PAUSE', + label='DFHack tools autopause game', + desc='Always pause the game when a DFHack tool window is shown (you can still unpause afterwards).', + default=true, + get_fn=function() return gui.DEFAULT_INITIAL_PAUSE end, + set_fn=function(val) gui.DEFAULT_INITIAL_PAUSE = val end, + }, + { + name='INTERCEPT_HANDLED_HOTKEYS', + label='Intercept handled hotkeys', + desc='Prevent key events handled by DFHack windows from also affecting the vanilla widgets.', + default=true, + get_fn=dfhack.internal.getSuppressDuplicateKeyboardEvents, + set_fn=dfhack.internal.setSuppressDuplicateKeyboardEvents, + }, + { + name='DOUBLE_CLICK_MS', + label='Mouse double click speed (ms)', + desc='How long to wait for the second click of a double click, in ms.', + default=500, + min=50, + get_fn=function() return widgets.DOUBLE_CLICK_MS end, + set_fn=function(val) widgets.DOUBLE_CLICK_MS = val end, + }, + { + name='SCROLL_DELAY_MS', + label='Mouse scroll repeat delay (ms)', + desc='The delay between events when holding the mouse button down on a scrollbar, in ms.', + default=20, + min=5, + get_fn=function() return widgets.SCROLL_DELAY_MS end, + set_fn=function(val) widgets.SCROLL_DELAY_MS = val end, + }, + { + name='SCROLL_INITIAL_DELAY_MS', + label='Mouse initial scroll repeat delay (ms)', + desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.', + default=300, + min=5, + get_fn=function() return widgets.SCROLL_INITIAL_DELAY_MS end, + set_fn=function(val) widgets.SCROLL_INITIAL_DELAY_MS = val end, + }, +} + +PREFERENCES_BY_NAME = {} +for _,data in ipairs(PREFERENCES_BY_IDX) do + PREFERENCES_BY_NAME[data.name] = data +end From cd64f6e45b80188102e62c7f4493ed61eb5fbbc4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 11:08:20 -0800 Subject: [PATCH 710/732] get Preferences tab working --- control-panel.lua | 84 +-- gui/control-panel.lua | 779 ++++++++------------------- internal/control-panel/common.lua | 105 +++- internal/control-panel/migration.lua | 130 +++++ 4 files changed, 468 insertions(+), 630 deletions(-) diff --git a/control-panel.lua b/control-panel.lua index 436db1c397..2f4574fb78 100644 --- a/control-panel.lua +++ b/control-panel.lua @@ -2,7 +2,6 @@ local argparse = require('argparse') local common = reqscript('internal/control-panel/common') -local helpdb = require('helpdb') local json = require('json') local persist = require('persist-table') local registry = reqscript('internal/control-panel/registry') @@ -16,11 +15,11 @@ local function apply_system_config() local enabled_map =common.get_enabled_map() for _, data in ipairs(registry.COMMANDS_BY_IDX) do if data.mode == 'system_enable' then - common.apply(data, enabled_map) + common.apply_command(data, enabled_map) end end for _, data in ipairs(registry.PREFERENCES_BY_IDX) do - local value = safe_index(config.data.preferences, data.name, 'val') + local value = safe_index(common.config.data.preferences, data.name, 'val') if value ~= nil then data.set_fn(value) end @@ -31,7 +30,7 @@ local function apply_autostart_config() local enabled_map =common.get_enabled_map() for _, data in ipairs(registry.COMMANDS_BY_IDX) do if data.mode == 'enable' or data.mode == 'run' then - common.apply(data, enabled_map) + common.apply_command(data, enabled_map) end end end @@ -41,9 +40,10 @@ local function apply_fort_loaded_config() apply_autostart_config() persist.GlobalTable[GLOBAL_KEY] = json.encode({autostart_done=true}) end + local enabled_repeats = json.decode(persist.GlobalTable[common.REPEATS_GLOBAL_KEY] or '') for _, data in ipairs(registry.COMMANDS_BY_IDX) do - if data.mode == 'repeat' then - common.apply(data) + if data.mode == 'repeat' and enabled_repeats[data.command] then + common.apply_command(data) end end end @@ -65,32 +65,11 @@ local function print_header(header) print(('-'):rep(#header)) end -local function get_first_word(str) - local word = str:trim():split(' +')[1] - if word:startswith(':') then word = word:sub(2) end - return word -end - -local function command_passes_filters(data, target_group, first_word, filter_strs) - if data.group ~= target_group then - return false - end - if dfhack.getHideArmokTools() and helpdb.is_entry(first_word) - and helpdb.get_entry_tags(first_word).armok - then - return false - end - if not utils.search_text(data.command, filter_strs) then - return false - end - return true -end - local function list_command_group(group, filter_strs, enabled_map) local header = ('Group: %s'):format(group) for idx, data in ipairs(registry.COMMANDS_BY_IDX) do - local first_word = get_first_word(data.command) - if not command_passes_filters(data, group, first_word, filter_strs) then + local first_word = common.get_first_word(data.command) + if not common.command_passes_filters(data, group, first_word, filter_strs) then goto continue end if header then @@ -103,13 +82,12 @@ local function list_command_group(group, filter_strs, enabled_map) extra = ' (global)' end print(('%d) %s%s'):format(idx, data.command, extra)) - local desc = data.desc or - (helpdb.is_entry(first_word) and helpdb.get_entry_short_help(first_word)) - if desc then + local desc = common.get_description(data) + if #desc > 0 then print((' %s'):format(desc)) end local default_value = not not data.default - local current_value = safe_index(config.data.commands, data.command, 'autostart') + local current_value = safe_index(common.config.data.commands, data.command, 'autostart') if current_value == nil then current_value = default_value end @@ -164,7 +142,10 @@ local function do_enable_disable(which, entries) local enabled_map =common.get_enabled_map() for _, entry in ipairs(entries) do local data = get_command_data(entry) - if common.apply(data, enabled_map, which == 'en') then + if data.mode ~= 'system_enable' and not dfhack.world.isFortressMode() then + qerror('must have a loaded fortress to enable '..data.name) + end + if common.apply_command(data, enabled_map, which == 'en') then print(('%sabled %s'):format(which, entry)) end end @@ -184,15 +165,11 @@ local function do_autostart_noautostart(which, entries) if not data then qerror(('autostart command or index not found: "%s"'):format(entry)) else - local enabled = which == 'en' - if enabled ~= not not data.default then - config.data.commands[entry].autostart = enabled - else - config.data.commands[entry] = nil - end + common.set_autostart(data, which == 'en') print(('%sabled autostart for: %s'):format(which, entry)) end end + common.config:write() end local function do_autostart(entries) @@ -209,27 +186,8 @@ local function do_set(params) if not data then qerror(('preference name not found: "%s"'):format(name)) end - local expected_type = type(data.default) - if expected_type == 'boolean' then - value = argparse.boolean(value) - end - local actual_type = type(value) - if actual_type ~= expected_type then - qerror(('"%s" has an unexpected value type: got: %s; expected: %s'):format( - params[2], actual_type, expected_type)) - end - if data.min and data.min > value then - qerror(('value too small: got: %s; minimum: %s'):format(value, data.min)) - end - data.set_fn(value) - if data.default ~= safe_index(config.data.preferences, name, 'val') then - config.data.preferences[name] = { - val=value, - version=data.version, - } - else - config.data.preferences[name] = nil - end + common.set_preference(data, value) + common.config:write() end local function do_reset(params) @@ -238,8 +196,8 @@ local function do_reset(params) if not data then qerror(('preference name not found: "%s"'):format(name)) end - data.set_fn(data.default) - config.data.preferences[name] = nil + common.set_preference(data, data.default) + common.config:write() end local command_switch = { diff --git a/gui/control-panel.lua b/gui/control-panel.lua index dbd89d3b8a..599187ea05 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -1,224 +1,13 @@ +local common = reqscript('internal/control-panel/common') local dialogs = require('gui.dialogs') local gui = require('gui') +local textures = require('gui.textures') local helpdb = require('helpdb') local overlay = require('plugins.overlay') -local repeatUtil = require('repeat-util') -local textures = require('gui.textures') +local registry = reqscript('internal/control-panel/registry') local utils = require('utils') local widgets = require('gui.widgets') - -local REGISTRY = { - -- automation tools - {command='autobutcher', tab='automation', mode='enable'}, - {command='autobutcher target 10 10 14 2 BIRD_GOOSE', tab='automation', mode='run'}, - {command='autobutcher target 10 10 14 2 BIRD_TURKEY', tab='automation', mode='run'}, - {command='autobutcher target 10 10 14 2 BIRD_CHICKEN', tab='automation', mode='run'}, - {command='autochop', tab='automation', mode='enable'}, - {command='autoclothing', tab='automation', mode='enable'}, - {command='autofarm', tab='automation', mode='enable'}, - {command='autofarm threshold 150 grass_tail_pig', tab='automation', mode='run'}, - {command='autofish', tab='automation', mode='enable'}, - --{command='autolabor', tab='automation', mode='enable'}, -- hide until it works better - {command='automilk', tab='automation', mode='repeat', - desc='Automatically milk creatures that are ready for milking.', - params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', '"{\\"job\\":\\"MilkCreature\\",\\"item_conditions\\":[{\\"condition\\":\\"AtLeast\\",\\"value\\":2,\\"flags\\":[\\"empty\\"],\\"item_type\\":\\"BUCKET\\"}]}"', ']'}}, - {command='autonestbox', tab='automation', mode='enable'}, - {command='autoshear', tab='automation', mode='repeat', - desc='Automatically shear creatures that are ready for shearing.', - params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, - {command='autoslab', tab='automation', mode='enable'}, - {command='ban-cooking all', tab='automation', mode='run'}, - {command='buildingplan set boulders false', tab='automation', mode='run'}, - {command='buildingplan set logs false', tab='automation', mode='run'}, - {command='cleanowned', tab='automation', mode='repeat', - desc='Encourage dwarves to drop tattered clothing and grab new ones.', - params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, - {command='nestboxes', tab='automation', mode='enable'}, - {command='orders-sort', tab='automation', mode='repeat', - desc='Sort manager orders by repeat frequency so one-time orders can be completed.', - params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, - {command='prioritize', tab='automation', mode='enable'}, - {command='seedwatch', tab='automation', mode='enable'}, - {command='suspendmanager', tab='automation', mode='enable'}, - {command='tailor', tab='automation', mode='enable'}, - {command='work-now', tab='automation', mode='enable'}, - - -- bugfix tools - {command='fix/blood-del', tab='bugfix', mode='run', default=true}, - {command='fix/dead-units', tab='bugfix', mode='repeat', default=true, - desc='Fix units still being assigned to burrows after death.', - params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, - {command='fix/empty-wheelbarrows', tab='bugfix', mode='repeat', default=true, - desc='Make abandoned full wheelbarrows usable again.', - params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, - {command='fix/general-strike', tab='bugfix', mode='repeat', default=true, - desc='Prevent dwarves from getting stuck and refusing to work.', - params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, - {command='fix/protect-nicks', tab='bugfix', mode='enable', default=true}, - {command='fix/stuck-instruments', tab='bugfix', mode='repeat', default=true, - desc='Fix activity references on stuck instruments to make them usable again.', - params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, - {command='preserve-tombs', tab='bugfix', mode='enable', default=true}, - - -- gameplay tools - {command='combine', tab='gameplay', mode='repeat', - desc='Combine partial stacks in stockpiles into full stacks.', - params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, - {command='drain-aquifer --top 2', tab='gameplay', mode='run'}, - {command='dwarfvet', tab='gameplay', mode='enable'}, - {command='emigration', tab='gameplay', mode='enable'}, - {command='fastdwarf', tab='gameplay', mode='enable'}, - {command='hermit', tab='gameplay', mode='enable'}, - {command='hide-tutorials', tab='gameplay', mode='system_enable'}, - {command='light-aquifers-only', tab='gameplay', mode='run'}, - {command='misery', tab='gameplay', mode='enable'}, - {command='orders-reevaluate', tab='gameplay', mode='repeat', - desc='Invalidates work orders once a month, allowing conditions to be rechecked.', - params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, - {command='starvingdead', tab='gameplay', mode='enable'}, - {command='warn-starving', tab='gameplay', mode='repeat', - desc='Show a warning dialog when units are starving or dehydrated.', - params={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, - {command='warn-stranded', tab='gameplay', mode='repeat', - desc='Show a warning dialog when units are stranded from all others.', - params={'--time', '2', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, -} - -local PREFERENCES = { - ['dfhack']={ - HIDE_CONSOLE_ON_STARTUP={label='Hide console on startup (MS Windows only)', type='bool', default=true, - desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, - HIDE_ARMOK_TOOLS={label='Mortal mode: hide "armok" tools', type='bool', default=false, - desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.'}, - }, - ['gui']={ - DEFAULT_INITIAL_PAUSE={label='DFHack tools autopause game', type='bool', default=true, - desc='Whether to pause the game when a DFHack tool window is shown.'}, - }, - ['gui.widgets']={ - DOUBLE_CLICK_MS={label='Mouse double click speed (ms)', type='int', default=500, min=50, - desc='How long to wait for the second click of a double click, in ms.'}, - SCROLL_INITIAL_DELAY_MS={label='Mouse initial scroll repeat delay (ms)', type='int', default=300, min=5, - desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.'}, - SCROLL_DELAY_MS={label='Mouse scroll repeat delay (ms)', type='int', default=20, min=5, - desc='The delay between events when holding the mouse button down on a scrollbar, in ms.'}, - }, - ['utils']={ - FILTER_FULL_TEXT={label='DFHack searches full text', type='bool', default=false, - desc='When searching, choose whether to match anywhere in the text (true) or just at the start of words (false).'}, - }, -} -local CPP_PREFERENCES = { - { - label='Prevent duplicate key events', - type='bool', - default=true, - desc='Whether to additionally pass key events through to DF when DFHack keybindings are triggered.', - init_fmt=':lua dfhack.internal.setSuppressDuplicateKeyboardEvents(%s)', - get_fn=dfhack.internal.getSuppressDuplicateKeyboardEvents, - set_fn=dfhack.internal.setSuppressDuplicateKeyboardEvents, - }, -} - -local function read_init_file(fname, config_map, matchers) - local ok, f = pcall(io.open, SYSTEM_INIT_FILE) - if not ok or not f then return end - for line in f:lines() do - line = line:trim() - if #line == 0 or (line:startswith('#') and not line:startswith('##')) then - goto continue - end - local negate, service - for _, matcher in ipairs(matchers) do - negate, service = line:match(matcher) - if service then - config_map[service] = #negate == 0 - break - end - end - ::continue:: - end -end - --- save_fn takes the file as a param and should call f:write() to write data -local function save_file(path, save_fn) - local ok, f = pcall(io.open, path, 'w') - if not ok or not f then - dialogs.showMessage('Error', - ('Cannot open file for writing: "%s"'):format(path)) - return - end - f:write('# DO NOT EDIT THIS FILE\n') - f:write('# Please use gui/control-panel to modify the contents of this file\n\n') - save_fn(f) - f:close() -end - -local function write_init_files(config_map) - save_file(SYSTEM_INIT_FILE, function(f) - for _,data in ipairs(REGISTRY) do - if data.mode ~= 'system_enable' then goto continue end - local command = data.command - local prefix = config_map[command] and '' or '##' - f:write(('%senable %s\n'):format(prefix, command)) - ::continue:: - end - end) - save_file(AUTOSTART_FILE, function(f) - for _,data in ipairs(REGISTRY) do - if data.mode == 'system_enable' or data.mode == 'repeat' then - goto continue - end - local command = data.command - local prefix = config_map[command] and '' or '##' - if data.mode == 'run' then - f:write(('%son-new-fortress %s\n'):format(prefix, command)) - elseif data.mode == 'enable' then - f:write(('%son-new-fortress enable %s\n'):format(prefix, command)) - else - error('unhandled mode: '.. data.mode) - end - ::continue:: - end - end) - save_file(REPEATS_FILE, function(f) - for _,data in ipairs(REGISTRY) do - if data.mode ~= 'repeat' then goto continue end - local command = data.command - local prefix = config_map[command] and '' or '##' - local command_str = ('%srepeat --name %s %s\n'): - format(prefix, command, table.concat(data.params, ' ')) - f:write(command_str) - ::continue:: - end - end) -end - -local function init_config_state() - local config_map = {} - read_init_file(SYSTEM_INIT_FILE, config_map, { - '^(#?#?)enable ([%S]+)$', - }) - read_init_file(AUTOSTART_FILE, config_map, { - '^(#?#?)on%-new%-fortress enable ([%S]+)$', - '^(#?#?)on%-new%-fortress (.+)', - }) - read_init_file(REPEATS_FILE, config_map, { - '^(#?#?)repeat %-%-name ([%S]+)', - }) - - for _, data in ipairs(REGISTRY) do - if data.default and config_map[data.command] == nil then - config_map[data.command] = true - end - end - - write_init_files(config_map) - - return config_map -end - local function get_icon_pens() local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} @@ -254,16 +43,90 @@ local ENABLED_PEN_LEFT, ENABLED_PEN_CENTER, ENABLED_PEN_RIGHT, -- ConfigPanel -- +-- provides common structure across control panel tabs ConfigPanel = defclass(ConfigPanel, widgets.Panel) ConfigPanel.ATTRS{ intro_text=DEFAULT_NIL, - is_enableable=DEFAULT_NIL, - is_configurable=DEFAULT_NIL, - select_label='Toggle enabled', } function ConfigPanel:init() + local main_panel = widgets.Panel{ + frame={t=0, b=7}, + autoarrange_subviews=true, + autoarrange_gap=1, + subviews={ + widgets.WrappedLabel{ + frame={t=0}, + text_to_wrap=self.intro_text, + }, + -- extended by subclasses + }, + } + self:init_main_panel(main_panel) + + local footer = widgets.Panel{ + view_id='footer', + frame={b=0, h=3}, + -- extended by subclasses + } + self:init_footer(footer) + self:addviews{ + main_panel, + widgets.WrappedLabel{ + view_id='desc', + frame={b=4, h=2}, + auto_height=false, + }, + footer, + } +end + +-- overridden by subclasses +function ConfigPanel:init_main_panel(panel) +end + +-- overridden by subclasses +function ConfigPanel:init_footer(panel) +end + +-- overridden by subclasses +function ConfigPanel:refresh() +end + +-- attach to lists in subclasses +-- choice.data is an entry from one of the registry tables +function ConfigPanel:on_select(_, choice) + local desc = self.subviews.desc + desc.text_to_wrap = choice and common.get_description(choice.data) or '' + if desc.frame_body then + desc:updateLayout() + end +end + +--[[ +-- +-- Services +-- + +Services = defclass(Services, ConfigPanel) +Services.ATTRS{ + group=DEFAULT_NIL, +} + +function Services:init() + self:addviews{ + widgets.TabBar{ + frame={t=0}, + labels={ + 'Automation', + 'Bug Fixes', + 'Gameplay', + }, + on_select=function(val) self.subpage = val self:refresh() end, + get_cur_page=function() return self.subpage end, + }, + widgets.Panel{ frame={t=0, b=7}, autoarrange_subviews=true, @@ -298,14 +161,14 @@ function ConfigPanel:init() widgets.HotkeyLabel{ view_id='show_help_label', frame={b=1, l=0}, - label='Show tool help or run commands', + label='Show tool help or run custom command', key='CUSTOM_CTRL_H', on_activate=self:callback('show_help') }, widgets.HotkeyLabel{ view_id='launch', frame={b=0, l=0}, - label='Launch config UI', + label='Launch tool-specific config UI', key='CUSTOM_CTRL_G', enabled=self.is_configurable, on_activate=self:callback('launch_config'), @@ -313,7 +176,18 @@ function ConfigPanel:init() } end -function ConfigPanel:onInput(keys) +function Services:get_choices() + local enabled_map = common.get_enabled_map() + local choices = {} + for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if command_passes_filters(data, self.group) then + table.insert(choices, {data=data, enabled=enabled_map[data.command]}) + end + end + return choices +end + +function Services:onInput(keys) local handled = ConfigPanel.super.onInput(self, keys) if keys._MOUSE_L then local list = self.subviews.list.list @@ -332,9 +206,11 @@ function ConfigPanel:onInput(keys) return handled end + + local COMMAND_REGEX = '^([%w/_-]+)' -function ConfigPanel:refresh() +function Services:refresh() local choices = {} for _,choice in ipairs(self:get_choices()) do local command = choice.target or choice.command @@ -379,7 +255,7 @@ function ConfigPanel:refresh() list.edit:setFocus(true) end -function ConfigPanel:on_select(idx, choice) +function Services:on_select(idx, choice) local desc = self.subviews.desc desc.text_to_wrap = choice and choice.desc or '' if desc.frame_body then @@ -391,7 +267,7 @@ function ConfigPanel:on_select(idx, choice) end end -function ConfigPanel:on_submit() +function Services:on_submit() if not utils.getval(self.is_enableable) then return false end _,choice = self.subviews.list:getSelected() if not choice then return end @@ -403,275 +279,70 @@ function ConfigPanel:on_submit() self:refresh() end -function ConfigPanel:show_help() +function Services:show_help() _,choice = self.subviews.list:getSelected() if not choice then return end local command = choice.target:match(COMMAND_REGEX) dfhack.run_command('gui/launcher', command .. ' ') end -function ConfigPanel:launch_config() +function Services:launch_config() if not utils.getval(self.is_configurable) then return false end _,choice = self.subviews.list:getSelected() if not choice or not choice.gui_config then return end dfhack.run_command(choice.gui_config) end --- --- Services --- - -Services = defclass(Services, ConfigPanel) -Services.ATTRS{ - services_list=DEFAULT_NIL, -} - -function Services:get_enabled_map() - local enabled_map = {} - local output = dfhack.run_command_silent('enable'):split('\n+') - for _,line in ipairs(output) do - local _,_,command,enabled_str,extra = line:find('%s*(%S+):%s+(%S+)%s*(.*)') - if enabled_str then - enabled_map[command] = enabled_str == 'on' - end - end - return enabled_map -end - -local function get_first_word(text) - local word = text:trim():split(' +')[1] - if word:startswith(':') then word = word:sub(2) end - return word -end - -function Services:get_choices() - local enabled_map = self:get_enabled_map() - local choices = {} - local hide_armok = dfhack.getHideArmokTools() - for _,service in ipairs(self.services_list) do - local entry_name = get_first_word(service) - if not hide_armok or not helpdb.is_entry(entry_name) - or not helpdb.get_entry_tags(entry_name).armok then - table.insert(choices, {target=service, enabled=enabled_map[service]}) - end - end - return choices -end -- --- FortServices +-- AutomationServices -- -FortServices = defclass(FortServices, Services) -FortServices.ATTRS{ - is_enableable=dfhack.world.isFortressMode, - is_configurable=function() return dfhack.world.isFortressMode() end, +AutomationServices = defclass(AutomationServices, Services) +AutomationServices.ATTRS{ intro_text='These tools can only be enabled when you have a fort loaded,'.. ' but once you enable them, they will stay enabled when you'.. ' save and reload your fort. If you want them to be'.. ' auto-enabled for new forts, please see the "Autostart" tab.', - services_list=FORT_SERVICES, + group='automation', } -- --- FortServicesAutostart +-- BugfixServices -- -FortServicesAutostart = defclass(FortServicesAutostart, Services) -FortServicesAutostart.ATTRS{ - is_enableable=true, - is_configurable=false, +BugfixServices = defclass(BugfixServices, Services) +BugfixServices.ATTRS{ intro_text='Tools that are enabled on this page will be auto-enabled for'.. ' you when you start a new fort, using the default'.. ' configuration. To see tools that are enabled right now in'.. ' an active fort, please see the "Fort" tab.', - services_list=FORT_AUTOSTART, -} - -function FortServicesAutostart:init() - local enabled_map = {} - local ok, f = pcall(io.open, AUTOSTART_FILE) - if ok and f then - local services_set = utils.invert(FORT_AUTOSTART) - for line in f:lines() do - line = line:trim() - if #line == 0 or line:startswith('#') then goto continue end - local service = line:match('^on%-new%-fortress enable ([%S]+)$') - or line:match('^on%-new%-fortress (.+)') - if service and services_set[service] then - enabled_map[service] = true - end - ::continue:: - end - end - self.enabled_map = enabled_map -end - -function FortServicesAutostart:get_enabled_map() - return self.enabled_map -end - -function FortServicesAutostart:on_submit() - _,choice = self.subviews.list:getSelected() - if not choice then return end - self.enabled_map[choice.target] = not choice.enabled - - local save_fn = function(f) - for service,enabled in pairs(self.enabled_map) do - if enabled then - if service:match(' ') then - f:write(('on-new-fortress %s\n'):format(service)) - else - f:write(('on-new-fortress enable %s\n'):format(service)) - end - end - end - end - save_file(AUTOSTART_FILE, save_fn) - self:refresh() -end - --- --- SystemServices --- - -local function system_service_is_configurable(gui_config) - return gui_config ~= 'gui/automelt' or dfhack.world.isFortressMode() -end - -SystemServices = defclass(SystemServices, Services) -SystemServices.ATTRS{ - title='System', - is_enableable=true, - is_configurable=system_service_is_configurable, - intro_text='These are DFHack system services that are not bound to' .. - ' a specific fort. Some of these are critical DFHack services' .. - ' that can be manually disabled, but will re-enable themselves' .. - ' when DF restarts.', - services_list=SYSTEM_SERVICES, + group='bugfix', } -function SystemServices:on_submit() - SystemServices.super.on_submit(self) - - local enabled_map = self:get_enabled_map() - local save_fn = function(f) - for _,service in ipairs(SYSTEM_USER_SERVICES) do - if enabled_map[service] then - f:write(('enable %s\n'):format(service)) - end - end - end - save_file(SYSTEM_INIT_FILE, save_fn) -end - -- --- RepeatAutostart +-- BugfixServices -- -RepeatAutostart = defclass(RepeatAutostart, ConfigPanel) -RepeatAutostart.ATTRS{ - title='Periodic', - is_enableable=true, - is_configurable=false, - intro_text='Tools that can run periodically to fix bugs or warn you of'.. - ' dangers that are otherwise difficult to detect (like'.. - ' starving caged animals).', +GameplayServices = defclass(GameplayServices, Services) +GameplayServices.ATTRS{ + intro_text='Tools that are enabled on this page will be auto-enabled for'.. + ' you when you start a new fort, using the default'.. + ' configuration. To see tools that are enabled right now in'.. + ' an active fort, please see the "Fort" tab.', + group='gameplay', } -function RepeatAutostart:init() - self.subviews.show_help_label.visible = false - self.subviews.launch.visible = false - local enabled_map = {} - local ok, f = pcall(io.open, REPEATS_FILE) - if ok and f then - for line in f:lines() do - line = line:trim() - if #line == 0 or line:startswith('#') then goto continue end - local service = line:match('^repeat %-%-name ([%S]+)') - if service then - enabled_map[service] = true - end - ::continue:: - end - end - self.enabled_map = enabled_map -end - -function RepeatAutostart:onInput(keys) - -- call grandparent's onInput since we don't want ConfigPanel's processing - local handled = RepeatAutostart.super.super.onInput(self, keys) - if keys._MOUSE_L then - local list = self.subviews.list.list - local idx = list:getIdxUnderMouse() - if idx then - local x = list:getMousePos() - if x <= 2 then - self:on_submit() - end - end - end - return handled -end - -function RepeatAutostart:refresh() - local choices = {} - for _,name in ipairs(REPEATS_LIST) do - local enabled = self.enabled_map[name] - local text = { - {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, - {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, - {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, - ' ', - name, - } - table.insert(choices, - {text=text, desc=REPEATS[name].desc, search_key=name, - name=name, enabled=enabled}) - end - local list = self.subviews.list - local filter = list:getFilter() - local selected = list:getSelected() - list:setChoices(choices) - list:setFilter(filter, selected) - list.edit:setFocus(true) -end - -function RepeatAutostart:on_submit() - _,choice = self.subviews.list:getSelected() - if not choice then return end - self.enabled_map[choice.name] = not choice.enabled - local run_commands = dfhack.isMapLoaded() - - local save_fn = function(f) - for name,enabled in pairs(self.enabled_map) do - if enabled then - local command_str = ('repeat --name %s %s\n'): - format(name, table.concat(REPEATS[name].command, ' ')) - f:write(command_str) - if run_commands then - dfhack.run_command(command_str) -- actually start it up too - end - elseif run_commands then - repeatUtil.cancel(name) - end - end - end - save_file(REPEATS_FILE, save_fn) - self:refresh() -end - -- -- Overlays -- Overlays = defclass(Overlays, ConfigPanel) Overlays.ATTRS{ - title='Overlays', is_enableable=true, is_configurable=false, intro_text='These are DFHack overlays that add information and'.. - ' functionality to vanilla screens.', + ' functionality to various native DF screens.', } function Overlays:init() @@ -696,15 +367,15 @@ function Overlays:get_choices() end return choices end - +]] -- --- Preferences +-- PreferencesTab -- IntegerInputDialog = defclass(IntegerInputDialog, widgets.Window) IntegerInputDialog.ATTRS{ visible=false, - frame={w=50, h=8}, + frame={w=50, h=11}, frame_title='Edit setting', frame_style=gui.PANEL_FRAME, on_hide=DEFAULT_NIL, @@ -715,37 +386,53 @@ function IntegerInputDialog:init() widgets.Label{ frame={t=0, l=0}, text={ - 'Please enter a new value for ', - {text=function() return self.id or '' end}, + 'Please enter a new value for ', NEWLINE, + { + gap=4, + text=function() return self.id or '' end, + }, NEWLINE, {text=self:callback('get_spec_str')}, }, }, widgets.EditField{ view_id='input_edit', - frame={t=3, l=0}, + frame={t=4, l=0}, on_char=function(ch) return ch:match('%d') end, }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + label='Save', + key='SELECT', + on_activate=function() self:hide(self.subviews.input_edit.text) end, + }, + widgets.HotkeyLabel{ + frame={b=0, r=0}, + label='Reset to default', + key='CUSTOM_CTRL_G', + auto_width=true, + on_activate=function() self.subviews.input_edit:setText(tostring(self.data.default)) end, + }, } end function IntegerInputDialog:get_spec_str() - if not self.spec or (not self.spec.min and not self.spec.max) then - return '' - end - local strs = {} - if self.spec.min then - table.insert(strs, ('at least %d'):format(self.spec.min)) + local data = self.data + local strs = { + ('default: %d'):format(data.default), + } + if data.min then + table.insert(strs, ('at least %d'):format(data.min)) end - if self.spec.max then - table.insert(strs, ('at most %d'):format(self.spec.max)) + if data.max then + table.insert(strs, ('at most %d'):format(data.max)) end return ('(%s)'):format(table.concat(strs, ', ')) end -function IntegerInputDialog:show(id, spec, initial) +function IntegerInputDialog:show(id, data, initial) self.visible = true - self.id, self.spec = id, spec + self.id, self.data = id, data local edit = self.subviews.input_edit edit:setText(tostring(initial)) edit:setFocus(true) @@ -761,33 +448,25 @@ function IntegerInputDialog:onInput(keys) if IntegerInputDialog.super.onInput(self, keys) then return true end - if keys.SELECT then - self:hide(self.subviews.input_edit.text) - return true - elseif keys.LEAVESCREEN or keys._MOUSE_R then + if keys.LEAVESCREEN or keys._MOUSE_R then self:hide() return true end end -Preferences = defclass(Preferences, ConfigPanel) -Preferences.ATTRS{ - title='Preferences', - is_enableable=true, - is_configurable=true, +PreferencesTab = defclass(PreferencesTab, ConfigPanel) +PreferencesTab.ATTRS{ intro_text='These are the customizable DFHack system settings.', - select_label='Edit setting', } -function Preferences:init() - self.subviews.show_help_label.visible = false - self.subviews.launch.visible = false - self:addviews{ - widgets.HotkeyLabel{ - frame={b=0, l=0}, - label='Restore defaults', - key='CUSTOM_CTRL_G', - on_activate=self:callback('restore_defaults') +function PreferencesTab:init_main_panel(panel) + panel:addviews{ + widgets.FilteredList{ + frame={t=5}, + view_id='list', + on_select=self:callback('on_select'), + on_double_click=self:callback('on_submit'), + row_height=2, }, IntegerInputDialog{ view_id='input_dlg', @@ -796,9 +475,29 @@ function Preferences:init() } end -function Preferences:onInput(keys) - -- call grandparent's onInput since we don't want ConfigPanel's processing - local handled = Preferences.super.super.onInput(self, keys) +function PreferencesTab:init_footer(panel) + panel:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='Toggle/edit setting', + key='SELECT', + on_activate=self:callback('on_submit') + }, + widgets.HotkeyLabel{ + frame={t=2, l=0}, + label='Restore defaults', + key='CUSTOM_CTRL_G', + on_activate=self:callback('restore_defaults') + }, + } +end + +function PreferencesTab:onInput(keys) + if self.subviews.input_dlg.visible then + self.subviews.input_dlg:onInput(keys) + return true + end + local handled = PreferencesTab.super.onInput(self, keys) if keys._MOUSE_L then local list = self.subviews.list.list local idx = list:getIdxUnderMouse() @@ -822,24 +521,17 @@ local function make_preference_text(label, value) } end -function Preferences:refresh() +function PreferencesTab:refresh() if self.subviews.input_dlg.visible then return end local choices = {} - for ctx_name,settings in pairs(PREFERENCES) do - local ctx_env = require(ctx_name) - for id,spec in pairs(settings) do - local text = make_preference_text(spec.label, ctx_env[id]) - table.insert(choices, - {text=text, desc=spec.desc, search_key=text[#text], - ctx_env=ctx_env, id=id, spec=spec}) - end - end - for _,spec in ipairs(CPP_PREFERENCES) do - local text = make_preference_text(spec.label, spec.get_fn()) - table.insert(choices, - {text=text, desc=spec.desc, search_key=text[#text], spec=spec}) + for _, data in ipairs(registry.PREFERENCES_BY_IDX) do + local text = make_preference_text(data.label, data.get_fn()) + table.insert(choices, { + text=text, + search_key=text[#text], + data=data + }) end - table.sort(choices, function(a, b) return a.spec.label < b.spec.label end) local list = self.subviews.list local filter = list:getFilter() local selected = list:getSelected() @@ -848,68 +540,38 @@ function Preferences:refresh() list.edit:setFocus(true) end -local function preferences_set_and_save(self, choice, val) - if choice.spec.set_fn then - choice.spec.set_fn(val) - else - choice.ctx_env[choice.id] = val - end - self:do_save() +local function preferences_set_and_save(self, data, val) + common.set_preference(data, val) + common.config:write() self:refresh() end -function Preferences:on_submit() +function PreferencesTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end - local cur_val - if choice.spec.get_fn then - cur_val = choice.spec.get_fn() - else - cur_val = choice.ctx_env[choice.id] - end - if choice.spec.type == 'bool' then - preferences_set_and_save(self, choice, not cur_val) - elseif choice.spec.type == 'int' then - self.subviews.input_dlg:show(choice.id or choice.spec.label, choice.spec, cur_val) + local data = choice.data + local cur_val = data.get_fn() + local data_type = type(data.default) + if data_type == 'boolean' then + preferences_set_and_save(self, data, not cur_val) + elseif data_type == 'number' then + self.subviews.input_dlg:show(data.label, data, cur_val) end end -function Preferences:set_val(val) +function PreferencesTab:set_val(val) _,choice = self.subviews.list:getSelected() if not choice or not val then return end - preferences_set_and_save(self, choice, val) -end - -function Preferences:do_save() - local save_fn = function(f) - for ctx_name,settings in pairs(PREFERENCES) do - local ctx_env = require(ctx_name) - for id in pairs(settings) do - f:write((':lua require("%s").%s=%s\n'):format( - ctx_name, id, tostring(ctx_env[id]))) - end - end - for _,spec in ipairs(CPP_PREFERENCES) do - local line = spec.init_fmt:format(spec.get_fn()) - f:write(('%s\n'):format(line)) - end - end - save_file(PREFERENCES_INIT_FILE, save_fn) + preferences_set_and_save(self, choice.data, val) end -function Preferences:restore_defaults() - for ctx_name,settings in pairs(PREFERENCES) do - local ctx_env = require(ctx_name) - for id,spec in pairs(settings) do - ctx_env[id] = spec.default - end +function PreferencesTab:restore_defaults() + for _,data in ipairs(registry.PREFERENCES_BY_IDX) do + common.set_preference(data, data.default) end - for _,spec in ipairs(CPP_PREFERENCES) do - spec.set_fn(spec.default) - end - os.remove(PREFERENCES_INIT_FILE) + common.config:write() self:refresh() - dialogs.showMessage('Success', 'Default preference settings restored.') + dialogs.showMessage('Success', 'Default preferences restored.') end -- @@ -931,10 +593,9 @@ function ControlPanel:init() widgets.TabBar{ frame={t=0}, labels={ - 'Automation', - 'Bugfixes', - 'Gameplay', - 'UI Overlays', + --'Enabled', + --'Autostart', + --'UI Overlays', 'Preferences', }, on_select=self:callback('set_page'), @@ -944,11 +605,10 @@ function ControlPanel:init() view_id='pages', frame={t=5, l=0, b=0, r=0}, subviews={ - Automation{}, - Bugfixes{}, - Gameplay{}, - Overlays{}, - Preferences{}, + --EnabledTab{}, + --AutostartTab{}, + --OverlaysTab{}, + PreferencesTab{}, }, }, } @@ -983,9 +643,4 @@ function ControlPanelScreen:onDismiss() view = nil end -if not view and ({...})[1] == '--check-defaults' then - init_config_state() - return -end - view = view and view:raise() or ControlPanelScreen{}:show() diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua index 6eff5053f1..d7cd1cec83 100644 --- a/internal/control-panel/common.lua +++ b/internal/control-panel/common.lua @@ -1,11 +1,17 @@ --@module = true +local helpdb = require('helpdb') +local json = require('json') local migration = reqscript('internal/control-panel/migration') +local persist = require('persist-table') local registry = reqscript('internal/control-panel/registry') local repeatUtil = require('repeat-util') +local utils = require('utils') local CONFIG_FILE = 'dfhack-config/control-panel.json' +REPEATS_GLOBAL_KEY = 'control-panel-repeats' + local function get_config() local f = json.open(CONFIG_FILE) local updated = false @@ -47,6 +53,12 @@ end config = config or get_config() +local function unmunge_repeat_name(munged_name) + if munged_name:startswith('control-panel/') then + return munged_name:sub(15) + end +end + function get_enabled_map() local enabled_map = {} local output = dfhack.run_command_silent('enable'):split('\n+') @@ -57,13 +69,58 @@ function get_enabled_map() end end -- repeat entries override tool names for control-panel - for name in pairs(repeatUtil.repeating) do - enabled_map[name] = true + for munged_name in pairs(repeatUtil.repeating) do + local name = unmunge_repeat_name(munged_name) + if name then + enabled_map[name] = true + end end return enabled_map end -function apply(data, enabled_map, enabled) +local function get_first_word(str) + local word = str:trim():split(' +')[1] + if word:startswith(':') then word = word:sub(2) end + return word +end + +function command_passes_filters(data, target_group, filter_strs) + if data.group ~= target_group then + return false + end + filter_strs = filter_strs or {} + local first_word = get_first_word(data.command) + if dfhack.getHideArmokTools() and helpdb.is_entry(first_word) + and helpdb.get_entry_tags(first_word).armok + then + return false + end + if not utils.search_text(data.command, filter_strs) then + return false + end + return true +end + +function get_description(data) + if data.desc then + return data.desc + end + local first_word = get_first_word(data.command) + return helpdb.is_entry(first_word) and helpdb.get_entry_short_help(first_word) or '' +end + +local function persist_enabled_repeats() + local cp_repeats = {} + for munged_name in pairs(repeatUtil.repeating) do + local name = unmunge_repeat_name(munged_name) + if name then + cp_repeats[name] = true + end + end + persist.GlobalTable[REPEATS_GLOBAL_KEY] = json.encode(cp_repeats) +end + +function apply_command(data, enabled_map, enabled) enabled_map = enabled_map or {} if enabled == nil then enabled = safe_index(config.data.commands, data.command, 'autostart') @@ -78,13 +135,15 @@ function apply(data, enabled_map, enabled) dfhack.run_command({enabled and 'enable' or 'disable', data.command}) end elseif data.mode == 'repeat' then + local munged_name = 'control-panel/' .. data.command if enabled then local command_str = ('repeat --name %s %s\n'): - format(data.command, table.concat(data.params, ' ')) + format(munged_name, table.concat(data.params, ' ')) dfhack.run_command(command_str) else - repeatUtil.cancel(data.command) + repeatUtil.cancel(munged_name) end + persist_enabled_repeats() elseif data.mode == 'run' then if enabled then dfhack.run_command(data.command) @@ -95,3 +154,39 @@ function apply(data, enabled_map, enabled) end return true end + +function set_preference(data, in_value) + local expected_type = type(data.default) + local value = in_value + if expected_type == 'boolean' and type(value) ~= 'boolean' then + value = argparse.boolean(value) + end + local actual_type = type(value) + if actual_type ~= expected_type then + qerror(('"%s" has an unexpected value type: got: %s; expected: %s'):format( + in_value, actual_type, expected_type)) + end + if data.min and data.min > value then + qerror(('value too small: got: %s; minimum: %s'):format(value, data.min)) + end + data.set_fn(value) + if data.default ~= safe_index(config.data.preferences, data.name, 'val') then + config.data.preferences[data.name] = { + val=value, + version=data.version, + } + else + config.data.preferences[data.name] = nil + end +end + +function set_autostart(data, enabled) + if enabled ~= not not data.default then + config.data.commands[data.command] = { + autostart=enabled, + version=data.version, + } + else + config.data.commands[data.command] = nil + end +end diff --git a/internal/control-panel/migration.lua b/internal/control-panel/migration.lua index 021fcc04d2..0fd4f5ab41 100644 --- a/internal/control-panel/migration.lua +++ b/internal/control-panel/migration.lua @@ -7,6 +7,136 @@ local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferenc local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' +local function save_tombstone_file(path) + local ok, f = pcall(io.open, path, 'w') + if not ok or not f then + dialogs.showMessage('Error', + ('Cannot open file for writing: "%s"'):format(path)) + return + end + f:write('# This file was once used by gui/control-panel\n') + f:write('# If you are on Steam, you can delete this file manually\n') + f:write('# by starting DFHack in the Steam client, then deleting\n') + f:write('# this file while DF is running. Otherwise Steam Cloud will\n') + f:write('# restore the file when you next run DFHack.\n') + f:write('#\n') + f:write('# If you\'re not on Steam, you can delete this file at any time.\n') + f:close() +end +--[[ +function SystemServices:on_submit() + SystemServices.super.on_submit(self) + + local enabled_map = self:get_enabled_map() + local save_fn = function(f) + for _,service in ipairs(SYSTEM_USER_SERVICES) do + if enabled_map[service] then + f:write(('enable %s\n'):format(service)) + end + end + end + save_file(SYSTEM_INIT_FILE, save_fn) +end + +function FortServicesAutostart:on_submit() + _,choice = self.subviews.list:getSelected() + if not choice then return end + self.enabled_map[choice.target] = not choice.enabled + + local save_fn = function(f) + for service,enabled in pairs(self.enabled_map) do + if enabled then + if service:match(' ') then + f:write(('on-new-fortress %s\n'):format(service)) + else + f:write(('on-new-fortress enable %s\n'):format(service)) + end + end + end + end + save_file(AUTOSTART_FILE, save_fn) + self:refresh() +end + +function FortServicesAutostart:init() + local enabled_map = {} + local ok, f = pcall(io.open, AUTOSTART_FILE) + if ok and f then + local services_set = utils.invert(FORT_AUTOSTART) + for line in f:lines() do + line = line:trim() + if #line == 0 or line:startswith('#') then goto continue end + local service = line:match('^on%-new%-fortress enable ([%S]+)$') + or line:match('^on%-new%-fortress (.+)') + if service and services_set[service] then + enabled_map[service] = true + end + ::continue:: + end + end + self.enabled_map = enabled_map +end + +function Preferences:do_save() + local save_fn = function(f) + for ctx_name,settings in pairs(PREFERENCES) do + local ctx_env = require(ctx_name) + for id in pairs(settings) do + f:write((':lua require("%s").%s=%s\n'):format( + ctx_name, id, tostring(ctx_env[id]))) + end + end + for _,spec in ipairs(CPP_PREFERENCES) do + local line = spec.init_fmt:format(spec.get_fn()) + f:write(('%s\n'):format(line)) + end + end + save_file(PREFERENCES_INIT_FILE, save_fn) +end + +function RepeatAutostart:init() + self.subviews.show_help_label.visible = false + self.subviews.launch.visible = false + local enabled_map = {} + local ok, f = pcall(io.open, REPEATS_FILE) + if ok and f then + for line in f:lines() do + line = line:trim() + if #line == 0 or line:startswith('#') then goto continue end + local service = line:match('^repeat %-%-name ([%S]+)') + if service then + enabled_map[service] = true + end + ::continue:: + end + end + self.enabled_map = enabled_map +end + +function RepeatAutostart:on_submit() + _,choice = self.subviews.list:getSelected() + if not choice then return end + self.enabled_map[choice.name] = not choice.enabled + local run_commands = dfhack.isMapLoaded() + + local save_fn = function(f) + for name,enabled in pairs(self.enabled_map) do + if enabled then + local command_str = ('repeat --name %s %s\n'): + format(name, table.concat(REPEATS[name].command, ' ')) + f:write(command_str) + if run_commands then + dfhack.run_command(command_str) -- actually start it up too + end + elseif run_commands then + repeatUtil.cancel(name) + end + end + end + save_file(REPEATS_FILE, save_fn) + self:refresh() +end +]] function migrate(config_data) -- read old files, add converted data to config_data, overwrite old files with -- a message that says they are deprecated and can be deleted with the proper procedure From 83d6955e55a23b81a1babfc3410e3f455467ae1d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 11:44:08 -0800 Subject: [PATCH 711/732] get overlay tab working --- gui/control-panel.lua | 128 +++++++++++++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 21 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 599187ea05..99f2bea3fc 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -332,42 +332,128 @@ GameplayServices.ATTRS{ ' an active fort, please see the "Fort" tab.', group='gameplay', } - +]] -- --- Overlays +-- OverlaysTab -- -Overlays = defclass(Overlays, ConfigPanel) -Overlays.ATTRS{ - is_enableable=true, - is_configurable=false, +OverlaysTab = defclass(OverlaysTab, ConfigPanel) +OverlaysTab.ATTRS{ intro_text='These are DFHack overlays that add information and'.. - ' functionality to various native DF screens.', + ' functionality to native DF screens. You can toggle whether'.. + ' they are enabled here, or you can reposition them with'.. + ' gui/overlay.', } -function Overlays:init() - self.subviews.launch.visible = false - self:addviews{ +function OverlaysTab:init_main_panel(panel) + panel:addviews{ + widgets.FilteredList{ + frame={t=5}, + view_id='list', + on_select=self:callback('on_select'), + on_double_click=self:callback('on_submit'), + row_height=2, + }, + } +end + +function OverlaysTab:init_footer(panel) + panel:addviews{ widgets.HotkeyLabel{ - frame={b=0, l=0}, + frame={t=0, l=0}, + label='Toggle overlay', + key='SELECT', + on_activate=self:callback('on_submit') + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, label='Launch overlay widget repositioning UI', key='CUSTOM_CTRL_G', on_activate=function() dfhack.run_script('gui/overlay') end, }, + widgets.HotkeyLabel{ + frame={t=2, l=0}, + label='Restore defaults', + key='CUSTOM_CTRL_D', + on_activate=self:callback('restore_defaults') + }, } end -function Overlays:get_choices() +function OverlaysTab:onInput(keys) + local handled = OverlaysTab.super.onInput(self, keys) + if keys._MOUSE_L then + local list = self.subviews.list.list + local idx = list:getIdxUnderMouse() + if idx then + local x = list:getMousePos() + if x <= 2 then + self:on_submit() + end + end + end + return handled +end + +local function make_overlay_text(label, enabled) + return { + {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, + {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, + {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, + ' ', + label, + } +end + +function OverlaysTab:refresh() local choices = {} local state = overlay.get_state() for _,name in ipairs(state.index) do - table.insert(choices, {command='overlay', - target=name, - enabled=state.config[name].enabled}) + enabled = state.config[name].enabled + local text = make_overlay_text(name, enabled) + table.insert(choices, { + text=text, + search_key=name, + data={ + name=name, + command='overlay', + desc=state.db[name].desc, + enabled=enabled, + }, + }) end - return choices + local list = self.subviews.list + local filter = list:getFilter() + local selected = list:getSelected() + list:setChoices(choices) + list:setFilter(filter, selected) + list.edit:setFocus(true) end -]] + +local function enable_overlay(name, enabled) + local tokens = {'overlay'} + table.insert(tokens, enabled and 'enable' or 'disable') + table.insert(tokens, name) + dfhack.run_command(tokens) +end + +function OverlaysTab:on_submit() + _,choice = self.subviews.list:getSelected() + if not choice then return end + local data = choice.data + enable_overlay(data.name, not data.enabled) + self:refresh() +end + +function OverlaysTab:restore_defaults() + local state = overlay.get_state() + for name, db_entry in pairs(state.db) do + enable_overlay(name, db_entry.widget.default_enabled) + end + self:refresh() + dialogs.showMessage('Success', 'Overlay defaults restored.') +end + -- -- PreferencesTab -- @@ -409,7 +495,7 @@ function IntegerInputDialog:init() widgets.HotkeyLabel{ frame={b=0, r=0}, label='Reset to default', - key='CUSTOM_CTRL_G', + key='CUSTOM_CTRL_D', auto_width=true, on_activate=function() self.subviews.input_edit:setText(tostring(self.data.default)) end, }, @@ -486,7 +572,7 @@ function PreferencesTab:init_footer(panel) widgets.HotkeyLabel{ frame={t=2, l=0}, label='Restore defaults', - key='CUSTOM_CTRL_G', + key='CUSTOM_CTRL_D', on_activate=self:callback('restore_defaults') }, } @@ -595,7 +681,7 @@ function ControlPanel:init() labels={ --'Enabled', --'Autostart', - --'UI Overlays', + 'UI Overlays', 'Preferences', }, on_select=self:callback('set_page'), @@ -607,7 +693,7 @@ function ControlPanel:init() subviews={ --EnabledTab{}, --AutostartTab{}, - --OverlaysTab{}, + OverlaysTab{}, PreferencesTab{}, }, }, From 8057eed6870f238a24d9a654039009fa4ef536dd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 13:24:52 -0800 Subject: [PATCH 712/732] get basic enable functionality working --- gui/control-panel.lua | 438 +++++++++++++++++----------- internal/control-panel/registry.lua | 2 +- 2 files changed, 261 insertions(+), 179 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 99f2bea3fc..910c211c05 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -2,7 +2,6 @@ local common = reqscript('internal/control-panel/common') local dialogs = require('gui.dialogs') local gui = require('gui') local textures = require('gui.textures') -local helpdb = require('helpdb') local overlay = require('plugins.overlay') local registry = reqscript('internal/control-panel/registry') local utils = require('utils') @@ -67,7 +66,16 @@ function ConfigPanel:init() local footer = widgets.Panel{ view_id='footer', frame={b=0, h=3}, - -- extended by subclasses + subviews={ + widgets.HotkeyLabel{ + frame={t=2, l=0}, + label='Restore defaults', + key='CUSTOM_CTRL_D', + auto_width=true, + on_activate=self:callback('restore_defaults') + }, + -- extended by subclasses + } } self:init_footer(footer) @@ -77,6 +85,7 @@ function ConfigPanel:init() view_id='desc', frame={b=4, h=2}, auto_height=false, + text_to_wrap='', -- updated in on_select }, footer, } @@ -94,6 +103,10 @@ end function ConfigPanel:refresh() end +-- overridden by subclasses +function ConfigPanel:restore_defaults() +end + -- attach to lists in subclasses -- choice.data is an entry from one of the registry tables function ConfigPanel:on_select(_, choice) @@ -104,91 +117,164 @@ function ConfigPanel:on_select(_, choice) end end ---[[ + -- --- Services +-- CommandTab -- -Services = defclass(Services, ConfigPanel) -Services.ATTRS{ - group=DEFAULT_NIL, +CommandTab = defclass(CommandTab, ConfigPanel) + +local Subtabs = { + automation=1, + bugfix=2, + gameplay=3, } +local Subtabs_revmap = utils.invert(Subtabs) + +function CommandTab:init() + self.subpage = Subtabs.automation + + self.blurbs = { + [Subtabs.automation]='These run in the background and'.. + ' help you manage your fort. They are always safe to enable, and allow'.. + ' you to avoid paying attention to aspects of gameplay that you find'.. + ' tedious or unfun.', + [Subtabs.bugfix]='These automatically fix dangerous or'.. + ' annoying vanilla bugs. You should generally have all of these enabled'.. + ' unless you have a specific reason not to.', + [Subtabs.gameplay]='These change or extend gameplay. Read'.. + ' their help docs to see what they do and enable the ones that appeal to'.. + ' you.', + } +end -function Services:init() - self:addviews{ +function CommandTab:init_main_panel(panel) + panel:addviews{ widgets.TabBar{ - frame={t=0}, + frame={t=5}, labels={ 'Automation', 'Bug Fixes', 'Gameplay', }, - on_select=function(val) self.subpage = val self:refresh() end, + on_select=function(val) + self.subpage = val + self:updateLayout() + self:refresh() + end, get_cur_page=function() return self.subpage end, }, - - widgets.Panel{ - frame={t=0, b=7}, - autoarrange_subviews=true, - autoarrange_gap=1, - subviews={ - widgets.WrappedLabel{ - frame={t=0}, - text_to_wrap=self.intro_text, - }, - widgets.FilteredList{ - frame={t=5}, - view_id='list', - on_select=self:callback('on_select'), - on_double_click=self:callback('on_submit'), - on_double_click2=self:callback('launch_config'), - row_height=2, - }, - }, - }, widgets.WrappedLabel{ - view_id='desc', - frame={b=4, h=2}, - auto_height=false, + frame={t=7}, + text_to_wrap=function() return self.blurbs[self.subpage] end, + }, + widgets.FilteredList{ + frame={t=9}, + view_id='list', + on_select=self:callback('on_select'), + on_double_click=self:callback('on_submit'), + on_double_click2=self:callback('launch_config'), + row_height=2, }, + } +end + +function CommandTab:init_footer(panel) + panel:addviews{ widgets.HotkeyLabel{ - frame={b=2, l=0}, - label=self.select_label, + frame={t=0, l=0}, + label='Toggle enabled', key='SELECT', - enabled=self.is_enableable, + auto_width=true, on_activate=self:callback('on_submit') }, widgets.HotkeyLabel{ - view_id='show_help_label', - frame={b=1, l=0}, - label='Show tool help or run custom command', - key='CUSTOM_CTRL_H', - on_activate=self:callback('show_help') - }, - widgets.HotkeyLabel{ - view_id='launch', - frame={b=0, l=0}, + frame={t=1, l=0}, label='Launch tool-specific config UI', key='CUSTOM_CTRL_G', - enabled=self.is_configurable, + auto_width=true, + enabled=self:callback('has_config'), on_activate=self:callback('launch_config'), }, + widgets.HotkeyLabel{ + frame={t=2, l=26}, + label='Show full tool help or run custom command', + auto_width=true, + key='CUSTOM_CTRL_H', + on_activate=self:callback('show_help'), + }, } end -function Services:get_choices() - local enabled_map = common.get_enabled_map() +function CommandTab:show_help() + _,choice = self.subviews.list:getSelected() + if not choice then return end + dfhack.run_command('gui/launcher', choice.data.command .. ' ') +end + +function CommandTab:has_config() + _,choice = self.subviews.list:getSelected() + return choice and choice.gui_config +end + +function CommandTab:launch_config() + _,choice = self.subviews.list:getSelected() + if not choice or not choice.gui_config then return end + dfhack.run_command(choice.gui_config) +end + +-- +-- AutostartTab +-- + +AutostartTab = defclass(AutostartTab, CommandTab) +AutostartTab.ATTRS{ + intro_text='Tools that are enabled on this page will be auto-run or auto-enabled'.. + ' for you when you start a new fort (or, for "global" tools, when you start the game). To see tools that are enabled'.. + ' right now, please click on the "Enabled" tab.', +} + +local function make_autostart_text(label, mode, enabled) + if mode == 'system_enable' then + label = label .. ' (global)' + end + return { + {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, + {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, + {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, + ' ', + {tile=BUTTON_PEN_LEFT}, + {tile=HELP_PEN_CENTER}, + {tile=BUTTON_PEN_RIGHT}, + ' ', + label, + } +end + +function AutostartTab:refresh() local choices = {} + local group = Subtabs_revmap[self.subpage] for _,data in ipairs(registry.COMMANDS_BY_IDX) do - if command_passes_filters(data, self.group) then - table.insert(choices, {data=data, enabled=enabled_map[data.command]}) - end + if not common.command_passes_filters(data, group) then goto continue end + local enabled = safe_index(common.config.data.commands, data.command, 'autostart') + table.insert(choices, { + text=make_autostart_text(data.command, data.mode, enabled), + search_key=data.command, + data=data, + enabled=enabled, + }) + ::continue:: end - return choices + local list = self.subviews.list + local filter = list:getFilter() + local selected = list:getSelected() + list:setChoices(choices) + list:setFilter(filter, selected) + list.edit:setFocus(true) end -function Services:onInput(keys) - local handled = ConfigPanel.super.onInput(self, keys) +function AutostartTab:onInput(keys) + local handled = EnabledTab.super.onInput(self, keys) if keys._MOUSE_L then local list = self.subviews.list.list local idx = list:getIdxUnderMouse() @@ -198,54 +284,88 @@ function Services:onInput(keys) self:on_submit() elseif x >= 4 and x <= 6 then self:show_help() - elseif x >= 8 and x <= 10 then - self:launch_config() end end end return handled end +function AutostartTab:on_submit() + _,choice = self.subviews.list:getSelected() + if not choice then return end + local data = choice.data + common.set_autostart(data, not data.enabled) + self:refresh() +end + +function AutostartTab:restore_defaults() + local group = Subtabs_revmap[self.subtab] + for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if not common.command_passes_filters(data, group) then goto continue end + common.set_autostart(data, data.default) + ::continue:: + end + self:refresh() + dialogs.showMessage('Success', 'Defaults restored.') +end + +-- +-- EnabledTab +-- + +EnabledTab = defclass(EnabledTab, CommandTab) +EnabledTab.ATTRS{ + intro_text='These are the tools that are enabled right now. Note that if a'.. + ' tool is not marked as "global", then it can only be enabled when you have a fort loaded.'.. + ' Once enabled, tools will stay enabled when you'.. + ' save and reload your fort. If you want them to be'.. + ' auto-enabled for new forts, please see the "Autostart" tab.', +} -local COMMAND_REGEX = '^([%w/_-]+)' +-- TODO +local function get_gui_config(command) + return 'gui/confirm' +end + +local function make_enabled_text(label, mode, enabled, gui_config) + if mode == 'system_enable' then + label = label .. ' (global)' + end + return { + {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, + {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, + {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, + ' ', + {tile=BUTTON_PEN_LEFT}, + {tile=HELP_PEN_CENTER}, + {tile=BUTTON_PEN_RIGHT}, + ' ', + {tile=gui_config and BUTTON_PEN_LEFT or gui.CLEAR_PEN}, + {tile=gui_config and CONFIGURE_PEN_CENTER or gui.CLEAR_PEN}, + {tile=gui_config and BUTTON_PEN_RIGHT or gui.CLEAR_PEN}, + ' ', + label, + } +end -function Services:refresh() +function EnabledTab:refresh() local choices = {} - for _,choice in ipairs(self:get_choices()) do - local command = choice.target or choice.command - command = command:match(COMMAND_REGEX) - local gui_config = 'gui/' .. command - local want_gui_config = utils.getval(self.is_configurable, gui_config) - and helpdb.is_entry(gui_config) - local enabled = choice.enabled - local function get_enabled_pen(enabled_pen, disabled_pen) - if not utils.getval(self.is_enableable) then - return gui.CLEAR_PEN - end - return enabled and enabled_pen or disabled_pen - end - local text = { - {tile=get_enabled_pen(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT)}, - {tile=get_enabled_pen(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER)}, - {tile=get_enabled_pen(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT)}, - ' ', - {tile=BUTTON_PEN_LEFT}, - {tile=HELP_PEN_CENTER}, - {tile=BUTTON_PEN_RIGHT}, - ' ', - {tile=want_gui_config and BUTTON_PEN_LEFT or gui.CLEAR_PEN}, - {tile=want_gui_config and CONFIGURE_PEN_CENTER or gui.CLEAR_PEN}, - {tile=want_gui_config and BUTTON_PEN_RIGHT or gui.CLEAR_PEN}, - ' ', - choice.target, - } - local desc = helpdb.is_entry(command) and - helpdb.get_entry_short_help(command) or '' - table.insert(choices, - {text=text, command=choice.command, target=choice.target, desc=desc, - search_key=choice.target, enabled=enabled, - gui_config=want_gui_config and gui_config}) + self.enabled_map = common.get_enabled_map() + local group = Subtabs_revmap[self.subpage] + for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'run' then goto continue end + if not common.command_passes_filters(data, group) then goto continue end + local enabled = self.enabled_map[data.command] + local gui_config = get_gui_config(data.command) + table.insert(choices, { + text=make_enabled_text(data.command, data.mode, enabled, gui_config), + search_key=data.command, + data=data, + enabled=enabled, + gui_config=gui_config, + }) + ::continue:: end local list = self.subviews.list local filter = list:getFilter() @@ -255,84 +375,51 @@ function Services:refresh() list.edit:setFocus(true) end -function Services:on_select(idx, choice) - local desc = self.subviews.desc - desc.text_to_wrap = choice and choice.desc or '' - if desc.frame_body then - desc:updateLayout() - end - if choice then - self.subviews.launch.enabled = utils.getval(self.is_configurable) - and not not choice.gui_config +function EnabledTab:onInput(keys) + local handled = EnabledTab.super.onInput(self, keys) + if keys._MOUSE_L then + local list = self.subviews.list.list + local idx = list:getIdxUnderMouse() + if idx then + local x = list:getMousePos() + if x <= 2 then + self:on_submit() + elseif x >= 4 and x <= 6 then + self:show_help() + elseif x >= 8 and x <= 10 then + self:launch_config() + end + end end + return handled end -function Services:on_submit() - if not utils.getval(self.is_enableable) then return false end +function EnabledTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end - local tokens = {} - table.insert(tokens, choice.command) - table.insert(tokens, choice.enabled and 'disable' or 'enable') - table.insert(tokens, choice.target) - dfhack.run_command(tokens) + local data = choice.data + common.apply_command(data, self.enabled_map, not data.enabled) self:refresh() end -function Services:show_help() - _,choice = self.subviews.list:getSelected() - if not choice then return end - local command = choice.target:match(COMMAND_REGEX) - dfhack.run_command('gui/launcher', command .. ' ') -end - -function Services:launch_config() - if not utils.getval(self.is_configurable) then return false end - _,choice = self.subviews.list:getSelected() - if not choice or not choice.gui_config then return end - dfhack.run_command(choice.gui_config) +function EnabledTab:restore_defaults() + local group = Subtabs_revmap[self.subtab] + for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'run' then goto continue end + if (data.mode == 'enable' or data.mode == 'repeat') + and not dfhack.world.isFortressMode() + then + goto continue + end + if not common.command_passes_filters(data, group) then goto continue end + common.apply_command(data, self.enabled_map, data.default) + ::continue:: + end + self:refresh() + dialogs.showMessage('Success', 'Defaults restored.') end --- --- AutomationServices --- - -AutomationServices = defclass(AutomationServices, Services) -AutomationServices.ATTRS{ - intro_text='These tools can only be enabled when you have a fort loaded,'.. - ' but once you enable them, they will stay enabled when you'.. - ' save and reload your fort. If you want them to be'.. - ' auto-enabled for new forts, please see the "Autostart" tab.', - group='automation', -} - --- --- BugfixServices --- - -BugfixServices = defclass(BugfixServices, Services) -BugfixServices.ATTRS{ - intro_text='Tools that are enabled on this page will be auto-enabled for'.. - ' you when you start a new fort, using the default'.. - ' configuration. To see tools that are enabled right now in'.. - ' an active fort, please see the "Fort" tab.', - group='bugfix', -} - --- --- BugfixServices --- - -GameplayServices = defclass(GameplayServices, Services) -GameplayServices.ATTRS{ - intro_text='Tools that are enabled on this page will be auto-enabled for'.. - ' you when you start a new fort, using the default'.. - ' configuration. To see tools that are enabled right now in'.. - ' an active fort, please see the "Fort" tab.', - group='gameplay', -} -]] -- -- OverlaysTab -- @@ -363,20 +450,16 @@ function OverlaysTab:init_footer(panel) frame={t=0, l=0}, label='Toggle overlay', key='SELECT', + auto_width=true, on_activate=self:callback('on_submit') }, widgets.HotkeyLabel{ frame={t=1, l=0}, label='Launch overlay widget repositioning UI', key='CUSTOM_CTRL_G', + auto_width=true, on_activate=function() dfhack.run_script('gui/overlay') end, }, - widgets.HotkeyLabel{ - frame={t=2, l=0}, - label='Restore defaults', - key='CUSTOM_CTRL_D', - on_activate=self:callback('restore_defaults') - }, } end @@ -454,6 +537,7 @@ function OverlaysTab:restore_defaults() dialogs.showMessage('Success', 'Overlay defaults restored.') end + -- -- PreferencesTab -- @@ -490,6 +574,7 @@ function IntegerInputDialog:init() frame={b=0, l=0}, label='Save', key='SELECT', + auto_width=true, on_activate=function() self:hide(self.subviews.input_edit.text) end, }, widgets.HotkeyLabel{ @@ -567,14 +652,9 @@ function PreferencesTab:init_footer(panel) frame={t=0, l=0}, label='Toggle/edit setting', key='SELECT', + auto_width=true, on_activate=self:callback('on_submit') }, - widgets.HotkeyLabel{ - frame={t=2, l=0}, - label='Restore defaults', - key='CUSTOM_CTRL_D', - on_activate=self:callback('restore_defaults') - }, } end @@ -660,6 +740,7 @@ function PreferencesTab:restore_defaults() dialogs.showMessage('Success', 'Default preferences restored.') end + -- -- ControlPanel -- @@ -667,9 +748,9 @@ end ControlPanel = defclass(ControlPanel, widgets.Window) ControlPanel.ATTRS { frame_title='DFHack Control Panel', - frame={w=61, h=36}, + frame={w=78, h=44}, resizable=true, - resize_min={h=28}, + resize_min={h=39}, autoarrange_subviews=true, autoarrange_gap=1, } @@ -679,8 +760,8 @@ function ControlPanel:init() widgets.TabBar{ frame={t=0}, labels={ - --'Enabled', - --'Autostart', + 'Enabled', + 'Autostart', 'UI Overlays', 'Preferences', }, @@ -691,8 +772,8 @@ function ControlPanel:init() view_id='pages', frame={t=5, l=0, b=0, r=0}, subviews={ - --EnabledTab{}, - --AutostartTab{}, + EnabledTab{}, + AutostartTab{}, OverlaysTab{}, PreferencesTab{}, }, @@ -712,6 +793,7 @@ function ControlPanel:set_page(val) self:updateLayout() end + -- -- ControlPanelScreen -- diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index ea4deaad79..b68f002be9 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -46,7 +46,7 @@ COMMANDS_BY_IDX = { {command='seedwatch', group='automation', mode='enable'}, {command='suspendmanager', group='automation', mode='enable'}, {command='tailor', group='automation', mode='enable'}, - {command='work-now', group='automation', mode='enable'}, + {command='work-now', group='automation', mode='system_enable'}, -- bugfix tools {command='fix/blood-del', group='bugfix', mode='run', default=true}, From 38d1a1821b3da8493fb1ca91b61d50824708cb02 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 14:15:33 -0800 Subject: [PATCH 713/732] everything mostly works now --- gui/control-panel.lua | 160 ++++++++++++++++------------ internal/control-panel/common.lua | 4 +- internal/control-panel/registry.lua | 2 +- 3 files changed, 96 insertions(+), 70 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 910c211c05..738bb1e099 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -1,6 +1,7 @@ local common = reqscript('internal/control-panel/common') local dialogs = require('gui.dialogs') local gui = require('gui') +local helpdb = require('helpdb') local textures = require('gui.textures') local overlay = require('plugins.overlay') local registry = reqscript('internal/control-panel/registry') @@ -135,16 +136,13 @@ function CommandTab:init() self.subpage = Subtabs.automation self.blurbs = { - [Subtabs.automation]='These run in the background and'.. - ' help you manage your fort. They are always safe to enable, and allow'.. - ' you to avoid paying attention to aspects of gameplay that you find'.. - ' tedious or unfun.', - [Subtabs.bugfix]='These automatically fix dangerous or'.. - ' annoying vanilla bugs. You should generally have all of these enabled'.. - ' unless you have a specific reason not to.', - [Subtabs.gameplay]='These change or extend gameplay. Read'.. - ' their help docs to see what they do and enable the ones that appeal to'.. - ' you.', + [Subtabs.automation]='These run in the background and help you manage your'.. + ' fort. They are always safe to enable, and allow you to concentrate on'.. + ' other aspects of gameplay that you find more enjoyable.', + [Subtabs.bugfix]='These automatically fix dangerous or annoying vanilla'.. + ' bugs. You should generally have all of these enabled.', + [Subtabs.gameplay]='These change or extend gameplay. Read their help docs to'.. + ' see what they do and enable the ones that appeal to you.', } end @@ -223,21 +221,47 @@ function CommandTab:launch_config() dfhack.run_command(choice.gui_config) end + -- --- AutostartTab +-- EnabledTab -- -AutostartTab = defclass(AutostartTab, CommandTab) -AutostartTab.ATTRS{ - intro_text='Tools that are enabled on this page will be auto-run or auto-enabled'.. - ' for you when you start a new fort (or, for "global" tools, when you start the game). To see tools that are enabled'.. - ' right now, please click on the "Enabled" tab.', +EnabledTab = defclass(EnabledTab, CommandTab) +EnabledTab.ATTRS{ + intro_text='These are the tools that can be enabled right now. Most tools can'.. + ' only be enabled when you have a fort loaded. Once enabled, tools'.. + ' will stay enabled when you save and reload your fort. If you want'.. + ' them to be auto-enabled for new forts, please see the "Autostart"'.. + ' tab.', } -local function make_autostart_text(label, mode, enabled) +function EnabledTab:init() + if not dfhack.world.isFortressMode() then + self.subpage = Subtabs.gameplay + end +end + +-- TODO +local function get_gui_config(command) + command = common.get_first_word(command) + local gui_config = 'gui/' .. command + if helpdb.is_entry(gui_config) then + return gui_config + end +end + +local function make_enabled_text(label, mode, enabled, gui_config) if mode == 'system_enable' then label = label .. ' (global)' end + + local function get_config_button_token(tile) + return { + tile=gui_config and tile or nil, + text=not gui_config and ' ' or nil, + } + end + return { {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, @@ -247,21 +271,32 @@ local function make_autostart_text(label, mode, enabled) {tile=HELP_PEN_CENTER}, {tile=BUTTON_PEN_RIGHT}, ' ', + get_config_button_token(BUTTON_PEN_LEFT), + get_config_button_token(CONFIGURE_PEN_CENTER), + get_config_button_token(BUTTON_PEN_RIGHT), + ' ', label, } end -function AutostartTab:refresh() +function EnabledTab:refresh() local choices = {} + self.enabled_map = common.get_enabled_map() local group = Subtabs_revmap[self.subpage] for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'run' then goto continue end + if data.mode ~= 'system_enable' and not dfhack.world.isFortressMode() then + goto continue + end if not common.command_passes_filters(data, group) then goto continue end - local enabled = safe_index(common.config.data.commands, data.command, 'autostart') + local enabled = self.enabled_map[data.command] + local gui_config = get_gui_config(data.command) table.insert(choices, { - text=make_autostart_text(data.command, data.mode, enabled), + text=make_enabled_text(data.command, data.mode, enabled, gui_config), search_key=data.command, data=data, enabled=enabled, + gui_config=gui_config, }) ::continue:: end @@ -273,7 +308,7 @@ function AutostartTab:refresh() list.edit:setFocus(true) end -function AutostartTab:onInput(keys) +function EnabledTab:onInput(keys) local handled = EnabledTab.super.onInput(self, keys) if keys._MOUSE_L then local list = self.subviews.list.list @@ -284,25 +319,33 @@ function AutostartTab:onInput(keys) self:on_submit() elseif x >= 4 and x <= 6 then self:show_help() + elseif x >= 8 and x <= 10 then + self:launch_config() end end end return handled end -function AutostartTab:on_submit() +function EnabledTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end local data = choice.data - common.set_autostart(data, not data.enabled) + common.apply_command(data, self.enabled_map, not choice.enabled) self:refresh() end -function AutostartTab:restore_defaults() - local group = Subtabs_revmap[self.subtab] +function EnabledTab:restore_defaults() + local group = Subtabs_revmap[self.subpage] for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'run' then goto continue end + if (data.mode == 'enable' or data.mode == 'repeat') + and not dfhack.world.isFortressMode() + then + goto continue + end if not common.command_passes_filters(data, group) then goto continue end - common.set_autostart(data, data.default) + common.apply_command(data, self.enabled_map, data.default) ::continue:: end self:refresh() @@ -311,24 +354,17 @@ end -- --- EnabledTab +-- AutostartTab -- -EnabledTab = defclass(EnabledTab, CommandTab) -EnabledTab.ATTRS{ - intro_text='These are the tools that are enabled right now. Note that if a'.. - ' tool is not marked as "global", then it can only be enabled when you have a fort loaded.'.. - ' Once enabled, tools will stay enabled when you'.. - ' save and reload your fort. If you want them to be'.. - ' auto-enabled for new forts, please see the "Autostart" tab.', +AutostartTab = defclass(AutostartTab, CommandTab) +AutostartTab.ATTRS{ + intro_text='Tools that are enabled on this page will be auto-run or auto-enabled'.. + ' for you when you start a new fort (or, for "global" tools, when you start the game). To see tools that are enabled'.. + ' right now, please click on the "Enabled" tab.', } --- TODO -local function get_gui_config(command) - return 'gui/confirm' -end - -local function make_enabled_text(label, mode, enabled, gui_config) +local function make_autostart_text(label, mode, enabled) if mode == 'system_enable' then label = label .. ' (global)' end @@ -341,29 +377,24 @@ local function make_enabled_text(label, mode, enabled, gui_config) {tile=HELP_PEN_CENTER}, {tile=BUTTON_PEN_RIGHT}, ' ', - {tile=gui_config and BUTTON_PEN_LEFT or gui.CLEAR_PEN}, - {tile=gui_config and CONFIGURE_PEN_CENTER or gui.CLEAR_PEN}, - {tile=gui_config and BUTTON_PEN_RIGHT or gui.CLEAR_PEN}, - ' ', label, } end -function EnabledTab:refresh() +function AutostartTab:refresh() local choices = {} - self.enabled_map = common.get_enabled_map() local group = Subtabs_revmap[self.subpage] for _,data in ipairs(registry.COMMANDS_BY_IDX) do - if data.mode == 'run' then goto continue end if not common.command_passes_filters(data, group) then goto continue end - local enabled = self.enabled_map[data.command] - local gui_config = get_gui_config(data.command) + local enabled = safe_index(common.config.data.commands, data.command, 'autostart') + if enabled == nil then + enabled = data.default + end table.insert(choices, { - text=make_enabled_text(data.command, data.mode, enabled, gui_config), + text=make_autostart_text(data.command, data.mode, enabled), search_key=data.command, data=data, enabled=enabled, - gui_config=gui_config, }) ::continue:: end @@ -375,7 +406,7 @@ function EnabledTab:refresh() list.edit:setFocus(true) end -function EnabledTab:onInput(keys) +function AutostartTab:onInput(keys) local handled = EnabledTab.super.onInput(self, keys) if keys._MOUSE_L then local list = self.subviews.list.list @@ -386,35 +417,30 @@ function EnabledTab:onInput(keys) self:on_submit() elseif x >= 4 and x <= 6 then self:show_help() - elseif x >= 8 and x <= 10 then - self:launch_config() end end end return handled end -function EnabledTab:on_submit() +function AutostartTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end local data = choice.data - common.apply_command(data, self.enabled_map, not data.enabled) + common.set_autostart(data, not choice.enabled) + common.config:write() self:refresh() end -function EnabledTab:restore_defaults() - local group = Subtabs_revmap[self.subtab] +function AutostartTab:restore_defaults() + local group = Subtabs_revmap[self.subpage] for _,data in ipairs(registry.COMMANDS_BY_IDX) do - if data.mode == 'run' then goto continue end - if (data.mode == 'enable' or data.mode == 'repeat') - and not dfhack.world.isFortressMode() - then - goto continue - end if not common.command_passes_filters(data, group) then goto continue end - common.apply_command(data, self.enabled_map, data.default) + print(data.command, data.default) + common.set_autostart(data, data.default) ::continue:: end + common.config:write() self:refresh() dialogs.showMessage('Success', 'Defaults restored.') end @@ -501,8 +527,8 @@ function OverlaysTab:refresh() name=name, command='overlay', desc=state.db[name].desc, - enabled=enabled, }, + enabled=enabled, }) end local list = self.subviews.list @@ -524,7 +550,7 @@ function OverlaysTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end local data = choice.data - enable_overlay(data.name, not data.enabled) + enable_overlay(data.name, not choice.enabled) self:refresh() end diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua index d7cd1cec83..b4bb2a8713 100644 --- a/internal/control-panel/common.lua +++ b/internal/control-panel/common.lua @@ -78,7 +78,7 @@ function get_enabled_map() return enabled_map end -local function get_first_word(str) +function get_first_word(str) local word = str:trim():split(' +')[1] if word:startswith(':') then word = word:sub(2) end return word @@ -170,7 +170,7 @@ function set_preference(data, in_value) qerror(('value too small: got: %s; minimum: %s'):format(value, data.min)) end data.set_fn(value) - if data.default ~= safe_index(config.data.preferences, data.name, 'val') then + if data.default ~= value then config.data.preferences[data.name] = { val=value, version=data.version, diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index b68f002be9..ea4deaad79 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -46,7 +46,7 @@ COMMANDS_BY_IDX = { {command='seedwatch', group='automation', mode='enable'}, {command='suspendmanager', group='automation', mode='enable'}, {command='tailor', group='automation', mode='enable'}, - {command='work-now', group='automation', mode='system_enable'}, + {command='work-now', group='automation', mode='enable'}, -- bugfix tools {command='fix/blood-del', group='bugfix', mode='run', default=true}, From 601da4991ac4d1240f020ad753aa6ba260b0654f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 15:09:54 -0800 Subject: [PATCH 714/732] implement migration --- gui/control-panel.lua | 46 ++++--- internal/control-panel/common.lua | 8 +- internal/control-panel/migration.lua | 184 +++++++++++++-------------- internal/control-panel/registry.lua | 14 +- 4 files changed, 123 insertions(+), 129 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 738bb1e099..f1beb8de3a 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -171,7 +171,6 @@ function CommandTab:init_main_panel(panel) view_id='list', on_select=self:callback('on_select'), on_double_click=self:callback('on_submit'), - on_double_click2=self:callback('launch_config'), row_height=2, }, } @@ -188,14 +187,6 @@ function CommandTab:init_footer(panel) }, widgets.HotkeyLabel{ frame={t=1, l=0}, - label='Launch tool-specific config UI', - key='CUSTOM_CTRL_G', - auto_width=true, - enabled=self:callback('has_config'), - on_activate=self:callback('launch_config'), - }, - widgets.HotkeyLabel{ - frame={t=2, l=26}, label='Show full tool help or run custom command', auto_width=true, key='CUSTOM_CTRL_H', @@ -215,12 +206,6 @@ function CommandTab:has_config() return choice and choice.gui_config end -function CommandTab:launch_config() - _,choice = self.subviews.list:getSelected() - if not choice or not choice.gui_config then return end - dfhack.run_command(choice.gui_config) -end - -- -- EnabledTab @@ -239,9 +224,24 @@ function EnabledTab:init() if not dfhack.world.isFortressMode() then self.subpage = Subtabs.gameplay end + + self.subviews.list.list.on_double_click2=self:callback('launch_config') +end + +function EnabledTab:init_footer(panel) + EnabledTab.super.init_footer(self, panel) + panel:addviews{ + widgets.HotkeyLabel{ + frame={t=2, l=26}, + label='Launch tool-specific config UI', + key='CUSTOM_CTRL_G', + auto_width=true, + enabled=self:callback('has_config'), + on_activate=self:callback('launch_config'), + }, + } end --- TODO local function get_gui_config(command) command = common.get_first_word(command) local gui_config = 'gui/' .. command @@ -327,6 +327,12 @@ function EnabledTab:onInput(keys) return handled end +function EnabledTab:launch_config() + _,choice = self.subviews.list:getSelected() + if not choice or not choice.gui_config then return end + dfhack.run_command(choice.gui_config) +end + function EnabledTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end @@ -349,7 +355,8 @@ function EnabledTab:restore_defaults() ::continue:: end self:refresh() - dialogs.showMessage('Success', 'Defaults restored.') + dialogs.showMessage('Success', + ('Enabled defaults restored for %s tools.'):format(group)) end @@ -442,7 +449,8 @@ function AutostartTab:restore_defaults() end common.config:write() self:refresh() - dialogs.showMessage('Success', 'Defaults restored.') + dialogs.showMessage('Success', + ('Autostart defaults restored for %s tools.'):format(group)) end @@ -774,7 +782,7 @@ end ControlPanel = defclass(ControlPanel, widgets.Window) ControlPanel.ATTRS { frame_title='DFHack Control Panel', - frame={w=78, h=44}, + frame={w=68, h=44}, resizable=true, resize_min={h=39}, autoarrange_subviews=true, diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua index b4bb2a8713..a2aa7b86b5 100644 --- a/internal/control-panel/common.lua +++ b/internal/control-panel/common.lua @@ -15,12 +15,12 @@ REPEATS_GLOBAL_KEY = 'control-panel-repeats' local function get_config() local f = json.open(CONFIG_FILE) local updated = false + -- ensure proper structure + ensure_key(f.data, 'commands') + ensure_key(f.data, 'preferences') if f.exists then - -- ensure proper structure - ensure_key(f.data, 'commands') - ensure_key(f.data, 'preferences') -- remove unknown or out of date entries from the loaded config - for k, v in pairs(f.data) do + for k in pairs(f.data) do if k ~= 'commands' and k ~= 'preferences' then updated = true f.data[k] = nil diff --git a/internal/control-panel/migration.lua b/internal/control-panel/migration.lua index 0fd4f5ab41..cb0a639a8f 100644 --- a/internal/control-panel/migration.lua +++ b/internal/control-panel/migration.lua @@ -1,11 +1,20 @@ -- migrate configuration from 50.11-r4 and prior to new format --@module = true +-- read old files, add converted data to config_data, overwrite old files with +-- a message that says they are deprecated and can be deleted with the proper +-- procedure. we can't delete them outright since steam may just restore them due to +-- Steam Cloud. We *could* delete them, though, if we know that we've been started +-- from Steam as DFHack and not as DF + +local argparse = require('argparse') +local registry = reqscript('internal/control-panel/registry') + -- init files local SYSTEM_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-system.init' -local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init' local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' +local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init' local function save_tombstone_file(path) local ok, f = pcall(io.open, path, 'w') @@ -23,123 +32,100 @@ local function save_tombstone_file(path) f:write('# If you\'re not on Steam, you can delete this file at any time.\n') f:close() end ---[[ -function SystemServices:on_submit() - SystemServices.super.on_submit(self) - local enabled_map = self:get_enabled_map() - local save_fn = function(f) - for _,service in ipairs(SYSTEM_USER_SERVICES) do - if enabled_map[service] then - f:write(('enable %s\n'):format(service)) - end - end +local function add_autostart(config_data, name) + if not registry.COMMANDS_BY_NAME[name].default then + config_data.commands[name] = {autostart=true} end - save_file(SYSTEM_INIT_FILE, save_fn) end -function FortServicesAutostart:on_submit() - _,choice = self.subviews.list:getSelected() - if not choice then return end - self.enabled_map[choice.target] = not choice.enabled - - local save_fn = function(f) - for service,enabled in pairs(self.enabled_map) do - if enabled then - if service:match(' ') then - f:write(('on-new-fortress %s\n'):format(service)) - else - f:write(('on-new-fortress enable %s\n'):format(service)) - end - end - end +local function add_preference(config_data, name, val) + local data = registry.PREFERENCES_BY_NAME[name] + if type(data.default) == 'boolean' then + ok, val = pcall(argparse.boolean, val) + if not ok then return end + elseif type(data.default) == 'number' then + val = tonumber(val) + if not val then return end + end + if data.default ~= val then + config_data.preferences[name] = {val=val} end - save_file(AUTOSTART_FILE, save_fn) - self:refresh() end -function FortServicesAutostart:init() - local enabled_map = {} - local ok, f = pcall(io.open, AUTOSTART_FILE) - if ok and f then - local services_set = utils.invert(FORT_AUTOSTART) - for line in f:lines() do - line = line:trim() - if #line == 0 or line:startswith('#') then goto continue end - local service = line:match('^on%-new%-fortress enable ([%S]+)$') - or line:match('^on%-new%-fortress (.+)') - if service and services_set[service] then - enabled_map[service] = true - end - ::continue:: +local function parse_lines(fname, line_fn) + local ok, f = pcall(io.open, fname) + if not ok or not f then return end + for line in f:lines() do + line = line:trim() + if #line > 0 and not line:startswith('#') then + line_fn(line) end end - self.enabled_map = enabled_map end -function Preferences:do_save() - local save_fn = function(f) - for ctx_name,settings in pairs(PREFERENCES) do - local ctx_env = require(ctx_name) - for id in pairs(settings) do - f:write((':lua require("%s").%s=%s\n'):format( - ctx_name, id, tostring(ctx_env[id]))) - end +local function migrate_system(config_data) + parse_lines(SYSTEM_INIT_FILE, function(line) + local service = line:match('^enable ([%S]+)$') + if not service then return end + local data = registry.COMMANDS_BY_NAME[service] + if data and (data.mode == 'system_enable' or data.command == 'work-now') then + add_autostart(config_data, service) end - for _,spec in ipairs(CPP_PREFERENCES) do - local line = spec.init_fmt:format(spec.get_fn()) - f:write(('%s\n'):format(line)) - end - end - save_file(PREFERENCES_INIT_FILE, save_fn) + end) + save_tombstone_file(SYSTEM_INIT_FILE) end -function RepeatAutostart:init() - self.subviews.show_help_label.visible = false - self.subviews.launch.visible = false - local enabled_map = {} - local ok, f = pcall(io.open, REPEATS_FILE) - if ok and f then - for line in f:lines() do - line = line:trim() - if #line == 0 or line:startswith('#') then goto continue end - local service = line:match('^repeat %-%-name ([%S]+)') - if service then - enabled_map[service] = true - end - ::continue:: +local function migrate_autostart(config_data) + parse_lines(AUTOSTART_FILE, function(line) + local service = line:match('^on%-new%-fortress enable ([%S]+)$') + or line:match('^on%-new%-fortress (.+)') + if not service then return end + local data = registry.COMMANDS_BY_NAME[service] + if data and (data.mode == 'enable' or data.mode == 'run') then + add_autostart(config_data, service) end - end - self.enabled_map = enabled_map + end) + save_tombstone_file(AUTOSTART_FILE) end -function RepeatAutostart:on_submit() - _,choice = self.subviews.list:getSelected() - if not choice then return end - self.enabled_map[choice.name] = not choice.enabled - local run_commands = dfhack.isMapLoaded() +local REPEAT_MAP = { + autoMilkCreature='automilk', + autoShearCreature='autoshear', + ['dead-units-burrow']='fix/dead-units', + ['empty-wheelbarrows']='fix/empty-wheelbarrows', + ['general-strike']='fix/general-strike', + ['stuck-instruments']='fix/stuck-instruments', +} - local save_fn = function(f) - for name,enabled in pairs(self.enabled_map) do - if enabled then - local command_str = ('repeat --name %s %s\n'): - format(name, table.concat(REPEATS[name].command, ' ')) - f:write(command_str) - if run_commands then - dfhack.run_command(command_str) -- actually start it up too - end - elseif run_commands then - repeatUtil.cancel(name) - end +local function migrate_repeats(config_data) + parse_lines(REPEATS_FILE, function(line) + local service = line:match('^repeat %-%-name ([%S]+)') + if not service then return end + service = REPEAT_MAP[service] or service + local data = registry.COMMANDS_BY_NAME[service] + if data and data.mode == 'repeat' then + add_autostart(config_data, service) end - end - save_file(REPEATS_FILE, save_fn) - self:refresh() + end) + save_tombstone_file(REPEATS_FILE) end -]] + +local function migrate_preferences(config_data) + parse_lines(PREFERENCES_INIT_FILE, function(line) + local name, val = line:match('^:lua .+%.([^=]+)=(.+)') + if not name or not val then return end + local data = registry.PREFERENCES_BY_NAME[name] + if data then + add_preference(config_data, name, val) + end + end) + save_tombstone_file(PREFERENCES_INIT_FILE) +end + function migrate(config_data) - -- read old files, add converted data to config_data, overwrite old files with - -- a message that says they are deprecated and can be deleted with the proper procedure - -- we can't delete them outright since steam may just restore them due to Steam Cloud - -- we *could* delete them if we know that we've been started from Steam as DFHack and not as DF + migrate_system(config_data) + migrate_autostart(config_data) + migrate_repeats(config_data) + migrate_preferences(config_data) end diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index ea4deaad79..0e458b0fd1 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -10,16 +10,16 @@ COMMANDS_BY_IDX = { -- automation tools {command='autobutcher', group='automation', mode='enable'}, {command='autobutcher target 10 10 14 2 BIRD_GOOSE', group='automation', mode='run', - desc='Set to autostart if you usually want to raise geese.'}, + desc='Enable if you usually want to raise geese.'}, {command='autobutcher target 10 10 14 2 BIRD_TURKEY', group='automation', mode='run', - desc='Set to autostart if you usually want to raise turkeys.'}, + desc='Enable if you usually want to raise turkeys.'}, {command='autobutcher target 10 10 14 2 BIRD_CHICKEN', group='automation', mode='run', - desc='Set to autostart if you usually want to raise chickens.'}, + desc='Enable if you usually want to raise chickens.'}, {command='autochop', group='automation', mode='enable'}, {command='autoclothing', group='automation', mode='enable'}, {command='autofarm', group='automation', mode='enable'}, {command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run', - desc='Set to autostart if you usually farm pig tails for the clothing industry.'}, + desc='Enable if you usually farm pig tails for the clothing industry.'}, {command='autofish', group='automation', mode='enable'}, --{command='autolabor', group='automation', mode='enable'}, -- hide until it works better {command='automilk', group='automation', mode='repeat', @@ -32,9 +32,9 @@ COMMANDS_BY_IDX = { {command='autoslab', group='automation', mode='enable'}, {command='ban-cooking all', group='automation', mode='run'}, {command='buildingplan set boulders false', group='automation', mode='run', - desc='Set to autostart if you usually don\'t want to use boulders for construction.'}, + desc='Enable if you usually don\'t want to use boulders for construction.'}, {command='buildingplan set logs false', group='automation', mode='run', - desc='Set to autostart if you usually don\'t want to use logs for construction.'}, + desc='Enable if you usually don\'t want to use logs for construction.'}, {command='cleanowned', group='automation', mode='repeat', desc='Encourage dwarves to drop tattered clothing and grab new ones.', params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, @@ -70,7 +70,7 @@ COMMANDS_BY_IDX = { desc='Combine partial stacks in stockpiles into full stacks.', params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, {command='drain-aquifer --top 2', group='gameplay', mode='run', - desc='Set to autostart to ensure that your maps have no more than 2 layers of aquifer.'}, + desc='Ensure that your maps have no more than 2 layers of aquifer.'}, {command='dwarfvet', group='gameplay', mode='enable'}, {command='emigration', group='gameplay', mode='enable'}, {command='fastdwarf', group='gameplay', mode='enable'}, From e07779f1c4aafe00742fe25cdb1c276216c6c85b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 15:46:29 -0800 Subject: [PATCH 715/732] finish docs --- docs/gui/control-panel.rst | 129 ++++++++++++++++++------------------- gui/control-panel.lua | 33 ++++++++-- 2 files changed, 93 insertions(+), 69 deletions(-) diff --git a/docs/gui/control-panel.rst b/docs/gui/control-panel.rst index a8933087fd..cec8bdba59 100644 --- a/docs/gui/control-panel.rst +++ b/docs/gui/control-panel.rst @@ -2,83 +2,82 @@ gui/control-panel ================= .. dfhack-tool:: - :summary: Configure DFHack. + :summary: Configure DFHack and manage active DFHack tools. :tags: dfhack The DFHack control panel allows you to quickly see and change what DFHack tools -are enabled now, which tools will run when you start a new fort, and how global -DFHack configuration options are set. It also provides convenient links to -relevant help pages and GUI configuration frontends. The control panel has -several pages that you can switch among by clicking on the tabs at the top of -the window. Each page has a search filter so you can quickly find the tools and -options that you're looking for. - -Fort Services -------------- - -The fort services page shows tools that you can enable in fort mode. You can -select the tool name to see a short description at the bottom of the list. Hit +are enabled, which tools will run when you start a new fort, which UI overlays +are enabled, and how global DFHack configuration options are set. It also +provides convenient links to relevant help pages and GUI configuration +frontends (where available). The control panel has several sections that you +can access by clicking on the tabs at the top of the window. Each tab has a +search filter so you can quickly find the tools and options that you're looking +for. + +The tabs can also be navigated with the keyboard, with the :kbd:`Ctrl`:kbd:`T` +and :kbd:`Ctrl`:kbd:`Y` hotkeys. These are the default hotkeys for navigating +DFHack tab bars. + +The "Enabled" tab +----------------- + +The "Enabled" tab shows tools that you can enable right now. You can select the +tool name to see a short description at the bottom of the list. Hit :kbd:`Enter`, double click on the tool name, or click on the toggle on the far left to enable or disable that tool. -Note that the fort services displayed on this page can only be enabled when a -fort is loaded. They will be disabled in the list and cannot be enabled or have -their GUI config screens shown until you have loaded a fortress. Once you do -enable them (after you've loaded a fort), they will save their state with your -fort and automatically re-enable themselves when you load your fort again. +Note that before a fort is loaded, there will be very few tools listed here. -You can hit :kbd:`Ctrl`:kbd:`H` or click on the help icon to show the help page -for the selected tool in `gui/launcher`. You can also use this as shortcut to +Tools are split into three subcategories: ``automation``, ``bugfix``, and +``gameplay``. In general, you'll probably want to start with only the +``bugfix`` tools enabled. As you become more comfortable with vanilla systems, +and some of them start to become less fun and more toilsome, you can enable +more of the ``automation`` tools to manage them for you. Finally, you can +examine the tools on the ``gameplay`` tab and enable whatever you think sounds +like fun :). + +The category subtabs can also be navigated with the keyboard, with the +:kbd:`Ctrl`:kbd:`N` and :kbd:`Ctrl`:kbd:`M` hotkeys. + +Once tools are enabled (possible after you've loaded a fort), they will save +their state with your fort and automatically re-enable themselves when you load +that same fort again. + +You can hit :kbd:`Ctrl`:kbd:`H` or click on the help icon to show the help page for the selected tool in `gui/launcher`. You can also use this as shortcut to run custom commandline commands to configure that tool manually. If the tool has an associated GUI config screen, a gear icon will also appear next to the help -icon. Hit :kbd:`Ctrl`:kbd:`G` or click on that icon to launch the relevant -configuration interface. +icon. Hit :kbd:`Ctrl`:kbd:`G`, click on the gear icon, or Shift-double click the tool name to launch the relevant configuration interface. .. _dfhack-examples-guide: -New Fort Autostart Commands ---------------------------- - -This page shows the tools that you can configure DFHack to auto-enable or -auto-run when you start a new fort. You'll recognize many tools from the -previous page here, but there are also useful one-time commands that you might -want to run at the start of a fort, like `ban-cooking all `. - -Periodic Maintenance Operations -------------------------------- - -This page shows commands that DFHack can regularly run for you in order to keep -your fort (and the game) running smoothly. For example, there are commands to -periodically enqueue orders for shearing animals that are ready to be shorn or -sort your manager orders so slow-moving daily orders won't prevent your -high-volume one-time orders from ever being completed. - -System Services ---------------- - -The system services page shows "core" DFHack tools that provide background -services to other tools. It is generally not advisable to turn these tools -off. If you do toggle them off in the control panel, they will be re-enabled -when you restart the game. If you really need to turn these tools off -permanently, add a line like ``disable toolname`` to your -``dfhack-config/init/dfhack.init`` file. - -Overlays --------- - -The overlays page allows you to easily see which overlays are enabled and lets -you toggle them on and off and see the help for the owning tools. If you want to -reposition any of the overlay widgets, hit :kbd:`Ctrl`:kbd:`G` or click on -the the hotkey hint to launch `gui/overlay`. - -Preferences ------------ - -The preferences page allows you to change DFHack's internal settings and -defaults, like whether DFHack tools pause the game when they come up, or how -long you can wait between clicks and still have it count as a double-click. Hit -:kbd:`Ctrl`:kbd:`G` or click on the hotkey hint at the bottom of the page to -restore all preferences to defaults. +The "Autostart" tab +------------------- + +This tab is organized similarly to the "Enabled" tab, but instead of tools you +can enable now, it shows the tools that you can configure DFHack to auto-enable +or auto-run when you start the game or a new fort. You'll recognize many tools +from the "Enabled" tab here, but there are also useful one-time commands that +you might want to run at the start of a fort, like +`ban-cooking all `. + +The "UI Overlays" tab +--------------------- + +The overlays tab allows you to easily see which overlays are enabled, lets you +toggle them on and off, and gives you links for the related help text (which is +normally added at the bottom of the help page for the tool that provides the +overlay). If you want to reposition any of the overlay widgets, hit +:kbd:`Ctrl`:kbd:`G` or click on the the hotkey hint to launch `gui/overlay`. + +The "Preferences" tab +--------------------- + +The preferences tab allows you to change DFHack's internal settings and +defaults, like whether DFHack's "mortal mode" is enabled -- hiding the god-mode +tools from the UI, whether DFHack tools pause the game when they come up, or how +long you can take between clicks and still have it count as a double-click. +Click on the gear icon or hit :kdb:`Enter` to toggle or edit the selected +preference. Usage ----- diff --git a/gui/control-panel.lua b/gui/control-panel.lua index f1beb8de3a..262de23c01 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -150,6 +150,8 @@ function CommandTab:init_main_panel(panel) panel:addviews{ widgets.TabBar{ frame={t=5}, + key='CUSTOM_CTRL_N', + key_back='CUSTOM_CTRL_M', labels={ 'Automation', 'Bug Fixes', @@ -195,10 +197,14 @@ function CommandTab:init_footer(panel) } end +local function launch_help(command) + dfhack.run_command('gui/launcher', command .. ' ') +end + function CommandTab:show_help() _,choice = self.subviews.list:getSelected() if not choice then return end - dfhack.run_command('gui/launcher', choice.data.command .. ' ') + launch_help(choice.data.command) end function CommandTab:has_config() @@ -489,7 +495,14 @@ function OverlaysTab:init_footer(panel) }, widgets.HotkeyLabel{ frame={t=1, l=0}, - label='Launch overlay widget repositioning UI', + label='Show overlay help', + auto_width=true, + key='CUSTOM_CTRL_H', + on_activate=self:callback('show_help'), + }, + widgets.HotkeyLabel{ + frame={t=2, l=26}, + label='Launch widget position adjustment UI', key='CUSTOM_CTRL_G', auto_width=true, on_activate=function() dfhack.run_script('gui/overlay') end, @@ -506,6 +519,8 @@ function OverlaysTab:onInput(keys) local x = list:getMousePos() if x <= 2 then self:on_submit() + elseif x >= 4 and x <= 6 then + self:show_help() end end end @@ -518,6 +533,10 @@ local function make_overlay_text(label, enabled) {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, ' ', + {tile=BUTTON_PEN_LEFT}, + {tile=HELP_PEN_CENTER}, + {tile=BUTTON_PEN_RIGHT}, + ' ', label, } end @@ -533,8 +552,8 @@ function OverlaysTab:refresh() search_key=name, data={ name=name, - command='overlay', - desc=state.db[name].desc, + command=name:match('^(.-)%.') or 'overlay', + desc=state.db[name].widget.desc, }, enabled=enabled, }) @@ -571,6 +590,12 @@ function OverlaysTab:restore_defaults() dialogs.showMessage('Success', 'Overlay defaults restored.') end +function OverlaysTab:show_help() + _,choice = self.subviews.list:getSelected() + if not choice then return end + launch_help(choice.data.command) +end + -- -- PreferencesTab From 31683dd724f71273620432d32284fa6192a50b7f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 15:55:45 -0800 Subject: [PATCH 716/732] fix doc typo --- docs/gui/control-panel.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/control-panel.rst b/docs/gui/control-panel.rst index cec8bdba59..4f3b8608f6 100644 --- a/docs/gui/control-panel.rst +++ b/docs/gui/control-panel.rst @@ -76,7 +76,7 @@ The preferences tab allows you to change DFHack's internal settings and defaults, like whether DFHack's "mortal mode" is enabled -- hiding the god-mode tools from the UI, whether DFHack tools pause the game when they come up, or how long you can take between clicks and still have it count as a double-click. -Click on the gear icon or hit :kdb:`Enter` to toggle or edit the selected +Click on the gear icon or hit :kbd:`Enter` to toggle or edit the selected preference. Usage From f70fda4579e0c13a6d8d23fb4114337269d7eaee Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 20:10:36 -0800 Subject: [PATCH 717/732] add descriptions to script overlays --- docs/gui/mod-manager.rst | 5 ++--- exportlegends.lua | 2 ++ gui/civ-alert.lua | 1 + gui/design.lua | 1 + gui/mod-manager.lua | 5 +++++ internal/caravan/movegoods.lua | 2 ++ internal/caravan/pedestal.lua | 1 + internal/caravan/trade.lua | 2 ++ internal/caravan/tradeagreement.lua | 1 + startdwarf.lua | 1 + suspendmanager.lua | 2 ++ trackstop.lua | 2 ++ unsuspend.lua | 1 + 13 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 3d88415dc7..8972fece72 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -5,9 +5,8 @@ gui/mod-manager :summary: Save and restore lists of active mods. :tags: dfhack interface -Adds an optional overlay to the mod list screen that -allows you to save and load mod list presets, as well -as set a default mod list preset for new worlds. +Adds an optional overlay to the mod list screen that allows you to save and +load mod list presets, as well as set a default mod list preset for new worlds. Usage ----- diff --git a/exportlegends.lua b/exportlegends.lua index 22373fd7a5..d04d117223 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -1031,6 +1031,7 @@ end LegendsOverlay = defclass(LegendsOverlay, overlay.OverlayWidget) LegendsOverlay.ATTRS{ + desc='Adds extended export progress bar to the legends main screen.', default_pos={x=2, y=2}, default_enabled=true, viewscreens='legends/Default', @@ -1085,6 +1086,7 @@ end DoneMaskOverlay = defclass(DoneMaskOverlay, overlay.OverlayWidget) DoneMaskOverlay.ATTRS{ + desc='Prevents legends mode from being exited while an export is in progress.', default_pos={x=-2, y=2}, default_enabled=true, viewscreens='legends', diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 1c6cb17519..7430d9e6ee 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -111,6 +111,7 @@ end CivalertOverlay = defclass(CivalertOverlay, overlay.OverlayWidget) CivalertOverlay.ATTRS{ + desc='Adds a button for activating a civilian alert when the squads panel is open.', default_pos={x=-15,y=-1}, default_enabled=true, viewscreens='dwarfmode', diff --git a/gui/design.lua b/gui/design.lua index 4ccb85697e..5a0458076b 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1784,6 +1784,7 @@ local DIMENSION_TOOLTIP_Y_OFFSET = 3 DimensionsOverlay = defclass(DimensionsOverlay, overlay.OverlayWidget) DimensionsOverlay.ATTRS{ + desc='Adds a tooltip that shows the selected dimensions when drawing boxes.', default_pos={x=1,y=1}, default_enabled=true, overlay_only=true, -- not player-repositionable diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 08e9ae5b03..0a14f75921 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -378,6 +378,7 @@ ModmanageOverlay = defclass(ModmanageOverlay, overlay.OverlayWidget) ModmanageOverlay.ATTRS { frame = { w=16, h=3 }, frame_style = gui.MEDIUM_FRAME, + desc = "Adds a link to the mod selection screen for accessing the mod manager.", default_pos = { x=5, y=-5 }, viewscreens = { "new_region/Mods" }, default_enabled=true, @@ -400,6 +401,7 @@ end NotificationOverlay = defclass(NotificationOverlay, overlay.OverlayWidget) NotificationOverlay.ATTRS { frame = { w=60, h=1 }, + desc = "Displays a message when a mod preset has been automatically applied.", default_pos = { x=3, y=-2 }, viewscreens = { "new_region" }, default_enabled=true, @@ -462,3 +464,6 @@ end if dfhack_flags.module then return end + +-- TODO: when invoked as a command, should show information on which mods are loaded +-- and give the player the option to export the list (or at least copy it to the clipboard) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index fa39d219b3..f9e3bed474 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -717,6 +717,7 @@ end MoveGoodsOverlay = defclass(MoveGoodsOverlay, overlay.OverlayWidget) MoveGoodsOverlay.ATTRS{ + desc='Adds link to trade depot building to launch the DFHack trade goods UI.', default_pos={x=-64, y=10}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING/TradeDepot', @@ -766,6 +767,7 @@ end AssignTradeOverlay = defclass(AssignTradeOverlay, overlay.OverlayWidget) AssignTradeOverlay.ATTRS{ + desc='Adds link to the trade goods screen to launch the DFHack trade goods UI.', default_pos={x=-41,y=-5}, default_enabled=true, viewscreens='dwarfmode/AssignTrade', diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index a7842891f9..859e0f10e8 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -666,6 +666,7 @@ end PedestalOverlay = defclass(PedestalOverlay, overlay.OverlayWidget) PedestalOverlay.ATTRS{ + desc='Adds link to the display furniture building panel to launch the DFHack display assignment UI.', default_pos={x=-40, y=34}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING/DisplayFurniture', diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index ded4a7c716..8533d7c63c 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -719,6 +719,7 @@ end TradeOverlay = defclass(TradeOverlay, overlay.OverlayWidget) TradeOverlay.ATTRS{ + desc='Adds convenience functions for working with bins to the trade screen.', default_pos={x=-3,y=-12}, default_enabled=true, viewscreens='dwarfmode/Trade/Default', @@ -803,6 +804,7 @@ end TradeBannerOverlay = defclass(TradeBannerOverlay, overlay.OverlayWidget) TradeBannerOverlay.ATTRS{ + desc='Adds link to the trade screen to launch the DFHack trade UI.', default_pos={x=-31,y=-7}, default_enabled=true, viewscreens='dwarfmode/Trade/Default', diff --git a/internal/caravan/tradeagreement.lua b/internal/caravan/tradeagreement.lua index 52f4b5e91a..14378237d8 100644 --- a/internal/caravan/tradeagreement.lua +++ b/internal/caravan/tradeagreement.lua @@ -6,6 +6,7 @@ local widgets = require('gui.widgets') TradeAgreementOverlay = defclass(TradeAgreementOverlay, overlay.OverlayWidget) TradeAgreementOverlay.ATTRS{ + desc='Adds select all/none functionality when requesting trade agreement items.', default_pos={x=45, y=-6}, default_enabled=true, viewscreens='dwarfmode/Diplomacy/Requests', diff --git a/startdwarf.lua b/startdwarf.lua index 5f779a5933..b0210c550e 100644 --- a/startdwarf.lua +++ b/startdwarf.lua @@ -6,6 +6,7 @@ local widgets = require('gui.widgets') StartDwarfOverlay = defclass(StartDwarfOverlay, overlay.OverlayWidget) StartDwarfOverlay.ATTRS{ + desc='Adds a scrollbar (if necessary) to the list of starting dwarves.', default_pos={x=5, y=9}, default_enabled=true, viewscreens='setupdwarfgame/Dwarves', diff --git a/suspendmanager.lua b/suspendmanager.lua index 283d99234f..e4aec16185 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -800,6 +800,7 @@ end -- Overlay Widgets StatusOverlay = defclass(StatusOverlay, overlay.OverlayWidget) StatusOverlay.ATTRS{ + desc='Adds information to suspended building panels about why it is suspended.', default_pos={x=-39,y=16}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', @@ -837,6 +838,7 @@ end ToggleOverlay = defclass(ToggleOverlay, overlay.OverlayWidget) ToggleOverlay.ATTRS{ + desc='Adds a link to suspended building panels for enabling suspendmanager.', default_pos={x=-57,y=23}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', diff --git a/trackstop.lua b/trackstop.lua index 6657209b08..8065261624 100644 --- a/trackstop.lua +++ b/trackstop.lua @@ -54,6 +54,7 @@ local DIRECTION_MAP_REVERSE = utils.invert(DIRECTION_MAP) TrackStopOverlay = defclass(TrackStopOverlay, overlay.OverlayWidget) TrackStopOverlay.ATTRS{ + desc='Adds widgets for reconfiguring trackstops after construction.', default_pos={x=-73, y=29}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING/Trap/TrackStop', @@ -166,6 +167,7 @@ end RollerOverlay = defclass(RollerOverlay, overlay.OverlayWidget) RollerOverlay.ATTRS{ + desc='Adds widgets for reconfiguring rollers after construction.', default_pos={x=-71, y=29}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING/Rollers', diff --git a/unsuspend.lua b/unsuspend.lua index 2a56f5dea9..3172f52a66 100644 --- a/unsuspend.lua +++ b/unsuspend.lua @@ -16,6 +16,7 @@ local textures = dfhack.textures.loadTileset('hack/data/art/unsuspend.png', 32, SuspendOverlay = defclass(SuspendOverlay, overlay.OverlayWidget) SuspendOverlay.ATTRS{ + desc='Annotates suspended buildings with a visible marker.', viewscreens='dwarfmode', default_enabled=true, overlay_only=true, From 727cd37eb7e936835962306c6d2b84d10bcfa3f4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 20:11:38 -0800 Subject: [PATCH 718/732] update changelog for control-panel --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 9e9753a058..438a02cacf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `control-panel`: new commandline interface for control panel functions ## New Features @@ -37,6 +38,7 @@ Template for new versions: ## Misc Improvements - `gui/control-panel`: reduce frequency for `warn-stranded` check to once every 2 days +- `gui/control-panel`: tools are now organized by type: automation, bugfix, and gameplay ## Removed From a6326af0e31470467118ae681d9ce3666a9bd279 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 22:42:24 -0800 Subject: [PATCH 719/732] respond to feedback on gui/control-panel - make the Enabled page pick up enable state changes from other sources - improve readability of the Preferences tab --- gui/control-panel.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 262de23c01..5ed42144df 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -365,6 +365,12 @@ function EnabledTab:restore_defaults() ('Enabled defaults restored for %s tools.'):format(group)) end +-- pick up enablement changes made from other sources (e.g. gui config tools) +function EnabledTab:onRenderFrame(dc, rect) + self:refresh() + EnabledTab.super.onRenderFrame(self, dc, rect) +end + -- -- AutostartTab @@ -696,7 +702,7 @@ function PreferencesTab:init_main_panel(panel) view_id='list', on_select=self:callback('on_select'), on_double_click=self:callback('on_submit'), - row_height=2, + row_height=3, }, IntegerInputDialog{ view_id='input_dlg', @@ -736,13 +742,15 @@ function PreferencesTab:onInput(keys) return handled end -local function make_preference_text(label, value) +local function make_preference_text(label, default, value) return { {tile=BUTTON_PEN_LEFT}, {tile=CONFIGURE_PEN_CENTER}, {tile=BUTTON_PEN_RIGHT}, ' ', - ('%s (%s)'):format(label, value), + label, + NEWLINE, + {gap=4, text=('(default: %s, current: %s)'):format(default, value)}, } end @@ -750,7 +758,7 @@ function PreferencesTab:refresh() if self.subviews.input_dlg.visible then return end local choices = {} for _, data in ipairs(registry.PREFERENCES_BY_IDX) do - local text = make_preference_text(data.label, data.get_fn()) + local text = make_preference_text(data.label, data.default, data.get_fn()) table.insert(choices, { text=text, search_key=text[#text], From 9c04e9082489564e9b957c10592bc2aa66682613 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 22:58:54 -0800 Subject: [PATCH 720/732] update doc references to control panel tabs --- docs/fix/empty-wheelbarrows.rst | 4 ++-- docs/fix/general-strike.rst | 3 +-- docs/warn-starving.rst | 2 +- docs/warn-stranded.rst | 17 ++++++++++------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index a6070d3091..eb6d155104 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -9,8 +9,8 @@ Empties all wheelbarrows which contain rocks that have become 'stuck' in them. This works around the issue encountered with :bug:`6074`, and should be run if you notice wheelbarrows lying around with rocks in them that aren't -being used in a task. This script can also be set to run periodically in -the background by toggling the Maintenance task in `gui/control-panel`. +being used in a task. This script is set to run periodically by default in +`gui/control-panel`. Usage ----- diff --git a/docs/fix/general-strike.rst b/docs/fix/general-strike.rst index 7bdd5ccf2c..71f4c5985f 100644 --- a/docs/fix/general-strike.rst +++ b/docs/fix/general-strike.rst @@ -8,8 +8,7 @@ fix/general-strike This script attempts to fix known causes of the "general strike bug", where dwarves just stop accepting work and stand around with "No job". -You can enable automatic running of this fix in the "Maintenance" tab of -`gui/control-panel`. +This script is set to run periodically by default in `gui/control-panel`. Usage ----- diff --git a/docs/warn-starving.rst b/docs/warn-starving.rst index 8e411ff285..44da819ca2 100644 --- a/docs/warn-starving.rst +++ b/docs/warn-starving.rst @@ -10,7 +10,7 @@ pause and you'll get a warning dialog telling you which units are in danger. This gives you a chance to rescue them (or take them out of their cages) before they die. -You can enable ``warn-starving`` notifications in `gui/control-panel` on the "Maintenance" tab. +This script is set to run periodically by default in `gui/control-panel`. Usage ----- diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 10a92a8f2a..825c86067a 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -2,20 +2,23 @@ warn-stranded ============= .. dfhack-tool:: - :summary: Reports citizens that are stranded and can't reach any other citizens. + :summary: Reports citizens who can't reach any other citizens. :tags: fort units -If any (live) groups of fort citizens are stranded from the main (largest) group, -the game will pause and you'll get a warning dialog telling you which citizens are isolated. -This gives you a chance to rescue them before they get overly stressed or start starving. +If any (live) groups of fort citizens are stranded from the main (largest) +group, the game will pause and you'll get a warning dialog telling you which +citizens are isolated. This gives you a chance to rescue them before they get +overly stressed or start starving. Each citizen will be put into a group with the other citizens stranded together. -There is a command line interface that can print status of citizens without pausing or bringing up a window. +There is a command line interface that can print status of citizens without +pausing or bringing up a window. -The GUI and command-line both also have the ability to ignore citizens so they don't trigger a pause and window. +The GUI and command-line both also have the ability to ignore citizens so they +don't trigger a pause and window. -You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. +You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Gameplay" subtab. Usage ----- From 2dd77a506de989e31ca42d77f668cff931c0e60b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 22:59:18 -0800 Subject: [PATCH 721/732] enable warn-starving by default, like it always has been --- internal/control-panel/registry.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 0e458b0fd1..491c4ade37 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -82,7 +82,7 @@ COMMANDS_BY_IDX = { desc='Invalidates all work orders once a month, allowing conditions to be rechecked.', params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, {command='starvingdead', group='gameplay', mode='enable'}, - {command='warn-starving', group='gameplay', mode='repeat', + {command='warn-starving', group='gameplay', mode='repeat', default=true, desc='Show a warning dialog when units are starving or dehydrated.', params={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, {command='warn-stranded', group='gameplay', mode='repeat', From 0cc856296213bdd28a0598da880f1db8fbf74681 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Dec 2023 23:22:58 -0800 Subject: [PATCH 722/732] support utf-8 chars in the quickstart guide --- quickstart-guide.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/quickstart-guide.lua b/quickstart-guide.lua index 8511cedfa3..e5352ce6ca 100644 --- a/quickstart-guide.lua +++ b/quickstart-guide.lua @@ -24,6 +24,7 @@ local function get_sections() end local prev_line = nil for line in lines do + line = dfhack.utf2df(line) if line:match('^[=-]+$') then add_section_widget(sections, section) section = {} From 34449f10abf89fc33d0321c925f87520a24c2a04 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 00:51:35 -0800 Subject: [PATCH 723/732] refresh enabled status without breaking scrolling --- control-panel.lua | 2 +- gui/control-panel.lua | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/control-panel.lua b/control-panel.lua index 2f4574fb78..ba1ea8e8e1 100644 --- a/control-panel.lua +++ b/control-panel.lua @@ -40,7 +40,7 @@ local function apply_fort_loaded_config() apply_autostart_config() persist.GlobalTable[GLOBAL_KEY] = json.encode({autostart_done=true}) end - local enabled_repeats = json.decode(persist.GlobalTable[common.REPEATS_GLOBAL_KEY] or '') + local enabled_repeats = json.decode(persist.GlobalTable[common.REPEATS_GLOBAL_KEY] or '') or {} for _, data in ipairs(registry.COMMANDS_BY_IDX) do if data.mode == 'repeat' and enabled_repeats[data.command] then common.apply_command(data) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 5ed42144df..9a34157603 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -256,11 +256,17 @@ local function get_gui_config(command) end end -local function make_enabled_text(label, mode, enabled, gui_config) +local function make_enabled_text(self, label, mode, gui_config) if mode == 'system_enable' then label = label .. ' (global)' end + local function get_enabled_button_token(enabled_tile, disabled_tile) + return { + tile=function() return self.enabled_map[label] and enabled_tile or disabled_tile end, + } + end + local function get_config_button_token(tile) return { tile=gui_config and tile or nil, @@ -269,9 +275,9 @@ local function make_enabled_text(label, mode, enabled, gui_config) end return { - {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, - {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, - {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, + get_enabled_button_token(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT), + get_enabled_button_token(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER), + get_enabled_button_token(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT), ' ', {tile=BUTTON_PEN_LEFT}, {tile=HELP_PEN_CENTER}, @@ -295,13 +301,11 @@ function EnabledTab:refresh() goto continue end if not common.command_passes_filters(data, group) then goto continue end - local enabled = self.enabled_map[data.command] local gui_config = get_gui_config(data.command) table.insert(choices, { - text=make_enabled_text(data.command, data.mode, enabled, gui_config), + text=make_enabled_text(self, data.command, data.mode, gui_config), search_key=data.command, data=data, - enabled=enabled, gui_config=gui_config, }) ::continue:: @@ -343,7 +347,7 @@ function EnabledTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end local data = choice.data - common.apply_command(data, self.enabled_map, not choice.enabled) + common.apply_command(data, self.enabled_map, not self.enabled_map[choice.data.command]) self:refresh() end @@ -367,7 +371,7 @@ end -- pick up enablement changes made from other sources (e.g. gui config tools) function EnabledTab:onRenderFrame(dc, rect) - self:refresh() + self.enabled_map = common.get_enabled_map() EnabledTab.super.onRenderFrame(self, dc, rect) end From 461ad3278651e301dff0bd00f5615443a327d73a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 01:24:20 -0800 Subject: [PATCH 724/732] add usage notes to full-heal docs --- docs/full-heal.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/full-heal.rst b/docs/full-heal.rst index 0680efb03b..280fd8d43d 100644 --- a/docs/full-heal.rst +++ b/docs/full-heal.rst @@ -35,3 +35,13 @@ Examples ``full-heal -r --keep_corpse --unit 23273`` Fully heal unit 23273. If this unit was dead, it will be resurrected without removing the corpse - creepy! + +Notes +----- + +If you have to repeatedly use `full-heal` on a dwarf only to have that dwarf's +syndrome return seconds later, then it's likely because said dwarf still has a +syndrome-causing residue on their body. To deal with this, either use +``clean units`` to decontaminate the dwarf or let a hospital worker wash the +residue off the dwarf and THEN do a `full-heal`. Syndromes like Beast Sickness +and Demon Sickness can by VERY NASTY, causing maladies like tissue necrosis. From 8a7f22bc32f8c6bb11dc64aab106656f1530bda8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 14:41:23 -0800 Subject: [PATCH 725/732] reinstate uniform-unstick reformat and clean up code fix handling of --multi option and add docs for it remove interaction with pre-v50 sidebar that no longer exists --- changelog.txt | 1 + docs/uniform-unstick.rst | 7 +- uniform-unstick.lua | 388 +++++++++++++++++---------------------- 3 files changed, 174 insertions(+), 222 deletions(-) diff --git a/changelog.txt b/changelog.txt index 438a02cacf..62ef56f096 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ Template for new versions: ## New Tools - `control-panel`: new commandline interface for control panel functions +- `uniform-unstick`: (reinstated) force squad members to drop items that they picked up in the wrong order so they can get everything equipped properly ## New Features diff --git a/docs/uniform-unstick.rst b/docs/uniform-unstick.rst index 3e877aae2d..f195a48616 100644 --- a/docs/uniform-unstick.rst +++ b/docs/uniform-unstick.rst @@ -3,7 +3,7 @@ uniform-unstick .. dfhack-tool:: :summary: Make military units reevaluate their uniforms. - :tags: unavailable + :tags: fort bugfix military This tool prompts military units to reevaluate their uniform, making them remove and drop potentially conflicting worn items. @@ -15,6 +15,9 @@ item for that bodypart (e.g. if you're still manufacturing them). Uniforms that have no issues are being properly worn will not be affected. +Note that this tool cannot fix the case where the same item is assigned to +multiple squad members. + Usage ----- @@ -42,3 +45,5 @@ Strategy options Remove to-equip items from containers or other's inventories and place them on the ground, ready to be claimed. This is most useful when someone else is wearing/holding the required items. +``--multi`` + Attempt to fix issues with uniforms that allow multiple items per body part. diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 469686e2a2..bdc94dd5e9 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -1,279 +1,225 @@ -- Prompt units to adjust their uniform. -local help = [====[ - -uniform-unstick -=============== - -Prompt units to reevaluate their uniform, by removing/dropping potentially conflicting worn items. - -Unlike a "replace clothing" designation, it won't remove additional clothing -if it's coexisting with a uniform item already on that body part. -It also won't remove clothing (e.g. shoes, trousers) if the unit has yet to claim an -armor item for that bodypart. (e.g. if you're still manufacturing them.) - -By default it simply prints info about the currently selected unit, -to actually drop items, you need to provide it the -drop option. - -The default algorithm assumes that there's only one armor item assigned per body part, -which means that it may miss cases where one piece of armor is blocked but the other -is present. The -multi option can possibly get around this, but at the cost of ignoring -left/right distinctions when dropping items. - -In some cases, an assigned armor item can't be put on because someone else is wearing/holding it. -The -free option will cause the assigned item to be removed from the container/dwarven inventory -and placed onto the ground, ready for pickup. - -In no cases should the command cause a uniform item that is being properly worn to be removed/dropped. - -Targets: - -:(no target): Force the selected dwarf to put on their uniform. -:-all: Force the uniform on all military dwarves. - -Options: - -:(none): Simply show identified issues (dry-run). -:-drop: Cause offending worn items to be placed on ground under unit. -:-free: Remove to-equip items from containers or other's inventories, and place on ground. -:-multi: Be more agressive in removing items, best for when uniforms have muliple items per body part. -]====] - local utils = require('utils') local validArgs = utils.invert({ - 'all', - 'drop', - 'free', - 'multi', - 'help' + 'all', + 'drop', + 'free', + 'multi', + 'help' }) -- Functions -function item_description(item) - return dfhack.df2console( dfhack.items.getDescription(item, 0, true) ) +local function item_description(item) + return dfhack.df2console(dfhack.items.getDescription(item, 0, true)) end -function get_item_pos(item) - local x, y, z = dfhack.items.getPosition(item) - if x == nil or y == nil or z == nil then - return nil - end +local function get_item_pos(item) + local x, y, z = dfhack.items.getPosition(item) + if not x or not y or not z then + return + end - if not dfhack.maps.isValidTilePos(x,y,z) then - print("NOT VALID TILE") - return nil - end - if not dfhack.maps.isTileVisible(x,y,z) then - print("NOT VISIBLE TILE") - return nil - end - return xyz2pos(x, y, z) + if dfhack.maps.isTileVisible(x, y, z) then + return xyz2pos(x, y, z) + end end -function find_squad_position(unit) - for i, squad in pairs( df.global.world.squads.all ) do - for i, position in pairs( squad.positions ) do - if position.occupant == unit.hist_figure_id then - return position - end +local function find_squad_position(unit) + for _, squad in ipairs(df.global.world.squads.all) do + for _, position in ipairs(squad.positions) do + if position.occupant == unit.hist_figure_id then + return position + end + end end - end - return nil end -function bodyparts_that_can_wear(unit, item) +local function bodyparts_that_can_wear(unit, item) + local bodyparts = {} + local unitparts = df.creature_raw.find(unit.race).caste[unit.caste].body_info.body_parts - local bodyparts = {} - local unitparts = df.creature_raw.find(unit.race).caste[unit.caste].body_info.body_parts - - if item._type == df.item_helmst then - for index, part in pairs(unitparts) do - if part.flags.HEAD then - table.insert(bodyparts, index) - end - end - elseif item._type == df.item_armorst then - for index, part in pairs(unitparts) do - if part.flags.UPPERBODY then - table.insert(bodyparts, index) - end - end - elseif item._type == df.item_glovesst then - for index, part in pairs(unitparts) do - if part.flags.GRASP then - table.insert(bodyparts, index) - end - end - elseif item._type == df.item_pantsst then - for index, part in pairs(unitparts) do - if part.flags.LOWERBODY then - table.insert(bodyparts, index) - end - end - elseif item._type == df.item_shoesst then - for index, part in pairs(unitparts) do - if part.flags.STANCE then - table.insert(bodyparts, index) - end + if item._type == df.item_helmst then + for index, part in ipairs(unitparts) do + if part.flags.HEAD then + table.insert(bodyparts, index) + end + end + elseif item._type == df.item_armorst then + for index, part in ipairs(unitparts) do + if part.flags.UPPERBODY then + table.insert(bodyparts, index) + end + end + elseif item._type == df.item_glovesst then + for index, part in ipairs(unitparts) do + if part.flags.GRASP then + table.insert(bodyparts, index) + end + end + elseif item._type == df.item_pantsst then + for index, part in ipairs(unitparts) do + if part.flags.LOWERBODY then + table.insert(bodyparts, index) + end + end + elseif item._type == df.item_shoesst then + for index, part in ipairs(unitparts) do + if part.flags.STANCE then + table.insert(bodyparts, index) + end + end + else + -- print("Ignoring item type for "..item_description(item) ) end - else - -- print("Ignoring item type for "..item_description(item) ) - end - return bodyparts + return bodyparts end -- Will figure out which items need to be moved to the floor, returns an item_id:item map -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.TranslateName( dfhack.units.getVisibleName(unit) ) ) +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.TranslateName(dfhack.units.getVisibleName(unit))) - if not silent then - print("Processing unit "..unit_name) - 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 = find_squad_position(unit) - if squad_position == nil then if not silent then - print("Unit "..unit_name.." does not have a military uniform.") + print("Processing unit " .. unit_name) end - return nil - end - -- Find all worn items which may be at issue. - local worn_items = {} -- map of item ids to item objects - local worn_parts = {} -- map of item ids to body part ids - for k, inv_item in pairs(unit.inventory) do - local item = inv_item.item - if inv_item.mode == df.unit_inventory_item.T_mode.Worn or inv_item.mode == df.unit_inventory_item.T_mode.Weapon then -- Include weapons so we can check we have them later - worn_items[ item.id ] = item - worn_parts[ item.id ] = inv_item.body_part_id - end - end + -- The return value + local to_drop = {} -- item id to item object - -- 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 loc, specs in pairs( squad_position.uniform ) do - for i, spec in pairs(specs) do - for i, assigned in pairs( 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 ) - end + -- First get squad position for an early-out for non-military dwarves + local squad_position = find_squad_position(unit) + if not squad_position then + if not silent then + print("Unit " .. unit_name .. " does not have a military uniform.") + end + return end - end - -- Figure out which assigned items are currently not being worn - - 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 worn_items[ u_id ] == nil then - print("Unit "..unit_name.." is missing an assigned item, object #"..u_id.." '"..item_description(item).."'" ) - missing_ids[ u_id ] = item - if args.free then - to_drop[ u_id ] = item - end - else - present_ids[ u_id ] = item + -- Find all worn items which may be at issue. + local worn_items = {} -- map of item ids to item objects + local worn_parts = {} -- map of item ids to body part ids + for _, inv_item in ipairs(unit.inventory) do + local item = inv_item.item + -- Include weapons so we can check we have them later + if inv_item.mode == df.unit_inventory_item.T_mode.Worn or + inv_item.mode == df.unit_inventory_item.T_mode.Weapon + then + worn_items[item.id] = item + worn_parts[item.id] = inv_item.body_part_id + end end - end - -- Figure out which worn items should be dropped + -- 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.uniform) do + for _, spec in ipairs(specs) do + for _, assigned in ipairs(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) + end + end + end - -- First, figure out which body parts are covered by the uniform pieces we have. - local covered = {} -- map of body part id to true/nil - for id, item in pairs( present_ids ) do - if item._type ~= df.item_weaponst and item._type ~= df.item_shieldst then -- weapons and shields don't "cover" the bodypart they're assigned to. (Needed to figure out if we're missing gloves.) - covered[ worn_parts[ id ] ] = true + -- Figure out which assigned items are currently not being worn + + 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 + print("Unit " .. unit_name .. " is missing an assigned item, object #" .. u_id .. " '" .. + item_description(item) .. "'") + missing_ids[u_id] = item + if args.free then + to_drop[u_id] = item + end + else + present_ids[u_id] = item + end end - end - if multi then - covered = {} -- Don't consider current covers - drop for anything which is missing - end + -- Figure out which worn items should be dropped + + -- First, figure out which body parts are covered by the uniform pieces we have. + -- 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 + -- 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 + end + end + end - -- Figure out body parts which should be covered but aren't - local uncovered = {} - for id, item in pairs(missing_ids) do - for i, bp in pairs( bodyparts_that_can_wear(unit, item) ) do - if not covered[bp] then - uncovered[bp] = true - end + -- Figure out body parts which should be covered but aren't + local uncovered = {} + for _, item in pairs(missing_ids) do + for _, bp in ipairs(bodyparts_that_can_wear(unit, item)) do + if not covered[bp] then + uncovered[bp] = true + end + end end - 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 - print("Unit "..unit_name.." potentially has object #"..w_id.." '"..item_description(item).."' blocking a missing uniform item.") - if args.drop then - to_drop[ w_id ] = item + -- 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 + print("Unit " .. + unit_name .. + " potentially has object #" .. + w_id .. " '" .. item_description(item) .. "' blocking a missing uniform item.") + if args.drop then + to_drop[w_id] = item + end + end end - end end - end - return to_drop + return to_drop end - -function do_drop( item_list ) - if item_list == nil then - return nil - end - - local mode_swap = false - if df.global.plotinfo.main.mode == df.ui_sidebar_mode.ViewUnits then - df.global.plotinfo.main.mode = df.ui_sidebar_mode.Default - mode_swap = true - end - - for id, item in pairs(item_list) do - local pos = get_item_pos(item) - if pos == nil then - dfhack.printerr("Could not find drop location for item #"..id.." "..item_description(item)) - else - local retval = dfhack.items.moveToGround( item, pos ) - if retval == false then - dfhack.printerr("Could not drop object #"..id.." "..item_description(item)) - else - print("Dropped item #"..id.." '"..item_description(item).."'") - end +local function do_drop(item_list) + if not item_list then + return end - end - if mode_swap then - df.global.plotinfo.main.mode = df.ui_sidebar_mode.ViewUnits - end + 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)) + else + if dfhack.items.moveToGround(item, pos) then + print("Dropped item #" .. id .. " '" .. item_description(item) .. "'") + else + dfhack.printerr("Could not drop object #" .. id .. " " .. item_description(item)) + end + end + end end -- Main -local args = utils.processArgs({...}, validArgs) +local args = utils.processArgs({ ... }, validArgs) if args.help then - print(help) + print(dfhack.script_help()) return end if args.all then - for k,unit in ipairs(df.global.world.units.active) do - if dfhack.units.isCitizen(unit) then - local to_drop = process(unit,args) - do_drop( to_drop ) + for _, unit in ipairs(dfhack.units.getCitizens(false)) do + do_drop(process(unit, args)) end - end else - local unit=dfhack.gui.getSelectedUnit() - if unit then - local to_drop = process(unit,args) - do_drop( to_drop ) - end + local unit = dfhack.gui.getSelectedUnit() + if unit then + do_drop(process(unit, args)) + else + qerror("Please select a unit if not running with --all") + end end From 363761060e0bf11736c63ddedee6a5792ed663f7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 16:28:21 -0800 Subject: [PATCH 726/732] remove item assignment if another unit has a claim and rewrite the incorrect and slow squad position find logic --- docs/uniform-unstick.rst | 6 +++--- uniform-unstick.lua | 40 +++++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/docs/uniform-unstick.rst b/docs/uniform-unstick.rst index f195a48616..2834a3c03f 100644 --- a/docs/uniform-unstick.rst +++ b/docs/uniform-unstick.rst @@ -42,8 +42,8 @@ Strategy options Force the unit to drop conflicting worn items onto the ground, where they can then be reclaimed in the correct order. ``--free`` - Remove to-equip items from containers or other's inventories and place them - on the ground, ready to be claimed. This is most useful when someone else - is wearing/holding the required items. + Remove items from the uniform assignment if someone else has a claim on + them. This will also remove items from containers and place them on the + ground, ready to be claimed. ``--multi`` Attempt to fix issues with uniforms that allow multiple items per body part. diff --git a/uniform-unstick.lua b/uniform-unstick.lua index bdc94dd5e9..714907fab3 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -26,13 +26,11 @@ local function get_item_pos(item) end end -local function find_squad_position(unit) - for _, squad in ipairs(df.global.world.squads.all) do - for _, position in ipairs(squad.positions) do - if position.occupant == unit.hist_figure_id then - return position - end - end +local function get_squad_position(unit) + local squad = df.squad.find(unit.military.squad_id) + if not squad then return end + if #squad.positions > unit.military.squad_position then + return squad.positions[unit.military.squad_position] end end @@ -90,7 +88,7 @@ local function process(unit, args) local to_drop = {} -- item id to item object -- First get squad position for an early-out for non-military dwarves - local squad_position = find_squad_position(unit) + local squad_position = get_squad_position(unit) if not squad_position then if not silent then print("Unit " .. unit_name .. " does not have a military uniform.") @@ -124,6 +122,7 @@ local function process(unit, args) end -- Figure out which assigned items are currently not being worn + -- and if some other unit is carrying the item, unassign it from this unit's uniform local present_ids = {} -- map of item ID to item object local missing_ids = {} -- map of item ID to item object @@ -131,9 +130,28 @@ local function process(unit, args) if not worn_items[u_id] then print("Unit " .. unit_name .. " is missing an assigned item, object #" .. u_id .. " '" .. item_description(item) .. "'") - missing_ids[u_id] = item - if args.free then - to_drop[u_id] = item + if dfhack.items.getGeneralRef(item, df.general_ref_type.UNIT_HOLDER) then + print(" Another unit has a claim on object #" .. u_id .. " '" .. item_description(item) .. "'") + if args.free then + print(" Removing from uniform") + assigned_items[u_id] = nil + for _, specs in ipairs(squad_position.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 + unit.military.pickup_flags.update = true + end + else + missing_ids[u_id] = item + if args.free then + to_drop[u_id] = item + end end else present_ids[u_id] = item From 6cb86a5ccf5a84b24777be29ec9859c5f025e334 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 16:37:32 -0800 Subject: [PATCH 727/732] strapped items are ok --- uniform-unstick.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 714907fab3..93b5357d3d 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -103,7 +103,8 @@ local function process(unit, args) local item = inv_item.item -- Include weapons so we can check we have them later if inv_item.mode == df.unit_inventory_item.T_mode.Worn or - inv_item.mode == df.unit_inventory_item.T_mode.Weapon + inv_item.mode == df.unit_inventory_item.T_mode.Weapon or + inv_item.mode == df.unit_inventory_item.T_mode.Strapped then worn_items[item.id] = item worn_parts[item.id] = inv_item.body_part_id From af63d0d02375b563d31e8279f7800d52c95289d0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 16:37:56 -0800 Subject: [PATCH 728/732] update docs --- docs/uniform-unstick.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/uniform-unstick.rst b/docs/uniform-unstick.rst index 2834a3c03f..7145dacc50 100644 --- a/docs/uniform-unstick.rst +++ b/docs/uniform-unstick.rst @@ -6,7 +6,11 @@ uniform-unstick :tags: fort bugfix military This tool prompts military units to reevaluate their uniform, making them -remove and drop potentially conflicting worn items. +remove and drop potentially conflicting worn items. If multiple units claim the +same item, the item will be unassigned from all units that are not already +wearing the item. If this happens, you'll have to click the "Update equipment" +button on the Squads "Equip" screen in order for them to get new equipment +assigned. Unlike a "replace clothing" designation, it won't remove additional clothing if it's coexisting with a uniform item already on that body part. It also won't @@ -15,9 +19,6 @@ item for that bodypart (e.g. if you're still manufacturing them). Uniforms that have no issues are being properly worn will not be affected. -Note that this tool cannot fix the case where the same item is assigned to -multiple squad members. - Usage ----- From cf02f0e7fc905acd9be5e507d220c7381b1f08cf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 16:54:48 -0800 Subject: [PATCH 729/732] add fix conflicts link to the squad equipment page --- docs/uniform-unstick.rst | 8 +++++ uniform-unstick.lua | 64 +++++++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/docs/uniform-unstick.rst b/docs/uniform-unstick.rst index 7145dacc50..569040d54b 100644 --- a/docs/uniform-unstick.rst +++ b/docs/uniform-unstick.rst @@ -48,3 +48,11 @@ Strategy options ground, ready to be claimed. ``--multi`` Attempt to fix issues with uniforms that allow multiple items per body part. + +Overlay +------- + +This script adds a small link to the squad equipment page that will run +``uniform-unstick --all --drop --free`` when clicked. If any items are +unassigned (they'll turn red on the equipment screen), hit the "Update +Equipment" button to get everything resolved. diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 93b5357d3d..89bcc2c65e 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -1,5 +1,8 @@ --- Prompt units to adjust their uniform. +--@ module=true + +local overlay = require('plugins.overlay') local utils = require('utils') +local widgets = require('gui.widgets') local validArgs = utils.invert({ 'all', @@ -220,25 +223,52 @@ local function do_drop(item_list) end end +local function main(args) + args = utils.processArgs(args, validArgs) --- Main + if args.help then + print(dfhack.script_help()) + return + end -local args = utils.processArgs({ ... }, validArgs) + if args.all then + for _, unit in ipairs(dfhack.units.getCitizens(false)) do + do_drop(process(unit, args)) + end + else + local unit = dfhack.gui.getSelectedUnit() + if unit then + do_drop(process(unit, args)) + else + qerror("Please select a unit if not running with --all") + end + end +end -if args.help then - print(dfhack.script_help()) - return +EquipOverlay = defclass(EquipOverlay, overlay.OverlayWidget) +EquipOverlay.ATTRS{ + desc='Adds a link to the equip screen to fix equipment conflicts.', + default_pos={x=-102,y=21}, + default_enabled=true, + viewscreens='dwarfmode/SquadEquipment', + frame={w=23, h=1}, +} + +function EquipOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0, r=0, h=1}, + label='Fix conflicts', + key='CUSTOM_CTRL_T', + on_activate=function() main{'--all', '--drop', '--free'} end, + }, + } end -if args.all then - for _, unit in ipairs(dfhack.units.getCitizens(false)) do - do_drop(process(unit, args)) - end -else - local unit = dfhack.gui.getSelectedUnit() - if unit then - do_drop(process(unit, args)) - else - qerror("Please select a unit if not running with --all") - end +OVERLAY_WIDGETS = {overlay=EquipOverlay} + +if dfhack_flags.module then + return end + +main({...}) From d64b7e9efaa69137d43273df6821ca205cff0757 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 17:38:21 -0800 Subject: [PATCH 730/732] add visual report window so players can see what there is to solve --- docs/uniform-unstick.rst | 9 +++-- uniform-unstick.lua | 83 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/docs/uniform-unstick.rst b/docs/uniform-unstick.rst index 569040d54b..58a207e064 100644 --- a/docs/uniform-unstick.rst +++ b/docs/uniform-unstick.rst @@ -53,6 +53,9 @@ Overlay ------- This script adds a small link to the squad equipment page that will run -``uniform-unstick --all --drop --free`` when clicked. If any items are -unassigned (they'll turn red on the equipment screen), hit the "Update -Equipment" button to get everything resolved. +``uniform-unstick --all`` and show the report when clicked. After reviewing the +report, you can right click to exit and do nothing or you can click the "Try to +resolve conflicts" button, which runs the equivalent of +``uniform-unstick --all --drop --free``. If any items are unassigned (they'll +turn red on the equipment screen), hit the "Update Equipment" button to +reassign equipment. diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 89bcc2c65e..a01b3886f3 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -1,5 +1,6 @@ --@ module=true +local gui = require('gui') local overlay = require('plugins.overlay') local utils = require('utils') local widgets = require('gui.widgets') @@ -245,26 +246,100 @@ local function main(args) end end +ReportWindow = defclass(ReportWindow, widgets.Window) +ReportWindow.ATTRS { + frame_title='Equipment conflict report', + frame={w=100, h=45}, + resizable=true, -- if resizing makes sense for your dialog + resize_min={w=50, h=20}, -- try to allow users to shrink your windows + autoarrange_subviews=1, + autoarrange_gap=1, + report=DEFAULT_NIL, +} + +function ReportWindow:init() + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0, r=0}, + label='Try to resolve conflicts', + key='CUSTOM_CTRL_T', + auto_width=true, + on_activate=function() + dfhack.run_script('uniform-unstick', '--all', '--drop', '--free') + self.parent_view:dismiss() + end, + }, + widgets.WrappedLabel{ + frame={t=2, l=0, r=0}, + text_pen=COLOR_LIGHTRED, + text_to_wrap='After resolving conflicts, be sure to click the "Update equipment" button to reassign new equipment!', + }, + widgets.WrappedLabel{ + frame={t=4, l=0, r=0}, + text_to_wrap=self.report, + }, + } +end + +ReportScreen = defclass(ReportScreen, gui.ZScreenModal) +ReportScreen.ATTRS { + focus_path='equipreport', + report=DEFAULT_NIL, +} + +function ReportScreen:init() + self:addviews{ReportWindow{report=self.report}} +end + EquipOverlay = defclass(EquipOverlay, overlay.OverlayWidget) EquipOverlay.ATTRS{ desc='Adds a link to the equip screen to fix equipment conflicts.', - default_pos={x=-102,y=21}, + default_pos={x=-101,y=21}, default_enabled=true, viewscreens='dwarfmode/SquadEquipment', - frame={w=23, h=1}, + frame={w=26, h=1}, } function EquipOverlay:init() self:addviews{ widgets.TextButton{ + view_id='button', + frame={t=0, l=0, r=0, h=1}, + label='Detect conflicts', + key='CUSTOM_CTRL_T', + on_activate=self:callback('run_report'), + }, + widgets.TextButton{ + view_id='button_good', frame={t=0, l=0, r=0, h=1}, - label='Fix conflicts', + label=' All good! ', + text_pen=COLOR_GREEN, key='CUSTOM_CTRL_T', - on_activate=function() main{'--all', '--drop', '--free'} end, + visible=false, }, } end +function EquipOverlay:run_report() + local output = dfhack.run_command_silent({'uniform-unstick', '--all'}) + if #output == 0 then + self.subviews.button.visible = false + self.subviews.button_good.visible = true + local end_ms = dfhack.getTickCount() + 5000 + local function label_reset() + if dfhack.getTickCount() < end_ms then + dfhack.timeout(10, 'frames', label_reset) + else + self.subviews.button_good.visible = false + self.subviews.button.visible = true + end + end + label_reset() + else + ReportScreen{report=output}:show() + end +end + OVERLAY_WIDGETS = {overlay=EquipOverlay} if dfhack_flags.module then From 295cf42f4715eac4dcdd21c572e62c9562412895 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 17:39:47 -0800 Subject: [PATCH 731/732] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 62ef56f096..eada935a6b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: - `uniform-unstick`: (reinstated) force squad members to drop items that they picked up in the wrong order so they can get everything equipped properly ## New Features +- `uniform-unstick`: add overlay to the squad equipment screen to show a equipment conflict report and give you a one-click button to fix ## Fixes - `warn-stranded`: Automatically ignore citizens who are gathering plants or digging to avoid issues with gathering fruit via stepladders and weird issues with digging From b9931acff2a780f282092e7125891e78626686c5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 31 Dec 2023 17:55:45 -0800 Subject: [PATCH 732/732] report on bad labors on squad units --- uniform-unstick.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index a01b3886f3..67016e2f85 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -79,6 +79,11 @@ local function bodyparts_that_can_wear(unit, item) return bodyparts end +local function print_bad_labor(unit_name, labor_name) + print("WARNING: Unit " .. unit_name .. " has the " .. labor_name .. + " labor enabled, which conflicts with military uniforms.") +end + -- Will figure out which items need to be moved to the floor, returns an item_id:item map local function process(unit, args) local silent = args.all -- Don't print details if we're iterating through all dwarves @@ -100,6 +105,14 @@ local function process(unit, args) return end + if unit.status.labors.MINE then + print_bad_labor(unit_name, "mining") + elseif unit.status.labors.CUTWOOD then + print_bad_labor(unit_name, "woodcutting") + elseif unit.status.labors.HUNT then + print_bad_labor(unit_name, "hunting") + end + -- Find all worn items which may be at issue. local worn_items = {} -- map of item ids to item objects local worn_parts = {} -- map of item ids to body part ids