From 2d68b21547b8c08cf0726026cf5ad8d233420f9a Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Tue, 14 Feb 2023 21:20:18 -0800 Subject: [PATCH 001/126] Show mouse hover on HotkeyLabels Labels show the hover colour when on_click is set, HotkeyLabels should also do the same when they are clickable. --- library/lua/gui/widgets.lua | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 77c6c2c28..3a6fd8494 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1274,9 +1274,15 @@ function Label:getTextWidth() return self.text_width end +-- Overridden by subclasses that also want to add new mouse handlers, see +-- HotkeyLabel. +function Label:shouldHover() + return self.on_click or self.on_rclick +end + function Label:onRenderBody(dc) local text_pen = self.text_pen - if self:getMousePos() and (self.on_click or self.on_rclick) then + if self:getMousePos() and self:shouldHover() then text_pen = self.text_hpen end render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self)) @@ -1432,6 +1438,11 @@ function HotkeyLabel:setLabel(label) self:initializeLabel() end +function HotkeyLabel:shouldHover() + -- When on_activate is set, text should also hover on mouseover + return HotkeyLabel.super.shouldHover(self) or self.on_activate +end + function HotkeyLabel:initializeLabel() self:setText{{key=self.key, key_sep=self.key_sep, text=self.label, on_activate=self.on_activate}} @@ -1475,6 +1486,12 @@ function CycleHotkeyLabel:init() } end +-- CycleHotkeyLabels are always clickable and therefore should always change +-- color when hovered. +function CycleHotkeyLabel:shouldHover() + return true +end + function CycleHotkeyLabel:cycle(backwards) local old_option_idx = self.option_idx if self.option_idx == #self.options and not backwards then From 0b48471607ba0c74daf147232c5efbca18c9ee5a Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Wed, 15 Feb 2023 21:19:44 -0800 Subject: [PATCH 002/126] Invert brightness of the background as well This required some tinkering. --- library/lua/gui/widgets.lua | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 3a6fd8494..15d9064c5 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1224,9 +1224,23 @@ function Label:init(args) -- use existing saved text if no explicit text was specified. this avoids -- overwriting pre-formatted text that subclasses may have already set self:setText(args.text or self.text) + + -- Inverts the brightness of the color + invert = function(color) + return (color + 8) % 16 + end + -- default pen is an inverted foreground/background if not self.text_hpen then - self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16 + local text_pen = dfhack.pen.parse(self.text_pen) + self.text_hpen = dfhack.pen.make(invert(text_pen.fg), nil, invert(text_pen.bg)) end + + -- text_hpen needs a character in order to paint the background using + -- Painter:fill(), so let's make it paint a space to show the background + -- color + local hpen_parsed = dfhack.pen.parse(self.text_hpen) + hpen_parsed.ch = string.byte(' ') + self.text_hpen = hpen_parsed end local function update_label_scrollbar(label) @@ -1280,6 +1294,14 @@ function Label:shouldHover() return self.on_click or self.on_rclick end +function Label:onRenderFrame(dc, rect) + Label.super.onRenderFrame(self, dc, rect) + -- Fill the background with text_hpen on hover + if self:getMousePos() and self:shouldHover() then + dc:fill(rect, self.text_hpen) + end +end + function Label:onRenderBody(dc) local text_pen = self.text_pen if self:getMousePos() and self:shouldHover() then From 0897ca913a46a61c41f655ed60b66a6f0c827006 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Wed, 15 Feb 2023 22:33:31 -0800 Subject: [PATCH 003/126] Support mouse-hover on lists as well --- library/lua/gui/widgets.lua | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 15d9064c5..ab551be1c 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1226,7 +1226,7 @@ function Label:init(args) self:setText(args.text or self.text) -- Inverts the brightness of the color - invert = function(color) + local invert = function(color) return (color + 8) % 16 end -- default pen is an inverted foreground/background @@ -1604,6 +1604,7 @@ List = defclass(List, Widget) List.ATTRS{ text_pen = COLOR_CYAN, + text_hpen = DEFAULT_NIL, -- pen to render list item when mouse is hovered over; defaults to text_pen with inverted brightness cursor_pen = COLOR_LIGHTCYAN, inactive_pen = DEFAULT_NIL, on_select = DEFAULT_NIL, @@ -1633,6 +1634,23 @@ function List:init(info) end self.last_select_click_ms = 0 -- used to track double-clicking on an item + + -- Inverts the brightness of the color + invert = function(color) + return (color + 8) % 16 + end + -- default pen is an inverted foreground/background + if not self.text_hpen then + local text_pen = dfhack.pen.parse(self.text_pen) + self.text_hpen = dfhack.pen.make(invert(text_pen.fg), nil, invert(text_pen.bg)) + end + + -- text_hpen needs a character in order to paint the background using + -- Painter:fill(), so let's make it paint a space to show the background + -- color + local hpen_parsed = dfhack.pen.parse(self.text_hpen) + hpen_parsed.ch = string.byte(' ') + self.text_hpen = hpen_parsed end function List:setChoices(choices, selected) @@ -1784,12 +1802,19 @@ function List:onRenderBody(dc) end end + local hoveridx = self:getIdxUnderMouse() for i = top,iend do local obj = choices[i] local current = (i == self.selected) - local cur_pen = self.cursor_pen - local cur_dpen = self.text_pen - local active_pen = current and cur_pen or cur_dpen + local hovered = (i == hoveridx) + local cur_pen = to_pen(self.cursor_pen) + local cur_dpen = to_pen(self.text_pen) + local active_pen = (current and cur_pen or cur_dpen) + + -- when mouse is over, always highlight it + if hovered then + cur_dpen = self.text_hpen + end if not getval(self.active) then cur_pen = self.inactive_pen or self.cursor_pen From 3e8d0f0f1efbe9a3d6ed648aa51195b09bca4e18 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Thu, 16 Feb 2023 21:38:27 -0800 Subject: [PATCH 004/126] Properly reverse BG/FG and apply per letter This puts pen creation deeper into the loop in render_text. Lists are current coloured completely wrong, though, and need fixing (and probably anywhere else where disabled is set). --- library/lua/gui/widgets.lua | 92 +++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index ab551be1c..e0f01701d 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1080,7 +1080,26 @@ local function is_disabled(token) (token.enabled ~= nil and not getval(token.enabled)) end -function render_text(obj,dc,x0,y0,pen,dpen,disabled) +-- Make the hover pen -- that is a pen that should render elements that has the +-- mouse hovering over it. if hpen is specified, it just checks the fields and +-- returns it (in parsed pen form) +local function make_hpen(pen, hpen) + if not hpen then + pen = dfhack.pen.parse(pen) + + -- Swap the foreground and background + hpen = dfhack.pen.make(pen.bg, nil, pen.fg + (pen.bold and 8 or 0)) + end + + -- text_hpen needs a character in order to paint the background using + -- Painter:fill(), so let's make it paint a space to show the background + -- color + local hpen_parsed = dfhack.pen.parse(hpen) + hpen_parsed.ch = string.byte(' ') + return hpen_parsed +end + +function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) local width = 0 for iline = dc and obj.start_line_num or 1, #obj.text_lines do local x, line = 0, obj.text_lines[iline] @@ -1120,16 +1139,25 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled) if dc then local tpen = getval(token.pen) + local dcpen = tpen or pen + + -- If disabled, figure out which dpen to use if disabled or is_disabled(token) then - dc:pen(getval(token.dpen) or tpen or dpen) + dccpen = getval(token.dpen) or tpen or dpen if keypen.fg ~= COLOR_BLACK then keypen.bold = false end - else - dc:pen(tpen or pen) + + -- if hovered *and* disabled, combine both effects + if hovered then + dcpen = make_hpen(dcpen) + end + elseif hovered then + dcpen = make_hpen(dcpen, getval(token.hpen) or hpen) end - end + dc:pen(dcpen) + end local width = getval(token.width) local padstr if width then @@ -1221,26 +1249,9 @@ function Label:init(args) self:addviews{self.scrollbar} - -- use existing saved text if no explicit text was specified. this avoids - -- overwriting pre-formatted text that subclasses may have already set self:setText(args.text or self.text) - -- Inverts the brightness of the color - local invert = function(color) - return (color + 8) % 16 - end - -- default pen is an inverted foreground/background - if not self.text_hpen then - local text_pen = dfhack.pen.parse(self.text_pen) - self.text_hpen = dfhack.pen.make(invert(text_pen.fg), nil, invert(text_pen.bg)) - end - - -- text_hpen needs a character in order to paint the background using - -- Painter:fill(), so let's make it paint a space to show the background - -- color - local hpen_parsed = dfhack.pen.parse(self.text_hpen) - hpen_parsed.ch = string.byte(' ') - self.text_hpen = hpen_parsed + -- self.text_hpen = make_hpen(self.text_pen, self.text_hpen) end local function update_label_scrollbar(label) @@ -1298,16 +1309,15 @@ function Label:onRenderFrame(dc, rect) Label.super.onRenderFrame(self, dc, rect) -- Fill the background with text_hpen on hover if self:getMousePos() and self:shouldHover() then - dc:fill(rect, self.text_hpen) + local hpen = make_hpen(self.text_pen, self.text_hpen) + dc:fill(rect, hpen) end end function Label:onRenderBody(dc) local text_pen = self.text_pen - if self:getMousePos() and self:shouldHover() then - text_pen = self.text_hpen - end - render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self)) + local hovered = self:getMousePos() and self:shouldHover() + render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self), self.text_hpen, hovered) end function Label:on_scrollbar(scroll_spec) @@ -1635,22 +1645,7 @@ function List:init(info) self.last_select_click_ms = 0 -- used to track double-clicking on an item - -- Inverts the brightness of the color - invert = function(color) - return (color + 8) % 16 - end - -- default pen is an inverted foreground/background - if not self.text_hpen then - local text_pen = dfhack.pen.parse(self.text_pen) - self.text_hpen = dfhack.pen.make(invert(text_pen.fg), nil, invert(text_pen.bg)) - end - - -- text_hpen needs a character in order to paint the background using - -- Painter:fill(), so let's make it paint a space to show the background - -- color - local hpen_parsed = dfhack.pen.parse(self.text_hpen) - hpen_parsed.ch = string.byte(' ') - self.text_hpen = hpen_parsed + -- self.text_hpen = make_hpen(self.text_pen, self.text_hpen) end function List:setChoices(choices, selected) @@ -1807,15 +1802,12 @@ function List:onRenderBody(dc) local obj = choices[i] local current = (i == self.selected) local hovered = (i == hoveridx) + -- cur_pen and cur_dpen can't be integers or background colors get + -- messed up in render_text for subsequent renders local cur_pen = to_pen(self.cursor_pen) local cur_dpen = to_pen(self.text_pen) local active_pen = (current and cur_pen or cur_dpen) - -- when mouse is over, always highlight it - if hovered then - cur_dpen = self.text_hpen - end - if not getval(self.active) then cur_pen = self.inactive_pen or self.cursor_pen end @@ -1828,7 +1820,7 @@ function List:onRenderBody(dc) paint_icon(icon, obj) end - render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current) + render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current, self.text_hpen, hovered) local ip = dc.width From 94ae9973cf00c5f88b228ea0d254cb92214b6e3a Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Thu, 16 Feb 2023 21:43:03 -0800 Subject: [PATCH 005/126] Re-add the invert_color function As requested, but it's not used anymore. --- library/lua/gui.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 4a30d947a..9a5038e69 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -987,4 +987,10 @@ function FramedScreen:onRenderFrame(dc, rect) paint_frame(dc,rect,self.frame_style,self.frame_title) end +-- Inverts the brightness of the color, optionally taking a "bold" parameter, +-- which you should include if you're reading the fg color of a pen. +function invert_color(color, bold) + color = bold and (color + 8) or color + return (color + 8) % 16 +end return _ENV From 61227eeca1a59f13fb228864517f6955526529e1 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Thu, 16 Feb 2023 21:54:44 -0800 Subject: [PATCH 006/126] Fix use of pens in render_text If you ever pass in a number to `dc:pen` rather than a pen table, it will assume the old pen's other attributes, such as `bg` and `bold`. To workaround this, we just never pass in a number, and always call `to_pen` aka `dfhack.pen.parse` first. --- library/lua/gui/widgets.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index e0f01701d..142e928d3 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1139,11 +1139,11 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) if dc then local tpen = getval(token.pen) - local dcpen = tpen or pen + local dcpen = to_pen(tpen or pen) -- If disabled, figure out which dpen to use if disabled or is_disabled(token) then - dccpen = getval(token.dpen) or tpen or dpen + dcpen = to_pen(getval(token.dpen) or tpen or dpen) if keypen.fg ~= COLOR_BLACK then keypen.bold = false end From 697f15224c8b3f421bcbf5a667a00bff47d88363 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Sat, 18 Feb 2023 16:06:03 -0800 Subject: [PATCH 007/126] Address PR comments, and remove BG fill BG fill eats up a lot of cycles anyway, and there's not a real tangible benefit in all cases, as it relies on the text label being sized appropriately (width-wise) to the container, or would otherwise require padding. --- docs/dev/Lua API.rst | 16 +++++++++++++++- library/lua/gui/widgets.lua | 17 ++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 02260513f..43c9d0cb0 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4662,7 +4662,9 @@ It has the following attributes: :text_pen: Specifies the pen for active text. :text_dpen: Specifies the pen for disabled text. -:text_hpen: Specifies the pen for text hovered over by the mouse, if a click handler is registered. +:text_hpen: Specifies the pen for text hovered over by the mouse, if a click + handler is registered. By default, this will invert the foreground + and background colors. :disabled: Boolean or a callback; if true, the label is disabled. :enabled: Boolean or a callback; if false, the label is disabled. :auto_height: Sets self.frame.h from the text height. @@ -4769,6 +4771,18 @@ The Label widget implements the following methods: ``+halfpage``, ``-halfpage``, ``home``, or ``end``. It returns the number of lines that were actually scrolled (negative for scrolling up). +* ``label:shouldHover()`` + + This method returns whether or not this widget should show a hover effect, + generally you want to return ``true`` if there is some type of mouse handler + present. For example, for a ``HotKeyLabel``:: + + function HotkeyLabel:shouldHover() + -- When on_activate is set, text should also hover on mouseover + return HotkeyLabel.super.shouldHover(self) or self.on_activate + end + + WrappedLabel class ------------------ diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 142e928d3..6a1726115 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1232,7 +1232,7 @@ Label = defclass(Label, Widget) Label.ATTRS{ text_pen = COLOR_WHITE, text_dpen = COLOR_DARKGREY, -- disabled - text_hpen = DEFAULT_NIL, -- highlight - default is text_pen with reversed brightness + text_hpen = DEFAULT_NIL, -- hover - default is to invert the fg/bg colors disabled = DEFAULT_NIL, enabled = DEFAULT_NIL, auto_height = true, @@ -1250,8 +1250,6 @@ function Label:init(args) self:addviews{self.scrollbar} self:setText(args.text or self.text) - - -- self.text_hpen = make_hpen(self.text_pen, self.text_hpen) end local function update_label_scrollbar(label) @@ -1305,15 +1303,6 @@ function Label:shouldHover() return self.on_click or self.on_rclick end -function Label:onRenderFrame(dc, rect) - Label.super.onRenderFrame(self, dc, rect) - -- Fill the background with text_hpen on hover - if self:getMousePos() and self:shouldHover() then - local hpen = make_hpen(self.text_pen, self.text_hpen) - dc:fill(rect, hpen) - end -end - function Label:onRenderBody(dc) local text_pen = self.text_pen local hovered = self:getMousePos() and self:shouldHover() @@ -1614,7 +1603,7 @@ List = defclass(List, Widget) List.ATTRS{ text_pen = COLOR_CYAN, - text_hpen = DEFAULT_NIL, -- pen to render list item when mouse is hovered over; defaults to text_pen with inverted brightness + text_hpen = DEFAULT_NIL, -- hover color, defaults to inverting the FG/BG pens for each text object cursor_pen = COLOR_LIGHTCYAN, inactive_pen = DEFAULT_NIL, on_select = DEFAULT_NIL, @@ -1644,8 +1633,6 @@ function List:init(info) end self.last_select_click_ms = 0 -- used to track double-clicking on an item - - -- self.text_hpen = make_hpen(self.text_pen, self.text_hpen) end function List:setChoices(choices, selected) From d18700c96462049f381eefc66e9ad447f4377aef Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Sat, 18 Feb 2023 16:15:16 -0800 Subject: [PATCH 008/126] Update List docs as well. --- docs/dev/Lua API.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 43c9d0cb0..1786c9028 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4917,6 +4917,8 @@ item to call the ``on_submit`` callback for that item. It has the following attributes: :text_pen: Specifies the pen for deselected list entries. +:text_hpen: Specifies the pen for entries that the mouse is hovered over. + Defaults to swapping the background/foreground colors. :cursor_pen: Specifies the pen for the selected entry. :inactive_pen: If specified, used for the cursor when the widget is not active. :icon_pen: Default pen for icons. From 15b00587a0d37f041badf27e2c4d88f2ce2b8ebb Mon Sep 17 00:00:00 2001 From: PopnROFL <126013417+PopnROFL@users.noreply.github.com> Date: Tue, 21 Feb 2023 20:13:57 -0700 Subject: [PATCH 009/126] Update CMakeLists.txt Increased MSVC version to the latest, and updated the error messaging so you know what version you have (if it's installed at all) --- CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 38eb6c92b..0fbdaf19b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,8 +61,10 @@ if(UNIX) endif() if(WIN32) - if((NOT MSVC) OR (MSVC_VERSION LESS 1930) OR (MSVC_VERSION GREATER 1934)) - message(SEND_ERROR "MSVC 2022 is required") + if(NOT MSVC) + message(SEND_ERROR "No MSVC found! MSVC 2022 version 1930 to 1935 is required.") + elseif((MSVC_VERSION LESS 1930) OR (MSVC_VERSION GREATER 1935)) + message(SEND_ERROR "MSVC 2022 version 1930 to 1935 is required, Version Found: ${MSVC_VERSION}") endif() endif() From 7901fdf6ec70610cf5d624ecac44c3276a226248 Mon Sep 17 00:00:00 2001 From: PopnROFL <126013417+PopnROFL@users.noreply.github.com> Date: Tue, 21 Feb 2023 20:19:37 -0700 Subject: [PATCH 010/126] Update CMakeLists.txt N++ defaulted to tabs. Fixing. --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0fbdaf19b..b58c54fe4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,8 +62,8 @@ endif() if(WIN32) if(NOT MSVC) - message(SEND_ERROR "No MSVC found! MSVC 2022 version 1930 to 1935 is required.") - elseif((MSVC_VERSION LESS 1930) OR (MSVC_VERSION GREATER 1935)) + message(SEND_ERROR "No MSVC found! MSVC 2022 version 1930 to 1935 is required.") + elseif((MSVC_VERSION LESS 1930) OR (MSVC_VERSION GREATER 1935)) message(SEND_ERROR "MSVC 2022 version 1930 to 1935 is required, Version Found: ${MSVC_VERSION}") endif() endif() From 3c24e67a9ae600139b96a48f395e0da6fd9916f2 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Wed, 22 Feb 2023 17:22:04 -0800 Subject: [PATCH 011/126] Address additional PR comments on_activate is likely to happen first so we shouldn't need to check the other. --- docs/dev/Lua API.rst | 6 ++++++ library/lua/gui/widgets.lua | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 1786c9028..7ead7e374 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3757,6 +3757,12 @@ Misc Wraps ``dfhack.screen.getKeyDisplay`` in order to allow using strings for the keycode argument. +* ``invert_color(color, bold)`` + + This inverts the brightness of ``color``. If this color is coming from a pen's + foreground color, include ``pen.bold`` in ``bold`` for this to work properly. + + ViewRect class -------------- diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 6a1726115..3ef27a651 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1461,7 +1461,7 @@ end function HotkeyLabel:shouldHover() -- When on_activate is set, text should also hover on mouseover - return HotkeyLabel.super.shouldHover(self) or self.on_activate + return self.on_activate or HotkeyLabel.super.shouldHover(self) end function HotkeyLabel:initializeLabel() From 1ed0a41dd199ccbc17338548b35c32a759e6ef1d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 23 Feb 2023 07:15:08 +0000 Subject: [PATCH 012/126] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 7917f062c..0cc481beb 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7917f062c403a47d4d190bafc2470b247c8aa642 +Subproject commit 0cc481bebfc02b88c7d1b0e6d70a79cfba72d7f1 diff --git a/scripts b/scripts index f2c2f6aa7..9b122d25e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f2c2f6aa7e7fe94871adf0a22d6966ddcac38afc +Subproject commit 9b122d25e5a5980da966f8016ed57e5fee4b2628 From b976097ccfab2ddee69104e42b188a0b6ebf19af Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 23 Feb 2023 21:01:58 -0800 Subject: [PATCH 013/126] sync spreadsheet to tags --- docs/plugins/strangemood.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/strangemood.rst b/docs/plugins/strangemood.rst index def862a4b..a863943e9 100644 --- a/docs/plugins/strangemood.rst +++ b/docs/plugins/strangemood.rst @@ -3,7 +3,7 @@ strangemood .. dfhack-tool:: :summary: Trigger a strange mood. - :tags: untested fort armok units + :tags: fort armok units Usage ----- From 29e069817761b13452a48ef84d7d224771db4782 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 23 Feb 2023 21:14:37 -0800 Subject: [PATCH 014/126] re-mark channel-safely as untested --- docs/plugins/channel-safely.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index c5dbc37f6..3acbe66cd 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -3,7 +3,7 @@ channel-safely .. dfhack-tool:: :summary: Auto-manage channel designations to keep dwarves safe. - :tags: fort auto + :tags: untested fort auto Multi-level channel projects can be dangerous, and managing the safety of your dwarves throughout the completion of such projects can be difficult and time From 6dbc22350f716c2d7550e6b8be485984b7960023 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 23 Feb 2023 21:32:48 -0800 Subject: [PATCH 015/126] log to console instead of announcements --- docs/changelog.txt | 1 + plugins/autobutcher.cpp | 17 ++++++----------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 51b81de0e..5508549c9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -47,6 +47,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: -@ `confirm`: fix fps drop when enabled ## Misc Improvements +- `autobutcher`: logs activity to the console terminal instead of making disruptive in-game announcements - DFHack tool windows that capture mouse clicks (and therefore prevent you from clicking on the "pause" button) now unconditionally pause the game when they open (but you can still unpause with the keyboard if you want to). Examples of this behavior: `gui/quickfort`, `gui/blueprint`, `gui/liquids` - `showmood`: now shows the number of items needed for cloth and bars in addition to the technically correct but always confusing "total dimension" (150 per bar or 10,000 per cloth) -@ Stopped mouse clicks from affecting the map when a click on a DFHack screen dismisses the window diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index bee3a4503..536c74f0f 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -19,8 +19,6 @@ #include "LuaTools.h" #include "PluginManager.h" -#include "modules/Gui.h" -#include "modules/Maps.h" #include "modules/Persistence.h" #include "modules/Units.h" #include "modules/World.h" @@ -805,8 +803,8 @@ static void autobutcher_cycle(color_ostream &out) { w->UpdateConfig(out); watched_races.emplace(unit->race, w); - string announce = "New race added to autobutcher watchlist: " + Units::getRaceNamePluralById(unit->race); - Gui::showAnnouncement(announce, 2, false); + INFO(cycle,out).print("New race added to autobutcher watchlist: %s\n", + Units::getRaceNamePluralById(unit->race).c_str()); } if (w->isWatched) { @@ -828,9 +826,8 @@ static void autobutcher_cycle(color_ostream &out) { if (slaughter_count) { std::stringstream ss; ss << slaughter_count; - string announce = Units::getRaceNamePluralById(w.first) + " marked for slaughter: " + ss.str(); - DEBUG(cycle,out).print("%s\n", announce.c_str()); - Gui::showAnnouncement(announce, 2, false); + INFO(cycle,out).print("%s marked for slaughter: %s\n", + Units::getRaceNamePluralById(w.first).c_str(), ss.str().c_str()); } } } @@ -954,10 +951,8 @@ static void autobutcher_setWatchListRace(color_ostream &out, unsigned id, unsign WatchedRace * w = new WatchedRace(out, id, watched, fk, mk, fa, ma); w->UpdateConfig(out); watched_races.emplace(id, w); - - string announce; - announce = "New race added to autobutcher watchlist: " + Units::getRaceNamePluralById(id); - Gui::showAnnouncement(announce, 2, false); + INFO(status,out).print("New race added to autobutcher watchlist: %s\n", + Units::getRaceNamePluralById(id).c_str()); } // remove entry from watchlist From be5444017777658b9bb1075c595bbc0b9fa74ec9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 23 Feb 2023 22:33:46 -0800 Subject: [PATCH 016/126] fix up changelog --- docs/changelog.txt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 51b81de0e..856e6143c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -41,9 +41,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: -@ `seedwatch`: fix saving and loading of seed stock targets - `autodump`: changed behaviour to only change ``dump`` and ``forbid`` flags if an item is successfully dumped. -@ `autochop`: generate default names for burrows with no assigned names -- ``Buildings::StockpileIterator``: check for stockpile items on block boundary. +- ``Buildings::StockpileIterator``: fix check for stockpile items on block boundary. - `dig-now`: fixed multi-layer channel designations only channeling every second layer -- `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment; add support for adamantine cloth (off by default); improve logging +- `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment -@ `confirm`: fix fps drop when enabled ## Misc Improvements @@ -52,20 +52,18 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: -@ Stopped mouse clicks from affecting the map when a click on a DFHack screen dismisses the window - `confirm`: configuration data is now persisted globally. - `dig-now`: added handling of dig designations that have been converted into active jobs +- `tailor`: add support for adamantine cloth (off by default); improve logging ## Documentation ## API - ``Gui::any_civzone_hotkey``, ``Gui::getAnyCivZone``, ``Gui::getSelectedCivZone``: new functions to operate on the new zone system -- Units module: added new predicates for: - - ``isGeldable()`` - - ``isMarkedForGelding()`` - - ``isPet()`` +- Units module: added new predicates for ``isGeldable()``, ``isMarkedForGelding()``, and ``isPet()`` ## Lua - ``dfhack.gui.getSelectedCivZone``: returns the Zone that the user has selected currently - ``widgets.FilteredList``: Added ``edit_on_change`` optional parameter to allow a custom callback on filter edit change. -- Added ``widgets.TabBar`` and ``widgets.Tab`` (migrated from control-panel.lua) +- ``widgets.TabBar``: new library widget (migrated from control-panel.lua) ## Removed From d8758fdfb735aaf9d30589b729f47b82b118790f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 24 Feb 2023 07:15:12 +0000 Subject: [PATCH 017/126] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 9b122d25e..e70393ff2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9b122d25e5a5980da966f8016ed57e5fee4b2628 +Subproject commit e70393ff2132e2f5d00ffeb154e8d9242f89a284 From 69b89e9a6b8653358a5d47f2d42fbc44687d3b0e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 12:44:06 -0800 Subject: [PATCH 018/126] revert code changes to dig-now (causing lockups) --- plugins/dig-now.cpp | 250 ++++++++------------------------------------ 1 file changed, 45 insertions(+), 205 deletions(-) diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index e94cb41da..6227f44f5 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -6,7 +6,6 @@ #include "PluginManager.h" #include "TileTypes.h" #include "LuaTools.h" -#include "Debug.h" #include "modules/Buildings.h" #include "modules/Gui.h" @@ -15,8 +14,6 @@ #include "modules/Random.h" #include "modules/Units.h" #include "modules/World.h" -#include "modules/EventManager.h" -#include "modules/Job.h" #include #include @@ -29,129 +26,12 @@ #include #include -#include -#include -#include - DFHACK_PLUGIN("dig-now"); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); -// Debugging -namespace DFHack { - DBG_DECLARE(dignow, general, DebugCategory::LINFO); - DBG_DECLARE(dignow, channels, DebugCategory::LINFO); -} - -#define COORD "%" PRIi16 " %" PRIi16 " %" PRIi16 -#define COORDARGS(id) id.x, id.y, id.z - using namespace DFHack; -struct designation{ - df::coord pos; - df::tile_designation type; - df::tile_occupancy occupancy; - designation() = default; - designation(const df::coord &c, const df::tile_designation &td, const df::tile_occupancy &to) : pos(c), type(td), occupancy(to) {} - - bool operator==(const designation &rhs) const { - return pos == rhs.pos; - } - - bool operator!=(const designation &rhs) const { - return !(rhs == *this); - } -}; - -namespace std { - template<> - struct hash { - std::size_t operator()(const designation &c) const { - std::hash hash_coord; - return hash_coord(c.pos); - } - }; -} - -class DesignationJobs { -private: - std::unordered_map designations; - std::unordered_map jobs; -public: - void load(MapExtras::MapCache &map) { - designations.clear(); - df::job_list_link* node = df::global::world->jobs.list.next; - while (node) { - df::job* job = node->item; - if(!job || !Maps::isValidTilePos(job->pos)) - continue; - - node = node->next; - df::tile_designation td = map.designationAt(job->pos); - df::tile_occupancy to = map.occupancyAt(job->pos); - const auto ctd = td.whole; - const auto cto = to.whole; - switch (job->job_type){ - case job_type::Dig: - td.bits.dig = tile_dig_designation::Default; - break; - case job_type::DigChannel: - td.bits.dig = tile_dig_designation::Channel; - break; - case job_type::CarveRamp: - td.bits.dig = tile_dig_designation::Ramp; - break; - case job_type::CarveUpwardStaircase: - td.bits.dig = tile_dig_designation::UpStair; - break; - case job_type::CarveDownwardStaircase: - td.bits.dig = tile_dig_designation::DownStair; - break; - case job_type::CarveUpDownStaircase: - td.bits.dig = tile_dig_designation::UpDownStair; - break; - case job_type::DetailWall: - case job_type::DetailFloor: { - df::tiletype tt = map.tiletypeAt(job->pos); - if (tileSpecial(tt) != df::tiletype_special::SMOOTH) { - td.bits.smooth = 1; - } - break; - } - case job_type::CarveTrack: - to.bits.carve_track_north = (job->item_category.whole >> 18) & 1; - to.bits.carve_track_south = (job->item_category.whole >> 19) & 1; - to.bits.carve_track_west = (job->item_category.whole >> 20) & 1; - to.bits.carve_track_east = (job->item_category.whole >> 21) & 1; - break; - default: - break; - } - if (ctd != td.whole || cto != to.whole) { - // we found a designation job - designations.emplace(job->pos, designation(job->pos, td, to)); - jobs.emplace(job->pos, job); - } - } - } - void remove(const df::coord &pos) { - if(jobs.count(pos)) { - Job::removeJob(jobs[pos]); - jobs.erase(pos); - } - } - designation get(const df::coord &pos) { - if (designations.count(pos)) { - return designations[pos]; - } - return {}; - } - bool count(const df::coord &pos) { - return jobs.count(pos); - } -}; - struct boulder_percent_options { // percent chance ([0..100]) for creating a boulder for the given rock type uint32_t layer; @@ -440,19 +320,8 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, std::vector &dug_tiles) { df::tiletype tt = map.tiletypeAt(pos); - if (!is_diggable(map, pos, tt)) { - DEBUG(general).print("dig_tile: not diggable\n"); + if (!is_diggable(map, pos, tt)) return false; - } - - /** The algorithm process seems to be: - * for each tile - * check for a designation - * if a designation exists send it to dig_tile - * - * dig_tile (below) then digs the layer below the channel designated tile - * thereby changing it and causing its designation to be lost - * */ df::tiletype target_type = df::tiletype::Void; switch(designation) { @@ -472,22 +341,19 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, DFCoord pos_below(pos.x, pos.y, pos.z-1); if (can_dig_channel(tt) && map.ensureBlockAt(pos_below) && is_diggable(map, pos_below, map.tiletypeAt(pos_below))) { - TRACE(channels).print("dig_tile: channeling at (" COORD ") [can_dig_channel: true]\n",COORDARGS(pos_below)); target_type = df::tiletype::OpenSpace; DFCoord pos_above(pos.x, pos.y, pos.z+1); - if (map.ensureBlockAt(pos_above)) { + if (map.ensureBlockAt(pos_above)) remove_ramp_top(map, pos_above); - } - df::tile_dig_designation td_below = map.designationAt(pos_below).bits.dig; - if (dig_tile(out, map, pos_below, df::tile_dig_designation::Ramp, dug_tiles)) { + df::tile_dig_designation td_below = + map.designationAt(pos_below).bits.dig; + if (dig_tile(out, map, pos_below, + df::tile_dig_designation::Ramp, dug_tiles)) { clean_ramps(map, pos_below); - if (td_below == df::tile_dig_designation::Default) { + if (td_below == df::tile_dig_designation::Default) dig_tile(out, map, pos_below, td_below, dug_tiles); - } return true; } - } else { - DEBUG(channels).print("dig_tile: failed to channel at (" COORD ") [can_dig_channel: false]\n", COORDARGS(pos_below)); } break; } @@ -541,8 +407,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, if (target_type == df::tiletype::Void || target_type == tt) return false; - dug_tiles.emplace_back(map, pos); - TRACE(general).print("dig_tile: digging the designation tile at (" COORD ")\n",COORDARGS(pos)); + dug_tiles.push_back(dug_tile_info(map, pos)); dig_type(map, pos, target_type); // let light filter down to newly exposed tiles @@ -729,12 +594,9 @@ static void do_dig(color_ostream &out, std::vector &dug_coords, item_coords_t &item_coords, const dig_now_options &options) { MapExtras::MapCache map; Random::MersenneRNG rng; - DesignationJobs jobs; - jobs.load(map); rng.init(); - std::unordered_set buffer; // go down levels instead of up so stacked ramps behave as expected for (int16_t z = options.end.z; z >= options.start.z; --z) { for (int16_t y = options.start.y; y <= options.end.y; ++y) { @@ -747,68 +609,46 @@ static void do_dig(color_ostream &out, std::vector &dug_coords, DFCoord pos(x, y, z); df::tile_designation td = map.designationAt(pos); df::tile_occupancy to = map.occupancyAt(pos); - if (jobs.count(pos)) { - buffer.emplace(jobs.get(pos)); - jobs.remove(pos); - // if it does get removed, then we're gonna buffer the jobs info then remove the job - } else if ((td.bits.dig != df::tile_dig_designation::No && !to.bits.dig_marked) - || td.bits.smooth == 1 - || to.bits.carve_track_north == 1 - || to.bits.carve_track_east == 1 - || to.bits.carve_track_south == 1 - || to.bits.carve_track_west == 1) { - - // we're only buffering designations, so that processing doesn't affect what we're buffering - buffer.emplace(pos, td, to); - } - } - } - } - - // process designations - for(auto &d : buffer) { - auto pos = d.pos; - auto td = d.type; - auto to = d.occupancy; - - if (td.bits.dig != df::tile_dig_designation::No && !to.bits.dig_marked) { - std::vector dug_tiles; - - if (dig_tile(out, map, pos, td.bits.dig, dug_tiles)) { - for (auto info: dug_tiles) { - td = map.designationAt(info.pos); - td.bits.dig = df::tile_dig_designation::No; - map.setDesignationAt(info.pos, td); - - dug_coords.push_back(info.pos); - refresh_adjacent_smooth_walls(map, info.pos); - if (info.imat < 0) - continue; - if (produces_item(options.boulder_percents, - map, rng, info)) { - auto k = std::make_pair(info.itype, info.imat); - item_coords[k].push_back(info.pos); + if (td.bits.dig != df::tile_dig_designation::No && + !to.bits.dig_marked) { + std::vector dug_tiles; + if (dig_tile(out, map, pos, td.bits.dig, dug_tiles)) { + for (auto info : dug_tiles) { + td = map.designationAt(info.pos); + td.bits.dig = df::tile_dig_designation::No; + map.setDesignationAt(info.pos, td); + + dug_coords.push_back(info.pos); + refresh_adjacent_smooth_walls(map, info.pos); + if (info.imat < 0) + continue; + if (produces_item(options.boulder_percents, + map, rng, info)) { + auto k = std::make_pair(info.itype, info.imat); + item_coords[k].push_back(info.pos); + } + } + } + } else if (td.bits.smooth == 1) { + if (smooth_tile(out, map, pos)) { + td = map.designationAt(pos); + td.bits.smooth = 0; + map.setDesignationAt(pos, td); + } + } else if (to.bits.carve_track_north == 1 + || to.bits.carve_track_east == 1 + || to.bits.carve_track_south == 1 + || to.bits.carve_track_west == 1) { + if (carve_tile(map, pos, to)) { + to = map.occupancyAt(pos); + to.bits.carve_track_north = 0; + to.bits.carve_track_east = 0; + to.bits.carve_track_south = 0; + to.bits.carve_track_west = 0; + map.setOccupancyAt(pos, to); } } } - } else if (td.bits.smooth == 1) { - if (smooth_tile(out, map, pos)) { - td = map.designationAt(pos); - td.bits.smooth = 0; - map.setDesignationAt(pos, td); - } - } else if (to.bits.carve_track_north == 1 - || to.bits.carve_track_east == 1 - || to.bits.carve_track_south == 1 - || to.bits.carve_track_west == 1) { - if (carve_tile(map, pos, to)) { - to = map.occupancyAt(pos); - to.bits.carve_track_north = 0; - to.bits.carve_track_east = 0; - to.bits.carve_track_south = 0; - to.bits.carve_track_west = 0; - map.setOccupancyAt(pos, to); - } } } From 934422264e5a54e9800a5a73066d007050a9c8c0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 12:44:26 -0800 Subject: [PATCH 019/126] remove entries for reverted code from changelog --- docs/changelog.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 57daef38c..9ee7ff102 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -42,7 +42,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `autodump`: changed behaviour to only change ``dump`` and ``forbid`` flags if an item is successfully dumped. -@ `autochop`: generate default names for burrows with no assigned names - ``Buildings::StockpileIterator``: fix check for stockpile items on block boundary. -- `dig-now`: fixed multi-layer channel designations only channeling every second layer - `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment -@ `confirm`: fix fps drop when enabled @@ -52,7 +51,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `showmood`: now shows the number of items needed for cloth and bars in addition to the technically correct but always confusing "total dimension" (150 per bar or 10,000 per cloth) -@ Stopped mouse clicks from affecting the map when a click on a DFHack screen dismisses the window - `confirm`: configuration data is now persisted globally. -- `dig-now`: added handling of dig designations that have been converted into active jobs - `tailor`: add support for adamantine cloth (off by default); improve logging ## Documentation From f922be87690b2e84dc8ea1fb973ae55c621b4441 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 13:25:04 -0800 Subject: [PATCH 020/126] fix more autolabor chattiness --- plugins/autolabor/autolabor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index 86fa9114b..53a01a6a6 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -841,7 +841,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) if (p1 || p2) { dwarf_info[dwarf].diplomacy = true; - INFO(cycle, out).print("Dwarf %i \"%s\" has a meeting, will be cleared of all labors\n", + DEBUG(cycle, out).print("Dwarf %i \"%s\" has a meeting, will be cleared of all labors\n", dwarf, dwarfs[dwarf]->name.first_name.c_str()); break; } From f84299bc463c403737fd5a26a5dd28c1effa0f6d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 24 Feb 2023 23:31:20 +0000 Subject: [PATCH 021/126] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 0cc481beb..e7143ec5e 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 0cc481bebfc02b88c7d1b0e6d70a79cfba72d7f1 +Subproject commit e7143ec5e29d88a114fb8e72091c54413944df1d From b8fdc985ec359d02d18e5b084dafe754175b2648 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 15:41:54 -0800 Subject: [PATCH 022/126] bump version and changelog to 50.07-alpha2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++---- library/xml | 2 +- scripts | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b58c54fe4..56c81ba72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,7 +192,7 @@ endif() # set up versioning. set(DF_VERSION "50.07") -set(DFHACK_RELEASE "alpha1") +set(DFHACK_RELEASE "alpha2") set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ee7ff102..d83417933 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,6 +35,20 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## New Plugins +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 50.07-alpha2 + ## Fixes -@ `nestboxes`: fixed bug causing nestboxes themselves to be forbidden, which prevented citizens from using them to lay eggs. Now only eggs are forbidden. - `autobutcher`: implemented work-around for Dwarf Fortress not setting nicknames properly, so that nicknames created in the in-game interface are detected & protect animals from being butchered properly. Note that nicknames for unnamed units are not currently saved by dwarf fortress - use ``enable fix/protect-nicks`` to fix any nicknames created/removed within dwarf fortress so they can be saved/reloaded when you reload the game. @@ -53,8 +67,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `confirm`: configuration data is now persisted globally. - `tailor`: add support for adamantine cloth (off by default); improve logging -## Documentation - ## API - ``Gui::any_civzone_hotkey``, ``Gui::getAnyCivZone``, ``Gui::getSelectedCivZone``: new functions to operate on the new zone system - Units module: added new predicates for ``isGeldable()``, ``isMarkedForGelding()``, and ``isPet()`` @@ -64,8 +76,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``widgets.FilteredList``: Added ``edit_on_change`` optional parameter to allow a custom callback on filter edit change. - ``widgets.TabBar``: new library widget (migrated from control-panel.lua) -## Removed - # 50.07-alpha1 ## Fixes diff --git a/library/xml b/library/xml index e7143ec5e..d4170eacf 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e7143ec5e29d88a114fb8e72091c54413944df1d +Subproject commit d4170eacfc0f82fcb7364e558d0e782dc497f7d5 diff --git a/scripts b/scripts index e70393ff2..c7345f6fe 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e70393ff2132e2f5d00ffeb154e8d9242f89a284 +Subproject commit c7345f6fe096bc6ce1700b70b4f7d4c65b2a3e57 From 4bf0849d51d96c67f16ad8efbedcfb8f7c529587 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 15:50:56 -0800 Subject: [PATCH 023/126] fix usage of squad equipment vector --- library/modules/Items.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 11d170725..bfdd525be 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1647,5 +1647,5 @@ bool Items::isSquadEquipment(df::item *item) return false; auto &vec = plotinfo->equipment.items_assigned[item->getType()]; - return binsearch_index(vec, &df::item::id, item->id) >= 0; + return binsearch_index(vec, item->id) >= 0; } From 00eb02c1bccf04f8399d762a827fad17f9451a00 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 24 Feb 2023 15:51:11 -0800 Subject: [PATCH 024/126] Implements plugin: channel-safely v1.2.4 - changes report* lookup in `NewReportEvent()` - adds a nullptr check - adds df::coord bound checking in various places - where the `get_*neighbours()` functions are used - `simulate_fall()` - `is_safe_to_dig_down()` and `is_safe_fall()` - adds nullptr checks to the `is_*job()` functions - added todo comments for `is_safe_to_dig_down()` --- plugins/channel-safely/channel-groups.cpp | 38 ++++++++++--------- .../channel-safely/channel-safely-plugin.cpp | 14 +++++-- plugins/channel-safely/include/inlines.h | 30 +++++++++++---- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 2650d92d0..c1a5b5953 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -21,24 +21,27 @@ void ChannelJobs::load_channel_jobs() { } bool ChannelJobs::has_cavein_conditions(const df::coord &map_pos) { - auto p = map_pos; - auto ttype = *Maps::getTileType(p); - if (!DFHack::isOpenTerrain(ttype)) { - // check shared neighbour for cave-in conditions - df::coord neighbours[4]; - get_connected_neighbours(map_pos, neighbours); - int connectedness = 4; - for (auto n: neighbours) { - if (active.count(n) || DFHack::isOpenTerrain(*Maps::getTileType(n))) { - connectedness--; + if likely(Maps::isValidTilePos(map_pos)) { + auto p = map_pos; + auto ttype = *Maps::getTileType(p); + if (!DFHack::isOpenTerrain(ttype)) { + // check shared neighbour for cave-in conditions + df::coord neighbours[4]; + get_connected_neighbours(map_pos, neighbours); + int connectedness = 4; + for (auto n: neighbours) { + if (!Maps::isValidTilePos(n) || active.count(n) || DFHack::isOpenTerrain(*Maps::getTileType(n))) { + connectedness--; + } } - } - if (!connectedness) { - // do what? - p.z--; - ttype = *Maps::getTileType(p); - if (DFHack::isOpenTerrain(ttype) || DFHack::isFloorTerrain(ttype)) { - return true; + if (!connectedness) { + // do what? + p.z--; + if (!Maps::isValidTilePos(p)) return false; + ttype = *Maps::getTileType(p); + if (DFHack::isOpenTerrain(ttype) || DFHack::isFloorTerrain(ttype)) { + return true; + } } } } @@ -88,6 +91,7 @@ void ChannelGroups::add(const df::coord &map_pos) { DEBUG(groups).print(" add(" COORD ")\n", COORDARGS(map_pos)); // and so we begin iterating the neighbours for (auto &neighbour: neighbors) { + if unlikely(!Maps::isValidTilePos(neighbour)) continue; // go to the next neighbour if this one doesn't have a group if (!groups_map.count(neighbour)) { TRACE(groups).print(" -> neighbour is not designated\n"); diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index adb668468..910e0ee7c 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -112,6 +112,10 @@ enum SettingConfigData { // dig-now.cpp df::coord simulate_fall(const df::coord &pos) { + if unlikely(!Maps::isValidTilePos(pos)) { + ERR(plugin).print("Error: simulate_fall(" COORD ") - invalid coordinate\n", COORDARGS(pos)); + return {}; + } df::coord resting_pos(pos); while (Maps::ensureTileBlock(resting_pos)) { @@ -130,6 +134,7 @@ df::coord simulate_area_fall(const df::coord &pos) { get_neighbours(pos, neighbours); df::coord lowest = simulate_fall(pos); for (auto p : neighbours) { + if unlikely(!Maps::isValidTilePos(p)) continue; auto nlow = simulate_fall(p); if (nlow.z < lowest.z) { lowest = nlow; @@ -299,10 +304,11 @@ namespace CSP { int32_t tick = df::global::world->frame_counter; auto report_id = (int32_t)(intptr_t(r)); if (df::global::world) { - std::vector &reports = df::global::world->status.reports; - size_t idx = -1; - idx = df::report::binsearch_index(reports, report_id); - df::report* report = reports.at(idx); + df::report* report = df::report::find(report_id); + if (!report) { + WARN(plugin).print("Error: NewReportEvent() received an invalid report_id - a report* cannot be found\n"); + return; + } switch (report->type) { case announcement_type::CANCEL_JOB: if (config.insta_dig) { diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index 172275778..a29f5a04d 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -64,11 +64,13 @@ inline uint8_t count_accessibility(const df::coord &unit_pos, const df::coord &m get_connected_neighbours(map_pos, connections); uint8_t accessibility = Maps::canWalkBetween(unit_pos, map_pos) ? 1 : 0; for (auto n: neighbours) { + if unlikely(!Maps::isValidTilePos(n)) continue; if (Maps::canWalkBetween(unit_pos, n)) { accessibility++; } } for (auto n : connections) { + if unlikely(Maps::isValidTilePos(n)) continue; if (Maps::canWalkBetween(unit_pos, n)) { accessibility++; } @@ -77,22 +79,22 @@ inline uint8_t count_accessibility(const df::coord &unit_pos, const df::coord &m } inline bool isEntombed(const df::coord &unit_pos, const df::coord &map_pos) { - if (Maps::canWalkBetween(unit_pos, map_pos)) { + if likely(Maps::canWalkBetween(unit_pos, map_pos)) { return false; } df::coord neighbours[8]; get_neighbours(map_pos, neighbours); return std::all_of(neighbours+0, neighbours+8, [&unit_pos](df::coord n) { - return !Maps::canWalkBetween(unit_pos, n); + return !Maps::isValidTilePos(n) || !Maps::canWalkBetween(unit_pos, n); }); } inline bool is_dig_job(const df::job* job) { - return job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel; + return job && (job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel); } inline bool is_channel_job(const df::job* job) { - return job->job_type == df::job_type::DigChannel; + return job && (job->job_type == df::job_type::DigChannel); } inline bool is_group_job(const ChannelGroups &groups, const df::job* job) { @@ -111,34 +113,48 @@ inline bool is_safe_fall(const df::coord &map_pos) { df::coord below(map_pos); for (uint8_t zi = 0; zi < config.fall_threshold; ++zi) { below.z--; + // falling out of bounds is probably considerably unsafe for a dwarf + if unlikely(!Maps::isValidTilePos(below)) { + return false; + } + // if we require vision, and we can't see below.. we'll need to assume it's safe to get anything done if (config.require_vision && Maps::getTileDesignation(below)->bits.hidden) { - return true; //we require vision, and we can't see below.. so we gotta assume it's safe + return true; } + // finally, if we're not looking at open space (air to fall through) it's safe to fall to df::tiletype type = *Maps::getTileType(below); if (!DFHack::isOpenTerrain(type)) { return true; } } + // we exceeded the fall threshold, so it's not a safe fall return false; } inline bool is_safe_to_dig_down(const df::coord &map_pos) { df::coord pos(map_pos); + // todo: probably should rely on is_safe_fall, it looks like it could be simplified a great deal for (uint8_t zi = 0; zi <= config.fall_threshold; ++zi) { - // assume safe if we can't see and need vision + // if we're digging out of bounds, the game can handle that (hopefully) + if unlikely(!Maps::isValidTilePos(pos)) { + return true; + } + // if we require vision, and we can't see the tiles in question.. we'll need to assume it's safe to dig to get anything done if (config.require_vision && Maps::getTileDesignation(pos)->bits.hidden) { return true; } + df::tiletype type = *Maps::getTileType(pos); if (zi == 0 && DFHack::isOpenTerrain(type)) { + // todo: remove? this is probably not useful.. and seems like the only considerable difference to is_safe_fall (aside from where each stops looking) // the starting tile is open space, that's obviously not safe return false; } else if (!DFHack::isOpenTerrain(type)) { // a tile after the first one is not open space return true; } - pos.z--; + pos.z--; // todo: this can probably move to the beginning of the loop } return false; } From 4813a15b35ebd0084e9ac892fa0b874990ebf256 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 24 Feb 2023 15:51:23 -0800 Subject: [PATCH 025/126] Updates changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ee7ff102..fab27741a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -44,6 +44,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``Buildings::StockpileIterator``: fix check for stockpile items on block boundary. - `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment -@ `confirm`: fix fps drop when enabled +- `channel-safely`: fix an out of bounds error regarding the REPORT event listener receiving (presumably) stale id's ## Misc Improvements - `autobutcher`: logs activity to the console terminal instead of making disruptive in-game announcements From 30ea58374cf7191dfb0f4ba55b90169a84dcfad5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 00:55:14 -0800 Subject: [PATCH 026/126] better detection of fire and magma safety --- docs/changelog.txt | 1 + library/modules/Materials.cpp | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4eb816c29..62fb484a0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `autobutcher`: implemented work-around for Dwarf Fortress not setting nicknames properly, so that nicknames created in the in-game interface are detected & protect animals from being butchered properly. Note that nicknames for unnamed units are not currently saved by dwarf fortress - use ``enable fix/protect-nicks`` to fix any nicknames created/removed within dwarf fortress so they can be saved/reloaded when you reload the game. -@ `seedwatch`: fix saving and loading of seed stock targets - `autodump`: changed behaviour to only change ``dump`` and ``forbid`` flags if an item is successfully dumped. +- ``dfhack.job.isSuitableMaterial``: now properly detects lack of fire and magma safety for vulnerable materials with high melting points -@ `autochop`: generate default names for burrows with no assigned names - ``Buildings::StockpileIterator``: fix check for stockpile items on block boundary. - `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 0854a85ce..a6141f1d8 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -513,8 +513,14 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma TEST(sewn_imageless, is_cloth); TEST(glass_making, MAT_FLAG(CRYSTAL_GLASSABLE)); - TEST(fire_safe, material->heat.melting_point > 11000); - TEST(magma_safe, material->heat.melting_point > 12000); + TEST(fire_safe, material->heat.melting_point > 11000 + && material->heat.boiling_point > 11000 + && material->heat.ignite_point > 11000 + && material->heat.heatdam_point > 11000); + TEST(magma_safe, material->heat.melting_point > 12000 + && material->heat.boiling_point > 12000 + && material->heat.ignite_point > 12000 + && material->heat.heatdam_point > 12000); TEST(deep_material, FLAG(inorganic, inorganic_flags::SPECIAL)); TEST(non_economic, !inorganic || !(plotinfo && vector_get(plotinfo->economic_stone, index))); From 472cab846fa60d1b798f193daee6c782a8f57900 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 16:58:17 -0800 Subject: [PATCH 027/126] move changelog entry to next version --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 62fb484a0..90439bf79 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -36,6 +36,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## New Plugins ## Fixes +- ``dfhack.job.isSuitableMaterial``: now properly detects lack of fire and magma safety for vulnerable materials with high melting points ## Misc Improvements @@ -54,7 +55,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `autobutcher`: implemented work-around for Dwarf Fortress not setting nicknames properly, so that nicknames created in the in-game interface are detected & protect animals from being butchered properly. Note that nicknames for unnamed units are not currently saved by dwarf fortress - use ``enable fix/protect-nicks`` to fix any nicknames created/removed within dwarf fortress so they can be saved/reloaded when you reload the game. -@ `seedwatch`: fix saving and loading of seed stock targets - `autodump`: changed behaviour to only change ``dump`` and ``forbid`` flags if an item is successfully dumped. -- ``dfhack.job.isSuitableMaterial``: now properly detects lack of fire and magma safety for vulnerable materials with high melting points -@ `autochop`: generate default names for burrows with no assigned names - ``Buildings::StockpileIterator``: fix check for stockpile items on block boundary. - `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment From a684f294c5027a960831f2940e449c6ac9eeb4b6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 17:05:08 -0800 Subject: [PATCH 028/126] add templated version of join_strings --- library/Core.cpp | 2 -- library/include/MiscUtils.h | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 5375daa3c..478693dbf 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -61,8 +61,6 @@ using namespace std; #include "LuaTools.h" #include "DFHackVersion.h" -#include "MiscUtils.h" - using namespace DFHack; #include "df/plotinfost.h" diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index c9a5f66d6..d14bdb6e9 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -404,6 +404,22 @@ DFHACK_EXPORT bool split_string(std::vector *out, bool squash_empty = false); DFHACK_EXPORT std::string join_strings(const std::string &separator, const std::vector &items); +template +inline std::string join_strings(const std::string &separator, T &items) { + std::stringstream ss; + + bool first = true; + for (auto &item : items) { + if (first) + first = false; + else + ss << separator; + ss << item; + } + + return ss.str(); +} + DFHACK_EXPORT std::string toUpper(const std::string &str); DFHACK_EXPORT std::string toLower(const std::string &str); DFHACK_EXPORT std::string to_search_normalized(const std::string &str); From 75b1cd748a085a698caba910d69c99a39359d05b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 18:58:50 -0800 Subject: [PATCH 029/126] convert otherwise unused THIN_FRAME to INTERIOR_FRAME without a signature --- library/lua/gui.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 9a5038e69..7791f3685 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -916,7 +916,8 @@ end WINDOW_FRAME = make_frame('Window', true) PANEL_FRAME = make_frame('Panel', false) MEDIUM_FRAME = make_frame('Medium', false) -THIN_FRAME = make_frame('Thin', false) +INTERIOR_FRAME = make_frame('Thin', false) +INTERIOR_FRAME.signature_pen = false -- for compatibility with pre-steam code GREY_LINE_FRAME = WINDOW_FRAME From 0febce5e8f6f28119aec489025defe91fe2cd8f0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 17:09:11 -0800 Subject: [PATCH 030/126] add docs --- docs/dev/Lua API.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 7ead7e374..bee602456 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4325,9 +4325,11 @@ There are the following predefined frame style tables: A frame suitable for overlay widget panels. -* ``THIN_FRAME`` +* ``INTERIOR_FRAME`` - A frame suitable for light accent elements. + A frame suitable for light interior accent elements. This frame does *not* have + a visible ``DFHack`` signature on it, so it must not be used as the most external + frame for a DFHack-owned UI. gui.widgets =========== From dafafefe112053010b6e56e675e7ccd4d4d665ed Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 17:11:20 -0800 Subject: [PATCH 031/126] update changelog --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4eb816c29..49a218183 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -44,8 +44,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API ## Lua +-@ ``gui.INTERIOR_FRAME``: a panel frame style for use in highlighting off interior areas of a UI ## Removed +-@ ``gui.THIN_FRAME``: replaced by ``gui.INTERIOR_FRAME`` # 50.07-alpha2 From 8b378735faeedfcac884c4b2db6b67f50aacb1b2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Feb 2023 14:23:07 -0800 Subject: [PATCH 032/126] don't fire HotkeyLabel if the label is disabled --- library/lua/gui/widgets.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 3ef27a651..9fbae2cee 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1472,7 +1472,8 @@ end function HotkeyLabel:onInput(keys) if HotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L_DOWN and self:getMousePos() and self.on_activate then + elseif keys._MOUSE_L_DOWN and self:getMousePos() and self.on_activate + and not is_disabled(self) then self.on_activate() return true end From 2e53c5bc6df67664317b183a9f97c0f5870b7545 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 17:14:27 -0800 Subject: [PATCH 033/126] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4eb816c29..d8b86e46a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -36,6 +36,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## New Plugins ## Fixes +-@ ``widgets.HotkeyLabel``: don't trigger on click if the widget is disabled ## Misc Improvements From 1cacc526e3520cd65d2e2cd5a247754d9ef636ec Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 17:16:30 -0800 Subject: [PATCH 034/126] allow token.tile to be a function --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 4 ++-- library/lua/gui/widgets.lua | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4eb816c29..ca83717ce 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -44,6 +44,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API ## Lua +- ``widgets.Label``: token ``tile`` properties can now be functions that return a value ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 7ead7e374..0c91ece19 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4704,8 +4704,8 @@ containing newlines, or a table with the following possible fields: * ``token.tile = pen`` - Specifies a pen or texture index to paint as one tile before the main part of - the token. + Specifies a pen or texture index (or a function that returns a pen or texture + index) to paint as one tile before the main part of the token. * ``token.width = ...`` diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 3ef27a651..46b047254 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1124,8 +1124,8 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) if token.tile then x = x + 1 if dc then - local tile_pen = tonumber(token.tile) and - to_pen{tile=token.tile} or token.tile + local tile = getval(token.tile) + local tile_pen = tonumber(tile) and to_pen{tile=tile} or tile dc:char(nil, tile_pen) if token.width then dc:advance(token.width-1) From d7d3dcb0beae631370dc4d824fdd8ce44f561dc2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 23:27:16 -0800 Subject: [PATCH 035/126] keep focus strings if they are already labeled i.e. don't add a "dfhack/" prefix if the focus string already has the string "dfhack" in it --- library/modules/Gui.cpp | 7 ++++++- library/modules/Screen.cpp | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index e3a7ba983..6ea0a5ff0 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -491,7 +491,12 @@ bool Gui::matchFocusString(std::string focus_string, df::viewscreen *top) { static void push_dfhack_focus_string(dfhack_viewscreen *vs, std::vector &focusStrings) { auto name = vs->getFocusString(); - focusStrings.push_back(name.empty() ? "dfhack" : "dfhack/" + name); + if (name.empty()) + name = "dfhack"; + else if (string::npos == name.find("dfhack/")) + name = "dfhack/" + name; + + focusStrings.push_back(name); } std::vector Gui::getFocusStrings(df::viewscreen* top) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index a78a36a3f..ca0876904 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -877,7 +877,7 @@ void dfhack_lua_viewscreen::update_focus(lua_State *L, int idx) if (focus.empty()) focus = "lua"; - else + else if (string::npos == focus.find("lua/")) focus = "lua/"+focus; } From ab4af88c92bbd3fa5666f1c8560abfd0f05fd8c9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 17:21:38 -0800 Subject: [PATCH 036/126] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4eb816c29..a3cd76aa9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -42,6 +42,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Documentation ## API +- Gui focus strings will no longer get the "dfhack/" prefix if the string "dfhack/" already exists in the focus string ## Lua From 88516a899afbfea08e080b282831e4574fb67df6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 13:04:41 -0800 Subject: [PATCH 037/126] allow map interface tiles to be cleared --- library/modules/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index a78a36a3f..1fbc6bd65 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -130,7 +130,7 @@ static bool doSetTile_map(const Pen &pen, int x, int y) { long texpos = pen.tile; if (!texpos && pen.ch) texpos = init->font.large_font_texpos[(uint8_t)pen.ch]; - if (texpos) + else vp->screentexpos_interface[index] = texpos; return true; } From f1d5551e51e4ec6389ade405c0872570a6a2f769 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 18:03:13 -0800 Subject: [PATCH 038/126] fix on-map character rendering --- library/modules/Screen.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 1fbc6bd65..b49db0d7e 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -130,8 +130,7 @@ static bool doSetTile_map(const Pen &pen, int x, int y) { long texpos = pen.tile; if (!texpos && pen.ch) texpos = init->font.large_font_texpos[(uint8_t)pen.ch]; - else - vp->screentexpos_interface[index] = texpos; + vp->screentexpos_interface[index] = texpos; return true; } From 49331384dfc72c93f0211979654ebacc5d66f2e1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Feb 2023 17:24:20 -0800 Subject: [PATCH 039/126] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4eb816c29..d0e23d136 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -44,6 +44,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API ## Lua +- ``dfhack.screen.paintTile()``: you can now explicitly clear the interface cursor from a map tile by passing ``0`` as the tile value ## Removed From cfa649b4ac7646076d5d0fe6b2c7c78d2742c2c7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 24 Feb 2023 20:47:58 -0600 Subject: [PATCH 040/126] clean up code for C++20 readiness two change: * remove use of `register` in `md5.cpp` * remove use of `using namespace std` in `Core.cpp` (which causes an ambiguous name resolution error between `byte` and `std::byte`). while there are other ways to resolve this, `using namespace std` is a code smell anyway, so eliminating it is the best option --- depends/md5/md5.cpp | 2 +- library/Core.cpp | 289 ++++++++++++++++++++++---------------------- 2 files changed, 145 insertions(+), 146 deletions(-) diff --git a/depends/md5/md5.cpp b/depends/md5/md5.cpp index 044df259e..8aa9ba38c 100644 --- a/depends/md5/md5.cpp +++ b/depends/md5/md5.cpp @@ -158,7 +158,7 @@ void MD5Final(unsigned char digest[16], MD5Context *ctx) */ void MD5Transform(uint32_t buf[4], uint32_t in[16]) { - register uint32_t a, b, c, d; + uint32_t a, b, c, d; a = buf[0]; b = buf[1]; diff --git a/library/Core.cpp b/library/Core.cpp index 5375daa3c..fd7626f17 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -35,7 +35,6 @@ distribution. #include #include #include -using namespace std; #include "Error.h" #include "MemAccess.h" @@ -99,7 +98,7 @@ using df::global::world; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = NULL); -size_t loadScriptFiles(Core* core, color_ostream& out, const vector& prefix, const std::string& folder); +size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder); namespace DFHack { @@ -160,9 +159,9 @@ struct CommandDepthCounter }; thread_local int CommandDepthCounter::depth = 0; -void Core::cheap_tokenise(string const& input, vector &output) +void Core::cheap_tokenise(std::string const& input, std::vector& output) { - string *cur = NULL; + std::string *cur = NULL; size_t i = 0; // Check the first non-space character @@ -234,13 +233,13 @@ void fHKthread(void * iodata) PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; if(plug_mgr == 0 || core == 0) { - cerr << "Hotkey thread has croaked." << endl; + std::cerr << "Hotkey thread has croaked." << std::endl; return; } bool keep_going = true; while(keep_going) { - std::string stuff = core->getHotkeyCmd(keep_going); // waits on mutex! + std::string stuff = core->getHotkeyCmd(keep_going); // waits on std::mutex! if(!stuff.empty()) { color_ostream_proxy out(core->getConsole()); @@ -256,10 +255,10 @@ void fHKthread(void * iodata) struct sortable { bool recolor; - string name; - string description; + std::string name; + std::string description; //FIXME: Nuke when MSVC stops failing at being C++11 compliant - sortable(bool recolor_,const string& name_,const string & description_): recolor(recolor_), name(name_), description(description_){}; + sortable(bool recolor_,const std::string& name_,const std::string & description_): recolor(recolor_), name(name_), description(description_){}; bool operator <(const sortable & rhs) const { if( name < rhs.name ) @@ -268,9 +267,9 @@ struct sortable }; }; -static string dfhack_version_desc() +static std::string dfhack_version_desc() { - stringstream s; + std::stringstream s; s << Version::dfhack_version() << " "; if (Version::is_release()) s << "(release)"; @@ -284,11 +283,11 @@ static string dfhack_version_desc() namespace { struct ScriptArgs { - const string *pcmd; - vector *pargs; + const std::string *pcmd; + std::vector *pargs; }; struct ScriptEnableState { - const string *pcmd; + const std::string *pcmd; bool pstate; }; } @@ -307,7 +306,7 @@ static bool init_run_script(color_ostream &out, lua_State *state, void *info) return true; } -static command_result runLuaScript(color_ostream &out, std::string name, vector &args) +static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) { ScriptArgs data; data.pcmd = &name; @@ -346,25 +345,25 @@ command_result Core::runCommand(color_ostream &out, const std::string &command) { if (!command.empty()) { - vector parts; + std::vector parts; Core::cheap_tokenise(command,parts); if(parts.size() == 0) return CR_NOT_IMPLEMENTED; - string first = parts[0]; + std::string first = parts[0]; parts.erase(parts.begin()); if (first[0] == '#') return CR_OK; - cerr << "Invoking: " << command << endl; + std::cerr << "Invoking: " << command << std::endl; return runCommand(out, first, parts); } else return CR_NOT_IMPLEMENTED; } -bool is_builtin(color_ostream &con, const string &command) { +bool is_builtin(color_ostream &con, const std::string &command) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -385,7 +384,7 @@ bool is_builtin(color_ostream &con, const string &command) { return lua_toboolean(L, -1); } -void get_commands(color_ostream &con, vector &commands) { +void get_commands(color_ostream &con, std::vector &commands) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -431,10 +430,10 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: return false; } -bool Core::addScriptPath(string path, bool search_before) +bool Core::addScriptPath(std::string path, bool search_before) { - lock_guard lock(script_path_mutex); - vector &vec = script_paths[search_before ? 0 : 1]; + std::lock_guard lock(script_path_mutex); + std::vector &vec = script_paths[search_before ? 0 : 1]; if (std::find(vec.begin(), vec.end(), path) != vec.end()) return false; if (!Filesystem::isdir(path)) @@ -443,13 +442,13 @@ bool Core::addScriptPath(string path, bool search_before) return true; } -bool Core::removeScriptPath(string path) +bool Core::removeScriptPath(std::string path) { - lock_guard lock(script_path_mutex); + std::lock_guard lock(script_path_mutex); bool found = false; for (int i = 0; i < 2; i++) { - vector &vec = script_paths[i]; + std::vector &vec = script_paths[i]; while (1) { auto it = std::find(vec.begin(), vec.end(), path); @@ -464,14 +463,14 @@ bool Core::removeScriptPath(string path) void Core::getScriptPaths(std::vector *dest) { - lock_guard lock(script_path_mutex); + std::lock_guard lock(script_path_mutex); dest->clear(); - string df_path = this->p->getPath() + "/"; + std::string df_path = this->p->getPath() + "/"; for (auto it = script_paths[0].begin(); it != script_paths[0].end(); ++it) dest->push_back(*it); dest->push_back(df_path + CONFIG_PATH + "scripts"); if (df::global::world && isWorldLoaded()) { - string save = World::ReadWorldFolder(); + std::string save = World::ReadWorldFolder(); if (save.size()) dest->push_back(df_path + "/save/" + save + "/scripts"); } @@ -481,13 +480,13 @@ void Core::getScriptPaths(std::vector *dest) } -string Core::findScript(string name) +std::string Core::findScript(std::string name) { - vector paths; + std::vector paths; getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { - string path = *it + "/" + name; + std::string path = *it + "/" + name; if (Filesystem::isfile(path)) return path; } @@ -497,7 +496,7 @@ string Core::findScript(string name) bool loadScriptPaths(color_ostream &out, bool silent = false) { using namespace std; - string filename(CONFIG_PATH + "script-paths.txt"); + std::string filename(CONFIG_PATH + "script-paths.txt"); ifstream file(filename); if (!file) { @@ -505,7 +504,7 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) out.printerr("Could not load %s\n", filename.c_str()); return false; } - string raw; + std::string raw; int line = 0; while (getline(file, raw)) { @@ -516,7 +515,7 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) if (!(ss >> ch) || ch == '#') continue; ss >> ws; // discard whitespace - string path; + std::string path; getline(ss, path); if (ch == '+' || ch == '-') { @@ -565,7 +564,7 @@ static std::string sc_event_name (state_change_event id) { return "SC_UNKNOWN"; } -void help_helper(color_ostream &con, const string &entry_name) { +void help_helper(color_ostream &con, const std::string &entry_name) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -583,7 +582,7 @@ void help_helper(color_ostream &con, const string &entry_name) { } } -void tags_helper(color_ostream &con, const string &tag) { +void tags_helper(color_ostream &con, const std::string &tag) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -601,11 +600,11 @@ void tags_helper(color_ostream &con, const string &tag) { } } -void ls_helper(color_ostream &con, const vector ¶ms) { - vector filter; +void ls_helper(color_ostream &con, const std::vector ¶ms) { + std::vector filter; bool skip_tags = false; bool show_dev_commands = false; - string exclude_strs = ""; + std::string exclude_strs = ""; bool in_exclude = false; for (auto str : params) { @@ -641,7 +640,7 @@ void ls_helper(color_ostream &con, const vector ¶ms) { } } -command_result Core::runCommand(color_ostream &con, const std::string &first_, vector &parts) +command_result Core::runCommand(color_ostream &con, const std::string &first_, std::vector &parts) { std::string first = first_; CommandDepthCounter counter; @@ -717,7 +716,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (p->size() && (*p)[0] == '-') { - if (p->find('a') != string::npos) + if (p->find('a') != std::string::npos) all = true; } } @@ -876,7 +875,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } con << parts[0]; bool builtin = is_builtin(con, parts[0]); - string lua_path = findScript(parts[0] + ".lua"); + std::string lua_path = findScript(parts[0] + ".lua"); Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); if (builtin) { @@ -933,31 +932,31 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { std::vector list = ListKeyBindings(parts[1]); if (list.empty()) - con << "No bindings." << endl; + con << "No bindings." << std::endl; for (size_t i = 0; i < list.size(); i++) - con << " " << list[i] << endl; + con << " " << list[i] << std::endl; } else { - con << "Usage:" << endl - << " keybinding list " << endl - << " keybinding clear [@context]..." << endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << endl - << "Later adds, and earlier items within one command have priority." << endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << endl - << "Context may be used to limit the scope of the binding, by" << endl - << "requiring the current context to have a certain prefix." << endl - << "Current UI context is: " << endl - << join_strings("\n", Gui::getCurFocus(true)) << endl; + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; } } else if (first == "alias") { if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) { - const string &name = parts[1]; - vector cmd(parts.begin() + 2, parts.end()); + const std::string &name = parts[1]; + std::vector cmd(parts.begin() + 2, parts.end()); if (!AddAlias(name, cmd, parts[0] == "replace")) { con.printerr("Could not add alias %s - already exists\n", name.c_str()); @@ -977,15 +976,15 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v auto aliases = ListAliases(); for (auto p : aliases) { - con << p.first << ": " << join_strings(" ", p.second) << endl; + con << p.first << ": " << join_strings(" ", p.second) << std::endl; } } else { - con << "Usage: " << endl - << " alias add|replace " << endl - << " alias delete|clear " << endl - << " alias list" << endl; + con << "Usage: " << std::endl + << " alias add|replace " << std::endl + << " alias delete|clear " << std::endl + << " alias list" << std::endl; } } else if (first == "fpause") @@ -1038,8 +1037,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } else { - con << "Usage:" << endl - << " script " << endl; + con << "Usage:" << std::endl + << " script " << std::endl; return CR_WRONG_USAGE; } } @@ -1065,13 +1064,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (parts.empty() || parts[0] == "help" || parts[0] == "?") { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << endl; - con << "Valid event names (SC_ prefix is optional):" << endl; + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + con << "Valid event names (SC_ prefix is optional):" << std::endl; for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) { std::string name = sc_event_name((state_change_event)i); if (name != "SC_UNKNOWN") - con << " " << name << endl; + con << " " << name << std::endl; } return CR_OK; } @@ -1081,7 +1080,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v parts.push_back(""); if (parts[1].size() && sc_event_id(parts[1]) == SC_UNKNOWN) { - con << "Unrecognized event name: " << parts[1] << endl; + con << "Unrecognized event name: " << parts[1] << std::endl; return CR_WRONG_USAGE; } for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) @@ -1100,13 +1099,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) { - con << "Usage: sc-script add EVENT path-to-script [-save]" << endl; + con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; return CR_WRONG_USAGE; } state_change_event evt = sc_event_id(parts[1]); if (evt == SC_UNKNOWN) { - con << "Unrecognized event: " << parts[1] << endl; + con << "Unrecognized event: " << parts[1] << std::endl; return CR_FAILURE; } bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); @@ -1115,7 +1114,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (script == *it) { - con << "Script already registered" << endl; + con << "Script already registered" << std::endl; return CR_FAILURE; } } @@ -1126,13 +1125,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) { - con << "Usage: sc-script remove EVENT path-to-script [-save]" << endl; + con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; return CR_WRONG_USAGE; } state_change_event evt = sc_event_id(parts[1]); if (evt == SC_UNKNOWN) { - con << "Unrecognized event: " << parts[1] << endl; + con << "Unrecognized event: " << parts[1] << std::endl; return CR_FAILURE; } bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); @@ -1145,13 +1144,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } else { - con << "Unrecognized script" << endl; + con << "Unrecognized script" << std::endl; return CR_FAILURE; } } else { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << endl; + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; return CR_WRONG_USAGE; } } @@ -1173,13 +1172,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v if (!svc) continue; - file << "// Plugin: " << plug->getName() << endl; + file << "// Plugin: " << plug->getName() << std::endl; svc->dumpMethods(file); } } else { - con << "Usage: devel/dump-rpc \"filename\"" << endl; + con << "Usage: devel/dump-rpc \"filename\"" << std::endl; return CR_WRONG_USAGE; } } @@ -1196,8 +1195,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } else if (res == CR_NOT_IMPLEMENTED) { - string completed; - string filename = findScript(first + ".lua"); + std::string completed; + std::string filename = findScript(first + ".lua"); bool lua = filename != ""; if ( !lua ) { filename = findScript(first + ".rb"); @@ -1234,22 +1233,22 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v return CR_OK; } -bool Core::loadScriptFile(color_ostream &out, string fname, bool silent) +bool Core::loadScriptFile(color_ostream &out, std::string fname, bool silent) { if(!silent) { INFO(script,out) << "Loading script: " << fname << std::endl; - cerr << "Loading script: " << fname << std::endl; + std::cerr << "Loading script: " << fname << std::endl; } - ifstream script(fname.c_str()); + std::ifstream script(fname.c_str()); if ( !script.good() ) { if(!silent) out.printerr("Error loading script: %s\n", fname.c_str()); return false; } - string command; + std::string command; while(script.good()) { - string temp; + std::string temp; getline(script,temp); bool doMore = false; if ( temp.length() > 0 ) { @@ -1333,18 +1332,18 @@ void fIOthread(void * iodata) while (true) { - string command = ""; + std::string command = ""; int ret; while ((ret = con.lineedit("[DFHack]# ",command, main_history)) == Console::RETRY); if(ret == Console::SHUTDOWN) { - cerr << "Console is shutting down properly." << endl; + std::cerr << "Console is shutting down properly." << std::endl; return; } else if(ret == Console::FAILURE) { - cerr << "Console caught an unspecified error." << endl; + std::cerr << "Console caught an unspecified error." << std::endl; continue; } else if(ret) @@ -1405,7 +1404,7 @@ Core::Core() : void Core::fatal (std::string output) { errorstate = true; - stringstream out; + std::stringstream out; out << output ; if (output[output.size() - 1] != '\n') out << '\n'; @@ -1421,7 +1420,7 @@ void Core::fatal (std::string output) out << "Check file stderr.log for details\n"; MessageBox(0,out.str().c_str(),"DFHack error!", MB_OK | MB_ICONERROR); #else - cout << "DFHack fatal error: " << out.str() << std::endl; + std::cout << "DFHack fatal error: " << out.str() << std::endl; #endif bool is_headless = bool(getenv("DFHACK_HEADLESS")); @@ -1459,16 +1458,16 @@ bool Core::Init() // this is handled as appropriate in Console-posix.cpp fprintf(stdout, "dfhack: redirecting stdout to stdout.log (again)\n"); if (!freopen("stdout.log", "w", stdout)) - cerr << "Could not redirect stdout to stdout.log" << endl; + std::cerr << "Could not redirect stdout to stdout.log" << std::endl; #endif fprintf(stderr, "dfhack: redirecting stderr to stderr.log\n"); if (!freopen("stderr.log", "w", stderr)) - cerr << "Could not redirect stderr to stderr.log" << endl; + std::cerr << "Could not redirect stderr to stderr.log" << std::endl; Filesystem::init(); - cerr << "DFHack build: " << Version::git_description() << "\n" - << "Starting with working directory: " << Filesystem::getcwd() << endl; + std::cerr << "DFHack build: " << Version::git_description() << "\n" + << "Starting with working directory: " << Filesystem::getcwd() << std::endl; // find out what we are... #ifdef LINUX_BUILD @@ -1477,7 +1476,7 @@ bool Core::Init() const char * path = "hack\\symbols.xml"; #endif auto local_vif = dts::make_unique(); - cerr << "Identifying DF version.\n"; + std::cerr << "Identifying DF version.\n"; try { local_vif->loadFile(path); @@ -1518,8 +1517,8 @@ bool Core::Init() "recompile.\n" "More details can be found in stderr.log in this folder.\n" ); - cout << msg << endl; - cerr << msg << endl; + std::cout << msg << std::endl; + std::cerr << msg << std::endl; fatal("Not a known DF version - XML version mismatch (see console or stderr.log)"); } else @@ -1529,13 +1528,13 @@ bool Core::Init() errorstate = true; return false; } - cerr << "Version: " << vinfo->getVersion() << endl; + std::cerr << "Version: " << vinfo->getVersion() << std::endl; p = std::move(local_p); // Init global object pointers df::global::InitGlobals(); - cerr << "Initializing Console.\n"; + std::cerr << "Initializing Console.\n"; // init the console. bool is_text_mode = (init && init->display.flag.is_set(init_display_flags::TEXT)); bool is_headless = bool(getenv("DFHACK_HEADLESS")); @@ -1551,29 +1550,29 @@ bool Core::Init() } else { - cerr << "endwin(): bind failed" << endl; + std::cerr << "endwin(): bind failed" << std::endl; } } else { - cerr << "Headless mode requires PRINT_MODE:TEXT" << endl; + std::cerr << "Headless mode requires PRINT_MODE:TEXT" << std::endl; } #else - cerr << "Headless mode not supported on Windows" << endl; + std::cerr << "Headless mode not supported on Windows" << std::endl; #endif } if (is_text_mode && !is_headless) { - cerr << "Console is not available. Use dfhack-run to send commands.\n"; + std::cerr << "Console is not available. Use dfhack-run to send commands.\n"; if (!is_text_mode) { - cout << "Console disabled.\n"; + std::cout << "Console disabled.\n"; } } else if(con.init(false)) - cerr << "Console is running.\n"; + std::cerr << "Console is running.\n"; else - cerr << "Console has failed to initialize!\n"; + std::cerr << "Console has failed to initialize!\n"; /* // dump offsets to a file std::ofstream dump("offsets.log"); @@ -1642,19 +1641,19 @@ bool Core::Init() return false; } - cerr << "Binding to SDL.\n"; + std::cerr << "Binding to SDL.\n"; if (!DFSDL::init(con)) { fatal("cannot bind SDL libraries"); return false; } - cerr << "Initializing textures.\n"; + std::cerr << "Initializing textures.\n"; Textures::init(con); - // create mutex for syncing with interactive tasks - cerr << "Initializing plugins.\n"; + // create std::mutex for syncing with interactive tasks + std::cerr << "Initializing plugins.\n"; // create plugin manager plug_mgr = new PluginManager(this); plug_mgr->init(); - cerr << "Starting the TCP listener.\n"; + std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); IODATA *temp = new IODATA; temp->core = this; @@ -1662,7 +1661,7 @@ bool Core::Init() if (!is_text_mode || is_headless) { - cerr << "Starting IO thread.\n"; + std::cerr << "Starting IO thread.\n"; // create IO thread d->iothread = std::thread{fIOthread, (void*)temp}; } @@ -1672,19 +1671,19 @@ bool Core::Init() d->iothread = std::thread{fInitthread, (void*)temp}; } - cerr << "Starting DF input capture thread.\n"; + std::cerr << "Starting DF input capture thread.\n"; // set up hotkey capture d->hotkeythread = std::thread(fHKthread, (void *) temp); started = true; modstate = 0; if (!listen.get()) - cerr << "TCP listen failed.\n"; + std::cerr << "TCP listen failed.\n"; if (df::global::game) { - vector args; - const string & raw = df::global::game->command_line.original; + std::vector args; + const std::string & raw = df::global::game->command_line.original; size_t offset = 0; while (offset < raw.size()) { @@ -1698,7 +1697,7 @@ bool Core::Init() else { size_t next = raw.find(" ", offset); - if (next == string::npos) + if (next == std::string::npos) { args.push_back(raw.substr(offset)); offset = raw.size(); @@ -1712,12 +1711,12 @@ bool Core::Init() } for (auto it = args.begin(); it != args.end(); ) { - const string & first = *it; + const std::string & first = *it; if (first.length() > 0 && first[0] == '+') { - vector cmd; + std::vector cmd; for (it++; it != args.end(); it++) { - const string & arg = *it; + const std::string & arg = *it; if (arg.length() > 0 && arg[0] == '+') { break; @@ -1727,12 +1726,12 @@ bool Core::Init() if (runCommand(con, first.substr(1), cmd) != CR_OK) { - cerr << "Error running command: " << first.substr(1); + std::cerr << "Error running command: " << first.substr(1); for (auto it2 = cmd.begin(); it2 != cmd.end(); it2++) { - cerr << " \"" << *it2 << "\""; + std::cerr << " \"" << *it2 << "\""; } - cerr << "\n"; + std::cerr << "\n"; } } else @@ -1742,7 +1741,7 @@ bool Core::Init() } } - cerr << "DFHack is running.\n"; + std::cerr << "DFHack is running.\n"; onStateChange(con, SC_CORE_INITIALIZED); @@ -1761,7 +1760,7 @@ bool Core::setHotkeyCmd( std::string cmd ) /// removes the hotkey command and gives it to the caller thread std::string Core::getHotkeyCmd( bool &keep_going ) { - string returner; + std::string returner; std::unique_lock lock(HotkeyMutex); HotkeyCond.wait(lock, [this]() -> bool {return this->hotkey_set;}); if (hotkey_set == SHUTDOWN) { @@ -1871,20 +1870,20 @@ void Core::doUpdate(color_ostream &out) // if the world changes if (new_wdata != last_world_data_ptr) { - // we check for map change too + // we check for std::map change too bool had_map = isMapLoaded(); last_world_data_ptr = new_wdata; last_local_map_ptr = new_mapdata; - // and if the world is going away, we report the map change first + // and if the world is going away, we report the std::map change first if(had_map) onStateChange(out, SC_MAP_UNLOADED); - // and if the world is appearing, we report map change after that + // and if the world is appearing, we report std::map change after that onStateChange(out, new_wdata ? SC_WORLD_LOADED : SC_WORLD_UNLOADED); if(isMapLoaded()) onStateChange(out, SC_MAP_LOADED); } - // otherwise just check for map change... + // otherwise just check for std::map change... else if (new_mapdata != last_local_map_ptr) { bool had_map = isMapLoaded(); @@ -1986,22 +1985,22 @@ void getFilesWithPrefixAndSuffix(const std::string& folder, const std::string& p return; } -size_t loadScriptFiles(Core* core, color_ostream& out, const vector& prefix, const std::string& folder) { - static const string suffix = ".init"; - vector scriptFiles; +size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder) { + static const std::string suffix = ".init"; + std::vector scriptFiles; for ( size_t a = 0; a < prefix.size(); a++ ) { getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles); } std::sort(scriptFiles.begin(), scriptFiles.end(), - [&](const string &a, const string &b) { - string a_base = a.substr(0, a.size() - suffix.size()); - string b_base = b.substr(0, b.size() - suffix.size()); + [&](const std::string &a, const std::string &b) { + std::string a_base = a.substr(0, a.size() - suffix.size()); + std::string b_base = b.substr(0, b.size() - suffix.size()); return a_base < b_base; }); size_t result = 0; for ( size_t a = 0; a < scriptFiles.size(); a++ ) { result++; - string path = ""; + std::string path = ""; if (folder != ".") path = folder + "/"; core->loadScriptFile(out, path + scriptFiles[a], false); @@ -2012,10 +2011,10 @@ size_t loadScriptFiles(Core* core, color_ostream& out, const vector namespace DFHack { namespace X { typedef state_change_event Key; - typedef vector Val; - typedef pair Entry; - typedef vector EntryVector; - typedef map InitVariationTable; + typedef std::vector Val; + typedef std::pair Entry; + typedef std::vector EntryVector; + typedef std::map InitVariationTable; EntryVector computeInitVariationTable(void* none, ...) { va_list list; @@ -2030,7 +2029,7 @@ namespace DFHack { const char *v = va_arg(list, const char *); if (!v || !v[0]) break; - val.push_back(string(v)); + val.push_back(std::string(v)); } result.push_back(Entry(key,val)); } @@ -2165,7 +2164,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) if (event == SC_WORLD_LOADED && Version::is_prerelease()) { runCommand(out, "gui/prerelease-warning"); - std::cerr << "loaded map in prerelease build" << std::endl; + std::cerr << "loaded std::map in prerelease build" << std::endl; } if (event == SC_WORLD_LOADED) @@ -2425,7 +2424,7 @@ bool Core::SelectHotkey(int sym, int modifiers) if (!binding.focus.empty()) { if (!Gui::matchFocusString(binding.focus)) { std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' !~ '%s'\n", + DEBUG(keybinding).print("skipping keybinding due to focus std::string mismatch: '%s' !~ '%s'\n", join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); continue; } @@ -2647,8 +2646,8 @@ bool Core::RunAlias(color_ostream &out, const std::string &name, return false; } - const string &first = aliases[name][0]; - vector parts(aliases[name].begin() + 1, aliases[name].end()); + const std::string &first = aliases[name][0]; + std::vector parts(aliases[name].begin() + 1, aliases[name].end()); parts.insert(parts.end(), parameters.begin(), parameters.end()); result = runCommand(out, first, parts); return true; From 87e06cf96038fe55e3b49a2251d4c7a39e8ec893 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 25 Feb 2023 02:42:28 -0600 Subject: [PATCH 041/126] deoops --- library/Core.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index fd7626f17..abd5a05f6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1648,7 +1648,7 @@ bool Core::Init() } std::cerr << "Initializing textures.\n"; Textures::init(con); - // create std::mutex for syncing with interactive tasks + // create mutex for syncing with interactive tasks std::cerr << "Initializing plugins.\n"; // create plugin manager plug_mgr = new PluginManager(this); @@ -1883,7 +1883,7 @@ void Core::doUpdate(color_ostream &out) if(isMapLoaded()) onStateChange(out, SC_MAP_LOADED); } - // otherwise just check for std::map change... + // otherwise just check for map change... else if (new_mapdata != last_local_map_ptr) { bool had_map = isMapLoaded(); @@ -2029,7 +2029,7 @@ namespace DFHack { const char *v = va_arg(list, const char *); if (!v || !v[0]) break; - val.push_back(std::string(v)); + val.emplace_back(v); } result.push_back(Entry(key,val)); } @@ -2164,7 +2164,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) if (event == SC_WORLD_LOADED && Version::is_prerelease()) { runCommand(out, "gui/prerelease-warning"); - std::cerr << "loaded std::map in prerelease build" << std::endl; + std::cerr << "loaded map in prerelease build" << std::endl; } if (event == SC_WORLD_LOADED) @@ -2424,7 +2424,7 @@ bool Core::SelectHotkey(int sym, int modifiers) if (!binding.focus.empty()) { if (!Gui::matchFocusString(binding.focus)) { std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus std::string mismatch: '%s' !~ '%s'\n", + DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' !~ '%s'\n", join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); continue; } From 0a65c423cef63f0dc3dc17dea3e37e3135aeb47f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 25 Feb 2023 04:07:24 -0600 Subject: [PATCH 042/126] a squirrel distracted me --- library/Core.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index abd5a05f6..af048d9c2 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1870,15 +1870,15 @@ void Core::doUpdate(color_ostream &out) // if the world changes if (new_wdata != last_world_data_ptr) { - // we check for std::map change too + // we check for map change too bool had_map = isMapLoaded(); last_world_data_ptr = new_wdata; last_local_map_ptr = new_mapdata; - // and if the world is going away, we report the std::map change first + // and if the world is going away, we report the map change first if(had_map) onStateChange(out, SC_MAP_UNLOADED); - // and if the world is appearing, we report std::map change after that + // and if the world is appearing, we report map change after that onStateChange(out, new_wdata ? SC_WORLD_LOADED : SC_WORLD_UNLOADED); if(isMapLoaded()) onStateChange(out, SC_MAP_LOADED); From c7f6ee57d7d4f2e567ff3cb905ab4cdd759dd472 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 25 Feb 2023 11:00:51 -0800 Subject: [PATCH 043/126] Update library/Core.cpp --- library/Core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index af048d9c2..3cccd15f1 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -239,7 +239,7 @@ void fHKthread(void * iodata) bool keep_going = true; while(keep_going) { - std::string stuff = core->getHotkeyCmd(keep_going); // waits on std::mutex! + std::string stuff = core->getHotkeyCmd(keep_going); // waits on mutex! if(!stuff.empty()) { color_ostream_proxy out(core->getConsole()); From 2b59d6ee3d0fdf7557b5aa5860c948e75c4fcb6c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:09:54 -0800 Subject: [PATCH 044/126] make dfhack.job.attachJobItem available to Lua --- docs/dev/Lua API.rst | 9 +++++++++ library/LuaApi.cpp | 1 + 2 files changed, 10 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index ef9b35f05..c2cc7f5cd 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1219,6 +1219,15 @@ Job module if there are any jobs with ``first_id <= id < job_next_id``, a lua list containing them. +* ``dfhack.job.attachJobItem(job, item, role, filter_idx, insert_idx)`` + + Attach a real item to this job. If the item is intended to satisfy a job_item + filter, the index of that filter should be passed in ``filter_idx``; otherwise, + pass ``-1``. Similarly, if you don't care where the item is inserted, pass + ``-1`` for ``insert_idx``. The ``role`` param is a ``df.job_item_ref.T_role``. + If the item needs to be brought to the job site, then the value should be + ``df.job_item_ref.T_role.Hauled``. + * ``dfhack.job.isSuitableItem(job_item, item_type, item_subtype)`` Does basic sanity checks to verify if the suggested item type matches diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 2dc2ddf8b..426c87566 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1659,6 +1659,7 @@ static bool jobItemEqual(const df::job_item *job1, const df::job_item *job2) } static const LuaWrapper::FunctionReg dfhack_job_module[] = { + WRAPM(Job,attachJobItem), WRAPM(Job,cloneJobStruct), WRAPM(Job,printItemDetails), WRAPM(Job,printJobDetails), From a536396bd8b7ede12cfa1cc65fe59c762b16cbac Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 09:41:22 -0800 Subject: [PATCH 045/126] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1192fb03d..dd109a218 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -47,6 +47,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - Gui focus strings will no longer get the "dfhack/" prefix if the string "dfhack/" already exists in the focus string ## Lua +- ``dfhack.job.attachJobItem()``: allows you to attach specific items to a job - ``dfhack.screen.paintTile()``: you can now explicitly clear the interface cursor from a map tile by passing ``0`` as the tile value - ``widgets.Label``: token ``tile`` properties can now be functions that return a value -@ ``gui.INTERIOR_FRAME``: a panel frame style for use in highlighting off interior areas of a UI From 656a26504ae5251d1c543a5a03a9dfebff8dc683 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 09:47:15 -0800 Subject: [PATCH 046/126] make FilteredList searching case insensitive by default --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 2 +- library/lua/gui/widgets.lua | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1192fb03d..4e06d0e67 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -49,6 +49,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Lua - ``dfhack.screen.paintTile()``: you can now explicitly clear the interface cursor from a map tile by passing ``0`` as the tile value - ``widgets.Label``: token ``tile`` properties can now be functions that return a value +-@ ``widgets.FilteredList``: search key matching is now case insensitive by default -@ ``gui.INTERIOR_FRAME``: a panel frame style for use in highlighting off interior areas of a UI ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index ef9b35f05..3b91d0ea9 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -5007,7 +5007,7 @@ construction that allows filtering the list by subwords of its items. In addition to passing through all attributes supported by List, it supports: -:case_sensitive: If true, matching is case sensitive. Defaults to true. +:case_sensitive: If ``true``, matching is case sensitive. Defaults to ``false``. :edit_pen: If specified, used instead of ``cursor_pen`` for the edit field. :edit_below: If true, the edit field is placed below the list instead of above. :edit_key: If specified, the edit field is disabled until this key is pressed. diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index d5d0ea7bf..ab018a50a 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1926,7 +1926,7 @@ end FilteredList = defclass(FilteredList, Widget) FilteredList.ATTRS { - case_sensitive = true, + case_sensitive = false, edit_below = false, edit_key = DEFAULT_NIL, edit_ignore_keys = DEFAULT_NIL, From b443f81ecdf0710fbf231c9bb9e4bcbf8975395a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 8 Feb 2023 18:47:10 -0800 Subject: [PATCH 047/126] print out more status info for buildingplan --- docs/plugins/buildingplan.rst | 49 +++++++++---------------------- plugins/buildingplan.cpp | 54 +++++++++++++++++++++++++++-------- plugins/lua/buildingplan.lua | 11 +++++-- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 1eb18b1d5..c51f79721 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -11,11 +11,14 @@ available, and they will be created in a suspended state. Buildingplan will periodically scan for appropriate items, and the jobs will be unsuspended when the items are available. -This is very useful when combined with manager work orders or `workflow` -- you -can set a constraint to always have one or two doors/beds/tables/chairs/etc. -available, and place as many as you like. Materials are used to build the -planned buildings as they are produced, with minimal space dedicated to -stockpiles. +This is very powerful when used with tools like `quickfort`, which allow you to +set a building plan according to a blueprint, and the buildings will simply be +built when you can build them. + +You can use manager work orders or `workflow` to ensure you always have one or +two doors/beds/tables/chairs/etc. available, and place as many as you like. +Materials are used to build the planned buildings as they are produced, with +minimal space dedicated to stockpiles. Usage ----- @@ -23,37 +26,27 @@ Usage :: enable buildingplan - buildingplan set + buildingplan [status] buildingplan set true|false -Running ``buildingplan set`` without parameters displays the current settings. - .. _buildingplan-settings: Global settings --------------- -The buildingplan plugin has global settings that can be set from the UI -(:kbd:`G` from any building placement screen, for example: -:kbd:`b`:kbd:`a`:kbd:`G`). These settings can also be set via the -``buildingplan set`` command. The available settings are: +The buildingplan plugin has several global settings that affect what materials +can be chosen when attaching items to planned buildings: -``all_enabled`` (default: false) - Enable planning mode for all building types. ``blocks``, ``boulders``, ``logs``, ``bars`` (defaults: true, true, true, false) Allow blocks, boulders, logs, or bars to be matched for generic "building material" items. -``quickfort_mode`` (default: false) - Enable compatibility mode for the legacy Python Quickfort (this setting is - not required for DFHack `quickfort`) -The settings for ``blocks``, ``boulders``, ``logs``, and ``bars`` are saved with -your fort, so you only have to set them once and they will be persisted in your -save. +These settings are saved with your fort, so you only have to set them once and +they will be persisted in your save. If you normally embark with some blocks on hand for early workshops, you might want to add this line to your ``dfhack-config/init/onMapLoad.init`` file to -always configure buildingplan to just use blocks for buildings and +always configure `buildingplan` to just use blocks for buildings and constructions:: on-new-fortress buildingplan set boulders false; buildingplan set logs false @@ -76,17 +69,3 @@ keep the filter values that were set when the building was placed. For example, you can be sure that all your constructed walls are the same color by setting a filter to accept only certain types of stone. - -Quickfort mode --------------- - -If you use the external Python Quickfort to apply building blueprints instead of -the native DFHack `quickfort` script, you must enable Quickfort mode. This -temporarily enables buildingplan for all building types and adds an extra blank -screen after every building placement. This "dummy" screen is needed for Python -Quickfort to interact successfully with Dwarf Fortress. - -Note that Quickfort mode is only for compatibility with the legacy Python -Quickfort. The DFHack `quickfort` script does not need this Quickfort mode to be -enabled. The `quickfort` script will successfully integrate with buildingplan as -long as the buildingplan plugin itself is enabled. diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp index 039c83b0f..81f026cbc 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -15,14 +15,14 @@ #include "df/job_item.h" #include "df/world.h" -#include +#include #include #include #include using std::map; using std::pair; -using std::queue; +using std::deque; using std::string; using std::unordered_map; using std::vector; @@ -34,11 +34,8 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(world); -// logging levels can be dynamically controlled with the `debugfilter` command. namespace DFHack { - // for configuration-related logging DBG_DECLARE(buildingplan, status, DebugCategory::LINFO); - // for logging during the periodic scan DBG_DECLARE(buildingplan, cycle, DebugCategory::LINFO); } @@ -108,7 +105,7 @@ static PersistentDataItem config; // building id -> PlannedBuilding unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) -map>>> tasks; +map>>> tasks; // note that this just removes the PlannedBuilding. the tasks will get dropped // as we discover them in the tasks queues and they fail to be found in planned_buildings. @@ -359,7 +356,7 @@ static void finalizeBuilding(color_ostream &out, df::building * bld) { Job::checkBuildingsNow(); } -static df::building * popInvalidTasks(color_ostream &out, queue> & task_queue) { +static df::building * popInvalidTasks(color_ostream &out, deque> & task_queue) { while (!task_queue.empty()) { auto & task = task_queue.front(); auto id = task.first; @@ -369,13 +366,13 @@ static df::building * popInvalidTasks(color_ostream &out, queue>> & buckets) { + map>> & buckets) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); auto item_vector = df::global::world->items.other[other_id]; DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", @@ -423,7 +420,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, // items so if buildingplan is turned off, the building will // be completed with the correct number of items. --job->job_items[filter_idx]->quantity; - task_queue.pop(); + task_queue.pop_front(); if (isJobReady(out, job)) { finalizeBuilding(out, bld); planned_buildings.at(id).remove(out); @@ -586,7 +583,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { // as invalid for (auto vector_id : vector_ids) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { - tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); + tasks[vector_id][bucket].push_back(std::make_pair(id, job_item_idx)); DEBUG(status,out).print("added task: %s/%s/%d,%d; " "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), @@ -609,13 +606,46 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { static void printStatus(color_ostream &out) { DEBUG(status,out).print("entering buildingplan_printStatus\n"); out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); - out.print(" finding materials for %zd buildings\n", planned_buildings.size()); out.print("Current settings:\n"); out.print(" use blocks: %s\n", get_config_bool(config, CONFIG_BLOCKS) ? "yes" : "no"); out.print(" use boulders: %s\n", get_config_bool(config, CONFIG_BOULDERS) ? "yes" : "no"); out.print(" use logs: %s\n", get_config_bool(config, CONFIG_LOGS) ? "yes" : "no"); out.print(" use bars: %s\n", get_config_bool(config, CONFIG_BARS) ? "yes" : "no"); out.print("\n"); + + map counts; + int32_t total = 0; + for (auto &buckets : tasks) { + for (auto &bucket_queue : buckets.second) { + deque> &tqueue = bucket_queue.second; + for (auto it = tqueue.begin(); it != tqueue.end();) { + auto & task = *it; + auto id = task.first; + df::building *bld = NULL; + if (!planned_buildings.count(id) || + !(bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out))) { + DEBUG(status,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", + id, task.second); + it = tqueue.erase(it); + continue; + } + auto *jitem = bld->jobs[0]->job_items[task.second]; + int32_t quantity = jitem->quantity; + if (quantity) { + string desc = toLower(ENUM_KEY_STR(item_type, jitem->item_type)); + counts[desc] += quantity; + total += quantity; + } + ++it; + } + } + } + + out.print("Waiting for %d item(s) to be produced or %zd building(s):\n", + total, planned_buildings.size()); + for (auto &count : counts) + out.print(" %3d %s\n", count.second, count.first.c_str()); + out.print("\n"); } static bool setSetting(color_ostream &out, string name, bool value) { diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 9b953dd7c..4420f8534 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -4,8 +4,6 @@ local _ENV = mkmodule('plugins.buildingplan') Native functions: - * void setSetting(string name, boolean value) - * bool isPlanModeEnabled(df::building_type type, int16_t subtype, int32_t custom) * bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom) * bool isPlannedBuilding(df::building *bld) * void addPlannedBuilding(df::building *bld) @@ -36,6 +34,15 @@ function parse_commandline(...) return false end + local command = table.remove(positionals, 1) + if not command or command == 'status' then + printStatus() + elseif command == 'set' then + setSetting(positionals[1], positionals[2] == 'true') + else + return false + end + return true end From 0cb1c09549574daefe92035e16dba656def022bb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 8 Feb 2023 19:26:39 -0800 Subject: [PATCH 048/126] implement skeletons for buildingplan overlays --- plugins/lua/buildingplan.lua | 78 ++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 4420f8534..86f8699eb 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -53,6 +53,84 @@ function get_num_filters(btype, subtype, custom) return 0 end +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) +PlannerOverlay.ATTRS{ + default_pos={x=46,y=18}, + default_enabled=true, + viewscreens='dwarfmode/Building/Placement', + frame={w=30, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function PlannerOverlay:init() + self:addviews{ + widgets.ToggleHotkeyLabel{ + frame={t=0, l=0}, + label='build when materials are available', + key='CUSTOM_CTRL_B', + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='configure materials', + key='CUSTOM_CTRL_E', + on_activate=do_export, + }, + } +end + +InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) +InspectorOverlay.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 InspectorOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Waiting for items:', + }, + widgets.Label{ + frame={t=1, l=0}, + text='items', + }, + widgets.HotkeyLabel{ + frame={t=2, l=0}, + label='make top priority', + key='CUSTOM_CTRL_T', + }, + } +end + +function InspectorOverlay:onInput(keys) + if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + return false + end + return InspectorOverlay.super.onInput(self, keys) +end + +function InspectorOverlay:render(dc) + if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + return + end + InspectorOverlay.super.render(self, dc) +end + +OVERLAY_WIDGETS = { + planner=PlannerOverlay, + inspector=InspectorOverlay, +} + + local dialogs = require('gui.dialogs') local guidm = require('gui.dwarfmode') From 1c3a5fa1700c1fb9e45f24942689fd1f529ee74e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 9 Feb 2023 00:13:53 -0800 Subject: [PATCH 049/126] initial building placement code --- plugins/lua/buildingplan.lua | 371 ++++++++++++++++++++++++----------- 1 file changed, 260 insertions(+), 111 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 86f8699eb..4167ebb41 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -13,6 +13,11 @@ local _ENV = mkmodule('plugins.buildingplan') --]] local argparse = require('argparse') +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') require('dfhack.buildings') local function process_args(opts, args) @@ -47,42 +52,278 @@ function parse_commandline(...) end function get_num_filters(btype, subtype, custom) - local filters = dfhack.buildings.getFiltersByType( - {}, btype, subtype, custom) - if filters then return #filters end - return 0 + local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) + return filters and #filters or 0 end -local gui = require('gui') -local overlay = require('plugins.overlay') -local widgets = require('gui.widgets') +-------------------------------- +-- Planner Overlay +-- + +local uibs = df.global.buildreq + +local function cur_building_has_no_area() + if uibs.building_type == df.building_type.Construction then return false end + local filters = dfhack.buildings.getFiltersByType({}, + uibs.building_type, uibs.building_subtype, uibs.custom_type) + -- this works because all variable-size buildings have either no item + -- filters or a quantity of -1 for their first (and only) item + return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) +end + +local function is_choosing_area() + return uibs.selection_pos.x >= 0 +end + +local function get_cur_area_dims() + if not is_choosing_area() then return 1, 1 end + return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, + math.abs(uibs.selection_pos.y - uibs.pos.y) + 1 +end + +local function get_cur_filters() + return dfhack.buildings.getFiltersByType({}, uibs.building_type, + uibs.building_subtype, uibs.custom_type) +end + +local function is_plannable() + return get_cur_filters() and + not (uibs.building_type == df.building_type.Construction + and uibs.building_subtype == df.construction_type.TrackNSEW) +end + +local direction_panel_frame = {t=4, h=13, w=46, r=28} + +local direction_panel_types = utils.invert{ + df.building_type.Bridge, + df.building_type.ScrewPump, + df.building_type.WaterWheel, + df.building_type.AxleHorizontal, + df.building_type.Rollers, +} + +local function has_direction_panel() + return direction_panel_types[uibs.building_type] + or (uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.TrackStop) +end + +local function is_over_direction_panel() + if not has_direction_panel() then return false end + local v = widgets.Widget{frame=direction_panel_frame} + local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) + v:updateLayout(gui.ViewRect{rect=rect}) + return v:getMousePos() +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 a reasonable label for the item based on the qualities of the filter +function get_item_label(idx) + local filter = get_cur_filters()[idx] + local desc = 'Unknown' + if filter.has_tool_use then + desc = to_title_case(df.tool_uses[filter.has_tool_use]) + end + if filter.item_type then + desc = to_title_case(df.item_type[filter.item_type]) + end + if filter.flags2 and filter.flags2.building_material then + desc = "Generic building material"; + if filter.flags2.fire_safe then + desc = "Fire-safe building material"; + end + if filter.flags2.magma_safe then + desc = "Magma-safe building material"; + end + elseif filter.vector_id then + desc = to_title_case(df.job_item_vector_id[filter.vector_id]) + end + + local quantity = filter.quantity or 1 + local dimx, dimy = get_cur_area_dims() + if quantity < 1 then + quantity = ((dimx * dimy) // 4) + 1 + else + quantity = quantity * dimx * dimy + end + return ('%s (need: %d)'):format(desc, quantity) +end + +ItemLine = defclass(ItemLine, widgets.Panel) +ItemLine.ATTRS{ + idx=DEFAULT_NIL, +} + +function ItemLine:init() + self.frame.h = 1 + self.visible = function() return #get_cur_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={{text=function() return get_item_label(self.idx) end}} + }, + } +end PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ - default_pos={x=46,y=18}, + default_pos={x=6,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', - frame={w=30, h=4}, - frame_style=gui.MEDIUM_FRAME, + frame={w=54, h=9}, + frame_style=gui.PANEL_FRAME, frame_background=gui.CLEAR_PEN, } function PlannerOverlay:init() self:addviews{ - widgets.ToggleHotkeyLabel{ - frame={t=0, l=0}, - label='build when materials are available', - key='CUSTOM_CTRL_B', + widgets.Label{ + frame={}, + auto_width=true, + text='No items required.', + visible=function() return #get_cur_filters() == 0 end, }, - widgets.HotkeyLabel{ - frame={t=1, l=0}, - label='configure materials', - key='CUSTOM_CTRL_E', - on_activate=do_export, + ItemLine{frame={t=0, l=0}, idx=1}, + ItemLine{frame={t=2, l=0}, idx=2}, + ItemLine{frame={t=4, l=0}, idx=3}, + ItemLine{frame={t=6, l=0}, idx=4}, + widgets.Label{ + frame={b=0, l=17}, + text={ + 'Selected area: ', + {text=function() + return ('%d x %d'):format(get_cur_area_dims()) + end + }, + }, + visible=is_choosing_area, }, } end +function PlannerOverlay:do_config() + dfhack.run_script('gui/buildingplan') +end + +function PlannerOverlay:onInput(keys) + if not is_plannable() then return false end + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + return false + end + if PlannerOverlay.super.onInput(self, keys) then + return true + end + if keys._MOUSE_L_DOWN then + if is_over_direction_panel() then return false end + if self:getMouseFramePos() then return true end + if #uibs.errors > 0 then return true end + local pos = dfhack.gui.getMousePos() + if pos then + if is_choosing_area() or cur_building_has_no_area() then + if #get_cur_filters() == 0 then + return false -- we don't add value; let the game place it + end + self:place_building() + uibs.selection_pos:clear() + return true + elseif not is_choosing_area() then + return false + end + end + end + return keys._MOUSE_L +end + +function PlannerOverlay:render(dc) + if not is_plannable() then return end + PlannerOverlay.super.render(self, dc) +end + +local to_pen = dfhack.pen.parse +local GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, + tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} +local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, + tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} + +function PlannerOverlay:onRenderFrame(dc, rect) + PlannerOverlay.super.onRenderFrame(self, dc, rect) + + if not is_choosing_area() then return end + + local bounds = { + x1 = math.min(uibs.selection_pos.x, uibs.pos.x), + x2 = math.max(uibs.selection_pos.x, uibs.pos.x), + y1 = math.min(uibs.selection_pos.y, uibs.pos.y), + y2 = math.max(uibs.selection_pos.y, uibs.pos.y), + } + + local pen = #uibs.errors > 0 and BAD_PEN or GOOD_PEN + + local function get_overlay_pen(pos) + return pen + end + + guidm.renderMapOverlay(get_overlay_pen, bounds) +end + +function PlannerOverlay:place_building() + local direction = uibs.direction + local has_selection = is_choosing_area() + local width = has_selection and math.abs(uibs.selection_pos.x - uibs.pos.x) + 1 or 1 + local height = has_selection and math.abs(uibs.selection_pos.y - uibs.pos.y) + 1 or 1 + local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( + width, height, uibs.building_type, uibs.building_subtype, + uibs.custom_type, direction) + -- get the upper-left corner of the building/area + local pos = xyz2pos( + has_selection and math.min(uibs.selection_pos.x, uibs.pos.x) or uibs.pos.x - adjusted_width//2, + has_selection and math.min(uibs.selection_pos.y, uibs.pos.y) or uibs.pos.y - adjusted_height//2, + uibs.pos.z + ) + local min_x, max_x = pos.x, pos.x + local min_y, max_y = pos.y, pos.y + if adjusted_width == 1 and adjusted_height == 1 and (width > 1 or height > 1) then + min_x = math.ceil(pos.x - width/2) + max_x = min_x + width - 1 + min_y = math.ceil(pos.y - height/2) + max_y = min_y + height - 1 + end + local blds = {} + for y=min_y,max_y do for x=min_x,max_x do + local bld, err = dfhack.buildings.constructBuilding{ + type=uibs.building_type, subtype=uibs.building_subtype, + custom=uibs.custom_type, pos=xyz2pos(x, y, pos.z), + width=adjusted_width, height=adjusted_height, direction=direction} + if err then + for _,b in ipairs(blds) do + dfhack.buildings.deconstruct(b) + end + dfhack.printerr(err) + return + end + -- assign fields for the types that need them. we can't pass them all in + -- to the call to constructBuilding since attempting to assign unrelated + -- fields to building types that don't support them causes errors. + for k,v in pairs(bld) do + if k == 'friction' then bld.friction = uibs.friction end + if k == 'use_dump' then bld.use_dump = uibs.use_dump end + if k == 'dump_x_shift' then bld.dump_x_shift = uibs.dump_x_shift end + if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_shift end + if k == 'speed' then bld.speed = uibs.speed end + end + table.insert(blds, bld) + end end + for _,bld in ipairs(blds) do + addPlannedBuilding(bld) + end +end + InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, @@ -134,48 +375,6 @@ OVERLAY_WIDGETS = { local dialogs = require('gui.dialogs') local guidm = require('gui.dwarfmode') -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_filter(btype, subtype, custom, reverse_idx) - local filters = dfhack.buildings.getFiltersByType( - {}, btype, subtype, custom) - if not filters or reverse_idx < 0 or reverse_idx >= #filters then - error(string.format('invalid index: %d', reverse_idx)) - end - return filters[#filters-reverse_idx] -end - --- returns a reasonable label for the item based on the qualities of the filter --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function get_item_label(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) - if filter.has_tool_use then - return to_title_case(df.tool_uses[filter.has_tool_use]) - end - if filter.item_type then - return to_title_case(df.item_type[filter.item_type]) - end - if filter.flags2 and filter.flags2.building_material then - if filter.flags2.fire_safe then - return "Fire-safe building material"; - end - if filter.flags2.magma_safe then - return "Magma-safe building material"; - end - return "Generic building material"; - end - if filter.vector_id then - return to_title_case(df.job_item_vector_id[filter.vector_id]) - end - return "Unknown"; -end - -- returns whether the items matched by the specified filter can have a quality -- rating. This also conveniently indicates whether an item can be decorated. -- does not need the core suspended @@ -191,56 +390,6 @@ function item_can_be_improved(btype, subtype, custom, reverse_idx) filter.item_type ~= df.item_type.BOULDER end --- needs the core suspended --- returns a vector of constructed buildings (usually of size 1, but potentially --- more for constructions) -function construct_buildings_from_ui_state() - local uibs = df.global.buildreq - local world = df.global.world - local direction = world.selected_direction - local _, width, height = dfhack.buildings.getCorrectSize( - world.building_width, world.building_height, uibs.building_type, - uibs.building_subtype, uibs.custom_type, direction) - -- the cursor is at the center of the building; we need the upper-left - -- corner of the building - local pos = guidm.getCursorPos() - pos.x = pos.x - math.floor(width/2) - pos.y = pos.y - math.floor(height/2) - local min_x, max_x = pos.x, pos.x - local min_y, max_y = pos.y, pos.y - if width == 1 and height == 1 and - (world.building_width > 1 or world.building_height > 1) then - min_x = math.ceil(pos.x - world.building_width/2) - max_x = min_x + world.building_width - 1 - min_y = math.ceil(pos.y - world.building_height/2) - max_y = min_y + world.building_height - 1 - end - local blds = {} - for y=min_y,max_y do for x=min_x,max_x do - local bld, err = dfhack.buildings.constructBuilding{ - type=uibs.building_type, subtype=uibs.building_subtype, - custom=uibs.custom_type, pos=xyz2pos(x, y, pos.z), - width=width, height=height, direction=direction} - if err then - for _,b in ipairs(blds) do - dfhack.buildings.deconstruct(b) - end - error(err) - end - -- assign fields for the types that need them. we can't pass them all in - -- to the call to constructBuilding since attempting to assign unrelated - -- fields to building types that don't support them causes errors. - for k,v in pairs(bld) do - if k == 'friction' then bld.friction = uibs.friction end - if k == 'use_dump' then bld.use_dump = uibs.use_dump end - if k == 'dump_x_shift' then bld.dump_x_shift = uibs.dump_x_shift end - if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_shift end - if k == 'speed' then bld.speed = uibs.speed end - end - table.insert(blds, bld) - end end - return blds -end -- -- GlobalSettings dialog From dd6f71c665f568c04751fcff0d864ada1620bc0c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 Feb 2023 02:10:07 -0800 Subject: [PATCH 050/126] handle stairs and 3 dimensions --- plugins/lua/buildingplan.lua | 133 ++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 27 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 4167ebb41..c06ddbc15 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -76,9 +76,10 @@ local function is_choosing_area() end local function get_cur_area_dims() - if not is_choosing_area() then return 1, 1 end + if not is_choosing_area() then return 1, 1, 1 end return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, - math.abs(uibs.selection_pos.y - uibs.pos.y) + 1 + math.abs(uibs.selection_pos.y - uibs.pos.y) + 1, + math.abs(uibs.selection_pos.z - uibs.pos.z) + 1 end local function get_cur_filters() @@ -92,6 +93,11 @@ local function is_plannable() and uibs.building_subtype == df.construction_type.TrackNSEW) end +local function is_stairs() + return uibs.building_type == df.building_type.Construction + and uibs.building_subtype == df.construction_type.UpDownStair +end + local direction_panel_frame = {t=4, h=13, w=46, r=28} local direction_panel_types = utils.invert{ @@ -108,9 +114,23 @@ local function has_direction_panel() and uibs.building_subtype == df.trap_type.TrackStop) end -local function is_over_direction_panel() - if not has_direction_panel() then return false end - local v = widgets.Widget{frame=direction_panel_frame} +local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} + +local function has_pressure_plate_panel() + return uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.PressurePlate +end + +local function is_over_options_panel() + local frame = nil + if has_direction_panel() then + frame = direction_panel_frame + elseif has_pressure_plate_panel() then + frame = pressure_plate_panel_frame + else + return false + end + local v = widgets.Widget{frame=frame} local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) v:updateLayout(gui.ViewRect{rect=rect}) return v:getMousePos() @@ -146,11 +166,11 @@ function get_item_label(idx) end local quantity = filter.quantity or 1 - local dimx, dimy = get_cur_area_dims() + local dimx, dimy, dimz = get_cur_area_dims() if quantity < 1 then - quantity = ((dimx * dimy) // 4) + 1 + quantity = (((dimx * dimy) // 4) + 1) * dimz else - quantity = quantity * dimx * dimy + quantity = quantity * dimx * dimy * dimz end return ('%s (need: %d)'):format(desc, quantity) end @@ -193,12 +213,36 @@ function PlannerOverlay:init() ItemLine{frame={t=2, l=0}, idx=2}, ItemLine{frame={t=4, l=0}, idx=3}, ItemLine{frame={t=6, l=0}, idx=4}, + widgets.CycleHotkeyLabel{ + view_id="stairs_top_subtype", + frame={t=2, l=0}, + key="CUSTOM_R", + label="Top Stair Type: ", + visible=is_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Down', value=df.construction_type.DownStair}, + }, + }, + widgets.CycleHotkeyLabel { + view_id="stairs_bottom_subtype", + frame={t=3, l=0}, + key="CUSTOM_B", + label="Bottom Stair Type: ", + visible=is_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Up', value=df.construction_type.UpStair}, + }, + }, widgets.Label{ frame={b=0, l=17}, text={ 'Selected area: ', {text=function() - return ('%d x %d'):format(get_cur_area_dims()) + return ('%d x %d x %d'):format(get_cur_area_dims()) end }, }, @@ -220,7 +264,7 @@ function PlannerOverlay:onInput(keys) return true end if keys._MOUSE_L_DOWN then - if is_over_direction_panel() then return false end + if is_over_options_panel() then return false end if self:getMouseFramePos() then return true end if #uibs.errors > 0 then return true end local pos = dfhack.gui.getMousePos() @@ -274,37 +318,72 @@ end function PlannerOverlay:place_building() local direction = uibs.direction - local has_selection = is_choosing_area() - local width = has_selection and math.abs(uibs.selection_pos.x - uibs.pos.x) + 1 or 1 - local height = has_selection and math.abs(uibs.selection_pos.y - uibs.pos.y) + 1 or 1 + local width, height, depth = get_cur_area_dims() local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( width, height, uibs.building_type, uibs.building_subtype, uibs.custom_type, direction) - -- get the upper-left corner of the building/area - local pos = xyz2pos( + -- get the upper-left corner of the building/area at min z-level + local has_selection = is_choosing_area() + local start_pos = xyz2pos( has_selection and math.min(uibs.selection_pos.x, uibs.pos.x) or uibs.pos.x - adjusted_width//2, has_selection and math.min(uibs.selection_pos.y, uibs.pos.y) or uibs.pos.y - adjusted_height//2, - uibs.pos.z + has_selection and math.min(uibs.selection_pos.z, uibs.pos.z) or uibs.pos.z ) - local min_x, max_x = pos.x, pos.x - local min_y, max_y = pos.y, pos.y - if adjusted_width == 1 and adjusted_height == 1 and (width > 1 or height > 1) then - min_x = math.ceil(pos.x - width/2) + if uibs.building_type == df.building_type.ScrewPump then + if direction == df.screw_pump_direction.FromSouth then + start_pos.y = start_pos.y + 1 + elseif direction == df.screw_pump_direction.FromEast then + start_pos.x = start_pos.x + 1 + end + end + local min_x, max_x = start_pos.x, start_pos.x + local min_y, max_y = start_pos.y, start_pos.y + local min_z, max_z = start_pos.z, start_pos.z + if adjusted_width == 1 and adjusted_height == 1 + and (width > 1 or height > 1 or depth > 1) then max_x = min_x + width - 1 - min_y = math.ceil(pos.y - height/2) max_y = min_y + height - 1 + max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end local blds = {} - for y=min_y,max_y do for x=min_x,max_x do - local bld, err = dfhack.buildings.constructBuilding{ - type=uibs.building_type, subtype=uibs.building_subtype, - custom=uibs.custom_type, pos=xyz2pos(x, y, pos.z), + local subtype = uibs.building_subtype + for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do + local pos = xyz2pos(x, y, z) + if is_stairs() then + if z == min_z then + subtype = self.subviews.stairs_bottom_subtype:getOptionValue() + if subtype == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape == df.tiletype_shape.STAIR_DOWN then + subtype = uibs.building_subtype + else + subtype = df.construction_type.UpStair + end + end + elseif z == max_z then + subtype = self.subviews.stairs_top_subtype:getOptionValue() + if subtype == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape == df.tiletype_shape.STAIR_UP then + subtype = uibs.building_subtype + else + subtype = df.construction_type.DownStair + end + end + else + subtype = uibs.building_subtype + end + end + local bld, err = dfhack.buildings.constructBuilding{pos=pos, + type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, width=adjusted_width, height=adjusted_height, direction=direction} if err then for _,b in ipairs(blds) do dfhack.buildings.deconstruct(b) end - dfhack.printerr(err) + dfhack.printerr(err .. (' (%d, %d, %d)'):format(pos.x, pos.y, pos.z)) return end -- assign fields for the types that need them. we can't pass them all in @@ -318,7 +397,7 @@ function PlannerOverlay:place_building() if k == 'speed' then bld.speed = uibs.speed end end table.insert(blds, bld) - end end + end end end for _,bld in ipairs(blds) do addPlannedBuilding(bld) end From 584e891154239ae26bdeaa03fd9a2f1eadcc19b3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 Feb 2023 02:21:19 -0800 Subject: [PATCH 051/126] more skeleton for inspector --- plugins/lua/buildingplan.lua | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index c06ddbc15..d9bcdb0d3 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -197,7 +197,7 @@ PlannerOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Building/Placement', frame={w=54, h=9}, - frame_style=gui.PANEL_FRAME, + frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -408,7 +408,7 @@ InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', - frame={w=30, h=5}, + frame={w=30, h=9}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -421,10 +421,27 @@ function InspectorOverlay:init() }, widgets.Label{ frame={t=1, l=0}, - text='items', + text='item1', }, - widgets.HotkeyLabel{ + widgets.Label{ frame={t=2, l=0}, + text='item2', + }, + widgets.Label{ + frame={t=3, l=0}, + text='item3', + }, + widgets.Label{ + frame={t=4, l=0}, + text='item4', + }, + widgets.HotkeyLabel{ + frame={t=5, l=0}, + label='adjust filters', + key='CUSTOM_CTRL_F', + }, + widgets.HotkeyLabel{ + frame={t=6, l=0}, label='make top priority', key='CUSTOM_CTRL_T', }, From c490be0271eb1a2ba5362bef55d73773aac16cee Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 Feb 2023 17:53:22 -0800 Subject: [PATCH 052/126] mark as tested to facilitate testing the commandline --- docs/plugins/buildingplan.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index c51f79721..a331e9eb8 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -3,7 +3,7 @@ buildingplan .. dfhack-tool:: :summary: Plan building construction before you have materials. - :tags: untested fort design buildings + :tags: fort design buildings This plugin adds a planning mode for building placement. You can then place furniture, constructions, and other buildings before the required materials are From a9d9e0e50c24d41dc27774b90142ac4e56b2c9ef Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Feb 2023 16:24:10 -0800 Subject: [PATCH 053/126] skeleton for quantity scanning --- plugins/buildingplan.cpp | 6 ++++++ plugins/lua/buildingplan.lua | 38 ++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp index 81f026cbc..a76e81d4a 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -707,6 +707,11 @@ static void scheduleCycle(color_ostream &out) { cycle_requested = true; } +static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering countAvailableItems\n"); + return 10; +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(printStatus), DFHACK_LUA_FUNCTION(setSetting), @@ -715,5 +720,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), + DFHACK_LUA_FUNCTION(countAvailableItems), DFHACK_LUA_END }; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index d9bcdb0d3..5da03e38f 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -143,8 +143,7 @@ local function to_title_case(str) return str end --- returns a reasonable label for the item based on the qualities of the filter -function get_item_label(idx) +function get_item_line_text(idx) local filter = get_cur_filters()[idx] local desc = 'Unknown' if filter.has_tool_use then @@ -154,17 +153,24 @@ function get_item_label(idx) desc = to_title_case(df.item_type[filter.item_type]) end if filter.flags2 and filter.flags2.building_material then - desc = "Generic building material"; + desc = "Generic material"; if filter.flags2.fire_safe then - desc = "Fire-safe building material"; + desc = "Fire-safe material"; end if filter.flags2.magma_safe then - desc = "Magma-safe building material"; + desc = "Magma-safe material"; end elseif filter.vector_id then desc = to_title_case(df.job_item_vector_id[filter.vector_id]) end + if desc:endswith('s') then + desc = desc:sub(1,-2) + end + if desc == 'Trappart' then + desc = 'Mechanism' + end + local quantity = filter.quantity or 1 local dimx, dimy, dimz = get_cur_area_dims() if quantity < 1 then @@ -172,7 +178,14 @@ function get_item_label(idx) else quantity = quantity * dimx * dimy * dimz end - return ('%s (need: %d)'):format(desc, quantity) + desc = ('%d %s%s'):format(quantity, desc, quantity == 1 and '' or 's') + + local available = countAvailableItems(uibs.building_type, + uibs.building_subtype, uibs.custom_type, idx - 1) + local note = available >= quantity and + 'Can build now' or 'Will wait for item' + + return ('%-21s%s%s'):format(desc:sub(1,21), (' '):rep(13), note) end ItemLine = defclass(ItemLine, widgets.Panel) @@ -186,7 +199,11 @@ function ItemLine:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text={{text=function() return get_item_label(self.idx) end}} + text={{text=function() return get_item_line_text(self.idx) end}}, + }, + widgets.Label{ + frame={t=0, l=22}, + text='[filter][x]', }, } end @@ -215,7 +232,7 @@ function PlannerOverlay:init() ItemLine{frame={t=6, l=0}, idx=4}, widgets.CycleHotkeyLabel{ view_id="stairs_top_subtype", - frame={t=2, l=0}, + frame={t=3, l=0}, key="CUSTOM_R", label="Top Stair Type: ", visible=is_stairs, @@ -227,7 +244,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel { view_id="stairs_bottom_subtype", - frame={t=3, l=0}, + frame={t=4, l=0}, key="CUSTOM_B", label="Bottom Stair Type: ", visible=is_stairs, @@ -242,7 +259,7 @@ function PlannerOverlay:init() text={ 'Selected area: ', {text=function() - return ('%d x %d x %d'):format(get_cur_area_dims()) + return ('%dx%dx%d'):format(get_cur_area_dims()) end }, }, @@ -401,6 +418,7 @@ function PlannerOverlay:place_building() for _,bld in ipairs(blds) do addPlannedBuilding(bld) end + scheduleCycle() end InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) From 4b7bc937a41c9eb889e59b19f93837629e249d06 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Feb 2023 17:43:03 -0800 Subject: [PATCH 054/126] remove old buildingplan files --- plugins/buildingplan/buildingplan-planner.cpp | 1074 --------------- plugins/buildingplan/buildingplan-planner.h | 140 -- plugins/buildingplan/buildingplan-rooms.cpp | 226 ---- plugins/buildingplan/buildingplan-rooms.h | 51 - plugins/buildingplan/buildingplan.cpp | 1168 ----------------- plugins/buildingplan/buildingplan.h | 8 - 6 files changed, 2667 deletions(-) delete mode 100644 plugins/buildingplan/buildingplan-planner.cpp delete mode 100644 plugins/buildingplan/buildingplan-planner.h delete mode 100644 plugins/buildingplan/buildingplan-rooms.cpp delete mode 100644 plugins/buildingplan/buildingplan-rooms.h delete mode 100644 plugins/buildingplan/buildingplan.cpp delete mode 100644 plugins/buildingplan/buildingplan.h diff --git a/plugins/buildingplan/buildingplan-planner.cpp b/plugins/buildingplan/buildingplan-planner.cpp deleted file mode 100644 index 07f23150a..000000000 --- a/plugins/buildingplan/buildingplan-planner.cpp +++ /dev/null @@ -1,1074 +0,0 @@ -#include -#include // for CHAR_BIT - -#include "df/building_design.h" -#include "df/building_doorst.h" -#include "df/building_type.h" -#include "df/general_ref_building_holderst.h" -#include "df/job_item.h" -#include "df/buildreq.h" - -#include "modules/Buildings.h" -#include "modules/Gui.h" -#include "modules/Job.h" - -#include "LuaTools.h" -#include "../uicommon.h" - -#include "buildingplan.h" - -static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints"; -static const std::string planned_building_persistence_key_v2 = "buildingplan/constraints2"; -static const std::string global_settings_persistence_key = "buildingplan/global"; - -/* - * ItemFilter - */ - -ItemFilter::ItemFilter() -{ - clear(); -} - -void ItemFilter::clear() -{ - min_quality = df::item_quality::Ordinary; - max_quality = df::item_quality::Masterful; - decorated_only = false; - clearMaterialMask(); - materials.clear(); -} - -bool ItemFilter::deserialize(std::string ser) -{ - clear(); - - std::vector tokens; - split_string(&tokens, ser, "/"); - if (tokens.size() != 5) - { - debug("invalid ItemFilter serialization: '%s'", ser.c_str()); - return false; - } - - if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1])) - return false; - - setMinQuality(atoi(tokens[2].c_str())); - setMaxQuality(atoi(tokens[3].c_str())); - decorated_only = static_cast(atoi(tokens[4].c_str())); - return true; -} - -bool ItemFilter::deserializeMaterialMask(std::string ser) -{ - if (ser.empty()) - return true; - - if (!parseJobMaterialCategory(&mat_mask, ser)) - { - debug("invalid job material category serialization: '%s'", ser.c_str()); - return false; - } - return true; -} - -bool ItemFilter::deserializeMaterials(std::string ser) -{ - if (ser.empty()) - return true; - - std::vector mat_names; - split_string(&mat_names, ser, ","); - for (auto m = mat_names.begin(); m != mat_names.end(); m++) - { - DFHack::MaterialInfo material; - if (!material.find(*m) || !material.isValid()) - { - debug("invalid material name serialization: '%s'", ser.c_str()); - return false; - } - materials.push_back(material); - } - return true; -} - -// format: mat,mask,elements/materials,list/minq/maxq/decorated -std::string ItemFilter::serialize() const -{ - std::ostringstream ser; - ser << bitfield_to_string(mat_mask, ",") << "/"; - if (!materials.empty()) - { - ser << materials[0].getToken(); - for (size_t i = 1; i < materials.size(); ++i) - ser << "," << materials[i].getToken(); - } - ser << "/" << static_cast(min_quality); - ser << "/" << static_cast(max_quality); - ser << "/" << static_cast(decorated_only); - return ser.str(); -} - -void ItemFilter::clearMaterialMask() -{ - mat_mask.whole = 0; -} - -void ItemFilter::addMaterialMask(uint32_t mask) -{ - mat_mask.whole |= mask; -} - -void ItemFilter::setMaterials(std::vector materials) -{ - this->materials = materials; -} - -static void clampItemQuality(df::item_quality *quality) -{ - if (*quality > item_quality::Artifact) - { - debug("clamping quality to Artifact"); - *quality = item_quality::Artifact; - } - if (*quality < item_quality::Ordinary) - { - debug("clamping quality to Ordinary"); - *quality = item_quality::Ordinary; - } -} - -void ItemFilter::setMinQuality(int quality) -{ - min_quality = static_cast(quality); - clampItemQuality(&min_quality); - if (max_quality < min_quality) - max_quality = min_quality; -} - -void ItemFilter::setMaxQuality(int quality) -{ - max_quality = static_cast(quality); - clampItemQuality(&max_quality); - if (max_quality < min_quality) - min_quality = max_quality; -} - -void ItemFilter::incMinQuality() { setMinQuality(min_quality + 1); } -void ItemFilter::decMinQuality() { setMinQuality(min_quality - 1); } -void ItemFilter::incMaxQuality() { setMaxQuality(max_quality + 1); } -void ItemFilter::decMaxQuality() { setMaxQuality(max_quality - 1); } - -void ItemFilter::toggleDecoratedOnly() { decorated_only = !decorated_only; } - -static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } - -uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; } - -std::vector ItemFilter::getMaterials() const -{ - std::vector descriptions; - transform_(materials, descriptions, material_to_string_fn); - - if (descriptions.size() == 0) - bitfield_to_string(&descriptions, mat_mask); - - if (descriptions.size() == 0) - descriptions.push_back("any"); - - return descriptions; -} - -std::string ItemFilter::getMinQuality() const -{ - return ENUM_KEY_STR(item_quality, min_quality); -} - -std::string ItemFilter::getMaxQuality() const -{ - return ENUM_KEY_STR(item_quality, max_quality); -} - -bool ItemFilter::getDecoratedOnly() const -{ - return decorated_only; -} - -bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat) const -{ - return mat_mask.whole ? mat.matches(mat_mask) : true; -} - -bool ItemFilter::matches(df::dfhack_material_category mask) const -{ - return mask.whole & mat_mask.whole; -} - -bool ItemFilter::matches(DFHack::MaterialInfo &material) const -{ - for (auto it = materials.begin(); it != materials.end(); ++it) - if (material.matches(*it)) - return true; - return false; -} - -bool ItemFilter::matches(df::item *item) const -{ - if (item->getQuality() < min_quality || item->getQuality() > max_quality) - return false; - - if (decorated_only && !item->hasImprovements()) - return false; - - auto imattype = item->getActualMaterial(); - auto imatindex = item->getActualMaterialIndex(); - auto item_mat = DFHack::MaterialInfo(imattype, imatindex); - - return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat); -} - - -/* - * PlannedBuilding - */ - -// format: itemfilterser|itemfilterser|... -static std::string serializeFilters(const std::vector &filters) -{ - std::ostringstream ser; - if (!filters.empty()) - { - ser << filters[0].serialize(); - for (size_t i = 1; i < filters.size(); ++i) - ser << "|" << filters[i].serialize(); - } - return ser.str(); -} - -static std::vector deserializeFilters(std::string ser) -{ - std::vector isers; - split_string(&isers, ser, "|"); - std::vector ret; - for (auto & iser : isers) - { - ItemFilter filter; - if (filter.deserialize(iser)) - ret.push_back(filter); - } - return ret; -} - -static size_t getNumFilters(BuildingTypeKey key) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 4) || !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "get_num_filters")) - { - debug("failed to push the lua method on the stack"); - return 0; - } - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - - if (!Lua::SafeCall(out, L, 3, 1)) - { - debug("lua call failed"); - return 0; - } - - int num_filters = lua_tonumber(L, -1); - lua_pop(L, 1); - return num_filters; -} - -PlannedBuilding::PlannedBuilding(df::building *building, const std::vector &filters) - : building(building), - building_id(building->id), - filters(filters) -{ - config = DFHack::World::AddPersistentData(planned_building_persistence_key_v2); - config.ival(0) = building_id; - config.val() = serializeFilters(filters); -} - -PlannedBuilding::PlannedBuilding(PersistentDataItem &config) - : config(config), - building(df::building::find(config.ival(0))), - building_id(config.ival(0)), - filters(deserializeFilters(config.val())) -{ - if (building) - { - if (filters.size() != - getNumFilters(toBuildingTypeKey(building))) - { - debug("invalid ItemFilter vector serialization: '%s'", - config.val().c_str()); - building = NULL; - } - } -} - -// Ensure the building still exists and is in a valid state. It can disappear -// for lots of reasons, such as running the game with the buildingplan plugin -// disabled, manually removing the building, modifying it via the API, etc. -bool PlannedBuilding::isValid() const -{ - return building && df::building::find(building_id) - && building->getBuildStage() == 0; -} - -void PlannedBuilding::remove() -{ - DFHack::World::DeletePersistentData(config); - building = NULL; -} - -df::building * PlannedBuilding::getBuilding() -{ - return building; -} - -const std::vector & PlannedBuilding::getFilters() const -{ - // if we want to be able to dynamically change the filters, we'll need to - // re-bucket the tasks in Planner. - return filters; -} - - -/* - * BuildingTypeKey - */ - -BuildingTypeKey toBuildingTypeKey( - df::building_type btype, int16_t subtype, int32_t custom) -{ - return std::make_tuple(btype, subtype, custom); -} - -BuildingTypeKey toBuildingTypeKey(df::building *bld) -{ - return std::make_tuple( - bld->getType(), bld->getSubtype(), bld->getCustomType()); -} - -BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs) -{ - return std::make_tuple( - uibs->building_type, uibs->building_subtype, uibs->custom_type); -} - -// rotates a size_t value left by count bits -// assumes count is not 0 or >= size_t_bits -// replace this with std::rotl when we move to C++20 -static std::size_t rotl_size_t(size_t val, uint32_t count) -{ - static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); - return val << count | val >> (size_t_bits - count); -} - -std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const -{ - // cast first param to appease gcc-4.8, which is missing the enum - // specializations for std::hash - std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); - std::size_t h2 = std::hash()(std::get<1>(key)); - std::size_t h3 = std::hash()(std::get<2>(key)); - - return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); -} - - -/* - * Planner - */ - -// convert v1 persistent data into v2 format -// we can remove this conversion code once v2 has been live for a while -void migrateV1ToV2() -{ - std::vector configs; - DFHack::World::GetPersistentData(&configs, planned_building_persistence_key_v1); - if (configs.empty()) - return; - - debug("migrating %zu persisted configs to new format", configs.size()); - for (auto config : configs) - { - df::building *bld = df::building::find(config.ival(1)); - if (!bld) - { - debug("buliding no longer exists; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - if (bld->getBuildStage() != 0 || bld->jobs.size() != 1 - || bld->jobs[0]->job_items.size() != 1) - { - debug("building in invalid state; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - // fix up the building so we can set the material properties later - bld->mat_type = -1; - bld->mat_index = -1; - - // the v1 filters are not initialized correctly and will match any item. - // we need to fix them up a bit. - auto filter = bld->jobs[0]->job_items[0]; - df::item_type type; - switch (bld->getType()) - { - case df::building_type::Armorstand: type = df::item_type::ARMORSTAND; break; - case df::building_type::Bed: type = df::item_type::BED; break; - case df::building_type::Chair: type = df::item_type::CHAIR; break; - case df::building_type::Coffin: type = df::item_type::COFFIN; break; - case df::building_type::Door: type = df::item_type::DOOR; break; - case df::building_type::Floodgate: type = df::item_type::FLOODGATE; break; - case df::building_type::Hatch: type = df::item_type::HATCH_COVER; break; - case df::building_type::GrateWall: type = df::item_type::GRATE; break; - case df::building_type::GrateFloor: type = df::item_type::GRATE; break; - case df::building_type::BarsVertical: type = df::item_type::BAR; break; - case df::building_type::BarsFloor: type = df::item_type::BAR; break; - case df::building_type::Cabinet: type = df::item_type::CABINET; break; - case df::building_type::Box: type = df::item_type::BOX; break; - case df::building_type::Weaponrack: type = df::item_type::WEAPONRACK; break; - case df::building_type::Statue: type = df::item_type::STATUE; break; - case df::building_type::Slab: type = df::item_type::SLAB; break; - case df::building_type::Table: type = df::item_type::TABLE; break; - case df::building_type::WindowGlass: type = df::item_type::WINDOW; break; - case df::building_type::AnimalTrap: type = df::item_type::ANIMALTRAP; break; - case df::building_type::Chain: type = df::item_type::CHAIN; break; - case df::building_type::Cage: type = df::item_type::CAGE; break; - case df::building_type::TractionBench: type = df::item_type::TRACTION_BENCH; break; - default: - debug("building has unhandled type; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - filter->item_type = type; - filter->item_subtype = -1; - filter->mat_type = -1; - filter->mat_index = -1; - filter->flags1.whole = 0; - filter->flags2.whole = 0; - filter->flags2.bits.allow_artifact = true; - filter->flags3.whole = 0; - filter->flags4 = 0; - filter->flags5 = 0; - filter->metal_ore = -1; - filter->min_dimension = -1; - filter->has_tool_use = df::tool_uses::NONE; - filter->quantity = 1; - - std::vector tokens; - split_string(&tokens, config.val(), "/"); - if (tokens.size() != 2) - { - debug("invalid v1 format; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - ItemFilter item_filter; - item_filter.deserializeMaterialMask(tokens[0]); - item_filter.deserializeMaterials(tokens[1]); - item_filter.setMinQuality(config.ival(2) - 1); - item_filter.setMaxQuality(config.ival(4) - 1); - if (config.ival(3) - 1) - item_filter.toggleDecoratedOnly(); - - // create the v2 record - std::vector item_filters; - item_filters.push_back(item_filter); - PlannedBuilding pb(bld, item_filters); - - // remove the v1 record - DFHack::World::DeletePersistentData(config); - debug("v1 %s(%d) record successfully migrated", - ENUM_KEY_STR(building_type, bld->getType()).c_str(), - bld->id); - } -} - -// assumes no setting has '=' or '|' characters -static std::string serialize_settings(std::map & settings) -{ - std::ostringstream ser; - for (auto & entry : settings) - { - ser << entry.first << "=" << (entry.second ? "1" : "0") << "|"; - } - return ser.str(); -} - -static void deserialize_settings(std::map & settings, - std::string ser) -{ - std::vector tokens; - split_string(&tokens, ser, "|"); - for (auto token : tokens) - { - if (token.empty()) - continue; - - std::vector parts; - split_string(&parts, token, "="); - if (parts.size() != 2) - { - debug("invalid serialized setting format: '%s'", token.c_str()); - continue; - } - std::string key = parts[0]; - if (settings.count(key) == 0) - { - debug("unknown serialized setting: '%s", key.c_str()); - continue; - } - settings[key] = static_cast(atoi(parts[1].c_str())); - debug("deserialized setting: %s = %d", key.c_str(), settings[key]); - } -} - -static DFHack::PersistentDataItem init_global_settings( - std::map & settings) -{ - settings.clear(); - settings["blocks"] = true; - settings["boulders"] = true; - settings["logs"] = true; - settings["bars"] = false; - - // load persistent global settings if they exist; otherwise create them - std::vector items; - DFHack::World::GetPersistentData(&items, global_settings_persistence_key); - if (items.size() == 1) - { - DFHack::PersistentDataItem & config = items[0]; - deserialize_settings(settings, config.val()); - return config; - } - - debug("initializing persistent global settings"); - DFHack::PersistentDataItem config = - DFHack::World::AddPersistentData(global_settings_persistence_key); - config.val() = serialize_settings(settings); - return config; -} - -const std::map & Planner::getGlobalSettings() const -{ - return global_settings; -} - -bool Planner::setGlobalSetting(std::string name, bool value) -{ - if (global_settings.count(name) == 0) - { - debug("attempted to set invalid setting: '%s'", name.c_str()); - return false; - } - debug("global setting '%s' %d -> %d", - name.c_str(), global_settings[name], value); - global_settings[name] = value; - if (config.isValid()) - config.val() = serialize_settings(global_settings); - return true; -} - -void Planner::reset() -{ - debug("resetting Planner state"); - default_item_filters.clear(); - planned_buildings.clear(); - tasks.clear(); - - config = init_global_settings(global_settings); - - migrateV1ToV2(); - - std::vector items; - DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v2); - debug("found data for %zu planned building(s)", items.size()); - - for (auto i = items.begin(); i != items.end(); i++) - { - PlannedBuilding pb(*i); - if (!pb.isValid()) - { - debug("discarding invalid planned building"); - pb.remove(); - continue; - } - - if (registerTasks(pb)) - planned_buildings.insert(std::make_pair(pb.getBuilding()->id, pb)); - } -} - -void Planner::addPlannedBuilding(df::building *bld) -{ - auto item_filters = getItemFilters(toBuildingTypeKey(bld)).get(); - // not a supported type - if (item_filters.empty()) - { - debug("failed to add building: unsupported type"); - return; - } - - // protect against multiple registrations - if (planned_buildings.count(bld->id) != 0) - { - debug("failed to add building: already registered"); - return; - } - - PlannedBuilding pb(bld, item_filters); - if (pb.isValid() && registerTasks(pb)) - { - for (auto job : bld->jobs) - job->flags.bits.suspend = true; - - planned_buildings.insert(std::make_pair(bld->id, pb)); - } - else - { - pb.remove(); - } -} - -static std::string getBucket(const df::job_item & ji, - const std::vector & item_filters) -{ - std::ostringstream ser; - - // pull out and serialize only known relevant fields. if we miss a few, then - // the filter bucket will be slighly less specific than it could be, but - // that's probably ok. we'll just end up bucketing slightly different items - // together. this is only a problem if the different filter at the front of - // the queue doesn't match any available items and blocks filters behind it - // that could be matched. - ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' - << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole - << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' - << ji.metal_ore << ':' << ji.has_tool_use; - - for (auto & item_filter : item_filters) - { - ser << ':' << item_filter.serialize(); - } - - return ser.str(); -} - -// get a list of item vectors that we should search for matches -static std::vector getVectorIds(df::job_item *job_item, - const std::map & global_settings) -{ - std::vector ret; - - // if the filter already has the vector_id set to something specific, use it - if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) - { - debug("using vector_id from job_item: %s", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); - ret.push_back(job_item->vector_id); - return ret; - } - - // if the filer is for building material, refer to our global settings for - // which vectors to search - if (job_item->flags2.bits.building_material) - { - if (global_settings.at("blocks")) - ret.push_back(df::job_item_vector_id::BLOCKS); - if (global_settings.at("boulders")) - ret.push_back(df::job_item_vector_id::BOULDER); - if (global_settings.at("logs")) - ret.push_back(df::job_item_vector_id::WOOD); - if (global_settings.at("bars")) - ret.push_back(df::job_item_vector_id::BAR); - } - - // fall back to IN_PLAY if no other vector was appropriate - if (ret.empty()) - ret.push_back(df::job_item_vector_id::IN_PLAY); - return ret; -} - -bool Planner::registerTasks(PlannedBuilding & pb) -{ - df::building * bld = pb.getBuilding(); - if (bld->jobs.size() != 1) - { - debug("unexpected number of jobs: want 1, got %zu", bld->jobs.size()); - return false; - } - auto job_items = bld->jobs[0]->job_items; - int num_job_items = job_items.size(); - if (num_job_items < 1) - { - debug("unexpected number of job items: want >0, got %d", num_job_items); - return false; - } - int32_t id = bld->id; - for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) - { - auto job_item = job_items[job_item_idx]; - auto bucket = getBucket(*job_item, pb.getFilters()); - auto vector_ids = getVectorIds(job_item, global_settings); - - // if there are multiple vector_ids, schedule duplicate tasks. after - // the correct number of items are matched, the extras will get popped - // as invalid - for (auto vector_id : vector_ids) - { - for (int item_num = 0; item_num < job_item->quantity; ++item_num) - { - tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); - debug("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, job_item_idx, tasks.size(), - tasks[vector_id].size(), tasks[vector_id][bucket].size()); - } - } - } - return true; -} - -PlannedBuilding * Planner::getPlannedBuilding(df::building *bld) -{ - if (!bld || planned_buildings.count(bld->id) == 0) - return NULL; - return &planned_buildings.at(bld->id); -} - -bool Planner::isPlannableBuilding(BuildingTypeKey key) -{ - return getNumFilters(key) >= 1; -} - -Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key) -{ - static std::vector empty_vector; - static const ItemFiltersWrapper empty_ret(empty_vector); - - size_t nfilters = getNumFilters(key); - if (nfilters < 1) - return empty_ret; - while (default_item_filters[key].size() < nfilters) - default_item_filters[key].push_back(ItemFilter()); - return ItemFiltersWrapper(default_item_filters[key]); -} - -// precompute a bitmask with bad item flags -struct BadFlags -{ - uint32_t whole; - - BadFlags() - { - df::item_flags flags; - #define F(x) flags.bits.x = true; - F(dump); F(forbid); F(garbage_collect); - F(hostile); F(on_fire); F(rotten); F(trader); - F(in_building); F(construction); F(in_job); - F(owned); F(in_chest); F(removed); F(encased); - #undef F - whole = flags.whole; - } -}; - -static bool itemPassesScreen(df::item * item) -{ - static BadFlags bad_flags; - return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile() - // TODO: make this configurable - && !(item->getType() == df::item_type::BOX && item->isBag()); -} - -static bool matchesFilters(df::item * item, - df::job_item * job_item, - const ItemFilter & item_filter) -{ - // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) - return false; - - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) - return false; - - if (job_item->flags2.bits.building_material && !item->isBuildMat()) - return false; - - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) - return false; - - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) - return false; - - return DFHack::Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) - && DFHack::Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), - item->getType()) - && item_filter.matches(item); -} - -// note that this just removes the PlannedBuilding. the tasks will get dropped -// as we discover them in the tasks queues and they fail their isValid() check. -// this "lazy" task cleaning algorithm works because there is no way to -// re-register a building once it has been removed -- if it fails isValid() -// then it has either been built or desroyed. therefore there is no chance of -// duplicate tasks getting added to the tasks queues. -void Planner::unregisterBuilding(int32_t id) -{ - if (planned_buildings.count(id) > 0) - { - planned_buildings.at(id).remove(); - planned_buildings.erase(id); - } -} - -static bool isJobReady(df::job * job) -{ - int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } - if (needed_items) - { - debug("building needs %d more item(s)", needed_items); - return false; - } - return true; -} - -static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) -{ - // we want the items in the opposite order of the filters - return a->job_item_idx > b->job_item_idx; -} - -// this function does not remove the job_items since their quantity fields are -// now all at 0, so there is no risk of having extra items attached. we don't -// remove them to keep the "finalize with buildingplan active" path as similar -// as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(df::building * bld) -{ - debug("finalizing building %d", bld->id); - auto job = bld->jobs[0]; - - // sort the items so they get added to the structure in the correct order - std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); - - // derive the material properties of the building and job from the first - // applicable item, though if any boulders are involved, it makes the whole - // structure "rough". - bool rough = false; - for (auto attached_item : job->items) - { - df::item *item = attached_item->item; - rough = rough || item->getType() == item_type::BOULDER; - if (bld->mat_type == -1) - { - bld->mat_type = item->getMaterial(); - job->mat_type = bld->mat_type; - } - if (bld->mat_index == -1) - { - bld->mat_index = item->getMaterialIndex(); - job->mat_index = bld->mat_index; - } - } - - if (bld->needsDesign()) - { - auto act = (df::building_actual *)bld; - if (!act->design) - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; - } - - // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); -} - -void Planner::popInvalidTasks(std::queue> & task_queue) -{ - while (!task_queue.empty()) - { - auto & task = task_queue.front(); - auto id = task.first; - if (planned_buildings.count(id) > 0) - { - PlannedBuilding & pb = planned_buildings.at(id); - if (pb.isValid() && - pb.getBuilding()->jobs[0]->job_items[task.second]->quantity) - { - break; - } - } - debug("discarding invalid task: bld=%d, job_item_idx=%d", - id, task.second); - task_queue.pop(); - unregisterBuilding(id); - } -} - -void Planner::doVector(df::job_item_vector_id vector_id, - std::map>> & buckets) -{ - auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); - auto item_vector = df::global::world->items.other[other_id]; - debug("matching %zu item(s) in vector %s against %zu filter bucket(s)", - item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - buckets.size()); - for (auto item_it = item_vector.rbegin(); - item_it != item_vector.rend(); - ++item_it) - { - auto item = *item_it; - if (!itemPassesScreen(item)) - continue; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();) - { - auto & task_queue = bucket_it->second; - popInvalidTasks(task_queue); - if (task_queue.empty()) - { - debug("removing empty bucket: %s/%s; %zu bucket(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; - } - auto & task = task_queue.front(); - auto id = task.first; - auto & pb = planned_buildings.at(id); - auto building = pb.getBuilding(); - auto job = building->jobs[0]; - auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx], - pb.getFilters()[filter_idx]) - && DFHack::Job::attachJobItem(job, item, - df::job_item_ref::Hauled, filter_idx)) - { - MaterialInfo material; - material.decode(item); - ItemTypeInfo item_type; - item_type.decode(item); - debug("attached %s %s to filter %d for %s(%d): %s/%s", - material.toString().c_str(), - item_type.toString().c_str(), - filter_idx, - ENUM_KEY_STR(building_type, building->getType()).c_str(), - id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); - // keep quantity aligned with the actual number of remaining - // items so if buildingplan is turned off, the building will - // be completed with the correct number of items. - --job->job_items[filter_idx]->quantity; - task_queue.pop(); - if (isJobReady(job)) - { - finalizeBuilding(building); - unregisterBuilding(id); - } - if (task_queue.empty()) - { - debug( - "removing empty item bucket: %s/%s; %zu left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - buckets.erase(bucket_it); - } - // we found a home for this item; no need to look further - break; - } - ++bucket_it; - } - if (buckets.empty()) - break; - } -} - -struct VectorsToScanLast -{ - std::vector vectors; - VectorsToScanLast() - { - // order is important here. we want to match boulders before wood and - // everything before bars. blocks are not listed here since we'll have - // already scanned them when we did the first pass through the buckets. - vectors.push_back(df::job_item_vector_id::BOULDER); - vectors.push_back(df::job_item_vector_id::WOOD); - vectors.push_back(df::job_item_vector_id::BAR); - } -}; - -void Planner::doCycle() -{ - debug("running cycle for %zu registered building(s)", - planned_buildings.size()); - static const VectorsToScanLast vectors_to_scan_last; - for (auto it = tasks.begin(); it != tasks.end();) - { - auto vector_id = it->first; - // we could make this a set, but it's only three elements - if (std::find(vectors_to_scan_last.vectors.begin(), - vectors_to_scan_last.vectors.end(), - vector_id) != vectors_to_scan_last.vectors.end()) - { - ++it; - continue; - } - - auto & buckets = it->second; - doVector(vector_id, buckets); - if (buckets.empty()) - { - debug("removing empty vector: %s; %zu vector(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - it = tasks.erase(it); - } - else - ++it; - } - for (auto vector_id : vectors_to_scan_last.vectors) - { - if (tasks.count(vector_id) == 0) - continue; - auto & buckets = tasks[vector_id]; - doVector(vector_id, buckets); - if (buckets.empty()) - { - debug("removing empty vector: %s; %zu vector(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - tasks.erase(vector_id); - } - } - debug("cycle done; %zu registered building(s) left", - planned_buildings.size()); -} - -Planner planner; diff --git a/plugins/buildingplan/buildingplan-planner.h b/plugins/buildingplan/buildingplan-planner.h deleted file mode 100644 index 7b1615704..000000000 --- a/plugins/buildingplan/buildingplan-planner.h +++ /dev/null @@ -1,140 +0,0 @@ -#pragma once - -#include -#include - -#include "df/building.h" -#include "df/dfhack_material_category.h" -#include "df/item_quality.h" -#include "df/job_item.h" - -#include "modules/Materials.h" -#include "modules/Persistence.h" - -class ItemFilter -{ -public: - ItemFilter(); - - void clear(); - bool deserialize(std::string ser); - std::string serialize() const; - - void addMaterialMask(uint32_t mask); - void clearMaterialMask(); - void setMaterials(std::vector materials); - - void incMinQuality(); - void decMinQuality(); - void incMaxQuality(); - void decMaxQuality(); - void toggleDecoratedOnly(); - - uint32_t getMaterialMask() const; - std::vector getMaterials() const; - std::string getMinQuality() const; - std::string getMaxQuality() const; - bool getDecoratedOnly() const; - - bool matches(df::dfhack_material_category mask) const; - bool matches(DFHack::MaterialInfo &material) const; - bool matches(df::item *item) const; - -private: - // remove friend declaration when we no longer need v1 deserialization - friend void migrateV1ToV2(); - - df::dfhack_material_category mat_mask; - std::vector materials; - df::item_quality min_quality; - df::item_quality max_quality; - bool decorated_only; - - bool deserializeMaterialMask(std::string ser); - bool deserializeMaterials(std::string ser); - void setMinQuality(int quality); - void setMaxQuality(int quality); - bool matchesMask(DFHack::MaterialInfo &mat) const; -}; - -class PlannedBuilding -{ -public: - PlannedBuilding(df::building *building, const std::vector &filters); - PlannedBuilding(DFHack::PersistentDataItem &config); - - bool isValid() const; - void remove(); - - df::building * getBuilding(); - const std::vector & getFilters() const; - -private: - DFHack::PersistentDataItem config; - df::building *building; - const df::building::key_field_type building_id; - const std::vector filters; -}; - -// building type, subtype, custom -typedef std::tuple BuildingTypeKey; - -BuildingTypeKey toBuildingTypeKey( - df::building_type btype, int16_t subtype, int32_t custom); -BuildingTypeKey toBuildingTypeKey(df::building *bld); -BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs); - -struct BuildingTypeKeyHash -{ - std::size_t operator() (const BuildingTypeKey & key) const; -}; - -class Planner -{ -public: - class ItemFiltersWrapper - { - public: - ItemFiltersWrapper(std::vector & item_filters) - : item_filters(item_filters) { } - std::vector::reverse_iterator rbegin() const { return item_filters.rbegin(); } - std::vector::reverse_iterator rend() const { return item_filters.rend(); } - const std::vector & get() const { return item_filters; } - private: - std::vector &item_filters; - }; - - const std::map & getGlobalSettings() const; - bool setGlobalSetting(std::string name, bool value); - - void reset(); - - void addPlannedBuilding(df::building *bld); - PlannedBuilding *getPlannedBuilding(df::building *bld); - - bool isPlannableBuilding(BuildingTypeKey key); - - // returns an empty vector if the type is not supported - ItemFiltersWrapper getItemFilters(BuildingTypeKey key); - - void doCycle(); - -private: - DFHack::PersistentDataItem config; - std::map global_settings; - std::unordered_map, - BuildingTypeKeyHash> default_item_filters; - // building id -> PlannedBuilding - std::unordered_map planned_buildings; - // vector id -> filter bucket -> queue of (building id, job_item index) - std::map>>> tasks; - - bool registerTasks(PlannedBuilding &plannedBuilding); - void unregisterBuilding(int32_t id); - void popInvalidTasks(std::queue> &task_queue); - void doVector(df::job_item_vector_id vector_id, - std::map>> & buckets); -}; - -extern Planner planner; diff --git a/plugins/buildingplan/buildingplan-rooms.cpp b/plugins/buildingplan/buildingplan-rooms.cpp deleted file mode 100644 index a08c85804..000000000 --- a/plugins/buildingplan/buildingplan-rooms.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include "buildingplan.h" - -#include -#include -#include - -#include -#include -#include - -using namespace DFHack; - -bool canReserveRoom(df::building *building) -{ - if (!building) - return false; - - if (building->jobs.size() > 0 && building->jobs[0]->job_type == df::job_type::DestroyBuilding) - return false; - - return building->is_room; -} - -std::vector getUniqueNoblePositions(df::unit *unit) -{ - std::vector np; - Units::getNoblePositions(&np, unit); - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == "MILITIA_CAPTAIN") - { - np.erase(iter); - break; - } - } - - return np; -} - -/* - * ReservedRoom - */ - -ReservedRoom::ReservedRoom(df::building *building, std::string noble_code) -{ - this->building = building; - config = DFHack::World::AddPersistentData("buildingplan/reservedroom"); - config.val() = noble_code; - config.ival(1) = building->id; - pos = df::coord(building->centerx, building->centery, building->z); -} - -ReservedRoom::ReservedRoom(PersistentDataItem &config, color_ostream &) -{ - this->config = config; - - building = df::building::find(config.ival(1)); - if (!building) - return; - pos = df::coord(building->centerx, building->centery, building->z); -} - -bool ReservedRoom::checkRoomAssignment() -{ - if (!isValid()) - return false; - - auto np = getOwnersNobleCode(); - bool correctOwner = false; - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == getCode()) - { - correctOwner = true; - break; - } - } - - if (correctOwner) - return true; - - for (auto iter = df::global::world->units.active.begin(); iter != df::global::world->units.active.end(); iter++) - { - df::unit* unit = *iter; - if (!Units::isCitizen(unit)) - continue; - - if (!Units::isActive(unit)) - continue; - - np = getUniqueNoblePositions(unit); - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == getCode()) - { - Buildings::setOwner(building, unit); - break; - } - } - } - - return true; -} - -void ReservedRoom::remove() { DFHack::World::DeletePersistentData(config); } - -bool ReservedRoom::isValid() -{ - if (!building) - return false; - - if (Buildings::findAtTile(pos) != building) - return false; - - return canReserveRoom(building); -} - -int32_t ReservedRoom::getId() -{ - if (!isValid()) - return 0; - - return building->id; -} - -std::string ReservedRoom::getCode() { return config.val(); } - -void ReservedRoom::setCode(const std::string &noble_code) { config.val() = noble_code; } - -std::vector ReservedRoom::getOwnersNobleCode() -{ - if (!building->owner) - return std::vector (); - - return getUniqueNoblePositions(building->owner); -} - -/* - * RoomMonitor - */ - -std::string RoomMonitor::getReservedNobleCode(int32_t buildingId) -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (buildingId == iter->getId()) - return iter->getCode(); - } - - return ""; -} - -void RoomMonitor::toggleRoomForPosition(int32_t buildingId, std::string noble_code) -{ - bool found = false; - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (buildingId != iter->getId()) - { - continue; - } - else - { - if (noble_code == iter->getCode()) - { - iter->remove(); - reservedRooms.erase(iter); - } - else - { - iter->setCode(noble_code); - } - found = true; - break; - } - } - - if (!found) - { - ReservedRoom room(df::building::find(buildingId), noble_code); - reservedRooms.push_back(room); - } -} - -void RoomMonitor::doCycle() -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end();) - { - if (iter->checkRoomAssignment()) - { - ++iter; - } - else - { - iter->remove(); - iter = reservedRooms.erase(iter); - } - } -} - -void RoomMonitor::reset(color_ostream &out) -{ - reservedRooms.clear(); - std::vector items; - DFHack::World::GetPersistentData(&items, "buildingplan/reservedroom"); - - for (auto i = items.begin(); i != items.end(); i++) - { - ReservedRoom rr(*i, out); - if (rr.isValid()) - addRoom(rr); - } -} - -void RoomMonitor::addRoom(ReservedRoom &rr) -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (iter->getId() == rr.getId()) - return; - } - - reservedRooms.push_back(rr); -} - -RoomMonitor roomMonitor; diff --git a/plugins/buildingplan/buildingplan-rooms.h b/plugins/buildingplan/buildingplan-rooms.h deleted file mode 100644 index 3880dbe06..000000000 --- a/plugins/buildingplan/buildingplan-rooms.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "modules/Persistence.h" -#include "modules/Units.h" - -class ReservedRoom -{ -public: - ReservedRoom(df::building *building, std::string noble_code); - - ReservedRoom(DFHack::PersistentDataItem &config, DFHack::color_ostream &out); - - bool checkRoomAssignment(); - void remove(); - bool isValid(); - - int32_t getId(); - std::string getCode(); - void setCode(const std::string &noble_code); - -private: - df::building *building; - DFHack::PersistentDataItem config; - df::coord pos; - - std::vector getOwnersNobleCode(); -}; - -class RoomMonitor -{ -public: - RoomMonitor() { } - - std::string getReservedNobleCode(int32_t buildingId); - - void toggleRoomForPosition(int32_t buildingId, std::string noble_code); - - void doCycle(); - - void reset(DFHack::color_ostream &out); - -private: - std::vector reservedRooms; - - void addRoom(ReservedRoom &rr); -}; - -bool canReserveRoom(df::building *building); -std::vector getUniqueNoblePositions(df::unit *unit); - -extern RoomMonitor roomMonitor; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp deleted file mode 100644 index cd4e84a6e..000000000 --- a/plugins/buildingplan/buildingplan.cpp +++ /dev/null @@ -1,1168 +0,0 @@ -#include "df/construction_type.h" -#include "df/entity_position.h" -#include "df/interface_key.h" -#include "df/buildreq.h" -#include "df/viewscreen_dwarfmodest.h" - -#include "modules/Gui.h" -#include "modules/Maps.h" -#include "modules/World.h" - -#include "Core.h" -#include "LuaTools.h" -#include "PluginManager.h" - -#include "../uicommon.h" -#include "../listcolumn.h" -#include "buildingplan.h" - -DFHACK_PLUGIN("buildingplan"); -#define PLUGIN_VERSION "2.0" -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(ui_build_selector); -REQUIRE_GLOBAL(world); // used in buildingplan library - -#define MAX_MASK 10 -#define MAX_MATERIAL 21 - -bool show_help = false; -bool quickfort_mode = false; -bool all_enabled = false; -bool in_dummy_screen = false; -std::unordered_map planmode_enabled; - -bool show_debugging = false; - -void debug(const char *fmt, ...) -{ - if (!show_debugging) - return; - - color_ostream_proxy out(Core::getInstance().getConsole()); - out.print("DEBUG(buildingplan): "); - va_list args; - va_start(args, fmt); - out.vprint(fmt, args); - va_end(args); - out.print("\n"); -} - -class ViewscreenChooseMaterial : public dfhack_viewscreen -{ -public: - ViewscreenChooseMaterial(ItemFilter &filter); - - void feed(set *input); - - void render(); - - std::string getFocusString() { return "buildingplan_choosemat"; } - -private: - ListColumn masks_column; - ListColumn materials_column; - int selected_column; - ItemFilter &filter; - - void addMaskEntry(df::dfhack_material_category &mask, const std::string &text) - { - auto entry = ListEntry(pad_string(text, MAX_MASK, false), mask); - if (filter.matches(mask)) - entry.selected = true; - - masks_column.add(entry); - } - - void populateMasks() - { - masks_column.clear(); - df::dfhack_material_category mask; - - mask.whole = 0; - mask.bits.stone = true; - addMaskEntry(mask, "Stone"); - - mask.whole = 0; - mask.bits.wood = true; - addMaskEntry(mask, "Wood"); - - mask.whole = 0; - mask.bits.metal = true; - addMaskEntry(mask, "Metal"); - - mask.whole = 0; - mask.bits.soap = true; - addMaskEntry(mask, "Soap"); - - masks_column.filterDisplay(); - } - - void populateMaterials() - { - materials_column.clear(); - df::dfhack_material_category selected_category; - std::vector selected_masks = masks_column.getSelectedElems(); - if (selected_masks.size() == 1) - selected_category = selected_masks[0]; - else if (selected_masks.size() > 1) - return; - - df::world_raws &raws = world->raws; - for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; i++) - { - auto obj = raws.mat_table.builtin[i]; - if (obj) - { - MaterialInfo material; - material.decode(i, -1); - addMaterialEntry(selected_category, material, material.toString()); - } - } - - for (size_t i = 0; i < raws.inorganics.size(); i++) - { - MaterialInfo material; - material.decode(0, i); - addMaterialEntry(selected_category, material, material.toString()); - } - - decltype(selected_category) wood_flag; - wood_flag.bits.wood = true; - if (!selected_category.whole || selected_category.bits.wood) - { - for (size_t i = 0; i < raws.plants.all.size(); i++) - { - df::plant_raw *p = raws.plants.all[i]; - for (size_t j = 0; p->material.size() > 1 && j < p->material.size(); j++) - { - if (p->material[j]->id != "WOOD") - continue; - - MaterialInfo material; - material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i); - auto name = material.toString(); - ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); - if (filter.matches(material)) - entry.selected = true; - - materials_column.add(entry); - } - } - } - materials_column.sort(); - } - - void addMaterialEntry(df::dfhack_material_category &selected_category, - MaterialInfo &material, std::string name) - { - if (!selected_category.whole || material.matches(selected_category)) - { - ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); - if (filter.matches(material)) - entry.selected = true; - - materials_column.add(entry); - } - } - - void validateColumn() - { - set_to_limit(selected_column, 1); - } - - void resize(int32_t x, int32_t y) - { - dfhack_viewscreen::resize(x, y); - masks_column.resize(); - materials_column.resize(); - } -}; - -const DFHack::MaterialInfo &material_info_identity_fn(const DFHack::MaterialInfo &m) { return m; } - -ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter &filter) - : filter(filter) -{ - selected_column = 0; - masks_column.setTitle("Type"); - masks_column.multiselect = true; - masks_column.allow_search = false; - masks_column.left_margin = 2; - materials_column.left_margin = MAX_MASK + 3; - materials_column.setTitle("Material"); - materials_column.multiselect = true; - - masks_column.changeHighlight(0); - - populateMasks(); - populateMaterials(); - - masks_column.selectDefaultEntry(); - materials_column.selectDefaultEntry(); - materials_column.changeHighlight(0); -} - -void ViewscreenChooseMaterial::feed(set *input) -{ - bool key_processed = false; - switch (selected_column) - { - case 0: - key_processed = masks_column.feed(input); - if (input->count(interface_key::SELECT)) - populateMaterials(); // Redo materials lists based on category selection - break; - case 1: - key_processed = materials_column.feed(input); - break; - } - - if (key_processed) - return; - - if (input->count(interface_key::LEAVESCREEN)) - { - input->clear(); - Screen::dismiss(this); - return; - } - if (input->count(interface_key::CUSTOM_SHIFT_C)) - { - filter.clear(); - masks_column.clearSelection(); - materials_column.clearSelection(); - populateMaterials(); - } - else if (input->count(interface_key::SEC_SELECT)) - { - // Convert list selections to material filters - filter.clearMaterialMask(); - - // Category masks - auto masks = masks_column.getSelectedElems(); - for (auto it = masks.begin(); it != masks.end(); ++it) - filter.addMaterialMask(it->whole); - - // Specific materials - auto materials = materials_column.getSelectedElems(); - std::vector materialInfos; - transform_(materials, materialInfos, material_info_identity_fn); - filter.setMaterials(materialInfos); - - Screen::dismiss(this); - } - else if (input->count(interface_key::STANDARDSCROLL_LEFT)) - { - --selected_column; - validateColumn(); - } - else if (input->count(interface_key::STANDARDSCROLL_RIGHT)) - { - selected_column++; - validateColumn(); - } - else if (enabler->tracking_on && enabler->mouse_lbut) - { - if (masks_column.setHighlightByMouse()) - selected_column = 0; - else if (materials_column.setHighlightByMouse()) - selected_column = 1; - - enabler->mouse_lbut = enabler->mouse_rbut = 0; - } -} - -void ViewscreenChooseMaterial::render() -{ - if (Screen::isDismissed(this)) - return; - - dfhack_viewscreen::render(); - - Screen::clear(); - Screen::drawBorder(" Building Material "); - - masks_column.display(selected_column == 0); - materials_column.display(selected_column == 1); - - int32_t y = gps->dimy - 3; - int32_t x = 2; - OutputHotkeyString(x, y, "Toggle", interface_key::SELECT); - x += 3; - OutputHotkeyString(x, y, "Save", interface_key::SEC_SELECT); - x += 3; - OutputHotkeyString(x, y, "Clear", interface_key::CUSTOM_SHIFT_C); - x += 3; - OutputHotkeyString(x, y, "Cancel", interface_key::LEAVESCREEN); -} - -//START Viewscreen Hook -static bool is_planmode_enabled(BuildingTypeKey key) -{ - return planmode_enabled[key] || quickfort_mode || all_enabled; -} - -static std::string get_item_label(const BuildingTypeKey &key, int item_idx) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 5) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "get_item_label")) - return "Failed push"; - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - Lua::Push(L, item_idx); - - if (!Lua::SafeCall(out, L, 4, 1)) - return "Failed call"; - - const char *s = lua_tostring(L, -1); - if (!s) - return "No string"; - - return s; -} - -static bool item_can_be_improved(const BuildingTypeKey &key, int item_idx) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 5) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "item_can_be_improved")) - return false; - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - Lua::Push(L, item_idx); - - if (!Lua::SafeCall(out, L, 4, 1)) - return false; - - return lua_toboolean(L, -1); -} - -static bool construct_planned_building() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - - CoreSuspendClaimer suspend; - Lua::StackUnwinder top(L); - - if (!(lua_checkstack(L, 1) && - Lua::PushModulePublic(out, L, "plugins.buildingplan", - "construct_buildings_from_ui_state") && - Lua::SafeCall(out, L, 0, 1))) - { - return false; - } - - // register all returned buildings with planner - lua_pushnil(L); - while (lua_next(L, -2) != 0) - { - auto bld = Lua::GetDFObject(L, -1); - if (!bld) - { - out.printerr( - "buildingplan: construct_buildings_from_ui_state() failed\n"); - return false; - } - - planner.addPlannedBuilding(bld); - lua_pop(L, 1); - } - - return true; -} - -static void show_global_settings_dialog() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 2) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "show_global_settings_dialog")) - { - debug("Failed to push the module"); - return; - } - - lua_newtable(L); - int ctable = lua_gettop(L); - Lua::SetField(L, quickfort_mode, ctable, "quickfort_mode"); - Lua::SetField(L, all_enabled, ctable, "all_enabled"); - - for (auto & setting : planner.getGlobalSettings()) - { - Lua::SetField(L, setting.second, ctable, setting.first.c_str()); - } - - if (!Lua::SafeCall(out, L, 1, 0)) - { - debug("Failed call to show_global_settings_dialog"); - return; - } -} - -static bool is_automaterial_enabled() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!(lua_checkstack(L, 1) && - Lua::PushModulePublic(out, L, "plugins.automaterial", "isEnabled") && - Lua::SafeCall(out, L, 0, 1))) - { - return false; - } - - return lua_toboolean(L, -1); -} - -static bool is_automaterial_managed(df::building_type type, int16_t subtype) -{ - return is_automaterial_enabled() - && type == df::building_type::Construction - && subtype < df::construction_type::TrackN; -} - -struct buildingplan_query_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - // no non-static fields allowed (according to VTableInterpose.h) - static df::building *bld; - static PlannedBuilding *pb; - static int filter_count; - static int filter_idx; - - // logic is reversed since we're starting at the last filter - bool hasNextFilter() const { return filter_idx > 0; } - bool hasPrevFilter() const { return filter_idx + 1 < filter_count; } - - bool isInPlannedBuildingQueryMode() - { - return (plotinfo->main.mode == df::ui_sidebar_mode::QueryBuilding || - plotinfo->main.mode == df::ui_sidebar_mode::BuildingItems) && - planner.getPlannedBuilding(world->selected_building); - } - - // reinit static fields when selected building changes - void initStatics() - { - df::building *cur_bld = world->selected_building; - if (bld != cur_bld) - { - bld = cur_bld; - pb = planner.getPlannedBuilding(bld); - filter_count = pb->getFilters().size(); - filter_idx = filter_count - 1; - } - } - - static void invalidateStatics() - { - bld = NULL; - } - - bool handleInput(set *input) - { - if (!isInPlannedBuildingQueryMode() || Gui::inRenameBuilding()) - return false; - - initStatics(); - - if (input->count(interface_key::SUSPENDBUILDING)) - return true; // Don't unsuspend planned buildings - if (input->count(interface_key::DESTROYBUILDING)) - { - // remove persistent data - pb->remove(); - // still allow the building to be removed - return false; - } - - // ctrl+Right - if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter()) - --filter_idx; - // ctrl+Left - else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter()) - ++filter_idx; - else - return false; - return true; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - static bool is_filter_satisfied(df::building *bld, int filter_idx) - { - if (!bld - || bld->jobs.size() < 1 - || int(bld->jobs[0]->job_items.size()) <= filter_idx) - return false; - - // if all items for this filter are attached, the quantity will be 0 - return bld->jobs[0]->job_items[filter_idx]->quantity == 0; - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (!isInPlannedBuildingQueryMode()) - return; - - initStatics(); - - // Hide suspend toggle option - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 20; - Screen::Pen pen(' ', COLOR_BLACK); - Screen::fillRect(pen, x, y, dims.menu_x2, y); - - bool attached = is_filter_satisfied(pb->getBuilding(), filter_idx); - - auto & filter = pb->getFilters()[filter_idx]; - y = 24; - std::string item_label = - stl_sprintf("Item %d of %d (%s)", filter_count - filter_idx, filter_count, attached ? "attached" : "pending"); - OutputString(COLOR_WHITE, x, y, "Planned Building Filter", true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, item_label.c_str(), true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, get_item_label(toBuildingTypeKey(bld), filter_idx).c_str(), true, left_margin); - ++y; - if (item_can_be_improved(toBuildingTypeKey(bld), filter_idx)) - { - OutputString(COLOR_BROWN, x, y, "Min Quality: ", false, left_margin); - OutputString(COLOR_BLUE, x, y, filter.getMinQuality(), true, left_margin); - OutputString(COLOR_BROWN, x, y, "Max Quality: ", false, left_margin); - OutputString(COLOR_BLUE, x, y, filter.getMaxQuality(), true, left_margin); - if (filter.getDecoratedOnly()) - OutputString(COLOR_BLUE, x, y, "Decorated Only", true, left_margin); - } - - OutputString(COLOR_BROWN, x, y, "Materials:", true, left_margin); - auto filters = filter.getMaterials(); - for (auto it = filters.begin(); it != filters.end(); ++it) - OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin); - - ++y; - if (hasPrevFilter()) - OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (hasNextFilter()) - OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } -}; - -df::building * buildingplan_query_hook::bld; -PlannedBuilding * buildingplan_query_hook::pb; -int buildingplan_query_hook::filter_count; -int buildingplan_query_hook::filter_idx; - -struct buildingplan_place_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - // no non-static fields allowed (according to VTableInterpose.h) - static BuildingTypeKey key; - static std::vector::reverse_iterator filter_rbegin; - static std::vector::reverse_iterator filter_rend; - static std::vector::reverse_iterator filter; - static int filter_count; - static int filter_idx; - - bool hasNextFilter() const { return filter + 1 != filter_rend; } - bool hasPrevFilter() const { return filter != filter_rbegin; } - - bool isInPlannedBuildingPlacementMode() - { - return plotinfo->main.mode == ui_sidebar_mode::Build && - df::global::ui_build_selector && - df::global::ui_build_selector->stage < 2 && - planner.isPlannableBuilding(toBuildingTypeKey(ui_build_selector)); - } - - // reinit static fields when selected building type changes - void initStatics() - { - BuildingTypeKey cur_key = toBuildingTypeKey(ui_build_selector); - if (key != cur_key) - { - key = cur_key; - auto wrapper = planner.getItemFilters(key); - filter_rbegin = wrapper.rbegin(); - filter_rend = wrapper.rend(); - filter = filter_rbegin; - filter_count = wrapper.get().size(); - filter_idx = filter_count - 1; - } - } - - static void invalidateStatics() - { - key = BuildingTypeKey(); - } - - bool handleInput(set *input) - { - if (!isInPlannedBuildingPlacementMode()) - { - show_help = false; - return false; - } - - initStatics(); - - if (in_dummy_screen) - { - if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT) - || input->count(interface_key::LEAVESCREEN)) - { - in_dummy_screen = false; - // pass LEAVESCREEN up to parent view - input->clear(); - input->insert(interface_key::LEAVESCREEN); - return false; - } - return true; - } - - if (input->count(interface_key::CUSTOM_P) || - input->count(interface_key::CUSTOM_G) || - input->count(interface_key::CUSTOM_D) || - input->count(interface_key::CUSTOM_Q) || - input->count(interface_key::CUSTOM_W) || - input->count(interface_key::CUSTOM_A) || - input->count(interface_key::CUSTOM_S) || - input->count(interface_key::CUSTOM_M)) - { - show_help = true; - } - - if (!quickfort_mode && !all_enabled - && input->count(interface_key::CUSTOM_SHIFT_P)) - { - planmode_enabled[key] = !planmode_enabled[key]; - if (!is_planmode_enabled(key)) - Gui::refreshSidebar(); - return true; - } - if (input->count(interface_key::CUSTOM_SHIFT_G)) - { - show_global_settings_dialog(); - return true; - } - - if (!is_planmode_enabled(key)) - return false; - - // if automaterial is enabled, let it handle building allocation and - // registration with planner - if (input->count(interface_key::SELECT) && - !is_automaterial_managed(ui_build_selector->building_type, - ui_build_selector->building_subtype)) - { - if (ui_build_selector->errors.size() == 0 && construct_planned_building()) - { - Gui::refreshSidebar(); - if (quickfort_mode) - in_dummy_screen = true; - } - return true; - } - - - - if (input->count(interface_key::CUSTOM_SHIFT_M)) - Screen::show(dts::make_unique(*filter), plugin_self); - - if (item_can_be_improved(key, filter_idx)) - { - if (input->count(interface_key::CUSTOM_SHIFT_Q)) - filter->decMinQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_W)) - filter->incMinQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_A)) - filter->decMaxQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_S)) - filter->incMaxQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_D)) - filter->toggleDecoratedOnly(); - } - - // ctrl+Right - if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter()) - { - ++filter; - --filter_idx; - } - // ctrl+Left - else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter()) - { - --filter; - ++filter_idx; - } - else - return false; - return true; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - initStatics(); - - bool plannable = isInPlannedBuildingPlacementMode(); - if (plannable && is_planmode_enabled(key)) - { - if (ui_build_selector->stage < 1) - // No materials but turn on cursor - ui_build_selector->stage = 1; - - for (auto iter = ui_build_selector->errors.begin(); - iter != ui_build_selector->errors.end();) - { - // FIXME Hide bags - if (((*iter)->find("Needs") != string::npos - && **iter != "Needs adjacent wall") - || (*iter)->find("No access") != string::npos) - iter = ui_build_selector->errors.erase(iter); - else - ++iter; - } - } - - INTERPOSE_NEXT(render)(); - - if (!plannable) - return; - - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - - if (in_dummy_screen) - { - Screen::Pen pen(' ',COLOR_BLACK); - int y = dims.y1 + 1; - Screen::fillRect(pen, x, y, dims.menu_x2, y + 20); - - ++y; - - OutputString(COLOR_BROWN, x, y, - "Placeholder for legacy Quickfort. This screen is not required for DFHack native quickfort.", - true, left_margin); - OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin); - return; - } - - int y = 23; - - if (is_automaterial_managed(ui_build_selector->building_type, - ui_build_selector->building_subtype)) - { - // avoid conflict with the automaterial plugin UI - y = 36; - } - - if (show_help) - { - OutputString(COLOR_BROWN, x, y, "Note: "); - OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin); - } - - OutputHotkeyString(x, y, "Planning Mode", interface_key::CUSTOM_SHIFT_P); - OutputString(COLOR_WHITE, x, y, ": "); - if (quickfort_mode) - OutputString(COLOR_YELLOW, x, y, "Quickfort", true, left_margin); - else if (all_enabled) - OutputString(COLOR_YELLOW, x, y, "All", true, left_margin); - else if (planmode_enabled[key]) - OutputString(COLOR_GREEN, x, y, "On", true, left_margin); - else - OutputString(COLOR_GREY, x, y, "Off", true, left_margin); - OutputHotkeyString(x, y, "Global Settings", interface_key::CUSTOM_SHIFT_G, - true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - - if (!is_planmode_enabled(key)) - return; - - y += 2; - std::string title = - stl_sprintf("Filter for Item %d of %d:", - filter_count - filter_idx, filter_count); - OutputString(COLOR_WHITE, x, y, title.c_str(), true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, get_item_label(key, filter_idx).c_str(), true, left_margin); - - if (item_can_be_improved(key, filter_idx)) - { - OutputHotkeyString(x, y, "Min Quality: ", "QW", false, 0, COLOR_WHITE, COLOR_LIGHTRED); - OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin); - - OutputHotkeyString(x, y, "Max Quality: ", "AS", false, 0, COLOR_WHITE, COLOR_LIGHTRED); - OutputString(COLOR_BROWN, x, y, filter->getMaxQuality(), true, left_margin); - - OutputToggleString(x, y, "Decorated Only", interface_key::CUSTOM_SHIFT_D, - filter->getDecoratedOnly(), true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } - - OutputHotkeyString(x, y, "Material Filter:", interface_key::CUSTOM_SHIFT_M, true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - auto filter_descriptions = filter->getMaterials(); - for (auto it = filter_descriptions.begin(); - it != filter_descriptions.end(); ++it) - OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin); - - y += 2; - if (hasPrevFilter()) - OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (hasNextFilter()) - OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } -}; - -BuildingTypeKey buildingplan_place_hook::key; -std::vector::reverse_iterator buildingplan_place_hook::filter_rbegin; -std::vector::reverse_iterator buildingplan_place_hook::filter_rend; -std::vector::reverse_iterator buildingplan_place_hook::filter; -int buildingplan_place_hook::filter_count; -int buildingplan_place_hook::filter_idx; - -struct buildingplan_room_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - std::vector getNoblePositionOfSelectedBuildingOwner() - { - std::vector np; - if (plotinfo->main.mode != df::ui_sidebar_mode::QueryBuilding || - !world->selected_building || - !world->selected_building->owner) - { - return np; - } - - switch (world->selected_building->getType()) - { - case building_type::Bed: - case building_type::Chair: - case building_type::Table: - break; - default: - return np; - } - - return getUniqueNoblePositions(world->selected_building->owner); - } - - bool isInNobleRoomQueryMode() - { - if (getNoblePositionOfSelectedBuildingOwner().size() > 0) - return canReserveRoom(world->selected_building); - else - return false; - } - - bool handleInput(set *input) - { - if (!isInNobleRoomQueryMode()) - return false; - - if (Gui::inRenameBuilding()) - return false; - auto np = getNoblePositionOfSelectedBuildingOwner(); - df::interface_key last_token = get_string_key(input); - if (last_token >= Screen::charToKey('1') - && last_token <= Screen::charToKey('9')) - { - size_t index = last_token - Screen::charToKey('1'); - if (index >= np.size()) - return false; - roomMonitor.toggleRoomForPosition(world->selected_building->id, np.at(index).position->code); - return true; - } - - return false; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (!isInNobleRoomQueryMode()) - return; - - auto np = getNoblePositionOfSelectedBuildingOwner(); - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 24; - OutputString(COLOR_BROWN, x, y, "DFHack", true, left_margin); - OutputString(COLOR_WHITE, x, y, "Auto-allocate to:", true, left_margin); - for (size_t i = 0; i < np.size() && i < 9; i++) - { - bool enabled = - roomMonitor.getReservedNobleCode(world->selected_building->id) - == np[i].position->code; - OutputToggleString(x, y, np[i].position->name[0].c_str(), - int_to_string(i+1).c_str(), enabled, true, left_margin); - } - } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, render); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, render); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, render); - -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -static bool setSetting(std::string name, bool value); - -static bool isTrue(std::string val) -{ - val = toLower(val); - return val == "on" || val == "true" || val == "y" || val == "yes" - || val == "1"; -} - -static command_result buildingplan_cmd(color_ostream &out, vector & parameters) -{ - if (parameters.empty()) - return CR_OK; - - std::string cmd = toLower(parameters[0]); - - if (cmd.size() >= 1 && cmd[0] == 'v') - { - out.print("buildingplan version: %s\n", PLUGIN_VERSION); - } - else if (parameters.size() >= 2 && cmd == "debug") - { - show_debugging = isTrue(parameters[1]); - out.print("buildingplan debugging: %s\n", - show_debugging ? "enabled" : "disabled"); - } - else if (cmd == "set") - { - if (!is_enabled) - { - out.printerr( - "ERROR: buildingplan must be enabled before you can" - " read or set buildingplan global settings."); - return CR_FAILURE; - } - - if (!DFHack::Core::getInstance().isMapLoaded()) - { - out.printerr( - "ERROR: A map must be loaded before you can read or set" - "buildingplan global settings. Try adding your" - "'buildingplan set' commands to the onMapLoad.init file.\n"); - return CR_FAILURE; - } - - if (parameters.size() == 1) - { - // display current settings - out.print("active settings:\n"); - - out.print(" all_enabled = %s\n", all_enabled ? "true" : "false"); - for (auto & setting : planner.getGlobalSettings()) - { - out.print(" %s = %s\n", setting.first.c_str(), - setting.second ? "true" : "false"); - } - - out.print(" quickfort_mode = %s\n", - quickfort_mode ? "true" : "false"); - } - else if (parameters.size() == 3) - { - // set a setting - std::string setting = toLower(parameters[1]); - bool val = isTrue(parameters[2]); - if (!setSetting(setting, val)) - { - out.printerr("ERROR: invalid parameter: '%s'\n", - parameters[1].c_str()); - } - } - else - { - out.printerr("ERROR: invalid syntax\n"); - } - } - - return CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (!gps) - return CR_FAILURE; - - if (enable != is_enabled) - { - if (DFHack::Core::getInstance().isMapLoaded()) - planner.reset(); - - if (!INTERPOSE_HOOK(buildingplan_query_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_place_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_room_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_query_hook, render).apply(enable) || - !INTERPOSE_HOOK(buildingplan_place_hook, render).apply(enable) || - !INTERPOSE_HOOK(buildingplan_room_hook, render).apply(enable)) - return CR_FAILURE; - - is_enabled = enable; - } - - return CR_OK; -} - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - commands.push_back( - PluginCommand("buildingplan", - "Plan building construction before you have materials.", - buildingplan_cmd)); - - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - buildingplan_place_hook::invalidateStatics(); - buildingplan_query_hook::invalidateStatics(); - planner.reset(); - roomMonitor.reset(out); - break; - default: - break; - } - - return CR_OK; -} - -static bool is_paused() -{ - return World::ReadPauseState() || - plotinfo->main.mode > df::ui_sidebar_mode::Squads || - !strict_virtual_cast(Gui::getCurViewscreen(true)); -} - -static bool cycle_requested = false; - -#define DAY_TICKS 1200 -DFhackCExport command_result plugin_onupdate(color_ostream &) -{ - if (Maps::IsValid() && !is_paused() - && (cycle_requested || world->frame_counter % (DAY_TICKS/2) == 0)) - { - planner.doCycle(); - roomMonitor.doCycle(); - cycle_requested = false; - } - - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown(color_ostream &) -{ - return CR_OK; -} - -// Lua API section - -static bool isPlanModeEnabled(df::building_type type, - int16_t subtype, - int32_t custom) { - return is_planmode_enabled(toBuildingTypeKey(type, subtype, custom)); -} - -static bool isPlannableBuilding(df::building_type type, - int16_t subtype, - int32_t custom) { - return planner.isPlannableBuilding( - toBuildingTypeKey(type, subtype, custom)); -} - -static bool isPlannedBuilding(df::building *bld) { - return !!planner.getPlannedBuilding(bld); -} - -static void addPlannedBuilding(df::building *bld) { - planner.addPlannedBuilding(bld); -} - -static void doCycle() { - planner.doCycle(); -} - -static void scheduleCycle() { - cycle_requested = true; -} - -static bool setSetting(std::string name, bool value) { - if (name == "quickfort_mode") - { - debug("setting quickfort_mode %d -> %d", quickfort_mode, value); - quickfort_mode = value; - return true; - } - if (name == "all_enabled") - { - debug("setting all_enabled %d -> %d", all_enabled, value); - all_enabled = value; - return true; - } - return planner.setGlobalSetting(name, value); -} - -DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(isPlanModeEnabled), - DFHACK_LUA_FUNCTION(isPlannableBuilding), - DFHACK_LUA_FUNCTION(isPlannedBuilding), - DFHACK_LUA_FUNCTION(addPlannedBuilding), - DFHACK_LUA_FUNCTION(doCycle), - DFHACK_LUA_FUNCTION(scheduleCycle), - DFHACK_LUA_FUNCTION(setSetting), - DFHACK_LUA_END -}; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h deleted file mode 100644 index e906ef1a7..000000000 --- a/plugins/buildingplan/buildingplan.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "buildingplan-planner.h" -#include "buildingplan-rooms.h" - -void debug(const char *fmt, ...) Wformat(printf,1,2); - -extern bool show_debugging; From 0faa160eaac6ff244ed5e5058f9657e01e028784 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Feb 2023 18:45:26 -0800 Subject: [PATCH 055/126] split buildingplan into a project --- plugins/CMakeLists.txt | 2 +- plugins/buildingplan/CMakeLists.txt | 10 +- plugins/{ => buildingplan}/buildingplan.cpp | 343 ++------------------ plugins/buildingplan/buildingplan.h | 28 ++ plugins/buildingplan/buildingplan_cycle.cpp | 275 ++++++++++++++++ plugins/buildingplan/itemfilter.cpp | 0 plugins/buildingplan/itemfilter.h | 1 + plugins/buildingplan/plannedbuilding.cpp | 36 ++ plugins/buildingplan/plannedbuilding.h | 25 ++ 9 files changed, 395 insertions(+), 325 deletions(-) rename plugins/{ => buildingplan}/buildingplan.cpp (55%) create mode 100644 plugins/buildingplan/buildingplan.h create mode 100644 plugins/buildingplan/buildingplan_cycle.cpp create mode 100644 plugins/buildingplan/itemfilter.cpp create mode 100644 plugins/buildingplan/itemfilter.h create mode 100644 plugins/buildingplan/plannedbuilding.cpp create mode 100644 plugins/buildingplan/plannedbuilding.h diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7940c3994..82722f16a 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -89,7 +89,7 @@ dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) #dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) -dfhack_plugin(buildingplan buildingplan.cpp LINK_LIBRARIES lua) +add_subdirectory(buildingplan) #dfhack_plugin(changeitem changeitem.cpp) dfhack_plugin(changelayer changelayer.cpp) dfhack_plugin(changevein changevein.cpp) diff --git a/plugins/buildingplan/CMakeLists.txt b/plugins/buildingplan/CMakeLists.txt index 1d34b169a..85475edaa 100644 --- a/plugins/buildingplan/CMakeLists.txt +++ b/plugins/buildingplan/CMakeLists.txt @@ -2,10 +2,12 @@ project(buildingplan) set(COMMON_HDRS buildingplan.h - buildingplan-planner.h - buildingplan-rooms.h + itemfilter.h + plannedbuilding.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) -dfhack_plugin(buildingplan buildingplan.cpp buildingplan-planner.cpp - buildingplan-rooms.cpp ${COMMON_HDRS} LINK_LIBRARIES lua) +dfhack_plugin(buildingplan + buildingplan.cpp buildingplan_cycle.cpp itemfilter.cpp plannedbuilding.cpp + ${COMMON_HDRS} + LINK_LIBRARIES lua) diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp similarity index 55% rename from plugins/buildingplan.cpp rename to plugins/buildingplan/buildingplan.cpp index a76e81d4a..3d26a7c24 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -1,28 +1,17 @@ -#include "Core.h" +#include "plannedbuilding.h" +#include "buildingplan.h" + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" -#include "modules/Items.h" -#include "modules/Job.h" -#include "modules/Materials.h" -#include "modules/Persistence.h" #include "modules/World.h" -#include "df/building.h" -#include "df/building_design.h" #include "df/item.h" #include "df/job_item.h" #include "df/world.h" -#include -#include -#include -#include - using std::map; -using std::pair; -using std::deque; using std::string; using std::unordered_map; using std::vector; @@ -40,72 +29,29 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; -static const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; - -enum ConfigValues { - CONFIG_BLOCKS = 1, - CONFIG_BOULDERS = 2, - CONFIG_LOGS = 3, - CONFIG_BARS = 4, -}; - -enum BuildingConfigValues { - BLD_CONFIG_ID = 0, -}; +const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; -static int get_config_val(PersistentDataItem &c, int index) { +int get_config_val(PersistentDataItem &c, int index) { if (!c.isValid()) return -1; return c.ival(index); } -static bool get_config_bool(PersistentDataItem &c, int index) { +bool get_config_bool(PersistentDataItem &c, int index) { return get_config_val(c, index) == 1; } -static void set_config_val(PersistentDataItem &c, int index, int value) { +void set_config_val(PersistentDataItem &c, int index, int value) { if (c.isValid()) c.ival(index) = value; } -static void set_config_bool(PersistentDataItem &c, int index, bool value) { +void set_config_bool(PersistentDataItem &c, int index, bool value) { set_config_val(c, index, value ? 1 : 0); } -class PlannedBuilding { -public: - const df::building::key_field_type id; - - PlannedBuilding(color_ostream &out, df::building *building) : id(building->id) { - DEBUG(status,out).print("creating persistent data for building %d\n", id); - bld_config = DFHack::World::AddPersistentData(BLD_CONFIG_KEY); - set_config_val(bld_config, BLD_CONFIG_ID, id); - } - - PlannedBuilding(DFHack::PersistentDataItem &bld_config) - : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } - - void remove(color_ostream &out); - - // Ensure the building still exists and is in a valid state. It can disappear - // for lots of reasons, such as running the game with the buildingplan plugin - // disabled, manually removing the building, modifying it via the API, etc. - df::building * getBuildingIfValidOrRemoveIfNot(color_ostream &out) { - auto bld = df::building::find(id); - bool valid = bld && bld->getBuildStage() == 0; - if (!valid) { - remove(out); - return NULL; - } - return bld; - } - -private: - DFHack::PersistentDataItem bld_config; -}; - static PersistentDataItem config; // building id -> PlannedBuilding unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) -map>>> tasks; +Tasks tasks; // note that this just removes the PlannedBuilding. the tasks will get dropped // as we discover them in the tasks queues and they fail to be found in planned_buildings. @@ -115,7 +61,7 @@ map>>> tasks; // no chance of duplicate tasks getting added to the tasks queues. void PlannedBuilding::remove(color_ostream &out) { DEBUG(status,out).print("removing persistent data for building %d\n", id); - DFHack::World::DeletePersistentData(config); + World::DeletePersistentData(config); if (planned_buildings.count(id) > 0) planned_buildings.erase(id); } @@ -124,7 +70,9 @@ static const int32_t CYCLE_TICKS = 600; // twice per game day static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle static command_result do_command(color_ostream &out, vector ¶meters); -static void do_cycle(color_ostream &out); +void buildingplan_cycle(color_ostream &out, Tasks &tasks, + unordered_map &planned_buildings); + static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { @@ -186,7 +134,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (event == DFHack::SC_WORLD_UNLOADED) { + if (event == SC_WORLD_UNLOADED) { DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); planned_buildings.clear(); tasks.clear(); @@ -196,6 +144,14 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan static bool cycle_requested = false; +static void do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; + cycle_requested = false; + + buildingplan_cycle(out, tasks, planned_buildings); +} + DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (!Core::getInstance().isWorldLoaded()) return CR_OK; @@ -249,259 +205,6 @@ static command_result do_command(color_ostream &out, vector ¶meters) return show_help ? CR_WRONG_USAGE : CR_OK; } -///////////////////////////////////////////////////// -// cycle logic -// - -struct BadFlags { - uint32_t whole; - - BadFlags() { - df::item_flags flags; - #define F(x) flags.bits.x = true; - F(dump); F(forbid); F(garbage_collect); - F(hostile); F(on_fire); F(rotten); F(trader); - F(in_building); F(construction); F(in_job); - F(owned); F(in_chest); F(removed); F(encased); - F(spider_web); - #undef F - whole = flags.whole; - } -}; - -static bool itemPassesScreen(df::item * item) { - static const BadFlags bad_flags; - return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile(); -} - -static bool matchesFilters(df::item * item, df::job_item * job_item) { - // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) - return false; - - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) - return false; - - if (job_item->flags2.bits.building_material && !item->isBuildMat()) - return false; - - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) - return false; - - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) - return false; - - return DFHack::Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) - && DFHack::Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), - item->getType()); -} - -static bool isJobReady(color_ostream &out, df::job * job) { - int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } - if (needed_items) { - DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); - return false; - } - return true; -} - -static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { - // we want the items in the opposite order of the filters - return a->job_item_idx > b->job_item_idx; -} - -// this function does not remove the job_items since their quantity fields are -// now all at 0, so there is no risk of having extra items attached. we don't -// remove them to keep the "finalize with buildingplan active" path as similar -// as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(color_ostream &out, df::building * bld) { - DEBUG(cycle,out).print("finalizing building %d\n", bld->id); - auto job = bld->jobs[0]; - - // sort the items so they get added to the structure in the correct order - std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); - - // derive the material properties of the building and job from the first - // applicable item. if any boulders are involved, it makes the whole - // structure "rough". - bool rough = false; - for (auto attached_item : job->items) { - df::item *item = attached_item->item; - rough = rough || item->getType() == df::item_type::BOULDER; - if (bld->mat_type == -1) { - bld->mat_type = item->getMaterial(); - job->mat_type = bld->mat_type; - } - if (bld->mat_index == -1) { - bld->mat_index = item->getMaterialIndex(); - job->mat_index = bld->mat_index; - } - } - - if (bld->needsDesign()) { - auto act = (df::building_actual *)bld; - if (!act->design) - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; - } - - // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); -} - -static df::building * popInvalidTasks(color_ostream &out, deque> & task_queue) { - while (!task_queue.empty()) { - auto & task = task_queue.front(); - auto id = task.first; - if (planned_buildings.count(id) > 0) { - auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); - if (bld && bld->jobs[0]->job_items[task.second]->quantity) - return bld; - } - DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); - task_queue.pop_front(); - } - return NULL; -} - -static void doVector(color_ostream &out, df::job_item_vector_id vector_id, - map>> & buckets) { - auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); - auto item_vector = df::global::world->items.other[other_id]; - DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", - item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - buckets.size()); - for (auto item_it = item_vector.rbegin(); - item_it != item_vector.rend(); - ++item_it) { - auto item = *item_it; - if (!itemPassesScreen(item)) - continue; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { - auto & task_queue = bucket_it->second; - auto bld = popInvalidTasks(out, task_queue); - if (!bld) { - DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; - } - auto & task = task_queue.front(); - auto id = task.first; - auto job = bld->jobs[0]; - auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx]) - && DFHack::Job::attachJobItem(job, item, - df::job_item_ref::Hauled, filter_idx)) - { - MaterialInfo material; - material.decode(item); - ItemTypeInfo item_type; - item_type.decode(item); - DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", - material.toString().c_str(), - item_type.toString().c_str(), - filter_idx, - ENUM_KEY_STR(building_type, bld->getType()).c_str(), - id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); - // keep quantity aligned with the actual number of remaining - // items so if buildingplan is turned off, the building will - // be completed with the correct number of items. - --job->job_items[filter_idx]->quantity; - task_queue.pop_front(); - if (isJobReady(out, job)) { - finalizeBuilding(out, bld); - planned_buildings.at(id).remove(out); - } - if (task_queue.empty()) { - DEBUG(cycle,out).print( - "removing empty item bucket: %s/%s; %zu left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - buckets.erase(bucket_it); - } - // we found a home for this item; no need to look further - break; - } - ++bucket_it; - } - if (buckets.empty()) - break; - } -} - -struct VectorsToScanLast { - std::vector vectors; - VectorsToScanLast() { - // order is important here. we want to match boulders before wood and - // everything before bars. blocks are not listed here since we'll have - // already scanned them when we did the first pass through the buckets. - vectors.push_back(df::job_item_vector_id::BOULDER); - vectors.push_back(df::job_item_vector_id::WOOD); - vectors.push_back(df::job_item_vector_id::BAR); - } -}; - -static void do_cycle(color_ostream &out) { - static const VectorsToScanLast vectors_to_scan_last; - - // mark that we have recently run - cycle_timestamp = world->frame_counter; - cycle_requested = false; - - DEBUG(cycle,out).print("running %s cycle for %zu registered buildings\n", - plugin_name, planned_buildings.size()); - - for (auto it = tasks.begin(); it != tasks.end(); ) { - auto vector_id = it->first; - // we could make this a set, but it's only three elements - if (std::find(vectors_to_scan_last.vectors.begin(), - vectors_to_scan_last.vectors.end(), - vector_id) != vectors_to_scan_last.vectors.end()) { - ++it; - continue; - } - - auto & buckets = it->second; - doVector(out, vector_id, buckets); - if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - it = tasks.erase(it); - } - else - ++it; - } - for (auto vector_id : vectors_to_scan_last.vectors) { - if (tasks.count(vector_id) == 0) - continue; - auto & buckets = tasks[vector_id]; - doVector(out, vector_id, buckets); - if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - tasks.erase(vector_id); - } - } - DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", - planned_buildings.size()); -} - ///////////////////////////////////////////////////// // Lua API // core will already be suspended when coming in through here @@ -617,7 +320,7 @@ static void printStatus(color_ostream &out) { int32_t total = 0; for (auto &buckets : tasks) { for (auto &bucket_queue : buckets.second) { - deque> &tqueue = bucket_queue.second; + Bucket &tqueue = bucket_queue.second; for (auto it = tqueue.begin(); it != tqueue.end();) { auto & task = *it; auto id = task.first; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h new file mode 100644 index 000000000..ac6d3a9a6 --- /dev/null +++ b/plugins/buildingplan/buildingplan.h @@ -0,0 +1,28 @@ +#pragma once + +#include "modules/Persistence.h" + +#include "df/job_item_vector_id.h" + +#include + +typedef std::deque> Bucket; +typedef std::map> Tasks; + +extern const std::string BLD_CONFIG_KEY; + +enum ConfigValues { + CONFIG_BLOCKS = 1, + CONFIG_BOULDERS = 2, + CONFIG_LOGS = 3, + CONFIG_BARS = 4, +}; + +enum BuildingConfigValues { + BLD_CONFIG_ID = 0, +}; + +int get_config_val(DFHack::PersistentDataItem &c, int index); +bool get_config_bool(DFHack::PersistentDataItem &c, int index); +void set_config_val(DFHack::PersistentDataItem &c, int index, int value); +void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp new file mode 100644 index 000000000..875cd432f --- /dev/null +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -0,0 +1,275 @@ +#include "plannedbuilding.h" +#include "buildingplan.h" + +#include "Debug.h" + +#include "modules/Items.h" +#include "modules/Job.h" +#include "modules/Materials.h" + +#include "df/building_design.h" +#include "df/item.h" +#include "df/job.h" +#include "df/job_item.h" +#include "df/world.h" + +#include + +using std::map; +using std::string; +using std::unordered_map; + +namespace DFHack { + DBG_EXTERN(buildingplan, cycle); +} + +using namespace DFHack; + +struct BadFlags { + uint32_t whole; + + BadFlags() { + df::item_flags flags; + #define F(x) flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(in_job); + F(owned); F(in_chest); F(removed); F(encased); + F(spider_web); + #undef F + whole = flags.whole; + } +}; + +static bool itemPassesScreen(df::item * item) { + static const BadFlags bad_flags; + return !(item->flags.whole & bad_flags.whole) + && !item->isAssignedToStockpile(); +} + +static bool matchesFilters(df::item * item, df::job_item * job_item) { + // check the properties that are not checked by Job::isSuitableItem() + if (job_item->item_type > -1 && job_item->item_type != item->getType()) + return false; + + if (job_item->item_subtype > -1 && + job_item->item_subtype != item->getSubtype()) + return false; + + if (job_item->flags2.bits.building_material && !item->isBuildMat()) + return false; + + if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) + return false; + + if (job_item->has_tool_use > df::tool_uses::NONE + && !item->hasToolUse(job_item->has_tool_use)) + return false; + + return Job::isSuitableItem( + job_item, item->getType(), item->getSubtype()) + && Job::isSuitableMaterial( + job_item, item->getMaterial(), item->getMaterialIndex(), + item->getType()); +} + +static bool isJobReady(color_ostream &out, df::job * job) { + int needed_items = 0; + for (auto job_item : job->job_items) { needed_items += job_item->quantity; } + if (needed_items) { + DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); + return false; + } + return true; +} + +static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { + // we want the items in the opposite order of the filters + return a->job_item_idx > b->job_item_idx; +} + +// this function does not remove the job_items since their quantity fields are +// now all at 0, so there is no risk of having extra items attached. we don't +// remove them to keep the "finalize with buildingplan active" path as similar +// as possible to the "finalize with buildingplan disabled" path. +static void finalizeBuilding(color_ostream &out, df::building * bld) { + DEBUG(cycle,out).print("finalizing building %d\n", bld->id); + auto job = bld->jobs[0]; + + // sort the items so they get added to the structure in the correct order + std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); + + // derive the material properties of the building and job from the first + // applicable item. if any boulders are involved, it makes the whole + // structure "rough". + bool rough = false; + for (auto attached_item : job->items) { + df::item *item = attached_item->item; + rough = rough || item->getType() == df::item_type::BOULDER; + if (bld->mat_type == -1) { + bld->mat_type = item->getMaterial(); + job->mat_type = bld->mat_type; + } + if (bld->mat_index == -1) { + bld->mat_index = item->getMaterialIndex(); + job->mat_index = bld->mat_index; + } + } + + if (bld->needsDesign()) { + auto act = (df::building_actual *)bld; + if (!act->design) + act->design = new df::building_design(); + act->design->flags.bits.rough = rough; + } + + // we're good to go! + job->flags.bits.suspend = false; + Job::checkBuildingsNow(); +} + +static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, + unordered_map &planned_buildings) { + while (!task_queue.empty()) { + auto & task = task_queue.front(); + auto id = task.first; + if (planned_buildings.count(id) > 0) { + auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); + if (bld && bld->jobs[0]->job_items[task.second]->quantity) + return bld; + } + DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); + task_queue.pop_front(); + } + return NULL; +} + +static void doVector(color_ostream &out, df::job_item_vector_id vector_id, + map &buckets, + unordered_map &planned_buildings) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + auto item_vector = df::global::world->items.other[other_id]; + DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", + item_vector.size(), + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + buckets.size()); + for (auto item_it = item_vector.rbegin(); + item_it != item_vector.rend(); + ++item_it) { + auto item = *item_it; + if (!itemPassesScreen(item)) + continue; + for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { + auto & task_queue = bucket_it->second; + auto bld = popInvalidTasks(out, task_queue, planned_buildings); + if (!bld) { + DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + bucket_it = buckets.erase(bucket_it); + continue; + } + auto & task = task_queue.front(); + auto id = task.first; + auto job = bld->jobs[0]; + auto filter_idx = task.second; + if (matchesFilters(item, job->job_items[filter_idx]) + && Job::attachJobItem(job, item, + df::job_item_ref::Hauled, filter_idx)) + { + MaterialInfo material; + material.decode(item); + ItemTypeInfo item_type; + item_type.decode(item); + DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", + material.toString().c_str(), + item_type.toString().c_str(), + filter_idx, + ENUM_KEY_STR(building_type, bld->getType()).c_str(), + id, + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str()); + // keep quantity aligned with the actual number of remaining + // items so if buildingplan is turned off, the building will + // be completed with the correct number of items. + --job->job_items[filter_idx]->quantity; + task_queue.pop_front(); + if (isJobReady(out, job)) { + finalizeBuilding(out, bld); + planned_buildings.at(id).remove(out); + } + if (task_queue.empty()) { + DEBUG(cycle,out).print( + "removing empty item bucket: %s/%s; %zu left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + buckets.erase(bucket_it); + } + // we found a home for this item; no need to look further + break; + } + ++bucket_it; + } + if (buckets.empty()) + break; + } +} + +struct VectorsToScanLast { + std::vector vectors; + VectorsToScanLast() { + // order is important here. we want to match boulders before wood and + // everything before bars. blocks are not listed here since we'll have + // already scanned them when we did the first pass through the buckets. + vectors.push_back(df::job_item_vector_id::BOULDER); + vectors.push_back(df::job_item_vector_id::WOOD); + vectors.push_back(df::job_item_vector_id::BAR); + } +}; + +void buildingplan_cycle(color_ostream &out, Tasks &tasks, + unordered_map &planned_buildings) { + static const VectorsToScanLast vectors_to_scan_last; + + DEBUG(cycle,out).print( + "running buildingplan cycle for %zu registered buildings\n", + planned_buildings.size()); + + for (auto it = tasks.begin(); it != tasks.end(); ) { + auto vector_id = it->first; + // we could make this a set, but it's only three elements + if (std::find(vectors_to_scan_last.vectors.begin(), + vectors_to_scan_last.vectors.end(), + vector_id) != vectors_to_scan_last.vectors.end()) { + ++it; + continue; + } + + auto & buckets = it->second; + doVector(out, vector_id, buckets, planned_buildings); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + it = tasks.erase(it); + } + else + ++it; + } + for (auto vector_id : vectors_to_scan_last.vectors) { + if (tasks.count(vector_id) == 0) + continue; + auto & buckets = tasks[vector_id]; + doVector(out, vector_id, buckets, planned_buildings); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + tasks.erase(vector_id); + } + } + DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", + planned_buildings.size()); +} diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h new file mode 100644 index 000000000..6f70f09be --- /dev/null +++ b/plugins/buildingplan/itemfilter.h @@ -0,0 +1 @@ +#pragma once diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp new file mode 100644 index 000000000..c03f56161 --- /dev/null +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -0,0 +1,36 @@ +#include "plannedbuilding.h" +#include "buildingplan.h" + +#include "Debug.h" + +#include "modules/World.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); + DBG_EXTERN(buildingplan, cycle); +} + +using namespace DFHack; + +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building) + : id(building->id) { + DEBUG(status,out).print("creating persistent data for building %d\n", id); + bld_config = World::AddPersistentData(BLD_CONFIG_KEY); + set_config_val(bld_config, BLD_CONFIG_ID, id); +} + +PlannedBuilding::PlannedBuilding(PersistentDataItem &bld_config) + : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } + +// Ensure the building still exists and is in a valid state. It can disappear +// for lots of reasons, such as running the game with the buildingplan plugin +// disabled, manually removing the building, modifying it via the API, etc. +df::building * PlannedBuilding::getBuildingIfValidOrRemoveIfNot(color_ostream &out) { + auto bld = df::building::find(id); + bool valid = bld && bld->getBuildStage() == 0; + if (!valid) { + remove(out); + return NULL; + } + return bld; +} diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h new file mode 100644 index 000000000..9f0273b82 --- /dev/null +++ b/plugins/buildingplan/plannedbuilding.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Core.h" + +#include "modules/Persistence.h" + +#include "df/building.h" + +class PlannedBuilding { +public: + const df::building::key_field_type id; + + PlannedBuilding(DFHack::color_ostream &out, df::building *building); + PlannedBuilding(DFHack::PersistentDataItem &bld_config); + + void remove(DFHack::color_ostream &out); + + // Ensure the building still exists and is in a valid state. It can disappear + // for lots of reasons, such as running the game with the buildingplan plugin + // disabled, manually removing the building, modifying it via the API, etc. + df::building * getBuildingIfValidOrRemoveIfNot(DFHack::color_ostream &out); + +private: + DFHack::PersistentDataItem bld_config; +}; From e5c3a2b519bda50f76c1d99f27f8bfed55af79c0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Feb 2023 16:54:38 -0800 Subject: [PATCH 056/126] dynamically count available materials when placing --- plugins/buildingplan/buildingplan.cpp | 157 ++++++++++++++++---- plugins/buildingplan/buildingplan.h | 4 + plugins/buildingplan/buildingplan_cycle.cpp | 5 +- plugins/lua/buildingplan.lua | 112 ++++++++++---- 4 files changed, 214 insertions(+), 64 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 3d26a7c24..2c5d96b94 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -47,11 +47,37 @@ void set_config_bool(PersistentDataItem &c, int index, bool value) { set_config_val(c, index, value ? 1 : 0); } +// building type, subtype, custom +typedef std::tuple BuildingTypeKey; + +// rotates a size_t value left by count bits +// assumes count is not 0 or >= size_t_bits +// replace this with std::rotl when we move to C++20 +static std::size_t rotl_size_t(size_t val, uint32_t count) +{ + static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); + return val << count | val >> (size_t_bits - count); +} + +struct BuildingTypeKeyHash { + std::size_t operator() (const BuildingTypeKey & key) const { + // cast first param to appease gcc-4.8, which is missing the enum + // specializations for std::hash + std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); + std::size_t h2 = std::hash()(std::get<1>(key)); + std::size_t h3 = std::hash()(std::get<2>(key)); + + return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); + } +}; + static PersistentDataItem config; +// for use in counting available materials for the UI +static unordered_map, BuildingTypeKeyHash> job_item_repo; // building id -> PlannedBuilding -unordered_map planned_buildings; +static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) -Tasks tasks; +static Tasks tasks; // note that this just removes the PlannedBuilding. the tasks will get dropped // as we discover them in the tasks queues and they fail to be found in planned_buildings. @@ -61,7 +87,7 @@ Tasks tasks; // no chance of duplicate tasks getting added to the tasks queues. void PlannedBuilding::remove(color_ostream &out) { DEBUG(status,out).print("removing persistent data for building %d\n", id); - World::DeletePersistentData(config); + World::DeletePersistentData(bld_config); if (planned_buildings.count(id) > 0) planned_buildings.erase(id); } @@ -106,6 +132,31 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { return CR_OK; } +static void validate_config(color_ostream &out, bool verbose = false) { + if (get_config_bool(config, CONFIG_BLOCKS) + || get_config_bool(config, CONFIG_BOULDERS) + || get_config_bool(config, CONFIG_LOGS) + || get_config_bool(config, CONFIG_BARS)) + return; + + if (verbose) + out.printerr("all contruction materials disabled; resetting config\n"); + + set_config_bool(config, CONFIG_BLOCKS, true); + set_config_bool(config, CONFIG_BOULDERS, true); + set_config_bool(config, CONFIG_LOGS, true); + set_config_bool(config, CONFIG_BARS, false); +} + +static void clear_job_item_repo() { + for (auto &entry : job_item_repo) { + for (auto &jitem : entry.second) { + delete jitem; + } + } + job_item_repo.clear(); +} + DFhackCExport command_result plugin_load_data (color_ostream &out) { cycle_timestamp = 0; config = World::GetPersistentData(CONFIG_KEY); @@ -113,15 +164,13 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { if (!config.isValid()) { DEBUG(status,out).print("no config found in this save; initializing\n"); config = World::AddPersistentData(CONFIG_KEY); - set_config_bool(config, CONFIG_BLOCKS, true); - set_config_bool(config, CONFIG_BOULDERS, true); - set_config_bool(config, CONFIG_LOGS, true); - set_config_bool(config, CONFIG_BARS, false); } + validate_config(out); DEBUG(status,out).print("loading persisted state\n"); planned_buildings.clear(); tasks.clear(); + clear_job_item_repo(); vector building_configs; World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); @@ -138,30 +187,11 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); planned_buildings.clear(); tasks.clear(); + clear_job_item_repo(); } return CR_OK; } -static bool cycle_requested = false; - -static void do_cycle(color_ostream &out) { - // mark that we have recently run - cycle_timestamp = world->frame_counter; - cycle_requested = false; - - buildingplan_cycle(out, tasks, planned_buildings); -} - -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (!Core::getInstance().isWorldLoaded()) - return CR_OK; - - if (is_enabled && - (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) - do_cycle(out); - return CR_OK; -} - static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, int nargs = 0, int nres = 0, Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, @@ -182,6 +212,27 @@ static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, std::forward(res_lambda)); } +static bool cycle_requested = false; + +static void do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; + cycle_requested = false; + + buildingplan_cycle(out, tasks, planned_buildings); + call_buildingplan_lua(&out, "reset_counts"); +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (!Core::getInstance().isWorldLoaded()) + return CR_OK; + + if (is_enabled && + (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) + do_cycle(out); + return CR_OK; +} + static command_result do_command(color_ostream &out, vector ¶meters) { CoreSuspender suspend; @@ -228,8 +279,7 @@ static string getBucket(const df::job_item & ji) { } // get a list of item vectors that we should search for matches -static vector getVectorIds(color_ostream &out, df::job_item *job_item) -{ +static vector getVectorIds(color_ostream &out, df::job_item *job_item) { std::vector ret; // if the filter already has the vector_id set to something specific, use it @@ -344,7 +394,7 @@ static void printStatus(color_ostream &out) { } } - out.print("Waiting for %d item(s) to be produced or %zd building(s):\n", + out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", total, planned_buildings.size()); for (auto &count : counts) out.print(" %3d %s\n", count.second, count.first.c_str()); @@ -365,6 +415,9 @@ static bool setSetting(color_ostream &out, string name, bool value) { out.printerr("unrecognized setting: '%s'\n", name.c_str()); return false; } + + validate_config(out, true); + call_buildingplan_lua(&out, "reset_counts"); return true; } @@ -412,7 +465,49 @@ static void scheduleCycle(color_ostream &out) { static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(status,out).print("entering countAvailableItems\n"); - return 10; + DEBUG(status,out).print( + "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + BuildingTypeKey key(type, subtype, custom); + auto &job_items = job_item_repo[key]; + if (index >= job_items.size()) { + for (int i = job_items.size(); i <= index; ++i) { + bool failed = false; + if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, + [&](lua_State *L) { + Lua::Push(L, type); + Lua::Push(L, subtype); + Lua::Push(L, custom); + Lua::Push(L, index+1); + }, + [&](lua_State *L) { + df::job_item *jitem = Lua::GetDFObject(L, -1); + DEBUG(status,out).print("retrieving job_item for index=%d: %p\n", + index, jitem); + if (!jitem) + failed = true; + else + job_items.emplace_back(jitem); + }) || failed) { + return 0; + } + } + } + + auto &jitem = job_items[index]; + auto vector_ids = getVectorIds(out, jitem); + + int count = 0; + for (auto vector_id : vector_ids) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + for (auto &item : df::global::world->items.other[other_id]) { + if (itemPassesScreen(item) && matchesFilters(item, jitem)) + ++count; + } + } + + DEBUG(status,out).print("found matches %d\n", count); + return count; } DFHACK_PLUGIN_LUA_FUNCTIONS { diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index ac6d3a9a6..0e7e288ac 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -2,6 +2,7 @@ #include "modules/Persistence.h" +#include "df/job_item.h" #include "df/job_item_vector_id.h" #include @@ -26,3 +27,6 @@ int get_config_val(DFHack::PersistentDataItem &c, int index); bool get_config_bool(DFHack::PersistentDataItem &c, int index); void set_config_val(DFHack::PersistentDataItem &c, int index, int value); void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); + +bool itemPassesScreen(df::item * item); +bool matchesFilters(df::item * item, df::job_item * job_item); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 875cd432f..6d5e4a405 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -10,7 +10,6 @@ #include "df/building_design.h" #include "df/item.h" #include "df/job.h" -#include "df/job_item.h" #include "df/world.h" #include @@ -41,13 +40,13 @@ struct BadFlags { } }; -static bool itemPassesScreen(df::item * item) { +bool itemPassesScreen(df::item * item) { static const BadFlags bad_flags; return !(item->flags.whole & bad_flags.whole) && !item->isAssignedToStockpile(); } -static bool matchesFilters(df::item * item, df::job_item * job_item) { +bool matchesFilters(df::item * item, df::job_item * job_item) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 5da03e38f..24ef90903 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -56,6 +56,19 @@ function get_num_filters(btype, subtype, custom) return filters and #filters or 0 end +function get_job_item(btype, subtype, custom, index) + local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) + if not filters or not filters[index] then return nil end + local obj = df.job_item:new() + obj:assign(filters[index]) + return obj +end + +local reset_counts_flag = false +function reset_counts() + reset_counts_flag = true +end + -------------------------------- -- Planner Overlay -- @@ -143,8 +156,32 @@ local function to_title_case(str) return str end -function get_item_line_text(idx) - local filter = get_cur_filters()[idx] +ItemLine = defclass(ItemLine, widgets.Panel) +ItemLine.ATTRS{ + idx=DEFAULT_NIL, +} + +function ItemLine:init() + self.frame.h = 1 + self.visible = function() return #get_cur_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={{text=function() return self:get_item_line_text() end}}, + }, + widgets.Label{ + frame={t=0, l=22}, + text='[filter][x]', + }, + } +end + +function ItemLine:reset() + self.desc = nil + self.available = nil +end + +local function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use then desc = to_title_case(df.tool_uses[filter.has_tool_use]) @@ -170,7 +207,10 @@ function get_item_line_text(idx) if desc == 'Trappart' then desc = 'Mechanism' end + return desc +end +local function get_quantity(filter) local quantity = filter.quantity or 1 local dimx, dimy, dimz = get_cur_area_dims() if quantity < 1 then @@ -178,34 +218,29 @@ function get_item_line_text(idx) else quantity = quantity * dimx * dimy * dimz end - desc = ('%d %s%s'):format(quantity, desc, quantity == 1 and '' or 's') + return quantity +end - local available = countAvailableItems(uibs.building_type, +function ItemLine:get_item_line_text() + local idx = self.idx + local filter = get_cur_filters()[idx] + local quantity = get_quantity(filter) + + self.desc = self.desc or get_desc(filter) + local line = ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') + + self.available = self.available or countAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) - local note = available >= quantity and + local note = self.available >= quantity and 'Can build now' or 'Will wait for item' - return ('%-21s%s%s'):format(desc:sub(1,21), (' '):rep(13), note) + return ('%-21s%s%s'):format(line:sub(1,21), (' '):rep(13), note) end -ItemLine = defclass(ItemLine, widgets.Panel) -ItemLine.ATTRS{ - idx=DEFAULT_NIL, -} - -function ItemLine:init() - self.frame.h = 1 - self.visible = function() return #get_cur_filters() >= self.idx end - self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text={{text=function() return get_item_line_text(self.idx) end}}, - }, - widgets.Label{ - frame={t=0, l=22}, - text='[filter][x]', - }, - } +function ItemLine:reduce_quantity() + if not self.available then return end + local filter = get_cur_filters()[self.idx] + self.available = math.max(0, self.available - get_quantity(filter)) end PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) @@ -226,10 +261,10 @@ function PlannerOverlay:init() text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, - ItemLine{frame={t=0, l=0}, idx=1}, - ItemLine{frame={t=2, l=0}, idx=2}, - ItemLine{frame={t=4, l=0}, idx=3}, - ItemLine{frame={t=6, l=0}, idx=4}, + ItemLine{view_id='item1', frame={t=0, l=0}, idx=1}, + ItemLine{view_id='item2', frame={t=2, l=0}, idx=2}, + ItemLine{view_id='item3', frame={t=4, l=0}, idx=3}, + ItemLine{view_id='item4', frame={t=6, l=0}, idx=4}, widgets.CycleHotkeyLabel{ view_id="stairs_top_subtype", frame={t=3, l=0}, @@ -268,13 +303,22 @@ function PlannerOverlay:init() } end -function PlannerOverlay:do_config() - dfhack.run_script('gui/buildingplan') +function PlannerOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_counts_flag = false end function PlannerOverlay:onInput(keys) if not is_plannable() then return false end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if uibs.selection_pos:isValid() then + uibs.selection_pos:clear() + return true + end + self:reset() return false end if PlannerOverlay.super.onInput(self, keys) then @@ -290,6 +334,10 @@ function PlannerOverlay:onInput(keys) if #get_cur_filters() == 0 then return false -- we don't add value; let the game place it end + self.subviews.item1:reduce_quantity() + self.subviews.item2:reduce_quantity() + self.subviews.item3:reduce_quantity() + self.subviews.item4:reduce_quantity() self:place_building() uibs.selection_pos:clear() return true @@ -315,6 +363,10 @@ local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) + if reset_counts_flag then + self:reset() + end + if not is_choosing_area() then return end local bounds = { From 18ad29dde4b09335730550b1f21d3b19f3504cc0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Feb 2023 19:10:42 -0800 Subject: [PATCH 057/126] show queue position --- plugins/buildingplan/buildingplan.cpp | 47 ++++++++- plugins/buildingplan/buildingplan.h | 1 + plugins/buildingplan/plannedbuilding.cpp | 62 +++++++++++- plugins/buildingplan/plannedbuilding.h | 6 +- plugins/lua/buildingplan.lua | 120 +++++++++++++++-------- 5 files changed, 189 insertions(+), 47 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 2c5d96b94..17db21cd2 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -175,7 +175,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); for (size_t idx = 0; idx < num_building_configs; ++idx) { - PlannedBuilding pb(building_configs[idx]); + PlannedBuilding pb(out, building_configs[idx]); registerPlannedBuilding(out, pb); } @@ -279,7 +279,7 @@ static string getBucket(const df::job_item & ji) { } // get a list of item vectors that we should search for matches -static vector getVectorIds(color_ostream &out, df::job_item *job_item) { +vector getVectorIds(color_ostream &out, df::job_item *job_item) { std::vector ret; // if the filter already has the vector_id set to something specific, use it @@ -310,6 +310,7 @@ static vector getVectorIds(color_ostream &out, df::job_i ret.push_back(df::job_item_vector_id::IN_PLAY); return ret; } + static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { df::building * bld = pb.getBuildingIfValidOrRemoveIfNot(out); if (!bld) @@ -385,7 +386,10 @@ static void printStatus(color_ostream &out) { auto *jitem = bld->jobs[0]->job_items[task.second]; int32_t quantity = jitem->quantity; if (quantity) { - string desc = toLower(ENUM_KEY_STR(item_type, jitem->item_type)); + string desc = "none"; + call_buildingplan_lua(&out, "get_desc", 1, 1, + [&](lua_State *L) { Lua::Push(L, jitem); }, + [&](lua_State *L) { desc = lua_tostring(L, -1); }); counts[desc] += quantity; total += quantity; } @@ -510,6 +514,42 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return count; } +static int getQueuePosition(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getQueuePosition\n"); + if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) + return 0; + + auto &job_items = bld->jobs[0]->job_items; + if (job_items.size() <= index) + return 0; + + PlannedBuilding &pb = planned_buildings.at(bld->id); + if (pb.vector_ids.size() <= index) + return 0; + + auto &job_item = job_items[index]; + + int min_pos = -1; + for (auto &vec_id : pb.vector_ids[index]) { + if (!tasks.count(vec_id)) + continue; + auto &buckets = tasks.at(vec_id); + string bucket_id = getBucket(*job_item); + if (!buckets.count(bucket_id)) + continue; + int bucket_pos = -1; + for (auto &task : buckets.at(bucket_id)) { + ++bucket_pos; + if (bld->id == task.first && index == task.second) + break; + } + if (bucket_pos++ >= 0) + min_pos = min_pos < 0 ? bucket_pos : std::min(min_pos, bucket_pos); + } + + return min_pos < 0 ? 0 : min_pos; +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(printStatus), DFHACK_LUA_FUNCTION(setSetting), @@ -519,5 +559,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_END }; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 0e7e288ac..7fe2478aa 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -28,5 +28,6 @@ bool get_config_bool(DFHack::PersistentDataItem &c, int index); void set_config_val(DFHack::PersistentDataItem &c, int index, int value); void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); +std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); bool matchesFilters(df::item * item, df::job_item * job_item); diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index c03f56161..f4f3564b7 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -2,25 +2,79 @@ #include "buildingplan.h" #include "Debug.h" +#include "MiscUtils.h" #include "modules/World.h" +#include "df/job.h" + namespace DFHack { DBG_EXTERN(buildingplan, status); - DBG_EXTERN(buildingplan, cycle); } +using std::string; +using std::vector; using namespace DFHack; +static vector> get_vector_ids(color_ostream &out, int bld_id) { + vector> ret; + + df::building *bld = df::building::find(bld_id); + + if (!bld || bld->jobs.size() != 1) + return ret; + + auto &job = bld->jobs[0]; + for (auto &jitem : job->job_items) { + ret.emplace_back(getVectorIds(out, jitem)); + } + return ret; +} + +static vector> deserialize(color_ostream &out, PersistentDataItem &bld_config) { + vector> ret; + + DEBUG(status,out).print("deserializing state for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), bld_config.val().c_str()); + + vector joined; + split_string(&joined, bld_config.val(), "|"); + for (auto &str : joined) { + vector lst; + split_string(&lst, str, ","); + vector ids; + for (auto &s : lst) + ids.emplace_back(df::job_item_vector_id(string_to_int(s))); + ret.emplace_back(ids); + } + + if (!ret.size()) + ret = get_vector_ids(out, get_config_val(bld_config, BLD_CONFIG_ID)); + + return ret; +} + +static string serialize(const vector> &vector_ids) { + vector joined; + for (auto &vec_list : vector_ids) { + joined.emplace_back(join_strings(",", vec_list)); + } + return join_strings("|", joined); +} + PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building) - : id(building->id) { + : id(building->id), vector_ids(get_vector_ids(out, id)) { DEBUG(status,out).print("creating persistent data for building %d\n", id); bld_config = World::AddPersistentData(BLD_CONFIG_KEY); set_config_val(bld_config, BLD_CONFIG_ID, id); + bld_config.val() = serialize(vector_ids); + DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); } -PlannedBuilding::PlannedBuilding(PersistentDataItem &bld_config) - : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } +PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) + : id(get_config_val(bld_config, BLD_CONFIG_ID)), + vector_ids(deserialize(out, bld_config)), + bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear // for lots of reasons, such as running the game with the buildingplan plugin diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h index 9f0273b82..592f0e4b3 100644 --- a/plugins/buildingplan/plannedbuilding.h +++ b/plugins/buildingplan/plannedbuilding.h @@ -5,13 +5,17 @@ #include "modules/Persistence.h" #include "df/building.h" +#include "df/job_item_vector_id.h" class PlannedBuilding { public: const df::building::key_field_type id; + // job_item idx -> list of vectors the task is linked to + const std::vector> vector_ids; + PlannedBuilding(DFHack::color_ostream &out, df::building *building); - PlannedBuilding(DFHack::PersistentDataItem &bld_config); + PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); void remove(DFHack::color_ostream &out); diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 24ef90903..376787952 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -70,7 +70,7 @@ function reset_counts() end -------------------------------- --- Planner Overlay +-- PlannerOverlay -- local uibs = df.global.buildreq @@ -181,7 +181,7 @@ function ItemLine:reset() self.available = nil end -local function get_desc(filter) +function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use then desc = to_title_case(df.tool_uses[filter.has_tool_use]) @@ -190,13 +190,15 @@ local function get_desc(filter) desc = to_title_case(df.item_type[filter.item_type]) end if filter.flags2 and filter.flags2.building_material then - desc = "Generic material"; + desc = 'Generic material'; if filter.flags2.fire_safe then - desc = "Fire-safe material"; + desc = 'Fire-safe material'; end if filter.flags2.magma_safe then - desc = "Magma-safe material"; + desc = 'Magma-safe material'; end + elseif filter.flags2 and filter.flags2.screw then + desc = 'Screw' elseif filter.vector_id then desc = to_title_case(df.job_item_vector_id[filter.vector_id]) end @@ -249,27 +251,30 @@ PlannerOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Building/Placement', frame={w=54, h=9}, - frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } function PlannerOverlay:init() self:addviews{ + widgets.Panel{ + frame={}, + frame_style=gui.MEDIUM_FRAME, + }, widgets.Label{ frame={}, auto_width=true, text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, - ItemLine{view_id='item1', frame={t=0, l=0}, idx=1}, - ItemLine{view_id='item2', frame={t=2, l=0}, idx=2}, - ItemLine{view_id='item3', frame={t=4, l=0}, idx=3}, - ItemLine{view_id='item4', frame={t=6, l=0}, idx=4}, + ItemLine{view_id='item1', frame={t=1, l=1, r=1}, idx=1}, + ItemLine{view_id='item2', frame={t=3, l=1, r=1}, idx=2}, + ItemLine{view_id='item3', frame={t=5, l=1, r=1}, idx=3}, + ItemLine{view_id='item4', frame={t=7, l=1, r=1}, idx=4}, widgets.CycleHotkeyLabel{ - view_id="stairs_top_subtype", - frame={t=3, l=0}, - key="CUSTOM_R", - label="Top Stair Type: ", + view_id='stairs_top_subtype', + frame={t=4, l=1}, + key='CUSTOM_R', + label='Top Stair Type: ', visible=is_stairs, options={ {label='Auto', value='auto'}, @@ -278,10 +283,10 @@ function PlannerOverlay:init() }, }, widgets.CycleHotkeyLabel { - view_id="stairs_bottom_subtype", - frame={t=4, l=0}, - key="CUSTOM_B", - label="Bottom Stair Type: ", + view_id='stairs_bottom_subtype', + frame={t=5, l=1}, + key='CUSTOM_B', + label='Bottom Stair Type: ', visible=is_stairs, options={ {label='Auto', value='auto'}, @@ -290,7 +295,7 @@ function PlannerOverlay:init() }, }, widgets.Label{ - frame={b=0, l=17}, + frame={b=1, l=17}, text={ 'Selected area: ', {text=function() @@ -300,6 +305,17 @@ function PlannerOverlay:init() }, visible=is_choosing_area, }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={b=0, l=1}, + key='CUSTOM_F', + label='Extra safety: ', + options={ + {label='None', value='none'}, + {label='Magma', value='magma'}, + {label='Fire', value='fire'}, + }, + }, } end @@ -473,12 +489,50 @@ function PlannerOverlay:place_building() scheduleCycle() end +-------------------------------- +-- InspectorOverlay +-- + +local function get_building_filters() + local bld = dfhack.gui.getSelectedBuilding() + return dfhack.buildings.getFiltersByType({}, + bld:getType(), bld:getSubtype(), bld:getCustomType()) +end + +InspectorLine = defclass(InspectorLine, widgets.Panel) +InspectorLine.ATTRS{ + idx=DEFAULT_NIL, +} + +function InspectorLine:init() + self.frame.h = 2 + self.visible = function() return #get_building_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={{text=function() return get_desc(get_building_filters()[self.idx]) end}}, + }, + widgets.Label{ + frame={t=1, l=2}, + text={{text=self:callback('get_status_line')}}, + }, + } +end + +function InspectorLine:get_status_line() + local queue_pos = getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1) + if queue_pos <= 0 then + return 'Item attached' + end + return ('Position in line: %d'):format(queue_pos) +end + InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', - frame={w=30, h=9}, + frame={w=30, h=14}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -489,29 +543,17 @@ function InspectorOverlay:init() frame={t=0, l=0}, text='Waiting for items:', }, - widgets.Label{ - frame={t=1, l=0}, - text='item1', - }, - widgets.Label{ - frame={t=2, l=0}, - text='item2', - }, - widgets.Label{ - frame={t=3, l=0}, - text='item3', - }, - widgets.Label{ - frame={t=4, l=0}, - text='item4', - }, + InspectorLine{view_id='item1', frame={t=2, l=0}, idx=1}, + InspectorLine{view_id='item2', frame={t=4, l=0}, idx=2}, + InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3}, + InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4}, widgets.HotkeyLabel{ - frame={t=5, l=0}, + frame={t=10, l=0}, label='adjust filters', key='CUSTOM_CTRL_F', }, widgets.HotkeyLabel{ - frame={t=6, l=0}, + frame={t=11, l=0}, label='make top priority', key='CUSTOM_CTRL_T', }, @@ -578,7 +620,7 @@ end -- does not need the core suspended. function show_global_settings_dialog(settings) GlobalSettings{ - frame_title="Buildingplan Global Settings", + frame_title='Buildingplan Global Settings', settings=settings, }:show() end From 56c8927316947ddbd494b36a25566b9e10cd9bd3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 03:08:55 -0800 Subject: [PATCH 058/126] better description string for inspection overlay --- plugins/buildingplan/buildingplan.cpp | 86 +++++++++++++++++---------- plugins/lua/buildingplan.lua | 20 +++---- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 17db21cd2..579c1e83f 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -357,6 +357,20 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { return true; } +static string get_desc_string(color_ostream &out, df::job_item *jitem, + const vector &vec_ids) { + vector descs; + for (auto &vec_id : vec_ids) { + df::job_item jitem_copy = *jitem; + jitem_copy.vector_id = vec_id; + call_buildingplan_lua(&out, "get_desc", 1, 1, + [&](lua_State *L) { Lua::Push(L, &jitem_copy); }, + [&](lua_State *L) { + descs.emplace_back(lua_tostring(L, -1)); }); + } + return join_strings(" or ", descs); +} + static void printStatus(color_ostream &out) { DEBUG(status,out).print("entering buildingplan_printStatus\n"); out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); @@ -369,31 +383,21 @@ static void printStatus(color_ostream &out) { map counts; int32_t total = 0; - for (auto &buckets : tasks) { - for (auto &bucket_queue : buckets.second) { - Bucket &tqueue = bucket_queue.second; - for (auto it = tqueue.begin(); it != tqueue.end();) { - auto & task = *it; - auto id = task.first; - df::building *bld = NULL; - if (!planned_buildings.count(id) || - !(bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out))) { - DEBUG(status,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", - id, task.second); - it = tqueue.erase(it); - continue; - } - auto *jitem = bld->jobs[0]->job_items[task.second]; - int32_t quantity = jitem->quantity; - if (quantity) { - string desc = "none"; - call_buildingplan_lua(&out, "get_desc", 1, 1, - [&](lua_State *L) { Lua::Push(L, jitem); }, - [&](lua_State *L) { desc = lua_tostring(L, -1); }); - counts[desc] += quantity; - total += quantity; - } - ++it; + for (auto &entry : planned_buildings) { + auto &pb = entry.second; + auto bld = pb.getBuildingIfValidOrRemoveIfNot(out); + if (!bld || bld->jobs.size() != 1) + continue; + auto &job_items = bld->jobs[0]->job_items; + if (job_items.size() != pb.vector_ids.size()) + continue; + int job_item_idx = 0; + for (auto &vec_ids : pb.vector_ids) { + auto &jitem = job_items[job_item_idx++]; + int32_t quantity = jitem->quantity; + if (quantity) { + counts[get_desc_string(out, jitem, vec_ids)] += quantity; + total += quantity; } } } @@ -514,20 +518,41 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return count; } -static int getQueuePosition(color_ostream &out, df::building *bld, int index) { - DEBUG(status,out).print("entering getQueuePosition\n"); +static bool validate_pb(color_ostream &out, df::building *bld, int index) { if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) - return 0; + return false; auto &job_items = bld->jobs[0]->job_items; if (job_items.size() <= index) - return 0; + return false; PlannedBuilding &pb = planned_buildings.at(bld->id); if (pb.vector_ids.size() <= index) + return false; + + return true; +} + +static string getDescString(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getDescString\n"); + if (!validate_pb(out, bld, index)) return 0; - auto &job_item = job_items[index]; + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &jitem = bld->jobs[0]->job_items[index]; + return get_desc_string(out, jitem, pb.vector_ids[index]); +} + +static int getQueuePosition(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getQueuePosition\n"); + if (!validate_pb(out, bld, index)) + return 0; + + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &job_item = bld->jobs[0]->job_items[index]; + + if (job_item->quantity <= 0) + return 0; int min_pos = -1; for (auto &vec_id : pb.vector_ids[index]) { @@ -559,6 +584,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_END }; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 376787952..d0b3417fb 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -183,13 +183,15 @@ end function get_desc(filter) local desc = 'Unknown' - if filter.has_tool_use then + if filter.has_tool_use and filter.has_tool_use > -1 then desc = to_title_case(df.tool_uses[filter.has_tool_use]) - end - if filter.item_type then + elseif filter.flags2 and filter.flags2.screw then + desc = 'Screw' + elseif filter.item_type and filter.item_type > -1 then desc = to_title_case(df.item_type[filter.item_type]) - end - if filter.flags2 and filter.flags2.building_material then + elseif filter.vector_id and filter.vector_id > -1 then + desc = to_title_case(df.job_item_vector_id[filter.vector_id]) + elseif filter.flags2 and filter.flags2.building_material then desc = 'Generic material'; if filter.flags2.fire_safe then desc = 'Fire-safe material'; @@ -197,10 +199,6 @@ function get_desc(filter) if filter.flags2.magma_safe then desc = 'Magma-safe material'; end - elseif filter.flags2 and filter.flags2.screw then - desc = 'Screw' - elseif filter.vector_id then - desc = to_title_case(df.job_item_vector_id[filter.vector_id]) end if desc:endswith('s') then @@ -208,6 +206,8 @@ function get_desc(filter) end if desc == 'Trappart' then desc = 'Mechanism' + elseif desc == 'Wood' then + desc = 'Log' end return desc end @@ -510,7 +510,7 @@ function InspectorLine:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text={{text=function() return get_desc(get_building_filters()[self.idx]) end}}, + text={{text=function() return getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1) end}}, }, widgets.Label{ frame={t=1, l=2}, From 0d3285678c3a0e3e058ad4fc742f0a0b4ed16b91 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 17:25:15 -0800 Subject: [PATCH 059/126] separate errors panel, fix pb vectors on load --- plugins/buildingplan/buildingplan.cpp | 15 +++-- plugins/lua/buildingplan.lua | 90 +++++++++++++++++++++------ 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 579c1e83f..e2db9c367 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -330,12 +330,11 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { auto job_item = job_items[job_item_idx]; auto bucket = getBucket(*job_item); - auto vector_ids = getVectorIds(out, job_item); // if there are multiple vector_ids, schedule duplicate tasks. after // the correct number of items are matched, the extras will get popped // as invalid - for (auto vector_id : vector_ids) { + for (auto vector_id : pb.vector_ids[job_item_idx]) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { tasks[vector_id][bucket].push_back(std::make_pair(id, job_item_idx)); DEBUG(status,out).print("added task: %s/%s/%d,%d; " @@ -402,10 +401,14 @@ static void printStatus(color_ostream &out) { } } - out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", - total, planned_buildings.size()); - for (auto &count : counts) - out.print(" %3d %s\n", count.second, count.first.c_str()); + if (planned_buildings.size()) { + out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", + total, planned_buildings.size()); + for (auto &count : counts) + out.print(" %3d %s\n", count.second, count.first.c_str()); + } else { + out.print("Currently no planned buildings\n"); + } out.print("\n"); } diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index d0b3417fb..5a12292ea 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -192,7 +192,7 @@ function get_desc(filter) elseif filter.vector_id and filter.vector_id > -1 then desc = to_title_case(df.job_item_vector_id[filter.vector_id]) elseif filter.flags2 and filter.flags2.building_material then - desc = 'Generic material'; + desc = 'Building material'; if filter.flags2.fire_safe then desc = 'Fire-safe material'; end @@ -236,7 +236,7 @@ function ItemLine:get_item_line_text() local note = self.available >= quantity and 'Can build now' or 'Will wait for item' - return ('%-21s%s%s'):format(line:sub(1,21), (' '):rep(13), note) + return ('%-21s%s%s'):format(line:sub(1,21), (' '):rep(14), note) end function ItemLine:reduce_quantity() @@ -245,34 +245,44 @@ function ItemLine:reduce_quantity() self.available = math.max(0, self.available - get_quantity(filter)) end +local function get_placement_errors() + local out = '' + for _,str in ipairs(uibs.errors) do + if #out > 0 then out = out .. NEWLINE end + out = out .. str.value + end + return out +end + PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ - default_pos={x=6,y=9}, + default_pos={x=5,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', - frame={w=54, h=9}, - frame_background=gui.CLEAR_PEN, + frame={w=56, h=18}, } function PlannerOverlay:init() - self:addviews{ - widgets.Panel{ - frame={}, - frame_style=gui.MEDIUM_FRAME, - }, + local main_panel = widgets.Panel{ + frame={t=0, l=0, r=0, h=14}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + } + + main_panel:addviews{ widgets.Label{ frame={}, auto_width=true, text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, - ItemLine{view_id='item1', frame={t=1, l=1, r=1}, idx=1}, - ItemLine{view_id='item2', frame={t=3, l=1, r=1}, idx=2}, - ItemLine{view_id='item3', frame={t=5, l=1, r=1}, idx=3}, - ItemLine{view_id='item4', frame={t=7, l=1, r=1}, idx=4}, + ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1}, + ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2}, + ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3}, + ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4}, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', - frame={t=4, l=1}, + frame={t=4, l=4}, key='CUSTOM_R', label='Top Stair Type: ', visible=is_stairs, @@ -284,7 +294,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', - frame={t=5, l=1}, + frame={t=5, l=4}, key='CUSTOM_B', label='Bottom Stair Type: ', visible=is_stairs, @@ -295,7 +305,7 @@ function PlannerOverlay:init() }, }, widgets.Label{ - frame={b=1, l=17}, + frame={b=3, l=17}, text={ 'Selected area: ', {text=function() @@ -307,15 +317,54 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='safety', - frame={b=0, l=1}, - key='CUSTOM_F', - label='Extra safety: ', + frame={b=0, l=2}, + key='CUSTOM_G', + label='Safety: ', options={ {label='None', value='none'}, {label='Magma', value='magma'}, {label='Fire', value='fire'}, }, }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='SELECT', + label='Choose item', + }, + widgets.HotkeyLabel{ + frame={b=1, l=21}, + key='CUSTOM_F', + label='Filter', + }, + widgets.HotkeyLabel{ + frame={b=1, l=33}, + key='CUSTOM_X', + label='Clear filter', + }, + } + + local error_panel = widgets.ResizingPanel{ + view_id='errors', + frame={t=14, l=0, r=0, h=3}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + } + + error_panel:addviews{ + widgets.WrappedLabel{ + text_pen=COLOR_LIGHTRED, + text_to_wrap=get_placement_errors, + }, + widgets.Label{ + text_pen=COLOR_GREEN, + text='OK to build', + visible=function() return #uibs.errors == 0 end, + }, + } + + self:addviews{ + main_panel, + error_panel, } end @@ -367,6 +416,7 @@ end function PlannerOverlay:render(dc) if not is_plannable() then return end + self.subviews.errors:updateLayout() PlannerOverlay.super.render(self, dc) end From 3f8be2cd9e579ecf912ddb12eff9e68ba13cd30b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 18:02:15 -0800 Subject: [PATCH 060/126] implement make_top_priority, cache inspector data --- plugins/buildingplan/buildingplan.cpp | 33 ++++++++++++++++++++++- plugins/lua/buildingplan.lua | 38 +++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index e2db9c367..dff04f0c2 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -336,7 +336,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { // as invalid for (auto vector_id : pb.vector_ids[job_item_idx]) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { - tasks[vector_id][bucket].push_back(std::make_pair(id, job_item_idx)); + tasks[vector_id][bucket].emplace_back(id, job_item_idx); DEBUG(status,out).print("added task: %s/%s/%d,%d; " "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), @@ -578,6 +578,36 @@ static int getQueuePosition(color_ostream &out, df::building *bld, int index) { return min_pos < 0 ? 0 : min_pos; } +static void makeTopPriority(color_ostream &out, df::building *bld) { + DEBUG(status,out).print("entering makeTopPriority\n"); + if (!validate_pb(out, bld, 0)) + return; + + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &job_items = bld->jobs[0]->job_items; + + for (int index = 0; index < job_items.size(); ++index) { + for (auto &vec_id : pb.vector_ids[index]) { + if (!tasks.count(vec_id)) + continue; + auto &buckets = tasks.at(vec_id); + string bucket_id = getBucket(*job_items[index]); + if (!buckets.count(bucket_id)) + continue; + auto &bucket = buckets.at(bucket_id); + for (auto taskit = bucket.begin(); taskit != bucket.end(); ++taskit) { + if (bld->id == taskit->first && index == taskit->second) { + auto task_bld_id = taskit->first; + auto task_job_item_idx = taskit->second; + bucket.erase(taskit); + bucket.emplace_front(task_bld_id, task_job_item_idx); + break; + } + } + } + } +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(printStatus), DFHACK_LUA_FUNCTION(setSetting), @@ -589,5 +619,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(countAvailableItems), DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), + DFHACK_LUA_FUNCTION(makeTopPriority), DFHACK_LUA_END }; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 5a12292ea..6bf35bc2a 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -65,8 +65,10 @@ function get_job_item(btype, subtype, custom, index) end local reset_counts_flag = false +local reset_inspector_flag = false function reset_counts() reset_counts_flag = true + reset_inspector_flag = true end -------------------------------- @@ -560,7 +562,7 @@ function InspectorLine:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text={{text=function() return getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1) end}}, + text={{text=self:callback('get_desc_string')}}, }, widgets.Label{ frame={t=1, l=2}, @@ -569,12 +571,24 @@ function InspectorLine:init() } end +function InspectorLine:get_desc_string() + if self.desc then return self.desc end + self.desc = getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1) + return self.desc +end + function InspectorLine:get_status_line() + if self.status then return self.status end local queue_pos = getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1) if queue_pos <= 0 then return 'Item attached' end - return ('Position in line: %d'):format(queue_pos) + self.status = ('Position in line: %d'):format(queue_pos) + return self.status +end + +function InspectorLine:reset() + self.status = nil end InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) @@ -606,14 +620,31 @@ function InspectorOverlay:init() frame={t=11, l=0}, label='make top priority', key='CUSTOM_CTRL_T', + on_activate=self:callback('make_top_priority'), }, } end +function InspectorOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_inspector_flag = false +end + +function InspectorOverlay:make_top_priority() + makeTopPriority(dfhack.gui.getSelectedBuilding()) + self:reset() +end + function InspectorOverlay:onInput(keys) if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then return false end + if keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + self:reset() + end return InspectorOverlay.super.onInput(self, keys) end @@ -621,6 +652,9 @@ function InspectorOverlay:render(dc) if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then return end + if reset_inspector_flag then + self:reset() + end InspectorOverlay.super.render(self, dc) end From 96fa7fa1e2951bb7c96b7de3bca6715aaf5a50ad Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 18:23:14 -0800 Subject: [PATCH 061/126] fix position of errors panel --- plugins/lua/buildingplan.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 6bf35bc2a..82172613d 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -261,7 +261,7 @@ PlannerOverlay.ATTRS{ default_pos={x=5,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', - frame={w=56, h=18}, + frame={w=56, h=20}, } function PlannerOverlay:init() @@ -347,17 +347,20 @@ function PlannerOverlay:init() local error_panel = widgets.ResizingPanel{ view_id='errors', - frame={t=14, l=0, r=0, h=3}, + frame={t=14, l=0, r=0}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } error_panel:addviews{ widgets.WrappedLabel{ + frame={t=0, l=0, r=0}, text_pen=COLOR_LIGHTRED, text_to_wrap=get_placement_errors, + visible=function() return #uibs.errors > 0 end, }, widgets.Label{ + frame={t=0, l=0, r=0}, text_pen=COLOR_GREEN, text='OK to build', visible=function() return #uibs.errors == 0 end, From b3198c88a0aa6885c6ca09f8fea5d63b4e0de615 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 18:43:06 -0800 Subject: [PATCH 062/126] only block mouse clicks over exactly the panel area --- plugins/lua/buildingplan.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 82172613d..b7ed789e4 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -266,6 +266,7 @@ PlannerOverlay.ATTRS{ function PlannerOverlay:init() local main_panel = widgets.Panel{ + view_id='main', frame={t=0, l=0, r=0, h=14}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, @@ -396,7 +397,14 @@ function PlannerOverlay:onInput(keys) end if keys._MOUSE_L_DOWN then if is_over_options_panel() then return false end - if self:getMouseFramePos() then return true end + local detect_rect = copyall(self.frame_rect) + detect_rect.height = self.subviews.main.frame_rect.height + + self.subviews.errors.frame_rect.height + detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1 + if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect}) + or self.subviews.errors:getMousePos() then + return true + end if #uibs.errors > 0 then return true end local pos = dfhack.gui.getMousePos() if pos then From e92a54deaa0ec4a48850b9c204839183f5864406 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 21:17:55 -0800 Subject: [PATCH 063/126] beginning of textures --- plugins/buildingplan/buildingplan.cpp | 4 ++-- plugins/lua/buildingplan.lua | 29 ++++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index dff04f0c2..080941823 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -220,7 +220,7 @@ static void do_cycle(color_ostream &out) { cycle_requested = false; buildingplan_cycle(out, tasks, planned_buildings); - call_buildingplan_lua(&out, "reset_counts"); + call_buildingplan_lua(&out, "signal_reset"); } DFhackCExport command_result plugin_onupdate(color_ostream &out) { @@ -428,7 +428,7 @@ static bool setSetting(color_ostream &out, string name, bool value) { } validate_config(out, true); - call_buildingplan_lua(&out, "reset_counts"); + call_buildingplan_lua(&out, "signal_reset"); return true; } diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b7ed789e4..2123f636e 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -64,9 +64,11 @@ function get_job_item(btype, subtype, custom, index) return obj end +local texpos_base = -1 local reset_counts_flag = false local reset_inspector_flag = false -function reset_counts() +function signal_reset() + texpos_base = dfhack.textures.getControlPanelTexposStart() reset_counts_flag = true reset_inspector_flag = true end @@ -168,12 +170,22 @@ function ItemLine:init() self.visible = function() return #get_cur_filters() >= self.idx end self:addviews{ widgets.Label{ - frame={t=0, l=0}, - text={{text=function() return self:get_item_line_text() end}}, + frame={t=0, l=23}, + text={ + {tile=2600}, + {gap=6, tile=2602}, + {tile=2600}, + {gap=1, tile=2602}, + }, }, widgets.Label{ - frame={t=0, l=22}, - text='[filter][x]', + frame={t=0, l=0}, + text={ + {width=21, text=function() return self:get_item_line_text() end}, + {gap=3, text='filter'}, + {gap=2, text='x'}, + {gap=3, text=function() return self.note end}, + }, }, } end @@ -231,14 +243,13 @@ function ItemLine:get_item_line_text() local quantity = get_quantity(filter) self.desc = self.desc or get_desc(filter) - local line = ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') self.available = self.available or countAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) - local note = self.available >= quantity and - 'Can build now' or 'Will wait for item' + self.note = self.available >= quantity and + 'Can build now' or 'Will build later' - return ('%-21s%s%s'):format(line:sub(1,21), (' '):rep(14), note) + return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end function ItemLine:reduce_quantity() From aa4ebe6398ed199bf9b8f3516cedfb9aa0570abd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 03:16:34 -0800 Subject: [PATCH 064/126] remove some cruft --- plugins/lua/buildingplan.lua | 130 ----------------------------------- 1 file changed, 130 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 2123f636e..7f8d996c8 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -685,10 +685,6 @@ OVERLAY_WIDGETS = { inspector=InspectorOverlay, } - -local dialogs = require('gui.dialogs') -local guidm = require('gui.dwarfmode') - -- returns whether the items matched by the specified filter can have a quality -- rating. This also conveniently indicates whether an item can be decorated. -- does not need the core suspended @@ -704,130 +700,4 @@ function item_can_be_improved(btype, subtype, custom, reverse_idx) filter.item_type ~= df.item_type.BOULDER end - --- --- GlobalSettings dialog --- - -local GlobalSettings = defclass(GlobalSettings, dialogs.MessageBox) -GlobalSettings.focus_path = 'buildingplan_globalsettings' - -GlobalSettings.ATTRS{ - settings = {} -} - -function GlobalSettings:onDismiss() - for k,v in pairs(self.settings) do - -- call back into C++ to save changes - setSetting(k, v) - end -end - --- does not need the core suspended. -function show_global_settings_dialog(settings) - GlobalSettings{ - frame_title='Buildingplan Global Settings', - settings=settings, - }:show() -end - -function GlobalSettings:toggle_setting(name) - self.settings[name] = not self.settings[name] -end - -function GlobalSettings:get_setting_string(name) - if self.settings[name] then return 'On' end - return 'Off' -end - -function GlobalSettings:get_setting_pen(name) - if self.settings[name] then return COLOR_LIGHTGREEN end - return COLOR_LIGHTRED -end - -function GlobalSettings:is_setting_enabled(name) - return self.settings[name] -end - -function GlobalSettings:make_setting_label_token(text, key, name, width) - return {text=text, key=key, key_sep=': ', key_pen=COLOR_LIGHTGREEN, - on_activate=self:callback('toggle_setting', name), width=width} -end - -function GlobalSettings:make_setting_value_token(name) - return {text=self:callback('get_setting_string', name), - enabled=self:callback('is_setting_enabled', name), - pen=self:callback('get_setting_pen', name), - dpen=COLOR_GRAY} -end - --- mockup: ---[[ - Buildingplan Global Settings - - e: Enable all: Off - Enables buildingplan for all building types. Use this to avoid having to - manually enable buildingplan for each building type that you want to plan. - Note that DFHack quickfort will use buildingplan to manage buildings - regardless of whether buildingplan is "enabled" for the building type. - - Allowed types for generic, fire-safe, and magma-safe building material: - b: Blocks: On - s: Boulders: On - w: Wood: On - r: Bars: Off - Changes to these settings will be applied to newly-planned buildings. - - A: Apply building material filter settings to existing planned buildings - Use this if your planned buildings can't be completed because the settings - above were too restrictive when the buildings were originally planned. - - M: Edit list of materials to avoid - potash - pearlash - ash - coal - Buildingplan will avoid using these material types when a planned building's - material filter is set to 'any'. They can stil be matched when they are - explicitly allowed by a planned building's material filter. Changes to this - list take effect for existing buildings immediately. - - g: Allow bags: Off - This allows bags to be placed where a 'coffer' is planned. - - f: Legacy Quickfort Mode: Off - Compatibility mode for the legacy Python-based Quickfort application. This - setting is not needed for DFHack quickfort. ---]] -function GlobalSettings:init() - - self.subviews.label:setText{ - self:make_setting_label_token('Enable all', 'CUSTOM_E', 'all_enabled', 12), - self:make_setting_value_token('all_enabled'), '\n', - ' Enables buildingplan for all building types. Use this to avoid having\n', - ' to manually enable buildingplan for each building type that you want\n', - ' to plan. Note that DFHack quickfort will use buildingplan to manage\n', - ' buildings regardless of whether buildingplan is "enabled" for the\n', - ' building type.\n', - '\n', - 'Allowed types for generic, fire-safe, and magma-safe building material:\n', - self:make_setting_label_token('Blocks', 'CUSTOM_B', 'blocks', 10), - self:make_setting_value_token('blocks'), '\n', - self:make_setting_label_token('Boulders', 'CUSTOM_S', 'boulders', 10), - self:make_setting_value_token('boulders'), '\n', - self:make_setting_label_token('Wood', 'CUSTOM_W', 'logs', 10), - self:make_setting_value_token('logs'), '\n', - self:make_setting_label_token('Bars', 'CUSTOM_R', 'bars', 10), - self:make_setting_value_token('bars'), '\n', - ' Changes to these settings will be applied to newly-planned buildings.\n', - ' If no types are enabled above, then any building material is allowed.\n', - '\n', - self:make_setting_label_token('Legacy Quickfort Mode', 'CUSTOM_F', - 'quickfort_mode', 23), - self:make_setting_value_token('quickfort_mode'), '\n', - ' Compatibility mode for the legacy Python-based Quickfort application.\n', - ' This setting is not needed for DFHack quickfort.' - } -end - return _ENV From c0cdd58b5080a43f94ec175e552c8c8c982e25ca Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 21:42:58 -0800 Subject: [PATCH 065/126] fix signed-unsigned compare --- plugins/buildingplan/buildingplan.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 080941823..e31914372 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -481,7 +481,7 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &job_items = job_item_repo[key]; - if (index >= job_items.size()) { + if (index >= (int)job_items.size()) { for (int i = job_items.size(); i <= index; ++i) { bool failed = false; if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, @@ -526,11 +526,11 @@ static bool validate_pb(color_ostream &out, df::building *bld, int index) { return false; auto &job_items = bld->jobs[0]->job_items; - if (job_items.size() <= index) + if ((int)job_items.size() <= index) return false; PlannedBuilding &pb = planned_buildings.at(bld->id); - if (pb.vector_ids.size() <= index) + if ((int)pb.vector_ids.size() <= index) return false; return true; @@ -586,7 +586,7 @@ static void makeTopPriority(color_ostream &out, df::building *bld) { PlannedBuilding &pb = planned_buildings.at(bld->id); auto &job_items = bld->jobs[0]->job_items; - for (int index = 0; index < job_items.size(); ++index) { + for (int index = 0; index < (int)job_items.size(); ++index) { for (auto &vec_id : pb.vector_ids[index]) { if (!tasks.count(vec_id)) continue; From c59ad78f40ab72dedd699c10cd8e0cc2e34d4906 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 23:02:34 -0800 Subject: [PATCH 066/126] more tokens, textures, and colors --- plugins/buildingplan/buildingplan.cpp | 53 +++++++++++++-------------- plugins/lua/buildingplan.lua | 52 +++++++++++++++++++------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index e31914372..687f46705 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -148,7 +148,30 @@ static void validate_config(color_ostream &out, bool verbose = false) { set_config_bool(config, CONFIG_BARS, false); } -static void clear_job_item_repo() { +static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); + + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static void clear_state(color_ostream &out) { + call_buildingplan_lua(&out, "signal_reset"); + planned_buildings.clear(); + tasks.clear(); for (auto &entry : job_item_repo) { for (auto &jitem : entry.second) { delete jitem; @@ -168,9 +191,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { validate_config(out); DEBUG(status,out).print("loading persisted state\n"); - planned_buildings.clear(); - tasks.clear(); - clear_job_item_repo(); + clear_state(out); vector building_configs; World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); @@ -185,33 +206,11 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == SC_WORLD_UNLOADED) { DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); - planned_buildings.clear(); - tasks.clear(); - clear_job_item_repo(); + clear_state(out); } return CR_OK; } -static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, - int nargs = 0, int nres = 0, - Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, - Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { - DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); - - CoreSuspender guard; - - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!out) - out = &Core::getInstance().getConsole(); - - return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, - nargs, nres, - std::forward(args_lambda), - std::forward(res_lambda)); -} - static bool cycle_requested = false; static void do_cycle(color_ostream &out) { diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 7f8d996c8..6b1abb84e 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -64,15 +64,34 @@ function get_job_item(btype, subtype, custom, index) return obj end -local texpos_base = -1 +local BUTTON_START_PEN, BUTTON_END_PEN = nil, nil local reset_counts_flag = false local reset_inspector_flag = false function signal_reset() - texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_START_PEN = nil + BUTTON_END_PEN = nil reset_counts_flag = true reset_inspector_flag = true end +local to_pen = dfhack.pen.parse +local function get_button_start_pen() + if not BUTTON_START_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_START_PEN = to_pen{ch='[', fg=COLOR_YELLOW, + tile=texpos_base > 0 and texpos_base + 13 or nil} + end + return BUTTON_START_PEN +end +local function get_button_end_pen() + if not BUTTON_END_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_END_PEN = to_pen{ch=']', fg=COLOR_YELLOW, + tile=texpos_base > 0 and texpos_base + 15 or nil} + end + return BUTTON_END_PEN +end + -------------------------------- -- PlannerOverlay -- @@ -172,19 +191,20 @@ function ItemLine:init() widgets.Label{ frame={t=0, l=23}, text={ - {tile=2600}, - {gap=6, tile=2602}, - {tile=2600}, - {gap=1, tile=2602}, + {tile=get_button_start_pen}, + {gap=6, tile=get_button_end_pen}, + {tile=get_button_start_pen}, + {gap=1, tile=get_button_end_pen}, }, }, widgets.Label{ frame={t=0, l=0}, text={ - {width=21, text=function() return self:get_item_line_text() end}, - {gap=3, text='filter'}, - {gap=2, text='x'}, - {gap=3, text=function() return self.note end}, + {width=21, text=self:callback('get_item_line_text')}, + {gap=3, text='filter', pen=COLOR_GREEN}, + {gap=2, text='x', pen=COLOR_GREEN}, + {gap=3, text=function() return self.note end, + pen=function() return self.note_pen end}, }, }, } @@ -246,8 +266,13 @@ function ItemLine:get_item_line_text() self.available = self.available or countAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) - self.note = self.available >= quantity and - 'Can build now' or 'Will build later' + if self.available >= quantity then + self.note_pen = COLOR_GREEN + self.note = 'Available now' + else + self.note_pen = COLOR_YELLOW + self.note = 'Will link later' + end return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end @@ -298,7 +323,7 @@ function PlannerOverlay:init() view_id='stairs_top_subtype', frame={t=4, l=4}, key='CUSTOM_R', - label='Top Stair Type: ', + label='Top Stair Type: ', visible=is_stairs, options={ {label='Auto', value='auto'}, @@ -444,7 +469,6 @@ function PlannerOverlay:render(dc) PlannerOverlay.super.render(self, dc) end -local to_pen = dfhack.pen.parse local GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, From daf691839fd73f4c40f3cc3cc4ae149d74b10e04 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Feb 2023 14:24:21 -0800 Subject: [PATCH 067/126] item selection, callback skeleton --- plugins/lua/buildingplan.lua | 139 +++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 30 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 6b1abb84e..b9be7e800 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -182,6 +182,10 @@ end ItemLine = defclass(ItemLine, widgets.Panel) ItemLine.ATTRS{ idx=DEFAULT_NIL, + is_selected_fn=DEFAULT_NIL, + on_select=DEFAULT_NIL, + on_filter=DEFAULT_NIL, + on_clear_filter=DEFAULT_NIL, } function ItemLine:init() @@ -189,16 +193,38 @@ function ItemLine:init() self.visible = function() return #get_cur_filters() >= self.idx end self:addviews{ widgets.Label{ - frame={t=0, l=23}, + frame={t=0, l=0}, + text='*', + auto_width=true, + visible=self.is_selected_fn, + }, + widgets.Label{ + frame={t=0, r=0}, + text='*', + auto_width=true, + visible=self.is_selected_fn, + on_click=self.on_filter, + }, + widgets.Label{ + frame={t=0, l=25}, text={ {tile=get_button_start_pen}, {gap=6, tile=get_button_end_pen}, + }, + auto_width=true, + on_click=function() self.on_filter(self.idx) end, + }, + widgets.Label{ + frame={t=0, l=33}, + text={ {tile=get_button_start_pen}, {gap=1, tile=get_button_end_pen}, }, + auto_width=true, + on_click=function() self.on_clear_filter(self.idx) end, }, widgets.Label{ - frame={t=0, l=0}, + frame={t=0, l=2}, text={ {width=21, text=self:callback('get_item_line_text')}, {gap=3, text='filter', pen=COLOR_GREEN}, @@ -215,6 +241,13 @@ function ItemLine:reset() self.available = nil end +function ItemLine:onInput(keys) + if keys._MOUSE_L_DOWN and self:getMousePos() then + self.on_select(self.idx) + end + return ItemLine.super.onInput(self, keys) +end + function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use and filter.has_tool_use > -1 then @@ -301,6 +334,8 @@ PlannerOverlay.ATTRS{ } function PlannerOverlay:init() + self.selected = 1 + local main_panel = widgets.Panel{ view_id='main', frame={t=0, l=0, r=0, h=14}, @@ -308,6 +343,14 @@ function PlannerOverlay:init() frame_background=gui.CLEAR_PEN, } + local function make_is_selected_fn(idx) + return function() return self.selected == idx end + end + + local function on_select_fn(idx) + self.selected = idx + end + main_panel:addviews{ widgets.Label{ frame={}, @@ -315,10 +358,22 @@ function PlannerOverlay:init() text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, - ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1}, - ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2}, - ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3}, - ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4}, + ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, + is_selected_fn=make_is_selected_fn(1), on_select=on_select_fn, + on_filter=self:callback('filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, + is_selected_fn=make_is_selected_fn(2), on_select=on_select_fn, + on_filter=self:callback('filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, + is_selected_fn=make_is_selected_fn(3), on_select=on_select_fn, + on_filter=self:callback('filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, + is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, + on_filter=self:callback('filter'), + on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', frame={t=4, l=4}, @@ -354,32 +409,43 @@ function PlannerOverlay:init() }, visible=is_choosing_area, }, - widgets.CycleHotkeyLabel{ - view_id='safety', - frame={b=0, l=2}, - key='CUSTOM_G', - label='Safety: ', - options={ - {label='None', value='none'}, - {label='Magma', value='magma'}, - {label='Fire', value='fire'}, + widgets.Panel{ + visible=function() return #get_cur_filters() > 0 end, + subviews={ + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='SELECT', + label='Choose item', + on_activate=function() self:choose(self.selected) end, + enabled=function() + return (self.subviews['item'..self.selected].available or 0) > 0 + end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=21}, + key='CUSTOM_F', + label='Filter', + on_activate=function() self:filter(self.selected) end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=33}, + key='CUSTOM_X', + label='Clear filter', + on_activate=function() self:clear_filter(self.selected) end, + }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={b=0, l=2}, + key='CUSTOM_G', + label='Safety: ', + options={ + {label='None', value='none'}, + {label='Magma', value='magma'}, + {label='Fire', value='fire'}, + }, + }, }, }, - widgets.HotkeyLabel{ - frame={b=1, l=0}, - key='SELECT', - label='Choose item', - }, - widgets.HotkeyLabel{ - frame={b=1, l=21}, - key='CUSTOM_F', - label='Filter', - }, - widgets.HotkeyLabel{ - frame={b=1, l=33}, - key='CUSTOM_X', - label='Clear filter', - }, } local error_panel = widgets.ResizingPanel{ @@ -418,6 +484,18 @@ function PlannerOverlay:reset() reset_counts_flag = false end +function PlannerOverlay:choose(idx) + print('choose', idx) +end + +function PlannerOverlay:filter(idx) + print('filter', idx) +end + +function PlannerOverlay:clear_filter(idx) + print('clear_filter', idx) +end + function PlannerOverlay:onInput(keys) if not is_plannable() then return false end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then @@ -425,6 +503,7 @@ function PlannerOverlay:onInput(keys) uibs.selection_pos:clear() return true end + self.selected = 1 self:reset() return false end From 66a14ecc7471e147a98c07b4466233e997021166 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Feb 2023 19:16:45 -0800 Subject: [PATCH 068/126] get UI semi-finalized, prep for item choosing --- plugins/buildingplan/buildingplan.cpp | 27 ++++++-- plugins/buildingplan/buildingplan.h | 2 + plugins/buildingplan/buildingplan_cycle.cpp | 8 +-- plugins/lua/buildingplan.lua | 75 +++++++++++++-------- 4 files changed, 77 insertions(+), 35 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 687f46705..2dbceba1f 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -319,12 +319,15 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { DEBUG(status,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); return false; } + auto job_items = bld->jobs[0]->job_items; - int num_job_items = job_items.size(); - if (num_job_items < 1) { - DEBUG(status,out).print("unexpected number of job items: want >0, got %d\n", num_job_items); - return false; + if (isJobReady(out, job_items)) { + // all items are already attached + finalizeBuilding(out, bld); + return true; } + + int num_job_items = job_items.size(); int32_t id = bld->id; for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { auto job_item = job_items[job_item_idx]; @@ -520,6 +523,19 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return count; } +static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering hasFilter\n"); + return false; +} + +static void setFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering setFilter\n"); +} + +static void clearFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering clearFilter\n"); +} + static bool validate_pb(color_ostream &out, df::building *bld, int index) { if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) return false; @@ -616,6 +632,9 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(hasFilter), + DFHACK_LUA_FUNCTION(setFilter), + DFHACK_LUA_FUNCTION(clearFilter), DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_FUNCTION(makeTopPriority), diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 7fe2478aa..01c72e370 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -31,3 +31,5 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); bool matchesFilters(df::item * item, df::job_item * job_item); +bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); +void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 6d5e4a405..069787f39 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -72,9 +72,9 @@ bool matchesFilters(df::item * item, df::job_item * job_item) { item->getType()); } -static bool isJobReady(color_ostream &out, df::job * job) { +bool isJobReady(color_ostream &out, const std::vector &jitems) { int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } + for (auto job_item : jitems) { needed_items += job_item->quantity; } if (needed_items) { DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); return false; @@ -91,7 +91,7 @@ static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { // now all at 0, so there is no risk of having extra items attached. we don't // remove them to keep the "finalize with buildingplan active" path as similar // as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(color_ostream &out, df::building * bld) { +void finalizeBuilding(color_ostream &out, df::building *bld) { DEBUG(cycle,out).print("finalizing building %d\n", bld->id); auto job = bld->jobs[0]; @@ -194,7 +194,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, // be completed with the correct number of items. --job->job_items[filter_idx]->quantity; task_queue.pop_front(); - if (isJobReady(out, job)) { + if (isJobReady(out, job->job_items)) { finalizeBuilding(out, bld); planned_buildings.at(id).remove(out); } diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b9be7e800..b6c13d5a7 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -198,13 +198,6 @@ function ItemLine:init() auto_width=true, visible=self.is_selected_fn, }, - widgets.Label{ - frame={t=0, r=0}, - text='*', - auto_width=true, - visible=self.is_selected_fn, - on_click=self.on_filter, - }, widgets.Label{ frame={t=0, l=25}, text={ @@ -228,7 +221,7 @@ function ItemLine:init() text={ {width=21, text=self:callback('get_item_line_text')}, {gap=3, text='filter', pen=COLOR_GREEN}, - {gap=2, text='x', pen=COLOR_GREEN}, + {gap=2, text='x', pen=self:callback('get_x_pen')}, {gap=3, text=function() return self.note end, pen=function() return self.note_pen end}, }, @@ -248,6 +241,10 @@ function ItemLine:onInput(keys) return ItemLine.super.onInput(self, keys) end +function ItemLine:get_x_pen() + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx) and COLOR_GREEN or COLOR_GREY +end + function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use and filter.has_tool_use > -1 then @@ -414,32 +411,52 @@ function PlannerOverlay:init() subviews={ widgets.HotkeyLabel{ frame={b=1, l=0}, - key='SELECT', - label='Choose item', - on_activate=function() self:choose(self.selected) end, - enabled=function() - return (self.subviews['item'..self.selected].available or 0) > 0 - end, + key='STRING_A042', + enabled=function() return #get_cur_filters() > 1 end, + on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=1}, + key='STRING_A047', + label='Prev/next item', + enabled=function() return #get_cur_filters() > 1 end, + on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, widgets.HotkeyLabel{ frame={b=1, l=21}, key='CUSTOM_F', - label='Filter', + label='Set filter', on_activate=function() self:filter(self.selected) end, }, widgets.HotkeyLabel{ - frame={b=1, l=33}, + frame={b=1, l=37}, key='CUSTOM_X', label='Clear filter', on_activate=function() self:clear_filter(self.selected) end, }, + widgets.CycleHotkeyLabel{ + view_id='choose', + frame={b=0, l=0}, + key='CUSTOM_I', + label='Choose exact items:', + options={{label='Yes', value=true}, + {label='No', value=false}}, + initial_option=false, + enabled=function() + for idx = 1,4 do + if (self.subviews['item'..idx].available or 0) > 0 then + return true + end + end + end, + }, widgets.CycleHotkeyLabel{ view_id='safety', - frame={b=0, l=2}, + frame={b=0, l=29}, key='CUSTOM_G', - label='Safety: ', + label='Building safety:', options={ - {label='None', value='none'}, + {label='Any', value='none'}, {label='Magma', value='magma'}, {label='Fire', value='fire'}, }, @@ -484,10 +501,6 @@ function PlannerOverlay:reset() reset_counts_flag = false end -function PlannerOverlay:choose(idx) - print('choose', idx) -end - function PlannerOverlay:filter(idx) print('filter', idx) end @@ -504,6 +517,8 @@ function PlannerOverlay:onInput(keys) return true end self.selected = 1 + self.subviews.choose:setOption(false) + self.subviews.safety:setOption('none') self:reset() return false end @@ -527,10 +542,6 @@ function PlannerOverlay:onInput(keys) if #get_cur_filters() == 0 then return false -- we don't add value; let the game place it end - self.subviews.item1:reduce_quantity() - self.subviews.item2:reduce_quantity() - self.subviews.item3:reduce_quantity() - self.subviews.item4:reduce_quantity() self:place_building() uibs.selection_pos:clear() return true @@ -607,6 +618,11 @@ function PlannerOverlay:place_building() max_y = min_y + height - 1 max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end + if self.subviews.choose:getOptionValue() then + -- TODO + -- open dialog, showing all items (restricted to current filters) + -- select items (doesn't have to be all required items) + end local blds = {} local subtype = uibs.building_subtype for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do @@ -660,7 +676,12 @@ function PlannerOverlay:place_building() end table.insert(blds, bld) end end end + self.subviews.item1:reduce_quantity() + self.subviews.item2:reduce_quantity() + self.subviews.item3:reduce_quantity() + self.subviews.item4:reduce_quantity() for _,bld in ipairs(blds) do + -- TODO: attach chosen items and reduce job_item quantity addPlannedBuilding(bld) end scheduleCycle() From 4001ef381508eb1a04ae9ecb82b73a3d2d36a05c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:09:54 -0800 Subject: [PATCH 069/126] implement selecting specific items --- plugins/buildingplan/buildingplan.cpp | 38 +++- plugins/lua/buildingplan.lua | 294 ++++++++++++++++++++++---- 2 files changed, 282 insertions(+), 50 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 2dbceba1f..d1e78b675 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -476,8 +476,8 @@ static void scheduleCycle(color_ostream &out) { cycle_requested = true; } -static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering countAvailableItems\n"); +static int scanAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, + int32_t custom, int index, vector *item_ids = NULL) { DEBUG(status,out).print( "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); @@ -514,8 +514,11 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 for (auto vector_id : vector_ids) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); for (auto &item : df::global::world->items.other[other_id]) { - if (itemPassesScreen(item) && matchesFilters(item, jitem)) + if (itemPassesScreen(item) && matchesFilters(item, jitem)) { + if (item_ids) + item_ids->emplace_back(item->id); ++count; + } } } @@ -523,6 +526,30 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return count; } +static int getAvailableItems(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + vector item_ids; + scanAvailableItems(*out, type, subtype, custom, index, &item_ids); + Lua::PushVector(L, item_ids); + return 1; +} + +static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print( + "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + return scanAvailableItems(out, type, subtype, custom, index); +} + static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(status,out).print("entering hasFilter\n"); return false; @@ -640,3 +667,8 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(makeTopPriority), DFHACK_LUA_END }; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getAvailableItems), + DFHACK_LUA_END +}; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b6c13d5a7..46d27d626 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -20,6 +20,8 @@ local utils = require('utils') local widgets = require('gui.widgets') require('dfhack.buildings') +local uibs = df.global.buildreq + local function process_args(opts, args) if args[1] == 'help' then opts.help = true @@ -64,12 +66,40 @@ function get_job_item(btype, subtype, custom, index) return obj end -local BUTTON_START_PEN, BUTTON_END_PEN = nil, nil +local function get_cur_filters() + return dfhack.buildings.getFiltersByType({}, uibs.building_type, + uibs.building_subtype, uibs.custom_type) +end + +local function is_choosing_area() + return uibs.selection_pos.x >= 0 +end + +local function get_cur_area_dims() + if not is_choosing_area() then return 1, 1, 1 end + return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, + math.abs(uibs.selection_pos.y - uibs.pos.y) + 1, + math.abs(uibs.selection_pos.z - uibs.pos.z) + 1 +end + +local function get_quantity(filter) + local quantity = filter.quantity or 1 + local dimx, dimy, dimz = get_cur_area_dims() + if quantity < 1 then + quantity = (((dimx * dimy) // 4) + 1) * dimz + else + quantity = quantity * dimx * dimy * dimz + end + return quantity +end + +local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil local reset_counts_flag = false local reset_inspector_flag = false function signal_reset() BUTTON_START_PEN = nil BUTTON_END_PEN = nil + SELECTED_ITEM_PEN = nil reset_counts_flag = true reset_inspector_flag = true end @@ -91,12 +121,169 @@ local function get_button_end_pen() end return BUTTON_END_PEN end +local function get_selected_item_pen() + if not SELECTED_ITEM_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + SELECTED_ITEM_PEN = to_pen{ch='x', fg=COLOR_GREEN, + tile=texpos_base > 0 and texpos_base + 9 or nil} + end + return SELECTED_ITEM_PEN +end -------------------------------- --- PlannerOverlay +-- ItemSelection -- -local uibs = df.global.buildreq +ItemSelection = defclass(ItemSelection, widgets.Window) +ItemSelection.ATTRS{ + frame_title='Choose items', + frame={w=60, h=30, l=4, t=8}, + resizable=true, + resize_min={w=56, h=20}, + index=DEFAULT_NIL, + selected_set=DEFAULT_NIL, +} + +function ItemSelection:init() + local filter = get_cur_filters()[self.index] + self.quantity = get_quantity(filter) + self.num_selected = 0 + + self:addviews{ + widgets.Label{ + frame={t=0}, + text={ + get_desc(filter), + self.quantity == 1 and '' or 's', + NEWLINE, + ('Select up to %d items ('):format(self.quantity), + {text=function() return self.num_selected end}, + ' selected)', + }, + }, + widgets.FilteredList{ + frame={t=3, l=0, r=0, b=0}, + case_sensitive=false, + choices=self:get_choices(), + icon_width=2, + on_submit=self:callback('toggle_item'), + }, + } +end + +local function make_search_key(str) + local out = '' + for c in str:gmatch("[%w%s]") do + out = out .. c + end + return out +end + +function ItemSelection:get_choices() + local item_ids = getAvailableItems(uibs.building_type, + uibs.building_subtype, uibs.custom_type, self.index - 1) + local buckets, selected_buckets = {}, {} + for _,item_id in ipairs(item_ids) do + local item = df.item.find(item_id) + if not item then goto continue end + local desc = dfhack.items.getDescription(item, 0, true) + if buckets[desc] then + local bucket = buckets[desc] + table.insert(bucket.item_ids, item_id) + bucket.quantity = bucket.quantity + 1 + else + local entry = { + text=desc, + search_key=make_search_key(desc), + icon=self:callback('get_entry_icon', item_id), + item_ids={item_id}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + selected=false, + } + buckets[desc] = entry + end + ::continue:: + end + local selected_qty = 0 + for bucket in pairs(selected_buckets) do + for _,item_id in ipairs(bucket.item_ids) do + self.selected_set[item_id] = true + end + selected_qty = selected_qty + bucket.quantity + bucket.selected = true + if selected_qty >= self.quantity then break end + end + self.num_selected = selected_qty + local choices = {} + for _,choice in pairs(buckets) do + choice.text = ('(%d) %s'):format(choice.quantity, choice.text) + table.insert(choices, choice) + end + local function choice_sort(a, b) + return a.item_type < b.item_type or + (a.item_type == b.item_type and a.item_subtype < b.item_subtype) or + (a.item_type == b.item_type and a.item_subtype == b.item_subtype and a.search_key < b.search_key) + end + table.sort(choices, choice_sort) + return choices +end + +function ItemSelection:toggle_item(_, choice) + if choice.selected then + for _,item_id in ipairs(choice.item_ids) do + self.selected_set[item_id] = nil + end + self.num_selected = self.num_selected - choice.quantity + choice.selected = false + elseif self.quantity > self.num_selected then + for _,item_id in ipairs(choice.item_ids) do + self.selected_set[item_id] = true + end + self.num_selected = self.num_selected + choice.quantity + choice.selected = true + end +end + +function ItemSelection:get_entry_icon(item_id) + return self.selected_set[item_id] and get_selected_item_pen() or nil +end + +ItemSelectionScreen = defclass(ItemSelectionScreen, gui.ZScreen) +ItemSelectionScreen.ATTRS { + focus_path='buildingplan/itemselection', + force_pause=true, + pass_pause=false, + pass_movement_keys=true, + pass_mouse_clicks=false, + defocusable=false, + index=DEFAULT_NIL, + on_submit=DEFAULT_NIL, +} + +function ItemSelectionScreen:init() + self.selected_set = {} + + self:addviews{ + ItemSelection{ + index=self.index, + selected_set=self.selected_set, + } + } +end + +function ItemSelectionScreen:onDismiss() + local selected_items = {} + for item_id in pairs(self.selected_set) do + table.insert(selected_items, item_id) + end + self.on_submit(selected_items) +end + +-------------------------------- +-- PlannerOverlay +-- local function cur_building_has_no_area() if uibs.building_type == df.building_type.Construction then return false end @@ -107,22 +294,6 @@ local function cur_building_has_no_area() return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) end -local function is_choosing_area() - return uibs.selection_pos.x >= 0 -end - -local function get_cur_area_dims() - if not is_choosing_area() then return 1, 1, 1 end - return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, - math.abs(uibs.selection_pos.y - uibs.pos.y) + 1, - math.abs(uibs.selection_pos.z - uibs.pos.z) + 1 -end - -local function get_cur_filters() - return dfhack.buildings.getFiltersByType({}, uibs.building_type, - uibs.building_subtype, uibs.custom_type) -end - local function is_plannable() return get_cur_filters() and not (uibs.building_type == df.building_type.Construction @@ -276,17 +447,6 @@ function get_desc(filter) return desc end -local function get_quantity(filter) - local quantity = filter.quantity or 1 - local dimx, dimy, dimz = get_cur_area_dims() - if quantity < 1 then - quantity = (((dimx * dimy) // 4) + 1) * dimz - else - quantity = quantity * dimx * dimy * dimz - end - return quantity -end - function ItemLine:get_item_line_text() local idx = self.idx local filter = get_cur_filters()[idx] @@ -357,19 +517,19 @@ function PlannerOverlay:init() }, ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, is_selected_fn=make_is_selected_fn(1), on_select=on_select_fn, - on_filter=self:callback('filter'), + on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, is_selected_fn=make_is_selected_fn(2), on_select=on_select_fn, - on_filter=self:callback('filter'), + on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, is_selected_fn=make_is_selected_fn(3), on_select=on_select_fn, - on_filter=self:callback('filter'), + on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, - on_filter=self:callback('filter'), + on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', @@ -426,7 +586,7 @@ function PlannerOverlay:init() frame={b=1, l=21}, key='CUSTOM_F', label='Set filter', - on_activate=function() self:filter(self.selected) end, + on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ frame={b=1, l=37}, @@ -501,8 +661,8 @@ function PlannerOverlay:reset() reset_counts_flag = false end -function PlannerOverlay:filter(idx) - print('filter', idx) +function PlannerOverlay:set_filter(idx) + print('set_filter', idx) end function PlannerOverlay:clear_filter(idx) @@ -539,11 +699,34 @@ function PlannerOverlay:onInput(keys) local pos = dfhack.gui.getMousePos() if pos then if is_choosing_area() or cur_building_has_no_area() then - if #get_cur_filters() == 0 then + local num_filters = #get_cur_filters() + if num_filters == 0 then return false -- we don't add value; let the game place it end - self:place_building() - uibs.selection_pos:clear() + local choose = self.subviews.choose + if choose.enabled() and choose:getOptionValue() then + local chosen_items = {} + local pending = num_filters + for idx = num_filters,1,-1 do + chosen_items[idx] = {} + if (self.subviews['item'..idx].available or 0) > 0 then + ItemSelectionScreen{ + index=self.selected, + on_submit=function(items) + chosen_items[idx] = items + pending = pending - 1 + if pending == 0 then + self:place_building(chosen_items) + end + end, + }:show() + else + pending = pending - 1 + end + end + else + self:place_building() + end return true elseif not is_choosing_area() then return false @@ -589,7 +772,7 @@ function PlannerOverlay:onRenderFrame(dc, rect) guidm.renderMapOverlay(get_overlay_pen, bounds) end -function PlannerOverlay:place_building() +function PlannerOverlay:place_building(chosen_items) local direction = uibs.direction local width, height, depth = get_cur_area_dims() local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( @@ -618,11 +801,6 @@ function PlannerOverlay:place_building() max_y = min_y + height - 1 max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end - if self.subviews.choose:getOptionValue() then - -- TODO - -- open dialog, showing all items (restricted to current filters) - -- select items (doesn't have to be all required items) - end local blds = {} local subtype = uibs.building_subtype for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do @@ -681,10 +859,32 @@ function PlannerOverlay:place_building() self.subviews.item3:reduce_quantity() self.subviews.item4:reduce_quantity() for _,bld in ipairs(blds) do - -- TODO: attach chosen items and reduce job_item quantity + -- attach chosen items and reduce job_item quantity + if chosen_items then + local job = bld.jobs[0] + local jitems = job.job_items + for idx=1,#get_cur_filters() do + local item_ids = chosen_items[idx] + while jitems[idx-1].quantity > 0 and #item_ids > 0 do + local item_id = item_ids[#item_ids] + local item = df.item.find(item_id) + if not item then + dfhack.printerr(('item no longer available: %d'):format(item_id)) + break + end + if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then + dfhack.printerr(('cannot attach item: %d'):format(item_id)) + break + end + jitems[idx-1].quantity = jitems[idx-1].quantity - 1 + item_ids[#item_ids] = nil + end + end + end addPlannedBuilding(bld) end scheduleCycle() + uibs.selection_pos:clear() end -------------------------------- From ee827f5ca19cc4c90860fe7de1c42f11d308373e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:18:15 -0800 Subject: [PATCH 070/126] remember mouse pos from before item choosing --- plugins/lua/buildingplan.lua | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 46d27d626..65bf918ce 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -75,11 +75,12 @@ local function is_choosing_area() return uibs.selection_pos.x >= 0 end -local function get_cur_area_dims() +local function get_cur_area_dims(pos) if not is_choosing_area() then return 1, 1, 1 end - return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, - math.abs(uibs.selection_pos.y - uibs.pos.y) + 1, - math.abs(uibs.selection_pos.z - uibs.pos.z) + 1 + pos = pos or uibs.pos + return math.abs(uibs.selection_pos.x - pos.x) + 1, + math.abs(uibs.selection_pos.y - pos.y) + 1, + math.abs(uibs.selection_pos.z - pos.z) + 1 end local function get_quantity(filter) @@ -716,7 +717,7 @@ function PlannerOverlay:onInput(keys) chosen_items[idx] = items pending = pending - 1 if pending == 0 then - self:place_building(chosen_items) + self:place_building(pos, chosen_items) end end, }:show() @@ -725,7 +726,7 @@ function PlannerOverlay:onInput(keys) end end else - self:place_building() + self:place_building(pos) end return true elseif not is_choosing_area() then @@ -756,11 +757,12 @@ function PlannerOverlay:onRenderFrame(dc, rect) if not is_choosing_area() then return end + local pos = uibs.pos local bounds = { - x1 = math.min(uibs.selection_pos.x, uibs.pos.x), - x2 = math.max(uibs.selection_pos.x, uibs.pos.x), - y1 = math.min(uibs.selection_pos.y, uibs.pos.y), - y2 = math.max(uibs.selection_pos.y, uibs.pos.y), + x1 = math.min(uibs.selection_pos.x, pos.x), + x2 = math.max(uibs.selection_pos.x, pos.x), + y1 = math.min(uibs.selection_pos.y, pos.y), + y2 = math.max(uibs.selection_pos.y, pos.y), } local pen = #uibs.errors > 0 and BAD_PEN or GOOD_PEN @@ -772,18 +774,18 @@ function PlannerOverlay:onRenderFrame(dc, rect) guidm.renderMapOverlay(get_overlay_pen, bounds) end -function PlannerOverlay:place_building(chosen_items) +function PlannerOverlay:place_building(pos, chosen_items) local direction = uibs.direction - local width, height, depth = get_cur_area_dims() + local width, height, depth = get_cur_area_dims(pos) local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( width, height, uibs.building_type, uibs.building_subtype, uibs.custom_type, direction) -- get the upper-left corner of the building/area at min z-level local has_selection = is_choosing_area() local start_pos = xyz2pos( - has_selection and math.min(uibs.selection_pos.x, uibs.pos.x) or uibs.pos.x - adjusted_width//2, - has_selection and math.min(uibs.selection_pos.y, uibs.pos.y) or uibs.pos.y - adjusted_height//2, - has_selection and math.min(uibs.selection_pos.z, uibs.pos.z) or uibs.pos.z + has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, + has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, + has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z ) if uibs.building_type == df.building_type.ScrewPump then if direction == df.screw_pump_direction.FromSouth then @@ -799,7 +801,7 @@ function PlannerOverlay:place_building(chosen_items) and (width > 1 or height > 1 or depth > 1) then max_x = min_x + width - 1 max_y = min_y + height - 1 - max_z = math.max(uibs.selection_pos.z, uibs.pos.z) + max_z = math.max(uibs.selection_pos.z, pos.z) end local blds = {} local subtype = uibs.building_subtype From 2477a239724258d632cb41dab54ce6cac0cc6843 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:25:07 -0800 Subject: [PATCH 071/126] pass correct job_item index for item selection --- plugins/lua/buildingplan.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 65bf918ce..327f9f685 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -712,7 +712,7 @@ function PlannerOverlay:onInput(keys) chosen_items[idx] = {} if (self.subviews['item'..idx].available or 0) > 0 then ItemSelectionScreen{ - index=self.selected, + index=idx, on_submit=function(items) chosen_items[idx] = items pending = pending - 1 From daa812b21eab7fa86e9d74c919f6c9e27d304e50 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:25:24 -0800 Subject: [PATCH 072/126] pluralize plural plurals --- plugins/buildingplan/buildingplan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index d1e78b675..ad21ada94 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -407,7 +407,7 @@ static void printStatus(color_ostream &out) { out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", total, planned_buildings.size()); for (auto &count : counts) - out.print(" %3d %s\n", count.second, count.first.c_str()); + out.print(" %3d %s%s\n", count.second, count.first.c_str(), count.second == 1 ? "" : "s"); } else { out.print("Currently no planned buildings\n"); } From a0785bded456708b49d6a174d7d01c3034c4bf8e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 00:57:30 -0800 Subject: [PATCH 073/126] implement heat safety --- plugins/buildingplan/buildingplan.cpp | 72 +++++++++++++++++---- plugins/buildingplan/buildingplan.h | 10 ++- plugins/buildingplan/buildingplan_cycle.cpp | 15 +++-- plugins/buildingplan/plannedbuilding.cpp | 6 +- plugins/buildingplan/plannedbuilding.h | 6 +- plugins/lua/buildingplan.lua | 32 +++++---- 6 files changed, 111 insertions(+), 30 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index ad21ada94..0a05c4ed5 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -74,6 +74,7 @@ struct BuildingTypeKeyHash { static PersistentDataItem config; // for use in counting available materials for the UI static unordered_map, BuildingTypeKeyHash> job_item_repo; +static unordered_map cur_heat_safety; // building id -> PlannedBuilding static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) @@ -456,13 +457,20 @@ static bool isPlannedBuilding(color_ostream &out, df::building *bld) { return bld && planned_buildings.count(bld->id) > 0; } +static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { + if (cur_heat_safety.count(key)) + return cur_heat_safety.at(key); + return HEAT_SAFETY_ANY; +} + static bool addPlannedBuilding(color_ostream &out, df::building *bld) { DEBUG(status,out).print("entering addPlannedBuilding\n"); if (!bld || planned_buildings.count(bld->id) || !isPlannableBuilding(out, bld->getType(), bld->getSubtype(), bld->getCustomType())) return false; - PlannedBuilding pb(out, bld); + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key)); return registerPlannedBuilding(out, pb); } @@ -482,6 +490,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); + HeatSafety heat = get_heat_safety_filter(key); auto &job_items = job_item_repo[key]; if (index >= (int)job_items.size()) { for (int i = job_items.size(); i <= index; ++i) { @@ -514,7 +523,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ for (auto vector_id : vector_ids) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); for (auto &item : df::global::world->items.other[other_id]) { - if (itemPassesScreen(item) && matchesFilters(item, jitem)) { + if (itemPassesScreen(item) && matchesFilters(item, jitem, heat)) { if (item_ids) item_ids->emplace_back(item->id); ++count; @@ -550,17 +559,56 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return scanAvailableItems(out, type, subtype, custom, index); } -static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering hasFilter\n"); +static bool hasMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering hasMaterialFilter\n"); return false; } -static void setFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering setFilter\n"); +static void setMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, string filter) { + DEBUG(status,out).print("entering setMaterialFilter\n"); + call_buildingplan_lua(&out, "signal_reset"); +} + +static int getMaterialFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + vector filter; + Lua::PushVector(L, filter); + return 1; +} + +static void setHeatSafetyFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int heat) { + DEBUG(status,out).print("entering setHeatSafetyFilter\n"); + BuildingTypeKey key(type, subtype, custom); + if (heat == HEAT_SAFETY_FIRE || heat == HEAT_SAFETY_MAGMA) + cur_heat_safety[key] = (HeatSafety)heat; + else + cur_heat_safety.erase(key); + call_buildingplan_lua(&out, "signal_reset"); } -static void clearFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering clearFilter\n"); +static int getHeatSafetyFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + DEBUG(status,*out).print( + "entering getHeatSafetyFilter building_type=%d subtype=%d custom=%d\n", + type, subtype, custom); + BuildingTypeKey key(type, subtype, custom); + HeatSafety heat = get_heat_safety_filter(key); + Lua::Push(L, heat); + return 1; } static bool validate_pb(color_ostream &out, df::building *bld, int index) { @@ -659,9 +707,9 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), - DFHACK_LUA_FUNCTION(hasFilter), - DFHACK_LUA_FUNCTION(setFilter), - DFHACK_LUA_FUNCTION(clearFilter), + DFHACK_LUA_FUNCTION(hasMaterialFilter), + DFHACK_LUA_FUNCTION(setMaterialFilter), + DFHACK_LUA_FUNCTION(setHeatSafetyFilter), DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_FUNCTION(makeTopPriority), @@ -670,5 +718,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(getAvailableItems), + DFHACK_LUA_COMMAND(getMaterialFilter), + DFHACK_LUA_COMMAND(getHeatSafetyFilter), DFHACK_LUA_END }; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 01c72e370..787987586 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -2,6 +2,7 @@ #include "modules/Persistence.h" +#include "df/building.h" #include "df/job_item.h" #include "df/job_item_vector_id.h" @@ -21,6 +22,13 @@ enum ConfigValues { enum BuildingConfigValues { BLD_CONFIG_ID = 0, + BLD_CONFIG_HEAT = 1, +}; + +enum HeatSafety { + HEAT_SAFETY_ANY = 0, + HEAT_SAFETY_FIRE = 1, + HEAT_SAFETY_MAGMA = 2, }; int get_config_val(DFHack::PersistentDataItem &c, int index); @@ -30,6 +38,6 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); -bool matchesFilters(df::item * item, df::job_item * job_item); +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 069787f39..703bab9b0 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -46,7 +46,7 @@ bool itemPassesScreen(df::item * item) { && !item->isAssignedToStockpile(); } -bool matchesFilters(df::item * item, df::job_item * job_item) { +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; @@ -65,10 +65,17 @@ bool matchesFilters(df::item * item, df::job_item * job_item) { && !item->hasToolUse(job_item->has_tool_use)) return false; + df::job_item jitem = *job_item; + if (heat == HEAT_SAFETY_MAGMA) { + jitem.flags2.bits.magma_safe = true; + jitem.flags2.bits.fire_safe = false; + } else if (heat == HEAT_SAFETY_FIRE && !jitem.flags2.bits.magma_safe) + jitem.flags2.bits.fire_safe = true; + return Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) + &jitem, item->getType(), item->getSubtype()) && Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), + &jitem, item->getMaterial(), item->getMaterialIndex(), item->getType()); } @@ -173,7 +180,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto id = task.first; auto job = bld->jobs[0]; auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx]) + if (matchesFilters(item, job->job_items[filter_idx], planned_buildings.at(id).heat_safety) && Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index f4f3564b7..eb55a95b4 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -62,11 +62,12 @@ static string serialize(const vector> &vector_ids return join_strings("|", joined); } -PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building) - : id(building->id), vector_ids(get_vector_ids(out, id)) { +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat) + : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat) { DEBUG(status,out).print("creating persistent data for building %d\n", id); bld_config = World::AddPersistentData(BLD_CONFIG_KEY); set_config_val(bld_config, BLD_CONFIG_ID, id); + set_config_val(bld_config, BLD_CONFIG_HEAT, heat_safety); bld_config.val() = serialize(vector_ids); DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); } @@ -74,6 +75,7 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building) PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) : id(get_config_val(bld_config, BLD_CONFIG_ID)), vector_ids(deserialize(out, bld_config)), + heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h index 592f0e4b3..0a67e0edc 100644 --- a/plugins/buildingplan/plannedbuilding.h +++ b/plugins/buildingplan/plannedbuilding.h @@ -1,5 +1,7 @@ #pragma once +#include "buildingplan.h" + #include "Core.h" #include "modules/Persistence.h" @@ -14,7 +16,9 @@ public: // job_item idx -> list of vectors the task is linked to const std::vector> vector_ids; - PlannedBuilding(DFHack::color_ostream &out, df::building *building); + const HeatSafety heat_safety; + + PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat); PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); void remove(DFHack::color_ostream &out); diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 327f9f685..e4aee907b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -251,14 +251,18 @@ function ItemSelection:get_entry_icon(item_id) return self.selected_set[item_id] and get_selected_item_pen() or nil end -ItemSelectionScreen = defclass(ItemSelectionScreen, gui.ZScreen) -ItemSelectionScreen.ATTRS { - focus_path='buildingplan/itemselection', +BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) +BuildingplanScreen.ATTRS { force_pause=true, pass_pause=false, pass_movement_keys=true, pass_mouse_clicks=false, defocusable=false, +} + +ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) +ItemSelectionScreen.ATTRS { + focus_path='buildingplan/itemselection', index=DEFAULT_NIL, on_submit=DEFAULT_NIL, } @@ -414,7 +418,8 @@ function ItemLine:onInput(keys) end function ItemLine:get_x_pen() - return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx) and COLOR_GREEN or COLOR_GREY + return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx) and + COLOR_GREEN or COLOR_GREY end function get_desc(filter) @@ -599,7 +604,7 @@ function PlannerOverlay:init() view_id='choose', frame={b=0, l=0}, key='CUSTOM_I', - label='Choose exact items:', + label='Choose from items:', options={{label='Yes', value=true}, {label='No', value=false}}, initial_option=false, @@ -617,10 +622,13 @@ function PlannerOverlay:init() key='CUSTOM_G', label='Building safety:', options={ - {label='Any', value='none'}, - {label='Magma', value='magma'}, - {label='Fire', value='fire'}, + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, + on_change=function(heat) + setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) + end, }, }, }, @@ -663,11 +671,11 @@ function PlannerOverlay:reset() end function PlannerOverlay:set_filter(idx) - print('set_filter', idx) + print('TODO: set_filter', idx) end function PlannerOverlay:clear_filter(idx) - print('clear_filter', idx) + setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx, "") end function PlannerOverlay:onInput(keys) @@ -679,8 +687,8 @@ function PlannerOverlay:onInput(keys) end self.selected = 1 self.subviews.choose:setOption(false) - self.subviews.safety:setOption('none') self:reset() + reset_counts_flag = true return false end if PlannerOverlay.super.onInput(self, keys) then @@ -753,6 +761,8 @@ function PlannerOverlay:onRenderFrame(dc, rect) if reset_counts_flag then self:reset() + self.subviews.safety:setOption(getHeatSafetyFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type)) end if not is_choosing_area() then return end From 273183e864ac2b19bb2741fddc5cc21b8bf03b15 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 01:58:17 -0800 Subject: [PATCH 074/126] allow cancel when choosing items --- plugins/lua/buildingplan.lua | 65 ++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index e4aee907b..7283d805f 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -135,24 +135,29 @@ end -- ItemSelection -- +local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true} +local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} + ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ frame_title='Choose items', - frame={w=60, h=30, l=4, t=8}, + frame={w=56, h=20, l=4, t=8}, + draggable=false, resizable=true, - resize_min={w=56, h=20}, index=DEFAULT_NIL, - selected_set=DEFAULT_NIL, + on_submit=DEFAULT_NIL, + on_cancel=DEFAULT_NIL, } function ItemSelection:init() local filter = get_cur_filters()[self.index] self.quantity = get_quantity(filter) self.num_selected = 0 + self.selected_set = {} self:addviews{ widgets.Label{ - frame={t=0}, + frame={t=0, l=0, r=10}, text={ get_desc(filter), self.quantity == 1 and '' or 's', @@ -162,6 +167,17 @@ function ItemSelection:init() ' selected)', }, }, + widgets.Label{ + frame={r=0, w=9, t=0, h=3}, + text_pen=BUILD_TEXT_PEN, + text_hpen=BUILD_TEXT_HPEN, + text={ + ' ', NEWLINE, + ' Build ', NEWLINE, + ' ', + }, + on_click=self:callback('submit'), + }, widgets.FilteredList{ frame={t=3, l=0, r=0, b=0}, case_sensitive=false, @@ -251,6 +267,22 @@ function ItemSelection:get_entry_icon(item_id) return self.selected_set[item_id] and get_selected_item_pen() or nil end +function ItemSelection:submit() + local selected_items = {} + for item_id in pairs(self.selected_set) do + table.insert(selected_items, item_id) + end + self.on_submit(selected_items) +end + +function ItemSelection:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + self.on_cancel() + return true + end + return ItemSelection.super.onInput(self, keys) +end + BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) BuildingplanScreen.ATTRS { force_pause=true, @@ -265,27 +297,19 @@ ItemSelectionScreen.ATTRS { focus_path='buildingplan/itemselection', index=DEFAULT_NIL, on_submit=DEFAULT_NIL, + on_cancel=DEFAULT_NIL, } function ItemSelectionScreen:init() - self.selected_set = {} - self:addviews{ ItemSelection{ index=self.index, - selected_set=self.selected_set, + on_submit=self.on_submit, + on_cancel=self.on_cancel, } } end -function ItemSelectionScreen:onDismiss() - local selected_items = {} - for item_id in pairs(self.selected_set) do - table.insert(selected_items, item_id) - end - self.on_submit(selected_items) -end - -------------------------------- -- PlannerOverlay -- @@ -714,20 +738,27 @@ function PlannerOverlay:onInput(keys) end local choose = self.subviews.choose if choose.enabled() and choose:getOptionValue() then - local chosen_items = {} + local chosen_items, active_screens = {}, {} local pending = num_filters for idx = num_filters,1,-1 do chosen_items[idx] = {} if (self.subviews['item'..idx].available or 0) > 0 then - ItemSelectionScreen{ + active_screens[idx] = ItemSelectionScreen{ index=idx, on_submit=function(items) chosen_items[idx] = items + active_screens[idx]:dismiss() + active_screens[idx] = nil pending = pending - 1 if pending == 0 then self:place_building(pos, chosen_items) end end, + on_cancel=function() + for i,scr in pairs(active_screens) do + scr:dismiss() + end + end, }:show() else pending = pending - 1 From e9555c29be83c3b21278aea082dffdb3f9fb2eeb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 02:03:39 -0800 Subject: [PATCH 075/126] initialize heat safety option to 'Any' --- plugins/lua/buildingplan.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 7283d805f..fcd7b83fd 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -650,6 +650,7 @@ function PlannerOverlay:init() {label='Magma', value=2, pen=COLOR_RED}, {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, + initial_option=0, on_change=function(heat) setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) end, From 348ac55f4cb5160122f073814de88dfb579e28d6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 21:17:03 -0800 Subject: [PATCH 076/126] allow singleton selection for items --- plugins/lua/buildingplan.lua | 238 +++++++++++++++++++++++++---------- 1 file changed, 171 insertions(+), 67 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index fcd7b83fd..cb5926723 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -131,6 +131,15 @@ local function get_selected_item_pen() return SELECTED_ITEM_PEN end +BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) +BuildingplanScreen.ATTRS { + force_pause=true, + pass_pause=false, + pass_movement_keys=true, + pass_mouse_clicks=false, + defocusable=false, +} + -------------------------------- -- ItemSelection -- @@ -154,15 +163,16 @@ function ItemSelection:init() self.quantity = get_quantity(filter) self.num_selected = 0 self.selected_set = {} + local plural = self.quantity == 1 and '' or 's' self:addviews{ widgets.Label{ frame={t=0, l=0, r=10}, text={ get_desc(filter), - self.quantity == 1 and '' or 's', + plural, NEWLINE, - ('Select up to %d items ('):format(self.quantity), + ('Select up to %d item%s ('):format(self.quantity, plural), {text=function() return self.num_selected end}, ' selected)', }, @@ -179,11 +189,52 @@ function ItemSelection:init() on_click=self:callback('submit'), }, widgets.FilteredList{ - frame={t=3, l=0, r=0, b=0}, + view_id='flist', + frame={t=3, l=0, r=0, b=4}, case_sensitive=false, choices=self:get_choices(), icon_width=2, - on_submit=self:callback('toggle_item'), + on_submit=self:callback('toggle_group'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=2}, + key='SELECT', + label='Use all/none selected', + auto_width=true, + on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.HotkeyLabel{ + frame={l=32, b=2}, + key='LEAVESCREEN', + label='Cancel build', + auto_width=true, + on_activate=function() self.on_cancel() end, + }, + widgets.HotkeyLabel{ + frame={l=0, b=1}, + key='KEYBOARD_CURSOR_RIGHT_FAST', + key_sep=' : ', + label='Use one selected', + auto_width=true, + on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, b=1, w=5}, + text_pen=COLOR_LIGHTGREEN, + text='Right', + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + key='KEYBOARD_CURSOR_LEFT_FAST', + key_sep=' : ', + label='Use one fewer selected', + auto_width=true, + on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, b=0, w=4}, + text_pen=COLOR_LIGHTGREEN, + text='Left', }, } end @@ -199,67 +250,79 @@ end function ItemSelection:get_choices() local item_ids = getAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index - 1) - local buckets, selected_buckets = {}, {} + local buckets = {} for _,item_id in ipairs(item_ids) do local item = df.item.find(item_id) if not item then goto continue end local desc = dfhack.items.getDescription(item, 0, true) if buckets[desc] then local bucket = buckets[desc] - table.insert(bucket.item_ids, item_id) - bucket.quantity = bucket.quantity + 1 + table.insert(bucket.data.item_ids, item_id) + bucket.data.quantity = bucket.data.quantity + 1 else local entry = { - text=desc, search_key=make_search_key(desc), icon=self:callback('get_entry_icon', item_id), - item_ids={item_id}, - item_type=item:getType(), - item_subtype=item:getSubtype(), - quantity=1, - selected=false, + data={ + item_ids={item_id}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + quality=item:getQuality(), + selected=0, + }, } buckets[desc] = entry end ::continue:: end - local selected_qty = 0 - for bucket in pairs(selected_buckets) do - for _,item_id in ipairs(bucket.item_ids) do - self.selected_set[item_id] = true - end - selected_qty = selected_qty + bucket.quantity - bucket.selected = true - if selected_qty >= self.quantity then break end - end - self.num_selected = selected_qty local choices = {} - for _,choice in pairs(buckets) do - choice.text = ('(%d) %s'):format(choice.quantity, choice.text) + for desc,choice in pairs(buckets) do + local data = choice.data + choice.text = { + {width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end}, + {gap=2, text=desc}, + } table.insert(choices, choice) end local function choice_sort(a, b) - return a.item_type < b.item_type or - (a.item_type == b.item_type and a.item_subtype < b.item_subtype) or - (a.item_type == b.item_type and a.item_subtype == b.item_subtype and a.search_key < b.search_key) + local ad, bd = a.data, b.data + return ad.item_type < bd.item_type or + (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) end table.sort(choices, choice_sort) return choices end -function ItemSelection:toggle_item(_, choice) - if choice.selected then - for _,item_id in ipairs(choice.item_ids) do - self.selected_set[item_id] = nil - end - self.num_selected = self.num_selected - choice.quantity - choice.selected = false - elseif self.quantity > self.num_selected then - for _,item_id in ipairs(choice.item_ids) do - self.selected_set[item_id] = true - end - self.num_selected = self.num_selected + choice.quantity - choice.selected = true +function ItemSelection:increment_group(idx, choice) + local data = choice.data + if self.quantity <= self.num_selected then return false end + if data.selected >= data.quantity then return false end + data.selected = data.selected + 1 + self.num_selected = self.num_selected + 1 + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = true + return true +end + +function ItemSelection:decrement_group(idx, choice) + local data = choice.data + if data.selected <= 0 then return false end + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = nil + self.num_selected = self.num_selected - 1 + data.selected = data.selected - 1 + return true +end + +function ItemSelection:toggle_group(idx, choice) + local data = choice.data + if data.selected > 0 then + while self:decrement_group(idx, choice) do end + else + while self:increment_group(idx, choice) do end end end @@ -279,19 +342,26 @@ function ItemSelection:onInput(keys) if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then self.on_cancel() return true + elseif keys._MOUSE_L_DOWN then + local list = self.subviews.flist.list + local idx = list:getIdxUnderMouse() + if idx then + list:setSelected(idx) + local modstate = dfhack.internal.getModstate() + if modstate & 2 > 0 then -- ctrl + local choice = list:getChoices()[idx] + if modstate & 1 > 0 then -- shift + self:decrement_group(idx, choice) + else + self:increment_group(idx, choice) + end + return true + end + end end return ItemSelection.super.onInput(self, keys) end -BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) -BuildingplanScreen.ATTRS { - force_pause=true, - pass_pause=false, - pass_movement_keys=true, - pass_mouse_clicks=false, - defocusable=false, -} - ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) ItemSelectionScreen.ATTRS { focus_path='buildingplan/itemselection', @@ -311,7 +381,48 @@ function ItemSelectionScreen:init() end -------------------------------- --- PlannerOverlay +-- FilterSelection +-- + +-- returns whether the items matched by the specified filter can have a quality +-- rating. This also conveniently indicates whether an item can be decorated. +local function can_be_improved(idx) + local filter = get_cur_filters()[idx] + if filter.flags2 and filter.flags2.building_material then + return false; + end + return filter.item_type ~= df.item_type.WOOD and + filter.item_type ~= df.item_type.BLOCKS and + filter.item_type ~= df.item_type.BAR and + filter.item_type ~= df.item_type.BOULDER +end + +FilterSelection = defclass(FilterSelection, widgets.Window) +FilterSelection.ATTRS{ + frame_title='Choose filters', + frame={w=60, h=40, l=4, t=8}, + draggable=false, + resizable=true, + index=DEFAULT_NIL, +} + +function FilterSelection:init() +end + +FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) +FilterSelectionScreen.ATTRS { + focus_path='buildingplan/filterselection', + index=DEFAULT_NIL, +} + +function FilterSelectionScreen:init() + self:addviews{ + FilterSelection{index=self.index} + } +end + +-------------------------------- +-- ItemLine -- local function cur_building_has_no_area() @@ -512,6 +623,10 @@ local function get_placement_errors() return out end +-------------------------------- +-- PlannerOverlay +-- + PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ default_pos={x=5,y=9}, @@ -932,7 +1047,7 @@ function PlannerOverlay:place_building(pos, chosen_items) end -------------------------------- --- InspectorOverlay +-- InspectorLine -- local function get_building_filters() @@ -981,6 +1096,10 @@ function InspectorLine:reset() self.status = nil end +-------------------------------- +-- InspectorOverlay +-- + InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, @@ -1053,19 +1172,4 @@ OVERLAY_WIDGETS = { inspector=InspectorOverlay, } --- returns whether the items matched by the specified filter can have a quality --- rating. This also conveniently indicates whether an item can be decorated. --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function item_can_be_improved(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) - if filter.flags2 and filter.flags2.building_material then - return false; - end - return filter.item_type ~= df.item_type.WOOD and - filter.item_type ~= df.item_type.BLOCKS and - filter.item_type ~= df.item_type.BAR and - filter.item_type ~= df.item_type.BOULDER -end - return _ENV From 69e9da2e79c3305ca97a6c5aa7f721e8a3ba5856 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 23:28:57 -0800 Subject: [PATCH 077/126] keep target area higlighted while choosing items --- plugins/buildingplan/buildingplan.cpp | 1 + plugins/lua/buildingplan.lua | 216 ++++++++++++++++---------- 2 files changed, 135 insertions(+), 82 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 0a05c4ed5..6c16b043e 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -171,6 +171,7 @@ static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, static void clear_state(color_ostream &out) { call_buildingplan_lua(&out, "signal_reset"); + call_buildingplan_lua(&out, "reload_cursors"); planned_buildings.clear(); tasks.clear(); for (auto &entry : job_item_repo) { diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index cb5926723..b24f5893a 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -75,17 +75,18 @@ local function is_choosing_area() return uibs.selection_pos.x >= 0 end -local function get_cur_area_dims(pos) - if not is_choosing_area() then return 1, 1, 1 end - pos = pos or uibs.pos - return math.abs(uibs.selection_pos.x - pos.x) + 1, - math.abs(uibs.selection_pos.y - pos.y) + 1, - math.abs(uibs.selection_pos.z - pos.z) + 1 +local function get_cur_area_dims(placement_data) + if not placement_data and not is_choosing_area() then return 1, 1, 1 end + local selection_pos = placement_data and placement_data.p1 or uibs.selection_pos + local pos = placement_data and placement_data.p2 or uibs.pos + return math.abs(selection_pos.x - pos.x) + 1, + math.abs(selection_pos.y - pos.y) + 1, + math.abs(selection_pos.z - pos.z) + 1 end -local function get_quantity(filter) +local function get_quantity(filter, placement_data) local quantity = filter.quantity or 1 - local dimx, dimy, dimz = get_cur_area_dims() + local dimx, dimy, dimz = get_cur_area_dims(placement_data) if quantity < 1 then quantity = (((dimx * dimy) // 4) + 1) * dimz else @@ -151,16 +152,16 @@ ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ frame_title='Choose items', frame={w=56, h=20, l=4, t=8}, - draggable=false, resizable=true, index=DEFAULT_NIL, + placement_data=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } function ItemSelection:init() local filter = get_cur_filters()[self.index] - self.quantity = get_quantity(filter) + self.quantity = get_quantity(filter, self.placement_data) self.num_selected = 0 self.selected_set = {} local plural = self.quantity == 1 and '' or 's' @@ -364,8 +365,9 @@ end ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) ItemSelectionScreen.ATTRS { - focus_path='buildingplan/itemselection', + focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection', index=DEFAULT_NIL, + placement_data=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } @@ -374,6 +376,7 @@ function ItemSelectionScreen:init() self:addviews{ ItemSelection{ index=self.index, + placement_data=self.placement_data, on_submit=self.on_submit, on_cancel=self.on_cancel, } @@ -401,7 +404,6 @@ FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', frame={w=60, h=40, l=4, t=8}, - draggable=false, resizable=true, index=DEFAULT_NIL, } @@ -411,7 +413,7 @@ end FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) FilterSelectionScreen.ATTRS { - focus_path='buildingplan/filterselection', + focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', index=DEFAULT_NIL, } @@ -818,6 +820,73 @@ function PlannerOverlay:clear_filter(idx) setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx, "") end +local function get_placement_data() + local pos = uibs.pos + local direction = uibs.direction + local width, height, depth = get_cur_area_dims() + local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( + width, height, uibs.building_type, uibs.building_subtype, + uibs.custom_type, direction) + -- get the upper-left corner of the building/area at min z-level + local has_selection = is_choosing_area() + local start_pos = xyz2pos( + has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, + has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, + has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z + ) + if uibs.building_type == df.building_type.ScrewPump then + if direction == df.screw_pump_direction.FromSouth then + start_pos.y = start_pos.y + 1 + elseif direction == df.screw_pump_direction.FromEast then + start_pos.x = start_pos.x + 1 + end + end + local min_x, max_x = start_pos.x, start_pos.x + local min_y, max_y = start_pos.y, start_pos.y + local min_z, max_z = start_pos.z, start_pos.z + if adjusted_width == 1 and adjusted_height == 1 + and (width > 1 or height > 1 or depth > 1) then + max_x = min_x + width - 1 + max_y = min_y + height - 1 + max_z = math.max(uibs.selection_pos.z, pos.z) + end + return { + p1=xyz2pos(min_x, min_y, min_z), + p2=xyz2pos(max_x, max_y, max_z), + width=adjusted_width, + height=adjusted_height + } +end + +function PlannerOverlay:save_placement() + self.saved_placement = get_placement_data() + if (uibs.selection_pos:isValid()) then + self.saved_selection_pos_valid = true + self.saved_selection_pos = copyall(uibs.selection_pos) + self.saved_pos = copyall(uibs.pos) + uibs.selection_pos:clear() + else + self.saved_selection_pos = copyall(self.saved_placement.p1) + self.saved_pos = copyall(self.saved_placement.p2) + self.saved_pos.x = self.saved_pos.x + self.saved_placement.width - 1 + self.saved_pos.y = self.saved_pos.y + self.saved_placement.height - 1 + end +end + +function PlannerOverlay:restore_placement() + if self.saved_selection_pos_valid then + uibs.selection_pos = self.saved_selection_pos + self.saved_selection_pos_valid = nil + else + uibs.selection_pos:clear() + end + self.saved_selection_pos = nil + self.saved_pos = nil + local placement_data = self.saved_placement + self.saved_placement = nil + return placement_data +end + function PlannerOverlay:onInput(keys) if not is_plannable() then return false end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then @@ -845,8 +914,7 @@ function PlannerOverlay:onInput(keys) return true end if #uibs.errors > 0 then return true end - local pos = dfhack.gui.getMousePos() - if pos then + if dfhack.gui.getMousePos() then if is_choosing_area() or cur_building_has_no_area() then local num_filters = #get_cur_filters() if num_filters == 0 then @@ -854,6 +922,7 @@ function PlannerOverlay:onInput(keys) end local choose = self.subviews.choose if choose.enabled() and choose:getOptionValue() then + self:save_placement() local chosen_items, active_screens = {}, {} local pending = num_filters for idx = num_filters,1,-1 do @@ -861,19 +930,21 @@ function PlannerOverlay:onInput(keys) if (self.subviews['item'..idx].available or 0) > 0 then active_screens[idx] = ItemSelectionScreen{ index=idx, + placement_data=self.saved_placement, on_submit=function(items) chosen_items[idx] = items active_screens[idx]:dismiss() active_screens[idx] = nil pending = pending - 1 if pending == 0 then - self:place_building(pos, chosen_items) + self:place_building(self:restore_placement(), chosen_items) end end, on_cancel=function() for i,scr in pairs(active_screens) do scr:dismiss() end + self:restore_placement() end, }:show() else @@ -881,7 +952,7 @@ function PlannerOverlay:onInput(keys) end end else - self:place_building(pos) + self:place_building(get_placement_data()) end return true elseif not is_choosing_area() then @@ -898,10 +969,12 @@ function PlannerOverlay:render(dc) PlannerOverlay.super.render(self, dc) end -local GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, - tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} -local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, - tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} +local GOOD_PEN, BAD_PEN +function reload_cursors() + GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} + BAD_PEN = to_pen{ch='X', fg=COLOR_RED, tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} +end +reload_cursors() function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) @@ -912,17 +985,18 @@ function PlannerOverlay:onRenderFrame(dc, rect) uibs.building_type, uibs.building_subtype, uibs.custom_type)) end - if not is_choosing_area() then return end + local selection_pos = self.saved_selection_pos or uibs.selection_pos + if not selection_pos or selection_pos.x < 0 then return end - local pos = uibs.pos + local pos = self.saved_pos or uibs.pos local bounds = { - x1 = math.min(uibs.selection_pos.x, pos.x), - x2 = math.max(uibs.selection_pos.x, pos.x), - y1 = math.min(uibs.selection_pos.y, pos.y), - y2 = math.max(uibs.selection_pos.y, pos.y), + x1 = math.min(selection_pos.x, pos.x), + x2 = math.max(selection_pos.x, pos.x), + y1 = math.min(selection_pos.y, pos.y), + y2 = math.max(selection_pos.y, pos.y), } - local pen = #uibs.errors > 0 and BAD_PEN or GOOD_PEN + local pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN local function get_overlay_pen(pos) return pen @@ -931,69 +1005,47 @@ function PlannerOverlay:onRenderFrame(dc, rect) guidm.renderMapOverlay(get_overlay_pen, bounds) end -function PlannerOverlay:place_building(pos, chosen_items) - local direction = uibs.direction - local width, height, depth = get_cur_area_dims(pos) - local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( - width, height, uibs.building_type, uibs.building_subtype, - uibs.custom_type, direction) - -- get the upper-left corner of the building/area at min z-level - local has_selection = is_choosing_area() - local start_pos = xyz2pos( - has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, - has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, - has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z - ) - if uibs.building_type == df.building_type.ScrewPump then - if direction == df.screw_pump_direction.FromSouth then - start_pos.y = start_pos.y + 1 - elseif direction == df.screw_pump_direction.FromEast then - start_pos.x = start_pos.x + 1 +function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2) + local subtype = uibs.building_subtype + if pos.z == corner1.z then + local opt = self.subviews.stairs_bottom_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_DOWN then + subtype = df.construction_type.UpStair + end + else + subtype = opt + end + elseif pos.z == corner2.z then + local opt = self.subviews.stairs_top_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_UP then + subtype = df.construction_type.DownStair + end + else + subtype = opt end end - local min_x, max_x = start_pos.x, start_pos.x - local min_y, max_y = start_pos.y, start_pos.y - local min_z, max_z = start_pos.z, start_pos.z - if adjusted_width == 1 and adjusted_height == 1 - and (width > 1 or height > 1 or depth > 1) then - max_x = min_x + width - 1 - max_y = min_y + height - 1 - max_z = math.max(uibs.selection_pos.z, pos.z) - end + return subtype +end + +function PlannerOverlay:place_building(placement_data, chosen_items) + local p1, p2 = placement_data.p1, placement_data.p2 local blds = {} local subtype = uibs.building_subtype - for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do + for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do local pos = xyz2pos(x, y, z) if is_stairs() then - if z == min_z then - subtype = self.subviews.stairs_bottom_subtype:getOptionValue() - if subtype == 'auto' then - local tt = dfhack.maps.getTileType(pos) - local shape = df.tiletype.attrs[tt].shape - if shape == df.tiletype_shape.STAIR_DOWN then - subtype = uibs.building_subtype - else - subtype = df.construction_type.UpStair - end - end - elseif z == max_z then - subtype = self.subviews.stairs_top_subtype:getOptionValue() - if subtype == 'auto' then - local tt = dfhack.maps.getTileType(pos) - local shape = df.tiletype.attrs[tt].shape - if shape == df.tiletype_shape.STAIR_UP then - subtype = uibs.building_subtype - else - subtype = df.construction_type.DownStair - end - end - else - subtype = uibs.building_subtype - end + subtype = self:get_stairs_subtype(pos, p1, p2) end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, - width=adjusted_width, height=adjusted_height, direction=direction} + width=placement_data.width, height=placement_data.height, + direction=uibs.direction} if err then for _,b in ipairs(blds) do dfhack.buildings.deconstruct(b) From f09eeee864bfa8234a5fd95cdabd4c51733d5ed5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 20 Feb 2023 00:11:05 -0800 Subject: [PATCH 078/126] only enable clear filter hotkey when a filter is set --- plugins/lua/buildingplan.lua | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b24f5893a..308afd797 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -555,7 +555,7 @@ function ItemLine:onInput(keys) end function ItemLine:get_x_pen() - return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx) and + return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and COLOR_GREEN or COLOR_GREY end @@ -707,11 +707,13 @@ function PlannerOverlay:init() text={ 'Selected area: ', {text=function() - return ('%dx%dx%d'):format(get_cur_area_dims()) + return ('%dx%dx%d'):format(get_cur_area_dims(self.saved_placement)) end }, }, - visible=is_choosing_area, + visible=function() + return not cur_building_has_no_area() and (self.saved_placement or is_choosing_area()) + end, }, widgets.Panel{ visible=function() return #get_cur_filters() > 0 end, @@ -719,6 +721,7 @@ function PlannerOverlay:init() widgets.HotkeyLabel{ frame={b=1, l=0}, key='STRING_A042', + auto_width=true, enabled=function() return #get_cur_filters() > 1 end, on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, }, @@ -726,6 +729,7 @@ function PlannerOverlay:init() frame={b=1, l=1}, key='STRING_A047', label='Prev/next item', + auto_width=true, enabled=function() return #get_cur_filters() > 1 end, on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, @@ -733,17 +737,22 @@ function PlannerOverlay:init() frame={b=1, l=21}, key='CUSTOM_F', label='Set filter', + auto_width=true, on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ frame={b=1, l=37}, key='CUSTOM_X', label='Clear filter', + auto_width=true, on_activate=function() self:clear_filter(self.selected) end, + enabled=function() + return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + end }, widgets.CycleHotkeyLabel{ view_id='choose', - frame={b=0, l=0}, + frame={b=0, l=0, w=25}, key='CUSTOM_I', label='Choose from items:', options={{label='Yes', value=true}, @@ -759,7 +768,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='safety', - frame={b=0, l=29}, + frame={b=0, l=29, w=25}, key='CUSTOM_G', label='Building safety:', options={ @@ -817,7 +826,7 @@ function PlannerOverlay:set_filter(idx) end function PlannerOverlay:clear_filter(idx) - setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx, "") + setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1, "") end local function get_placement_data() From 1957ad4cdfeb353a4c69780e591c966578c8951e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 20 Feb 2023 15:25:36 -0800 Subject: [PATCH 079/126] move the filter window a bit to the side, can pause --- plugins/lua/buildingplan.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 308afd797..5b048ff7c 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -134,8 +134,6 @@ end BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) BuildingplanScreen.ATTRS { - force_pause=true, - pass_pause=false, pass_movement_keys=true, pass_mouse_clicks=false, defocusable=false, @@ -366,6 +364,8 @@ end ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) ItemSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection', + force_pause=true, + pass_pause=false, index=DEFAULT_NIL, placement_data=DEFAULT_NIL, on_submit=DEFAULT_NIL, @@ -403,7 +403,7 @@ end FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', - frame={w=60, h=40, l=4, t=8}, + frame={w=60, h=40, l=30, t=8}, resizable=true, index=DEFAULT_NIL, } @@ -822,7 +822,7 @@ function PlannerOverlay:reset() end function PlannerOverlay:set_filter(idx) - print('TODO: set_filter', idx) + FilterSelectionScreen{index=idx}:show() end function PlannerOverlay:clear_filter(idx) From 4f2d86f50af4a85f6c1062f5f55d5e6920e47b6c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 13:04:53 -0800 Subject: [PATCH 080/126] implement hollow area placement for constructions --- plugins/buildingplan/buildingplan.cpp | 6 ++--- plugins/lua/buildingplan.lua | 37 ++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 6c16b043e..828ae4954 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -560,8 +560,8 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return scanAvailableItems(out, type, subtype, custom, index); } -static bool hasMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering hasMaterialFilter\n"); +static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering hasFilter\n"); return false; } @@ -708,7 +708,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), - DFHACK_LUA_FUNCTION(hasMaterialFilter), + DFHACK_LUA_FUNCTION(hasFilter), DFHACK_LUA_FUNCTION(setMaterialFilter), DFHACK_LUA_FUNCTION(setHeatSafetyFilter), DFHACK_LUA_FUNCTION(getDescString), diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 5b048ff7c..fe74ca7c0 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -146,6 +146,8 @@ BuildingplanScreen.ATTRS { local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true} local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} +local recently_selected = {} + ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ frame_title='Choose items', @@ -442,8 +444,12 @@ local function is_plannable() and uibs.building_subtype == df.construction_type.TrackNSEW) end -local function is_stairs() +local function is_construction() return uibs.building_type == df.building_type.Construction +end + +local function is_stairs() + return is_construction and uibs.building_subtype == df.construction_type.UpDownStair end @@ -555,7 +561,7 @@ function ItemLine:onInput(keys) end function ItemLine:get_x_pen() - return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and COLOR_GREEN or COLOR_GREY end @@ -678,6 +684,17 @@ function PlannerOverlay:init() is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, + widgets.CycleHotkeyLabel{ + view_id='hollow', + frame={t=3, l=4}, + key='CUSTOM_H', + label='Hollow area:', + visible=is_construction, + options={ + {label='No', value=false}, + {label='Yes', value=true}, + }, + }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', frame={t=4, l=4}, @@ -747,7 +764,7 @@ function PlannerOverlay:init() auto_width=true, on_activate=function() self:clear_filter(self.selected) end, enabled=function() - return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) end }, widgets.CycleHotkeyLabel{ @@ -904,6 +921,7 @@ function PlannerOverlay:onInput(keys) return true end self.selected = 1 + self.subviews.hollow:setOption(false) self.subviews.choose:setOption(false) self:reset() reset_counts_flag = true @@ -1005,10 +1023,16 @@ function PlannerOverlay:onRenderFrame(dc, rect) y2 = math.max(selection_pos.y, pos.y), } + local hollow = self.subviews.hollow:getOptionValue() local pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN local function get_overlay_pen(pos) - return pen + if not hollow then return pen end + if pos.x == bounds.x1 or pos.x == bounds.x2 or + pos.y == bounds.y1 or pos.y == bounds.y2 then + return pen + end + return gui.TRANSPARENT_PEN end guidm.renderMapOverlay(get_overlay_pen, bounds) @@ -1045,8 +1069,12 @@ end function PlannerOverlay:place_building(placement_data, chosen_items) local p1, p2 = placement_data.p1, placement_data.p2 local blds = {} + local hollow = self.subviews.hollow:getOptionValue() local subtype = uibs.building_subtype for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do + if hollow and x ~= p1.x and x ~= p2.x and y ~= p1.y and y ~= p2.y then + goto continue + end local pos = xyz2pos(x, y, z) if is_stairs() then subtype = self:get_stairs_subtype(pos, p1, p2) @@ -1073,6 +1101,7 @@ function PlannerOverlay:place_building(placement_data, chosen_items) if k == 'speed' then bld.speed = uibs.speed end end table.insert(blds, bld) + ::continue:: end end end self.subviews.item1:reduce_quantity() self.subviews.item2:reduce_quantity() From c52b2c27c8c5b67b5d8c5ebf68bc5c03cd9f2e1e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 15:05:06 -0800 Subject: [PATCH 081/126] implement automaterial in buildingplan --- plugins/lua/buildingplan.lua | 133 +++++++++++++++++++++++++++++------ 1 file changed, 111 insertions(+), 22 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index fe74ca7c0..370ab5159 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -146,7 +146,40 @@ BuildingplanScreen.ATTRS { local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true} local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} -local recently_selected = {} +-- map of building type -> {set=set of recently used, list=list of recently used} +-- most recent entries are at the *end* of the list +local recently_used = {} + +local function sort_by_type(a, b) + local ad, bd = a.data, b.data + return ad.item_type < bd.item_type or + (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) +end + +local function sort_by_recency(a, b) + local tracker = recently_used[uibs.building_type] + if not tracker then return sort_by_type(a, b) end + local recent_a, recent_b = tracker.set[a.search_key], tracker.set[b.search_key] + -- if they're both in the set, return the one with the greater index, + -- indicating more recent + if recent_a and recent_b then return recent_a > recent_b end + if recent_a and not recent_b then return true end + if not recent_a and recent_b then return false end + return sort_by_type(a, b) +end + +local function sort_by_name(a, b) + return a.search_key < b.search_key or + (a.search_key == b.search_key and sort_by_type(a, b)) +end + +local function sort_by_quantity(a, b) + local ad, bd = a.data, b.data + return ad.quantity > bd.quantity or + (ad.quantity == bd.quantity and sort_by_type(a, b)) +end ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ @@ -193,53 +226,80 @@ function ItemSelection:init() view_id='flist', frame={t=3, l=0, r=0, b=4}, case_sensitive=false, - choices=self:get_choices(), + choices=self:get_choices(sort_by_recency), icon_width=2, on_submit=self:callback('toggle_group'), }, - widgets.HotkeyLabel{ + widgets.CycleHotkeyLabel{ frame={l=0, b=2}, + key='CUSTOM_CTRL_X', + label='Sort by:', + options={ + {label='Recently used', value=sort_by_recency}, + {label='Name', value=sort_by_name}, + {label='Amount', value=sort_by_quantity}, + }, + on_change=self:callback('on_sort'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=1}, key='SELECT', - label='Use all/none selected', + label='Use all/none', auto_width=true, on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, }, widgets.HotkeyLabel{ - frame={l=32, b=2}, + frame={l=22, b=1}, + key='CUSTOM_CTRL_D', + label='Build', + auto_width=true, + on_activate=self:callback('submit'), + }, + widgets.HotkeyLabel{ + frame={l=38, b=1}, key='LEAVESCREEN', - label='Cancel build', + label='Go back', auto_width=true, - on_activate=function() self.on_cancel() end, + on_activate=self:callback('on_cancel'), }, widgets.HotkeyLabel{ - frame={l=0, b=1}, + frame={l=0, b=0}, key='KEYBOARD_CURSOR_RIGHT_FAST', key_sep=' : ', - label='Use one selected', + label='Use one', auto_width=true, on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, }, widgets.Label{ - frame={l=6, b=1, w=5}, + frame={l=6, b=0, w=5}, text_pen=COLOR_LIGHTGREEN, text='Right', }, widgets.HotkeyLabel{ - frame={l=0, b=0}, + frame={l=23, b=0}, key='KEYBOARD_CURSOR_LEFT_FAST', key_sep=' : ', - label='Use one fewer selected', + label='Use one fewer', auto_width=true, on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, }, widgets.Label{ - frame={l=6, b=0, w=4}, + frame={l=29, b=0, w=4}, text_pen=COLOR_LIGHTGREEN, text='Left', }, } end +-- resort and restore selection +function ItemSelection:on_sort(sort_fn) + local flist = self.subviews.flist + local saved_filter = flist:getFilter() + flist:setFilter('') + flist:setChoices(self:get_choices(sort_fn), flist:getSelected()) + flist:setFilter(saved_filter) +end + local function make_search_key(str) local out = '' for c in str:gmatch("[%w%s]") do @@ -248,7 +308,7 @@ local function make_search_key(str) return out end -function ItemSelection:get_choices() +function ItemSelection:get_choices(sort_fn) local item_ids = getAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index - 1) local buckets = {} @@ -286,14 +346,7 @@ function ItemSelection:get_choices() } table.insert(choices, choice) end - local function choice_sort(a, b) - local ad, bd = a.data, b.data - return ad.item_type < bd.item_type or - (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or - (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or - (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) - end - table.sort(choices, choice_sort) + table.sort(choices, sort_fn) return choices end @@ -331,11 +384,47 @@ function ItemSelection:get_entry_icon(item_id) return self.selected_set[item_id] and get_selected_item_pen() or nil end +local function track_recently_used(choices) + -- use same set for all subtypes + local tracker = ensure_key(recently_used, uibs.building_type) + for _,choice in ipairs(choices) do + local data = choice.data + if data.selected <= 0 then goto continue end + local key = choice.search_key + local recent_set = ensure_key(tracker, 'set') + local recent_list = ensure_key(tracker, 'list') + if recent_set[key] then + if recent_list[#recent_list] ~= key then + for i,v in ipairs(recent_list) do + if v == key then + table.remove(recent_list, i) + table.insert(recent_list, key) + break + end + end + tracker.set = utils.invert(recent_list) + end + else + -- only keep most recent 10 + if #recent_list >= 10 then + -- remove least recently used from list and set + recent_set[table.remove(recent_list, 1)] = nil + end + table.insert(recent_list, key) + recent_set[key] = #recent_list + end + ::continue:: + end +end + function ItemSelection:submit() local selected_items = {} for item_id in pairs(self.selected_set) do table.insert(selected_items, item_id) end + if #selected_items > 0 then + track_recently_used(self.subviews.flist:getChoices()) + end self.on_submit(selected_items) end From a0798178a6380422bc1b6ee12678f0140d6a633f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 15:42:30 -0800 Subject: [PATCH 082/126] ensure item quantity is correct when hollow --- plugins/lua/buildingplan.lua | 50 +++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 370ab5159..9d8eb463b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -84,15 +84,16 @@ local function get_cur_area_dims(placement_data) math.abs(selection_pos.z - pos.z) + 1 end -local function get_quantity(filter, placement_data) +local function get_quantity(filter, hollow, placement_data) local quantity = filter.quantity or 1 local dimx, dimy, dimz = get_cur_area_dims(placement_data) if quantity < 1 then - quantity = (((dimx * dimy) // 4) + 1) * dimz - else - quantity = quantity * dimx * dimy * dimz + return (((dimx * dimy) // 4) + 1) * dimz + end + if hollow and dimx > 2 and dimy > 2 then + return quantity * (2*dimx + 2*dimy - 4) * dimz end - return quantity + return quantity * dimx * dimy * dimz end local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil @@ -187,14 +188,13 @@ ItemSelection.ATTRS{ frame={w=56, h=20, l=4, t=8}, resizable=true, index=DEFAULT_NIL, - placement_data=DEFAULT_NIL, + quantity=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } function ItemSelection:init() local filter = get_cur_filters()[self.index] - self.quantity = get_quantity(filter, self.placement_data) self.num_selected = 0 self.selected_set = {} local plural = self.quantity == 1 and '' or 's' @@ -458,7 +458,7 @@ ItemSelectionScreen.ATTRS { force_pause=true, pass_pause=false, index=DEFAULT_NIL, - placement_data=DEFAULT_NIL, + quantity=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } @@ -467,7 +467,7 @@ function ItemSelectionScreen:init() self:addviews{ ItemSelection{ index=self.index, - placement_data=self.placement_data, + quantity=self.quantity, on_submit=self.on_submit, on_cancel=self.on_cancel, } @@ -591,6 +591,7 @@ ItemLine = defclass(ItemLine, widgets.Panel) ItemLine.ATTRS{ idx=DEFAULT_NIL, is_selected_fn=DEFAULT_NIL, + is_hollow_fn=DEFAULT_NIL, on_select=DEFAULT_NIL, on_filter=DEFAULT_NIL, on_clear_filter=DEFAULT_NIL, @@ -688,7 +689,7 @@ end function ItemLine:get_item_line_text() local idx = self.idx local filter = get_cur_filters()[idx] - local quantity = get_quantity(filter) + local quantity = get_quantity(filter, self.is_hollow_fn()) self.desc = self.desc or get_desc(filter) @@ -708,7 +709,7 @@ end function ItemLine:reduce_quantity() if not self.available then return end local filter = get_cur_filters()[self.idx] - self.available = math.max(0, self.available - get_quantity(filter)) + self.available = math.max(0, self.available - get_quantity(filter, self.is_hollow_fn())) end local function get_placement_errors() @@ -750,6 +751,10 @@ function PlannerOverlay:init() self.selected = idx end + local function is_hollow_fn() + return self.subviews.hollow:getOptionValue() + end + main_panel:addviews{ widgets.Label{ frame={}, @@ -758,20 +763,20 @@ function PlannerOverlay:init() visible=function() return #get_cur_filters() == 0 end, }, ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, - is_selected_fn=make_is_selected_fn(1), on_select=on_select_fn, - on_filter=self:callback('set_filter'), + is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, - is_selected_fn=make_is_selected_fn(2), on_select=on_select_fn, - on_filter=self:callback('set_filter'), + is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, - is_selected_fn=make_is_selected_fn(3), on_select=on_select_fn, - on_filter=self:callback('set_filter'), + is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, - is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, - on_filter=self:callback('set_filter'), + is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='hollow', @@ -1032,13 +1037,15 @@ function PlannerOverlay:onInput(keys) if #uibs.errors > 0 then return true end if dfhack.gui.getMousePos() then if is_choosing_area() or cur_building_has_no_area() then - local num_filters = #get_cur_filters() + local filters = get_cur_filters() + local num_filters = #filters if num_filters == 0 then return false -- we don't add value; let the game place it end local choose = self.subviews.choose if choose.enabled() and choose:getOptionValue() then self:save_placement() + local is_hollow = self.subviews.hollow:getOptionValue() local chosen_items, active_screens = {}, {} local pending = num_filters for idx = num_filters,1,-1 do @@ -1046,7 +1053,8 @@ function PlannerOverlay:onInput(keys) if (self.subviews['item'..idx].available or 0) > 0 then active_screens[idx] = ItemSelectionScreen{ index=idx, - placement_data=self.saved_placement, + quantity=get_quantity(filters[idx], is_hollow, + self.saved_placement), on_submit=function(items) chosen_items[idx] = items active_screens[idx]:dismiss() From 097e955796269e07b17e24edfb81875d1857cb59 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 18:05:15 -0800 Subject: [PATCH 083/126] infrastructure for item filtering --- plugins/buildingplan/buildingplan.cpp | 174 ++++++++++-------- plugins/buildingplan/buildingplan.h | 4 +- plugins/buildingplan/buildingplan_cycle.cpp | 9 +- plugins/buildingplan/itemfilter.cpp | 189 ++++++++++++++++++++ plugins/buildingplan/itemfilter.h | 38 ++++ plugins/buildingplan/plannedbuilding.cpp | 56 ++++-- plugins/buildingplan/plannedbuilding.h | 5 +- plugins/lua/buildingplan.lua | 6 +- 8 files changed, 393 insertions(+), 88 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 828ae4954..7359d842c 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -73,8 +73,9 @@ struct BuildingTypeKeyHash { static PersistentDataItem config; // for use in counting available materials for the UI -static unordered_map, BuildingTypeKeyHash> job_item_repo; +static unordered_map, BuildingTypeKeyHash> job_item_cache; static unordered_map cur_heat_safety; +static unordered_map, BuildingTypeKeyHash> cur_item_filters; // building id -> PlannedBuilding static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) @@ -96,6 +97,87 @@ void PlannedBuilding::remove(color_ostream &out) { static const int32_t CYCLE_TICKS = 600; // twice per game day static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle +static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); + + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static int get_num_filters(color_ostream &out, BuildingTypeKey key) { + int num_filters = 0; + if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + }, + [&](lua_State *L) { + num_filters = lua_tonumber(L, -1); + })) { + return 0; + } + return num_filters; +} + +static vector & get_job_items(color_ostream &out, BuildingTypeKey key) { + if (job_item_cache.count(key)) + return job_item_cache[key]; + const int num_filters = get_num_filters(out, key); + auto &jitems = job_item_cache[key]; + for (int index = 0; index < num_filters; ++index) { + bool failed = false; + if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + Lua::Push(L, index+1); + }, + [&](lua_State *L) { + df::job_item *jitem = Lua::GetDFObject(L, -1); + DEBUG(status,out).print("retrieving job_item for (%d, %d, %d) index=%d: %p\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem); + if (!jitem) + failed = true; + else + jitems.emplace_back(jitem); + }) || failed) { + jitems.clear(); + break; + } + } + return jitems; +} + +static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { + if (cur_heat_safety.count(key)) + return cur_heat_safety.at(key); + return HEAT_SAFETY_ANY; +} + +static vector & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { + if (cur_item_filters.count(key)) + return cur_item_filters[key]; + + vector &filters = cur_item_filters[key]; + filters.resize(get_job_items(out, key).size()); + return filters; +} + static command_result do_command(color_ostream &out, vector ¶meters); void buildingplan_cycle(color_ostream &out, Tasks &tasks, unordered_map &planned_buildings); @@ -149,37 +231,19 @@ static void validate_config(color_ostream &out, bool verbose = false) { set_config_bool(config, CONFIG_BARS, false); } -static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, - int nargs = 0, int nres = 0, - Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, - Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { - DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); - - CoreSuspender guard; - - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!out) - out = &Core::getInstance().getConsole(); - - return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, - nargs, nres, - std::forward(args_lambda), - std::forward(res_lambda)); -} - static void clear_state(color_ostream &out) { call_buildingplan_lua(&out, "signal_reset"); call_buildingplan_lua(&out, "reload_cursors"); planned_buildings.clear(); tasks.clear(); - for (auto &entry : job_item_repo) { + cur_heat_safety.clear(); + cur_item_filters.clear(); + for (auto &entry : job_item_cache ) { for (auto &jitem : entry.second) { delete jitem; } } - job_item_repo.clear(); + job_item_cache.clear(); } DFhackCExport command_result plugin_load_data (color_ostream &out) { @@ -199,7 +263,15 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { const size_t num_building_configs = building_configs.size(); for (size_t idx = 0; idx < num_building_configs; ++idx) { PlannedBuilding pb(out, building_configs[idx]); - registerPlannedBuilding(out, pb); + df::building *bld = df::building::find(pb.id); + if (!bld) { + WARN(status).print("cannot find building %d; halting load\n", pb.id); + } + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + if (pb.item_filters.size() != get_item_filters(out, key).size()) + WARN(status).print("loaded state for building %d doesn't match world\n", pb.id); + else + registerPlannedBuilding(out, pb); } return CR_OK; @@ -438,30 +510,12 @@ static bool setSetting(color_ostream &out, string name, bool value) { static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) { DEBUG(status,out).print("entering isPlannableBuilding\n"); - int num_filters = 0; - if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, - [&](lua_State *L) { - Lua::Push(L, type); - Lua::Push(L, subtype); - Lua::Push(L, custom); - }, - [&](lua_State *L) { - num_filters = lua_tonumber(L, -1); - })) { - return false; - } - return num_filters >= 1; + return get_num_filters(out, BuildingTypeKey(type, subtype, custom)) >= 1; } static bool isPlannedBuilding(color_ostream &out, df::building *bld) { TRACE(status,out).print("entering isPlannedBuilding\n"); - return bld && planned_buildings.count(bld->id) > 0; -} - -static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { - if (cur_heat_safety.count(key)) - return cur_heat_safety.at(key); - return HEAT_SAFETY_ANY; + return bld && planned_buildings.count(bld->id); } static bool addPlannedBuilding(color_ostream &out, df::building *bld) { @@ -471,7 +525,7 @@ static bool addPlannedBuilding(color_ostream &out, df::building *bld) { bld->getCustomType())) return false; BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); - PlannedBuilding pb(out, bld, get_heat_safety_filter(key)); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key)); return registerPlannedBuilding(out, pb); } @@ -492,30 +546,10 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = get_heat_safety_filter(key); - auto &job_items = job_item_repo[key]; - if (index >= (int)job_items.size()) { - for (int i = job_items.size(); i <= index; ++i) { - bool failed = false; - if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, - [&](lua_State *L) { - Lua::Push(L, type); - Lua::Push(L, subtype); - Lua::Push(L, custom); - Lua::Push(L, index+1); - }, - [&](lua_State *L) { - df::job_item *jitem = Lua::GetDFObject(L, -1); - DEBUG(status,out).print("retrieving job_item for index=%d: %p\n", - index, jitem); - if (!jitem) - failed = true; - else - job_items.emplace_back(jitem); - }) || failed) { - return 0; - } - } - } + auto &job_items = get_job_items(out, key); + if (job_items.size() <= index) + return 0; + auto &item_filters = get_item_filters(out, key); auto &jitem = job_items[index]; auto vector_ids = getVectorIds(out, jitem); @@ -524,7 +558,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ for (auto vector_id : vector_ids) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); for (auto &item : df::global::world->items.other[other_id]) { - if (itemPassesScreen(item) && matchesFilters(item, jitem, heat)) { + if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, item_filters[index])) { if (item_ids) item_ids->emplace_back(item->id); ++count; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 787987586..4f0d374e7 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -1,5 +1,7 @@ #pragma once +#include "itemfilter.h" + #include "modules/Persistence.h" #include "df/building.h" @@ -38,6 +40,6 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat); +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 703bab9b0..a904bc5a8 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -46,7 +46,7 @@ bool itemPassesScreen(df::item * item) { && !item->isAssignedToStockpile(); } -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) { +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; @@ -76,7 +76,8 @@ bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) { &jitem, item->getType(), item->getSubtype()) && Job::isSuitableMaterial( &jitem, item->getMaterial(), item->getMaterialIndex(), - item->getType()); + item->getType()) + && item_filter.matches(item); } bool isJobReady(color_ostream &out, const std::vector &jitems) { @@ -180,7 +181,9 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto id = task.first; auto job = bld->jobs[0]; auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx], planned_buildings.at(id).heat_safety) + auto &pb = planned_buildings.at(id); + if (matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, + pb.item_filters[filter_idx]) && Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index e69de29bb..bd35c848a 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -0,0 +1,189 @@ +#include "itemfilter.h" + +#include "Debug.h" + +#include "df/item.h" + +using namespace DFHack; + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +ItemFilter::ItemFilter() { + clear(); +} + +void ItemFilter::clear() { + min_quality = df::item_quality::Ordinary; + max_quality = df::item_quality::Masterful; + decorated_only = false; + mat_mask.whole = 0; + materials.clear(); +} + +bool ItemFilter::isEmpty() { + return min_quality == df::item_quality::Ordinary + && max_quality == df::item_quality::Masterful + && !decorated_only + && !mat_mask.whole + && materials.empty(); +} + +static bool deserializeMaterialMask(std::string ser, df::dfhack_material_category mat_mask) { + if (ser.empty()) + return true; + + if (!parseJobMaterialCategory(&mat_mask, ser)) { + DEBUG(status).print("invalid job material category serialization: '%s'", ser.c_str()); + return false; + } + return true; +} + +static bool deserializeMaterials(std::string ser, std::vector &materials) { + if (ser.empty()) + return true; + + std::vector mat_names; + split_string(&mat_names, ser, ","); + for (auto m = mat_names.begin(); m != mat_names.end(); m++) { + DFHack::MaterialInfo material; + if (!material.find(*m) || !material.isValid()) { + DEBUG(status).print("invalid material name serialization: '%s'", ser.c_str()); + return false; + } + materials.push_back(material); + } + return true; +} + +ItemFilter::ItemFilter(std::string serialized) { + clear(); + + std::vector tokens; + split_string(&tokens, serialized, "/"); + if (tokens.size() != 5) { + DEBUG(status).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + return; + } + + if (!deserializeMaterialMask(tokens[0], mat_mask) || !deserializeMaterials(tokens[1], materials)) + return; + + setMinQuality(atoi(tokens[2].c_str())); + setMaxQuality(atoi(tokens[3].c_str())); + decorated_only = static_cast(atoi(tokens[4].c_str())); +} + +// format: mat,mask,elements/materials,list/minq/maxq/decorated +std::string ItemFilter::serialize() const { + std::ostringstream ser; + ser << bitfield_to_string(mat_mask, ",") << "/"; + if (!materials.empty()) { + ser << materials[0].getToken(); + for (size_t i = 1; i < materials.size(); ++i) + ser << "," << materials[i].getToken(); + } + ser << "/" << static_cast(min_quality); + ser << "/" << static_cast(max_quality); + ser << "/" << static_cast(decorated_only); + return ser.str(); +} + +static void clampItemQuality(df::item_quality *quality) { + if (*quality > df::item_quality::Artifact) { + DEBUG(status).print("clamping quality to Artifact"); + *quality = df::item_quality::Artifact; + } + if (*quality < df::item_quality::Ordinary) { + DEBUG(status).print("clamping quality to Ordinary"); + *quality = df::item_quality::Ordinary; + } +} + +void ItemFilter::setMinQuality(int quality) { + min_quality = static_cast(quality); + clampItemQuality(&min_quality); + if (max_quality < min_quality) + max_quality = min_quality; +} + +void ItemFilter::setMaxQuality(int quality) { + max_quality = static_cast(quality); + clampItemQuality(&max_quality); + if (max_quality < min_quality) + min_quality = max_quality; +} + +void ItemFilter::setDecoratedOnly(bool decorated) { + decorated_only = decorated; +} + +void ItemFilter::setMaterialMask(uint32_t mask) { + mat_mask.whole = mask; +} + +void ItemFilter::setMaterials(const std::vector &materials) { + this->materials = materials; +} + +std::string ItemFilter::getMinQuality() const { + return ENUM_KEY_STR(item_quality, min_quality); +} + +std::string ItemFilter::getMaxQuality() const { + return ENUM_KEY_STR(item_quality, max_quality); +} + +bool ItemFilter::getDecoratedOnly() const { + return decorated_only; +} + +uint32_t ItemFilter::getMaterialMask() const { + return mat_mask.whole; +} + +static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } + +std::vector ItemFilter::getMaterials() const { + std::vector descriptions; + transform_(materials, descriptions, material_to_string_fn); + + if (descriptions.size() == 0) + bitfield_to_string(&descriptions, mat_mask); + + if (descriptions.size() == 0) + descriptions.push_back("any"); + + return descriptions; +} + +static bool matchesMask(DFHack::MaterialInfo &mat, df::dfhack_material_category mat_mask) { + return mat_mask.whole ? mat.matches(mat_mask) : true; +} + +bool ItemFilter::matches(df::dfhack_material_category mask) const { + return mask.whole & mat_mask.whole; +} + +bool ItemFilter::matches(DFHack::MaterialInfo &material) const { + for (auto it = materials.begin(); it != materials.end(); ++it) + if (material.matches(*it)) + return true; + return false; +} + +bool ItemFilter::matches(df::item *item) const { + if (item->getQuality() < min_quality || item->getQuality() > max_quality) + return false; + + if (decorated_only && !item->hasImprovements()) + return false; + + auto imattype = item->getActualMaterial(); + auto imatindex = item->getActualMaterialIndex(); + auto item_mat = DFHack::MaterialInfo(imattype, imatindex); + + return (materials.size() == 0) ? matchesMask(item_mat, mat_mask) : matches(item_mat); +} diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 6f70f09be..134d3b249 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -1 +1,39 @@ #pragma once + +#include "modules/Materials.h" + +#include "df/dfhack_material_category.h" +#include "df/item_quality.h" + +class ItemFilter { +public: + ItemFilter(); + ItemFilter(std::string serialized); + + void clear(); + bool isEmpty(); + std::string serialize() const; + + void setMinQuality(int quality); + void setMaxQuality(int quality); + void setDecoratedOnly(bool decorated); + void setMaterialMask(uint32_t mask); + void setMaterials(const std::vector &materials); + + std::string getMinQuality() const; + std::string getMaxQuality() const; + bool getDecoratedOnly() const; + uint32_t getMaterialMask() const; + std::vector getMaterials() const; + + bool matches(df::dfhack_material_category mask) const; + bool matches(DFHack::MaterialInfo &material) const; + bool matches(df::item *item) const; + +private: + df::item_quality min_quality; + df::item_quality max_quality; + bool decorated_only; + df::dfhack_material_category mat_mask; + std::vector materials; +}; diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index eb55a95b4..c68d668bf 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -31,14 +31,18 @@ static vector> get_vector_ids(color_ostream &out, return ret; } -static vector> deserialize(color_ostream &out, PersistentDataItem &bld_config) { +static vector> deserialize_vector_ids(color_ostream &out, PersistentDataItem &bld_config) { vector> ret; - DEBUG(status,out).print("deserializing state for building %d: %s\n", - get_config_val(bld_config, BLD_CONFIG_ID), bld_config.val().c_str()); + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + const string &serialized = rawstrs[0]; + + DEBUG(status,out).print("deserializing vector ids for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); vector joined; - split_string(&joined, bld_config.val(), "|"); + split_string(&joined, serialized, ";"); for (auto &str : joined) { vector lst; split_string(&lst, str, ","); @@ -54,28 +58,60 @@ static vector> deserialize(color_ostream &out, Pe return ret; } -static string serialize(const vector> &vector_ids) { +static std::vector deserialize_item_filters(color_ostream &out, PersistentDataItem &bld_config) { + std::vector ret; + + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + if (rawstrs.size() < 2) + return ret; + const string &serialized = rawstrs[1]; + + DEBUG(status,out).print("deserializing item filters for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); + + vector filterstrs; + split_string(&filterstrs, serialized, ";"); + for (auto &str : filterstrs) { + ret.emplace_back(str); + } + + return ret; +} + +static string serialize(const vector> &vector_ids, const vector &item_filters) { vector joined; for (auto &vec_list : vector_ids) { joined.emplace_back(join_strings(",", vec_list)); } - return join_strings("|", joined); + std::ostringstream out; + out << join_strings(";", joined) << "|"; + + joined.clear(); + for (auto &filter : item_filters) { + joined.emplace_back(filter.serialize()); + } + out << join_strings(";", joined); + + return out.str(); } -PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat) - : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat) { +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat, const vector &item_filters) + : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat), + item_filters(item_filters) { DEBUG(status,out).print("creating persistent data for building %d\n", id); bld_config = World::AddPersistentData(BLD_CONFIG_KEY); set_config_val(bld_config, BLD_CONFIG_ID, id); set_config_val(bld_config, BLD_CONFIG_HEAT, heat_safety); - bld_config.val() = serialize(vector_ids); + bld_config.val() = serialize(vector_ids, item_filters); DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); } PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) : id(get_config_val(bld_config, BLD_CONFIG_ID)), - vector_ids(deserialize(out, bld_config)), + vector_ids(deserialize_vector_ids(out, bld_config)), heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), + item_filters(deserialize_item_filters(out, bld_config)), bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h index 0a67e0edc..5bd09ba5a 100644 --- a/plugins/buildingplan/plannedbuilding.h +++ b/plugins/buildingplan/plannedbuilding.h @@ -1,6 +1,7 @@ #pragma once #include "buildingplan.h" +#include "itemfilter.h" #include "Core.h" @@ -18,7 +19,9 @@ public: const HeatSafety heat_safety; - PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat); + const std::vector item_filters; + + PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat, const std::vector &item_filters); PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); void remove(DFHack::color_ostream &out); diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 9d8eb463b..b6cbb383b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -1292,7 +1292,7 @@ InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', - frame={w=30, h=14}, + frame={w=30, h=15}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -1308,12 +1308,12 @@ function InspectorOverlay:init() InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3}, InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4}, widgets.HotkeyLabel{ - frame={t=10, l=0}, + frame={t=11, l=0}, label='adjust filters', key='CUSTOM_CTRL_F', }, widgets.HotkeyLabel{ - frame={t=11, l=0}, + frame={t=12, l=0}, label='make top priority', key='CUSTOM_CTRL_T', on_activate=self:callback('make_top_priority'), From 60de4619a294ab84cf82c61d8839f540ef326534 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 08:34:55 -0800 Subject: [PATCH 084/126] fix signed unsigned compare --- plugins/buildingplan/buildingplan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 7359d842c..3a73372c3 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -547,7 +547,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ BuildingTypeKey key(type, subtype, custom); HeatSafety heat = get_heat_safety_filter(key); auto &job_items = get_job_items(out, key); - if (job_items.size() <= index) + if (index < 0 || job_items.size() <= (size_t)index) return 0; auto &item_filters = get_item_filters(out, key); From 4cc262c796f58126a964cfe12fdee17f19280bc4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 15:08:11 -0800 Subject: [PATCH 085/126] overhaul serialization; persist item filters --- plugins/buildingplan/CMakeLists.txt | 5 +- plugins/buildingplan/buildingplan.cpp | 79 ++++++++++----------- plugins/buildingplan/buildingplan.h | 11 ++- plugins/buildingplan/buildingplan_cycle.cpp | 2 +- plugins/buildingplan/buildingtypekey.cpp | 59 +++++++++++++++ plugins/buildingplan/buildingtypekey.h | 22 ++++++ plugins/buildingplan/defaultitemfilters.cpp | 60 ++++++++++++++++ plugins/buildingplan/defaultitemfilters.h | 24 +++++++ plugins/buildingplan/itemfilter.cpp | 55 +++++++++----- plugins/buildingplan/itemfilter.h | 7 +- plugins/buildingplan/plannedbuilding.cpp | 26 ++----- 11 files changed, 264 insertions(+), 86 deletions(-) create mode 100644 plugins/buildingplan/buildingtypekey.cpp create mode 100644 plugins/buildingplan/buildingtypekey.h create mode 100644 plugins/buildingplan/defaultitemfilters.cpp create mode 100644 plugins/buildingplan/defaultitemfilters.h diff --git a/plugins/buildingplan/CMakeLists.txt b/plugins/buildingplan/CMakeLists.txt index 85475edaa..118b2a1d1 100644 --- a/plugins/buildingplan/CMakeLists.txt +++ b/plugins/buildingplan/CMakeLists.txt @@ -2,12 +2,15 @@ project(buildingplan) set(COMMON_HDRS buildingplan.h + buildingtypekey.h + defaultitemfilters.h itemfilter.h plannedbuilding.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) dfhack_plugin(buildingplan - buildingplan.cpp buildingplan_cycle.cpp itemfilter.cpp plannedbuilding.cpp + buildingplan.cpp buildingplan_cycle.cpp buildingtypekey.cpp + defaultitemfilters.cpp itemfilter.cpp plannedbuilding.cpp ${COMMON_HDRS} LINK_LIBRARIES lua) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 3a73372c3..4fa119ebc 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -1,5 +1,7 @@ -#include "plannedbuilding.h" #include "buildingplan.h" +#include "buildingtypekey.h" +#include "defaultitemfilters.h" +#include "plannedbuilding.h" #include "Debug.h" #include "LuaTools.h" @@ -29,6 +31,7 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; +const string FILTER_CONFIG_KEY = string(plugin_name) + "/filter"; const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; int get_config_val(PersistentDataItem &c, int index) { @@ -47,35 +50,11 @@ void set_config_bool(PersistentDataItem &c, int index, bool value) { set_config_val(c, index, value ? 1 : 0); } -// building type, subtype, custom -typedef std::tuple BuildingTypeKey; - -// rotates a size_t value left by count bits -// assumes count is not 0 or >= size_t_bits -// replace this with std::rotl when we move to C++20 -static std::size_t rotl_size_t(size_t val, uint32_t count) -{ - static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); - return val << count | val >> (size_t_bits - count); -} - -struct BuildingTypeKeyHash { - std::size_t operator() (const BuildingTypeKey & key) const { - // cast first param to appease gcc-4.8, which is missing the enum - // specializations for std::hash - std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); - std::size_t h2 = std::hash()(std::get<1>(key)); - std::size_t h3 = std::hash()(std::get<2>(key)); - - return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); - } -}; - static PersistentDataItem config; // for use in counting available materials for the UI -static unordered_map, BuildingTypeKeyHash> job_item_cache; +static unordered_map, BuildingTypeKeyHash> job_item_cache; static unordered_map cur_heat_safety; -static unordered_map, BuildingTypeKeyHash> cur_item_filters; +static unordered_map cur_item_filters; // building id -> PlannedBuilding static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) @@ -133,7 +112,7 @@ static int get_num_filters(color_ostream &out, BuildingTypeKey key) { return num_filters; } -static vector & get_job_items(color_ostream &out, BuildingTypeKey key) { +static const vector & get_job_items(color_ostream &out, BuildingTypeKey key) { if (job_item_cache.count(key)) return job_item_cache[key]; const int num_filters = get_num_filters(out, key); @@ -169,13 +148,11 @@ static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { return HEAT_SAFETY_ANY; } -static vector & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { +static DefaultItemFilters & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { if (cur_item_filters.count(key)) - return cur_item_filters[key]; - - vector &filters = cur_item_filters[key]; - filters.resize(get_job_items(out, key).size()); - return filters; + return cur_item_filters.at(key); + cur_item_filters.emplace(key, DefaultItemFilters(out, key, get_job_items(out, key))); + return cur_item_filters.at(key); } static command_result do_command(color_ostream &out, vector ¶meters); @@ -258,6 +235,14 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DEBUG(status,out).print("loading persisted state\n"); clear_state(out); + + vector filter_configs; + World::GetPersistentData(&filter_configs, FILTER_CONFIG_KEY); + for (auto &cfg : filter_configs) { + BuildingTypeKey key = DefaultItemFilters::getKey(cfg); + cur_item_filters.emplace(key, DefaultItemFilters(out, cfg, get_job_items(out, key))); + } + vector building_configs; World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); @@ -265,13 +250,17 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { PlannedBuilding pb(out, building_configs[idx]); df::building *bld = df::building::find(pb.id); if (!bld) { - WARN(status).print("cannot find building %d; halting load\n", pb.id); + INFO(status).print("building %d no longer exists; skipping\n", pb.id); + pb.remove(out); + continue; } BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); - if (pb.item_filters.size() != get_item_filters(out, key).size()) + if (pb.item_filters.size() != get_item_filters(out, key).getItemFilters().size()) { WARN(status).print("loaded state for building %d doesn't match world\n", pb.id); - else - registerPlannedBuilding(out, pb); + pb.remove(out); + continue; + } + registerPlannedBuilding(out, pb); } return CR_OK; @@ -352,7 +341,7 @@ static string getBucket(const df::job_item & ji) { } // get a list of item vectors that we should search for matches -vector getVectorIds(color_ostream &out, df::job_item *job_item) { +vector getVectorIds(color_ostream &out, const df::job_item *job_item) { std::vector ret; // if the filter already has the vector_id set to something specific, use it @@ -525,7 +514,7 @@ static bool addPlannedBuilding(color_ostream &out, df::building *bld) { bld->getCustomType())) return false; BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); - PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key)); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key).getItemFilters()); return registerPlannedBuilding(out, pb); } @@ -549,7 +538,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ auto &job_items = get_job_items(out, key); if (index < 0 || job_items.size() <= (size_t)index) return 0; - auto &item_filters = get_item_filters(out, key); + auto &item_filters = get_item_filters(out, key).getItemFilters(); auto &jitem = job_items[index]; auto vector_ids = getVectorIds(out, jitem); @@ -595,7 +584,13 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 } static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering hasFilter\n"); + TRACE(status,out).print("entering hasFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key); + for (auto &filter : filters.getItemFilters()) { + if (filter.isEmpty()) + return true; + } return false; } diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 4f0d374e7..eef9808e6 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -13,6 +13,7 @@ typedef std::deque> Bucket; typedef std::map> Tasks; +extern const std::string FILTER_CONFIG_KEY; extern const std::string BLD_CONFIG_KEY; enum ConfigValues { @@ -22,6 +23,12 @@ enum ConfigValues { CONFIG_BARS = 4, }; +enum FilterConfigValues { + FILTER_CONFIG_TYPE = 0, + FILTER_CONFIG_SUBTYPE = 1, + FILTER_CONFIG_CUSTOM = 2, +}; + enum BuildingConfigValues { BLD_CONFIG_ID = 0, BLD_CONFIG_HEAT = 1, @@ -38,8 +45,8 @@ bool get_config_bool(DFHack::PersistentDataItem &c, int index); void set_config_val(DFHack::PersistentDataItem &c, int index, int value); void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); -std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); +std::vector getVectorIds(DFHack::color_ostream &out, const df::job_item *job_item); bool itemPassesScreen(df::item * item); -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); +bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index a904bc5a8..655dc8c1a 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -46,7 +46,7 @@ bool itemPassesScreen(df::item * item) { && !item->isAssignedToStockpile(); } -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { +bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; diff --git a/plugins/buildingplan/buildingtypekey.cpp b/plugins/buildingplan/buildingtypekey.cpp new file mode 100644 index 000000000..664fdf27d --- /dev/null +++ b/plugins/buildingplan/buildingtypekey.cpp @@ -0,0 +1,59 @@ +#include "buildingplan.h" +#include "buildingtypekey.h" + +#include "Debug.h" +#include "MiscUtils.h" + +using std::string; +using std::vector; + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using namespace DFHack; + +// building type, subtype, custom +BuildingTypeKey::BuildingTypeKey(df::building_type type, int16_t subtype, int32_t custom) + : tuple(type, subtype, custom) { } + +static BuildingTypeKey deserialize(color_ostream &out, const std::string &serialized) { + vector key_parts; + split_string(&key_parts, serialized, ","); + if (key_parts.size() != 3) { + WARN(status,out).print("invalid key_str: '%s'\n", serialized.c_str()); + return BuildingTypeKey(df::building_type::NONE, -1, -1); + } + return BuildingTypeKey((df::building_type)string_to_int(key_parts[0]), + string_to_int(key_parts[1]), string_to_int(key_parts[2])); +} + +BuildingTypeKey::BuildingTypeKey(color_ostream &out, const std::string &serialized) + :tuple(deserialize(out, serialized)) { } + +string BuildingTypeKey::serialize() const { + std::ostringstream ser; + ser << std::get<0>(*this) << ","; + ser << std::get<1>(*this) << ","; + ser << std::get<2>(*this); + return ser.str(); +} + +// rotates a size_t value left by count bits +// assumes count is not 0 or >= size_t_bits +// replace this with std::rotl when we move to C++20 +static std::size_t rotl_size_t(size_t val, uint32_t count) +{ + static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); + return val << count | val >> (size_t_bits - count); +} + +std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const { + // cast first param to appease gcc-4.8, which is missing the enum + // specializations for std::hash + std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); + std::size_t h2 = std::hash()(std::get<1>(key)); + std::size_t h3 = std::hash()(std::get<2>(key)); + + return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); +} diff --git a/plugins/buildingplan/buildingtypekey.h b/plugins/buildingplan/buildingtypekey.h new file mode 100644 index 000000000..81bb043c5 --- /dev/null +++ b/plugins/buildingplan/buildingtypekey.h @@ -0,0 +1,22 @@ +#pragma once + +#include "df/building_type.h" + +#include +#include + +namespace DFHack { + class color_ostream; +} + +// building type, subtype, custom +struct BuildingTypeKey : public std::tuple { + BuildingTypeKey(df::building_type type, int16_t subtype, int32_t custom); + BuildingTypeKey(DFHack::color_ostream &out, const std::string & serialized); + + std::string serialize() const; +}; + +struct BuildingTypeKeyHash { + std::size_t operator() (const BuildingTypeKey & key) const; +}; diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp new file mode 100644 index 000000000..4cc6f11cf --- /dev/null +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -0,0 +1,60 @@ +#include "defaultitemfilters.h" + +#include "Debug.h" +#include "MiscUtils.h" + +#include "modules/World.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using std::string; +using std::vector; +using namespace DFHack; + +BuildingTypeKey DefaultItemFilters::getKey(PersistentDataItem &filter_config) { + return BuildingTypeKey( + (df::building_type)get_config_val(filter_config, FILTER_CONFIG_TYPE), + get_config_val(filter_config, FILTER_CONFIG_SUBTYPE), + get_config_val(filter_config, FILTER_CONFIG_CUSTOM)); +} + +DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, const std::vector &jitems) + : key(key) { + DEBUG(status,out).print("creating persistent data for filter key %d,%d,%d\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key)); + filter_config = World::AddPersistentData(FILTER_CONFIG_KEY); + set_config_val(filter_config, FILTER_CONFIG_TYPE, std::get<0>(key)); + set_config_val(filter_config, FILTER_CONFIG_SUBTYPE, std::get<1>(key)); + set_config_val(filter_config, FILTER_CONFIG_CUSTOM, std::get<2>(key)); + item_filters.resize(jitems.size()); + filter_config.val() = serialize_item_filters(item_filters); +} + +DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &filter_config, const std::vector &jitems) + : key(getKey(filter_config)), filter_config(filter_config) { + auto &serialized = filter_config.val(); + DEBUG(status,out).print("deserializing item filters for key %d,%d,%d: %s\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + std::vector filters = deserialize_item_filters(out, serialized); + if (filters.size() != jitems.size()) { + WARN(status,out).print("ignoring invalid filters_str for key %d,%d,%d: '%s'\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + item_filters.resize(jitems.size()); + } else + item_filters = filters; +} + +void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { + if (item_filters.size() <= index) { + WARN(status,out).print("invalid index for filter key %d,%d,%d: %d\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index); + return; + } + + item_filters[index] = filter; + filter_config.val() = serialize_item_filters(item_filters); + DEBUG(status,out).print("updated item filter and persisted for key %d,%d,%d: %s\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), filter_config.val().c_str()); +} diff --git a/plugins/buildingplan/defaultitemfilters.h b/plugins/buildingplan/defaultitemfilters.h new file mode 100644 index 000000000..4d1d5cbd2 --- /dev/null +++ b/plugins/buildingplan/defaultitemfilters.h @@ -0,0 +1,24 @@ +#pragma once + +#include "buildingplan.h" +#include "buildingtypekey.h" + +#include "modules/Persistence.h" + +class DefaultItemFilters { +public: + static BuildingTypeKey getKey(DFHack::PersistentDataItem &filter_config); + + const BuildingTypeKey key; + + DefaultItemFilters(DFHack::color_ostream &out, BuildingTypeKey key, const std::vector &jitems); + DefaultItemFilters(DFHack::color_ostream &out, DFHack::PersistentDataItem &filter_config, const std::vector &jitems); + + void setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index); + + const std::vector & getItemFilters() const { return item_filters; } + +private: + DFHack::PersistentDataItem filter_config; + std::vector item_filters; +}; diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index bd35c848a..a714b62d4 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -4,12 +4,15 @@ #include "df/item.h" -using namespace DFHack; - namespace DFHack { DBG_EXTERN(buildingplan, status); } +using std::string; +using std::vector; + +using namespace DFHack; + ItemFilter::ItemFilter() { clear(); } @@ -22,7 +25,7 @@ void ItemFilter::clear() { materials.clear(); } -bool ItemFilter::isEmpty() { +bool ItemFilter::isEmpty() const { return min_quality == df::item_quality::Ordinary && max_quality == df::item_quality::Masterful && !decorated_only @@ -30,7 +33,7 @@ bool ItemFilter::isEmpty() { && materials.empty(); } -static bool deserializeMaterialMask(std::string ser, df::dfhack_material_category mat_mask) { +static bool deserializeMaterialMask(string ser, df::dfhack_material_category mat_mask) { if (ser.empty()) return true; @@ -41,11 +44,11 @@ static bool deserializeMaterialMask(std::string ser, df::dfhack_material_categor return true; } -static bool deserializeMaterials(std::string ser, std::vector &materials) { +static bool deserializeMaterials(string ser, vector &materials) { if (ser.empty()) return true; - std::vector mat_names; + vector mat_names; split_string(&mat_names, ser, ","); for (auto m = mat_names.begin(); m != mat_names.end(); m++) { DFHack::MaterialInfo material; @@ -58,13 +61,13 @@ static bool deserializeMaterials(std::string ser, std::vector tokens; + vector tokens; split_string(&tokens, serialized, "/"); if (tokens.size() != 5) { - DEBUG(status).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + DEBUG(status,out).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); return; } @@ -77,7 +80,7 @@ ItemFilter::ItemFilter(std::string serialized) { } // format: mat,mask,elements/materials,list/minq/maxq/decorated -std::string ItemFilter::serialize() const { +string ItemFilter::serialize() const { std::ostringstream ser; ser << bitfield_to_string(mat_mask, ",") << "/"; if (!materials.empty()) { @@ -124,15 +127,15 @@ void ItemFilter::setMaterialMask(uint32_t mask) { mat_mask.whole = mask; } -void ItemFilter::setMaterials(const std::vector &materials) { +void ItemFilter::setMaterials(const vector &materials) { this->materials = materials; } -std::string ItemFilter::getMinQuality() const { +string ItemFilter::getMinQuality() const { return ENUM_KEY_STR(item_quality, min_quality); } -std::string ItemFilter::getMaxQuality() const { +string ItemFilter::getMaxQuality() const { return ENUM_KEY_STR(item_quality, max_quality); } @@ -144,10 +147,10 @@ uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; } -static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } +static string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } -std::vector ItemFilter::getMaterials() const { - std::vector descriptions; +vector ItemFilter::getMaterials() const { + vector descriptions; transform_(materials, descriptions, material_to_string_fn); if (descriptions.size() == 0) @@ -187,3 +190,23 @@ bool ItemFilter::matches(df::item *item) const { return (materials.size() == 0) ? matchesMask(item_mat, mat_mask) : matches(item_mat); } + +vector deserialize_item_filters(color_ostream &out, const string &serialized) { + std::vector filters; + + vector filter_strs; + split_string(&filter_strs, serialized, ";"); + for (auto &str : filter_strs) { + filters.emplace_back(out, str); + } + + return filters; +} + +string serialize_item_filters(const vector &filters) { + vector strs; + for (auto &filter : filters) { + strs.emplace_back(filter.serialize()); + } + return join_strings(";", strs); +} diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 134d3b249..6eb7551b4 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -8,10 +8,10 @@ class ItemFilter { public: ItemFilter(); - ItemFilter(std::string serialized); + ItemFilter(DFHack::color_ostream &out, std::string serialized); void clear(); - bool isEmpty(); + bool isEmpty() const; std::string serialize() const; void setMinQuality(int quality); @@ -37,3 +37,6 @@ private: df::dfhack_material_category mat_mask; std::vector materials; }; + +std::vector deserialize_item_filters(DFHack::color_ostream &out, const std::string &serialized); +std::string serialize_item_filters(const std::vector &filters); diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index c68d668bf..27be36a5b 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -58,25 +58,14 @@ static vector> deserialize_vector_ids(color_ostre return ret; } -static std::vector deserialize_item_filters(color_ostream &out, PersistentDataItem &bld_config) { +static std::vector get_item_filters(color_ostream &out, PersistentDataItem &bld_config) { std::vector ret; vector rawstrs; split_string(&rawstrs, bld_config.val(), "|"); if (rawstrs.size() < 2) return ret; - const string &serialized = rawstrs[1]; - - DEBUG(status,out).print("deserializing item filters for building %d: %s\n", - get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); - - vector filterstrs; - split_string(&filterstrs, serialized, ";"); - for (auto &str : filterstrs) { - ret.emplace_back(str); - } - - return ret; + return deserialize_item_filters(out, rawstrs[1]); } static string serialize(const vector> &vector_ids, const vector &item_filters) { @@ -85,14 +74,7 @@ static string serialize(const vector> &vector_ids joined.emplace_back(join_strings(",", vec_list)); } std::ostringstream out; - out << join_strings(";", joined) << "|"; - - joined.clear(); - for (auto &filter : item_filters) { - joined.emplace_back(filter.serialize()); - } - out << join_strings(";", joined); - + out << join_strings(";", joined) << "|" << serialize_item_filters(item_filters); return out.str(); } @@ -111,7 +93,7 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_con : id(get_config_val(bld_config, BLD_CONFIG_ID)), vector_ids(deserialize_vector_ids(out, bld_config)), heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), - item_filters(deserialize_item_filters(out, bld_config)), + item_filters(get_item_filters(out, bld_config)), bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear From 20a0390c50c3386ba8b16a824814bd057241acfd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 18:06:30 -0800 Subject: [PATCH 086/126] no building shadow when other windows are up --- plugins/lua/buildingplan.lua | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b6cbb383b..f3a6ef6ce 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -500,6 +500,70 @@ FilterSelection.ATTRS{ } function FilterSelection:init() + self:addviews{ + widgets.Panel{ + view_id='options_panel', + frame={l=0, t=0, b=5, w=10}, + autoarrange_subviews=true, + subviews={ + widgets.Panel{ + view_id='quality_panel', + frame={}, + frame_style=gui.MEDIUM_FRAME, + frame_title='Item quality', + subviews={ + }, + }, + widgets.Panel{ + view_id='building_panel', + frame={}, + frame_style=gui.MEDIUM_FRAME, + frame_title='Building options', + subviews={ + }, + }, + widgets.Panel{ + view_id='global_panel', + frame={}, + frame_style=gui.MEDIUM_FRAME, + frame_title='Global options', + subviews={ + }, + }, + }, + }, + widgets.Panel{ + view_id='materials_panel', + frame={l=10, t=0, b=5, r=0}, + subviews={ + widgets.Panel{ + view_id='materials_top', + frame={l=0, t=0, r=0, h=5}, + subviews={ + }, + }, + widgets.Panel{ + view_id='materials_lists', + frame={l=0, t=5, r=0, b=0}, + frame_style=gui.MEDIUM_FRAME, + subviews={ + widgets.Panel{ + view_id='materials_categories', + frame={l=0, t=0, b=0, w=20}, + subviews={ + }, + }, + widgets.Panel{ + view_id='materials_mats', + frame={l=21, t=0, r=0, b=0}, + subviews={ + }, + }, + }, + }, + }, + }, + } end FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) @@ -514,6 +578,14 @@ function FilterSelectionScreen:init() } end +function FilterSelectionScreen:onShow() + df.global.game.main_interface.bottom_mode_selected = -1 +end + +function FilterSelectionScreen:onDismiss() + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT +end + -------------------------------- -- ItemLine -- @@ -1048,6 +1120,7 @@ function PlannerOverlay:onInput(keys) local is_hollow = self.subviews.hollow:getOptionValue() local chosen_items, active_screens = {}, {} local pending = num_filters + df.global.game.main_interface.bottom_mode_selected = -1 for idx = num_filters,1,-1 do chosen_items[idx] = {} if (self.subviews['item'..idx].available or 0) > 0 then @@ -1061,6 +1134,7 @@ function PlannerOverlay:onInput(keys) active_screens[idx] = nil pending = pending - 1 if pending == 0 then + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT self:place_building(self:restore_placement(), chosen_items) end end, @@ -1068,6 +1142,7 @@ function PlannerOverlay:onInput(keys) for i,scr in pairs(active_screens) do scr:dismiss() end + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT self:restore_placement() end, }:show() From dadecdcf45f9a2aa914b8a5caa13ba57013fb73f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 18:14:11 -0800 Subject: [PATCH 087/126] fix inspector screen not resetting the description --- plugins/lua/buildingplan.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index f3a6ef6ce..ee2aff21b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -1355,6 +1355,7 @@ function InspectorLine:get_status_line() end function InspectorLine:reset() + self.desc = nil self.status = nil end From 4b2645469686e678846811a0c66769b8ec9f8a9d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 18:59:15 -0800 Subject: [PATCH 088/126] start of filters dialog --- plugins/lua/buildingplan.lua | 106 +++++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index ee2aff21b..aef04e020 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -494,7 +494,7 @@ end FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', - frame={w=60, h=40, l=30, t=8}, + frame={w=80, h=53, l=30, t=8}, resizable=true, index=DEFAULT_NIL, } @@ -503,38 +503,122 @@ function FilterSelection:init() self:addviews{ widgets.Panel{ view_id='options_panel', - frame={l=0, t=0, b=5, w=10}, + frame={l=0, t=0, b=5, w=30}, autoarrange_subviews=true, subviews={ widgets.Panel{ view_id='quality_panel', - frame={}, - frame_style=gui.MEDIUM_FRAME, + frame={l=0, r=0, h=23}, + frame_style=gui.INTERIOR_FRAME, frame_title='Item quality', subviews={ + widgets.Label{ + frame={l=0, t=0}, + text='updown hotkeys', + }, + widgets.Panel{ + view_id='quality_slider', + frame={l=0, t=2, w=3, h=15}, + frame_background=to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '}, + }, + widgets.Label{ + frame={l=3, t=3}, + text='- Artifact (num)', + }, + widgets.Label{ + frame={l=3, t=5}, + text='- Masterful (num)', + }, + widgets.Label{ + frame={l=3, t=7}, + text='- Exceptional (num)', + }, + widgets.Label{ + frame={l=3, t=9}, + text='- Superior (num)', + }, + widgets.Label{ + frame={l=3, t=11}, + text='- FinelyCrafted (num)', + }, + widgets.Label{ + frame={l=3, t=13}, + text='- WellCrafted (num)', + }, + widgets.Label{ + frame={l=3, t=15}, + text='- Ordinary (num)', + }, + widgets.Label{ + frame={l=0, t=18}, + text='updown hotkeys', + }, + widgets.CycleHotkeyLabel{ + frame={l=0, t=20}, + label='Decorated only:', + options={'No', 'Yes'}, + }, }, }, - widgets.Panel{ + widgets.ResizingPanel{ view_id='building_panel', - frame={}, - frame_style=gui.MEDIUM_FRAME, + frame={l=0, r=0}, + frame_style=gui.INTERIOR_FRAME, frame_title='Building options', + autoarrange_subviews=true, + autoarrange_gap=1, subviews={ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap='These options will affect all items for the current building type.', + }, + widgets.CycleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_G', + label='Building safety:', + options={ + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + }, }, }, widgets.Panel{ view_id='global_panel', - frame={}, - frame_style=gui.MEDIUM_FRAME, + frame={l=0, r=0, b=0}, + frame_style=gui.INTERIOR_FRAME, frame_title='Global options', + autoarrange_subviews=true, + autoarrange_gap=1, subviews={ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap='These options will affect the selection of "Generic Materials" for future buildings.', + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + label='Blocks', + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + label='Logs', + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + label='Boulders', + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + label='Bars', + }, }, }, }, }, widgets.Panel{ view_id='materials_panel', - frame={l=10, t=0, b=5, r=0}, + frame={l=30, t=0, b=5, r=0}, subviews={ widgets.Panel{ view_id='materials_top', @@ -545,7 +629,7 @@ function FilterSelection:init() widgets.Panel{ view_id='materials_lists', frame={l=0, t=5, r=0, b=0}, - frame_style=gui.MEDIUM_FRAME, + frame_style=gui.INTERIOR_FRAME, subviews={ widgets.Panel{ view_id='materials_categories', From f0ca7ad4257b2ab7b69486831ed133908a9437b9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 20:34:54 -0800 Subject: [PATCH 089/126] fix all buildings being identified as constructions --- plugins/lua/buildingplan.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index aef04e020..905a5558f 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -694,7 +694,7 @@ local function is_construction() end local function is_stairs() - return is_construction + return is_construction() and uibs.building_subtype == df.construction_type.UpDownStair end From d8e440806c8ad617f29ff2b462d2c0ce407735d2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 23:19:04 -0800 Subject: [PATCH 090/126] fix signed/unsigned compare --- plugins/buildingplan/defaultitemfilters.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp index 4cc6f11cf..36d074363 100644 --- a/plugins/buildingplan/defaultitemfilters.cpp +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -47,7 +47,7 @@ DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &f } void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { - if (item_filters.size() <= index) { + if (index < 0 || item_filters.size() <= (size_t)index) { WARN(status,out).print("invalid index for filter key %d,%d,%d: %d\n", std::get<0>(key), std::get<1>(key), std::get<2>(key), index); return; From fbd3cd44d60b3f57d27d1b694fc720bb061ecd4f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 23 Feb 2023 01:15:22 -0800 Subject: [PATCH 091/126] initial mock of filter dialog --- plugins/lua/buildingplan.lua | 183 ++++++++++++++++++++++++++++++----- 1 file changed, 157 insertions(+), 26 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 905a5558f..54e8c9d8d 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -491,6 +491,11 @@ local function can_be_improved(idx) filter.item_type ~= df.item_type.BOULDER end +local OPTIONS_COL_WIDTH = 28 +local TYPE_COL_WIDTH = 20 +local HEADER_HEIGHT = 5 +local FOOTER_HEIGHT = 4 + FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', @@ -499,62 +504,76 @@ FilterSelection.ATTRS{ index=DEFAULT_NIL, } +local STANDIN_PEN = to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '} + function FilterSelection:init() self:addviews{ widgets.Panel{ view_id='options_panel', - frame={l=0, t=0, b=5, w=30}, + frame={l=0, t=0, b=FOOTER_HEIGHT, w=OPTIONS_COL_WIDTH}, autoarrange_subviews=true, subviews={ widgets.Panel{ view_id='quality_panel', - frame={l=0, r=0, h=23}, + frame={l=0, r=0, h=24}, + frame_inset={t=1}, frame_style=gui.INTERIOR_FRAME, frame_title='Item quality', subviews={ - widgets.Label{ + widgets.HotkeyLabel{ frame={l=0, t=0}, - text='updown hotkeys', + key='CUSTOM_SHIFT_Q', + }, + widgets.HotkeyLabel{ + frame={l=1, t=0}, + key='CUSTOM_SHIFT_W', + label='Set max quality', }, widgets.Panel{ view_id='quality_slider', frame={l=0, t=2, w=3, h=15}, - frame_background=to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '}, + frame_background=STANDIN_PEN, }, widgets.Label{ frame={l=3, t=3}, - text='- Artifact (num)', + text='- Artifact (1)', }, widgets.Label{ frame={l=3, t=5}, - text='- Masterful (num)', + text='- Masterful (3)', }, widgets.Label{ frame={l=3, t=7}, - text='- Exceptional (num)', + text='- Exceptional (34)', }, widgets.Label{ frame={l=3, t=9}, - text='- Superior (num)', + text='- Superior (50)', }, widgets.Label{ frame={l=3, t=11}, - text='- FinelyCrafted (num)', + text='- FinelyCrafted (67)', }, widgets.Label{ frame={l=3, t=13}, - text='- WellCrafted (num)', + text='- WellCrafted (79)', }, widgets.Label{ frame={l=3, t=15}, - text='- Ordinary (num)', + text='- Ordinary (206)', }, - widgets.Label{ + widgets.HotkeyLabel{ frame={l=0, t=18}, - text='updown hotkeys', + key='CUSTOM_SHIFT_Z', + }, + widgets.HotkeyLabel{ + frame={l=1, t=18}, + key='CUSTOM_SHIFT_X', + label='Set min quality', }, widgets.CycleHotkeyLabel{ frame={l=0, t=20}, + key='CUSTOM_SHIFT_D', label='Decorated only:', options={'No', 'Yes'}, }, @@ -563,6 +582,7 @@ function FilterSelection:init() widgets.ResizingPanel{ view_id='building_panel', frame={l=0, r=0}, + frame_inset={t=1}, frame_style=gui.INTERIOR_FRAME, frame_title='Building options', autoarrange_subviews=true, @@ -574,7 +594,7 @@ function FilterSelection:init() }, widgets.CycleHotkeyLabel{ frame={l=0}, - key='CUSTOM_G', + key='CUSTOM_SHIFT_G', label='Building safety:', options={ {label='Any', value=0}, @@ -587,30 +607,41 @@ function FilterSelection:init() widgets.Panel{ view_id='global_panel', frame={l=0, r=0, b=0}, + frame_inset={t=1}, frame_style=gui.INTERIOR_FRAME, frame_title='Global options', autoarrange_subviews=true, - autoarrange_gap=1, subviews={ widgets.WrappedLabel{ frame={l=0}, text_to_wrap='These options will affect the selection of "Generic Materials" for future buildings.', }, + widgets.Panel{ + frame={h=1}, + }, widgets.ToggleHotkeyLabel{ frame={l=0}, + key='CUSTOM_SHIFT_B', label='Blocks', + label_width=8, }, widgets.ToggleHotkeyLabel{ frame={l=0}, + key='CUSTOM_SHIFT_L', label='Logs', + label_width=8, }, widgets.ToggleHotkeyLabel{ frame={l=0}, + key='CUSTOM_SHIFT_O', label='Boulders', + label_width=8, }, widgets.ToggleHotkeyLabel{ frame={l=0}, + key='CUSTOM_SHIFT_P', label='Bars', + label_width=8, }, }, }, @@ -618,38 +649,138 @@ function FilterSelection:init() }, widgets.Panel{ view_id='materials_panel', - frame={l=30, t=0, b=5, r=0}, + frame={l=OPTIONS_COL_WIDTH, t=0, b=FOOTER_HEIGHT, r=0}, subviews={ widgets.Panel{ - view_id='materials_top', - frame={l=0, t=0, r=0, h=5}, + view_id='header', + frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, subviews={ + widgets.EditField{ + frame={l=1, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('%l') end, + }, + widgets.CycleHotkeyLabel{ + frame={l=1, t=2, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_R', + options={'name', 'available'}, + }, + widgets.ToggleHotkeyLabel{ + frame={l=24, t=2, w=24}, + label='Hide unavailable:', + key='CUSTOM_SHIFT_H', + initial_option=false, + }, + widgets.Label{ + frame={l=1, b=0}, + text='Type', + text_pen=COLOR_LIGHTRED, + }, + widgets.Label{ + frame={l=TYPE_COL_WIDTH, b=0}, + text='Material', + text_pen=COLOR_LIGHTRED, + }, }, }, widgets.Panel{ view_id='materials_lists', - frame={l=0, t=5, r=0, b=0}, + frame={l=0, t=HEADER_HEIGHT, r=0, b=0}, frame_style=gui.INTERIOR_FRAME, subviews={ - widgets.Panel{ + widgets.List{ view_id='materials_categories', - frame={l=0, t=0, b=0, w=20}, - subviews={ + frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3}, + scroll_keys={}, + choices={ + {text='Stone', key='CUSTOM_SHIFT_S'}, + {text='Wood', key='CUSTOM_SHIFT_W'}, + {text='Metal', key='CUSTOM_SHIFT_M'}, + {text='Other', key='CUSTOM_SHIFT_O'}, }, }, - widgets.Panel{ + widgets.List{ view_id='materials_mats', - frame={l=21, t=0, r=0, b=0}, - subviews={ + frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, + choices={ + {text='9 - granite'}, + {text='0 - graphite'}, }, }, }, }, + widgets.Panel{ + view_id='divider', + frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=0, w=1}, + on_render=self:callback('draw_divider'), + } }, }, + widgets.Panel{ + view_id='footer', + frame={l=0, r=0, b=0, h=FOOTER_HEIGHT}, + frame_inset={l=20, t=1}, + subviews={ + widgets.HotkeyLabel{ + frame={l=0, t=0}, + label='Toggle', + auto_width=true, + key='SELECT', + }, + widgets.HotkeyLabel{ + frame={l=0, t=2}, + label='Done', + auto_width=true, + key='LEAVESCREEN', + }, + widgets.HotkeyLabel{ + frame={l=30, t=0}, + label='Select all', + auto_width=true, + key='CUSTOM_SHIFT_A', + }, + widgets.HotkeyLabel{ + frame={l=30, t=1}, + label='Invert selection', + auto_width=true, + key='CUSTOM_SHIFT_I', + }, + widgets.HotkeyLabel{ + frame={l=30, t=2}, + label='Clear selection', + auto_width=true, + key='CUSTOM_SHIFT_C', + }, + }, + } } end +local texpos = dfhack.textures.getThinBordersTexposStart() +local tp = function(offset) + if texpos == -1 then return nil end + return texpos + offset +end + +local TOP_PEN = to_pen{tile=tp(10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK} +local MID_PEN = to_pen{tile=tp(4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK} +local BOT_PEN = to_pen{tile=tp(11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} + +function FilterSelection:draw_divider(dc) + local y2 = dc.height - 1 + for y=0,y2 do + dc:seek(0, y) + if y == 0 then + dc:char(nil, TOP_PEN) + elseif y == y2 then + dc:char(nil, BOT_PEN) + else + dc:char(nil, MID_PEN) + end + end +end + FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) FilterSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', From ce3ee386fdc48e39d624014e7d87b9cca71b2b83 Mon Sep 17 00:00:00 2001 From: 20k Date: Sat, 21 Jan 2023 20:07:12 +0000 Subject: [PATCH 092/126] makeSquad, updateRoomAssignments --- docs/dev/Lua API.rst | 13 ++ library/LuaApi.cpp | 2 + library/include/modules/Units.h | 3 + library/modules/Units.cpp | 278 ++++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index c2cc7f5cd..b089fb8ea 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1589,6 +1589,19 @@ Units module Returns a table of the cutoffs used by the above stress level functions. +* ``dfhack.units.makeSquad(assignment_id)`` + + Creates a new squad associated with the assignment. Fails if one already exists + Note: This function does not name the squad, but they are otherwise complete + +* ``dfhack.units.updateRoomAssignments(squad_id, assignment_id, squad_use_flags)`` + + Sets the sleep, train, indiv_eq, and squad_eq flags when training at a barracks + +* ``dfhack.units.getSquadName(squad)`` + + Returns the name of a squad + Action Timer API ~~~~~~~~~~~~~~~~ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 426c87566..af56bb4c3 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1812,6 +1812,8 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getGoalType), WRAPM(Units, getGoalName), WRAPM(Units, isGoalAchieved), + WRAPM(Units, makeSquad), + WRAPM(Units, updateRoomAssignments), WRAPM(Units, getSquadName), WRAPM(Units, getPhysicalDescription), WRAPM(Units, getRaceName), diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 0edebacdc..f3c7baf6a 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -37,6 +37,7 @@ distribution. #include "df/mental_attribute_type.h" #include "df/misc_trait_type.h" #include "df/physical_attribute_type.h" +#include "df/squad.h" #include "df/unit.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" @@ -223,6 +224,8 @@ DFHACK_EXPORT std::string getGoalName(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT bool isGoalAchieved(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT std::string getSquadName(df::unit *unit); +DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); +DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); DFHACK_EXPORT df::activity_entry *getMainSocialActivity(df::unit *unit); DFHACK_EXPORT df::activity_event *getMainSocialEvent(df::unit *unit); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 9a415fb34..0ab507517 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -33,6 +33,7 @@ distribution. #include #include #include +#include using namespace std; #include "VersionInfo.h" @@ -51,6 +52,7 @@ using namespace std; #include "MiscUtils.h" #include "df/activity_entry.h" +#include "df/building_civzonest.h" #include "df/burrow.h" #include "df/caste_raw.h" #include "df/creature_raw.h" @@ -61,6 +63,7 @@ using namespace std; #include "df/entity_raw_flags.h" #include "df/identity_type.h" #include "df/game_mode.h" +#include "df/global_objects.h" #include "df/histfig_entity_link_positionst.h" #include "df/histfig_relationship_type.h" #include "df/historical_entity.h" @@ -72,6 +75,10 @@ using namespace std; #include "df/job.h" #include "df/nemesis_record.h" #include "df/squad.h" +#include "df/squad_position.h" +#include "df/squad_schedule_order.h" +#include "df/squad_order.h" +#include "df/squad_order_trainst.h" #include "df/tile_occupancy.h" #include "df/plotinfost.h" #include "df/unit_inventory_item.h" @@ -1972,6 +1979,277 @@ std::string Units::getSquadName(df::unit *unit) return Translation::TranslateName(&squad->name, true); } +//only works for making squads for fort mode player controlled dwarf squads +//could be extended straightforwardly by passing in entity +df::squad* Units::makeSquad(int32_t assignment_id) +{ + if (df::global::squad_next_id == nullptr || df::global::plotinfo == nullptr) + return nullptr; + + df::language_name name; + name.type = df::language_name_type::Squad; + + for (int i=0; i < 7; i++) + { + name.words[i] = -1; + name.parts_of_speech[i] = df::part_of_speech::Noun; + } + + df::historical_entity* fort = df::historical_entity::find(df::global::plotinfo->group_id); + + df::entity_position_assignment* found_assignment = nullptr; + + for (auto* assignment : fort->positions.assignments) + { + if (assignment->id == assignment_id) + { + found_assignment = assignment; + break; + } + } + + if (found_assignment == nullptr) + return nullptr; + + //this function does not attempt to delete or replace squads for assignments + if (found_assignment->squad_id != -1) + return nullptr; + + df::entity_position* corresponding_position = nullptr; + + for (auto* position : fort->positions.own) + { + if (position->id == found_assignment->position_id) + { + corresponding_position = position; + break; + } + } + + if (corresponding_position == nullptr) + return nullptr; + + df::squad* result = new df::squad(); + result->id = *df::global::squad_next_id; + result->cur_routine_idx = 0; + result->uniform_priority = result->id + 1; //no idea why, but seems to hold + result->activity = -1; //?? + result->carry_food = 2; + result->carry_water = 1; + result->entity_id = df::global::plotinfo->group_id; + result->leader_position = corresponding_position->id; + result->leader_assignment = found_assignment->id; + result->unk_1 = -1; + result->name = name; + result->ammo.unk_v50_1 = 0; + + int16_t squad_size = corresponding_position->squad_size; + + for (int i=0; i < squad_size; i++) + { + //construct for squad_position seems to set all the attributes correctly + //except I've observed unk_2 is -1 generally + df::squad_position* pos = new df::squad_position(); + pos->unk_2 = -1; + pos->flags.whole = 0; + + result->positions.push_back(pos); + } + + const auto& routines = df::global::plotinfo->alerts.routines; + + for (const auto& routine : routines) + { + df::squad_schedule_entry* asched = (df::squad_schedule_entry*)malloc(sizeof(df::squad_schedule_entry) * 12); + + for(int kk=0; kk < 12; kk++) + { + new (&asched[kk]) df::squad_schedule_entry; + + for(int jj=0; jj < squad_size; jj++) + { + int32_t* order_assignments = new int32_t(); + *order_assignments = -1; + + asched[kk].order_assignments.push_back(order_assignments); + } + } + + auto insert_training_order = [asched, squad_size](int month) + { + df::squad_schedule_order* order = new df::squad_schedule_order(); + order->min_count = squad_size; + //assumed + order->positions.resize(squad_size); + + df::squad_order* s_order = df::allocate(); + + s_order->unk_v40_1 = -1; + s_order->unk_v40_2 = -1; + s_order->year = *df::global::cur_year; + s_order->year_tick = *df::global::cur_year_tick; + s_order->unk_v40_3 = -1; + s_order->unk_1 = 0; + + order->order = s_order; + + asched[month].orders.push_back(order); + //wear uniform while training + asched[month].uniform_mode = 0; + }; + + //I thought this was a terrible hack, but its literally how dwarf fortress does it 1:1 + //Off duty: No orders, Sleep/room at will. Equip/orders only + if (routine->name == "Off duty") + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 0; + asched[i].uniform_mode = 1; + } + } + //Staggered Training: Training orders at 3 4 5, 9 10 11, sleep/room at will. Equip/orders only, except train months which are equip/always + //always seen the training indices 0 1 2 6 7 8, so its unclear. Check if squad id matters + else if (routine->name == "Staggered training") + { + //this is semi randomised for different squads + //appears to be something like squad.id & 1, it isn't smart + //if you alternate squad creation, its 'correctly' staggered + //but it'll also happily not stagger them if you eg delete a squad and make another + std::array indices; + + if ((*df::global::squad_next_id) & 1) + { + indices = {3, 4, 5, 9, 10, 11}; + } + else + { + indices = {0, 1, 2, 6, 7, 8}; + } + + for (int index : indices) + { + insert_training_order(index); + //still sleep in room at will even when training + asched[index].sleep_mode = 0; + } + } + //see above, but with all indices + else if (routine->name == "Constant training") + { + for (int i=0; i < 12; i++) + { + insert_training_order(i); + //still sleep in room at will even when training + asched[i].sleep_mode = 0; + } + } + else if (routine->name == "Ready") + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 2; + asched[i].uniform_mode = 0; + } + } + else + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 0; + asched[i].uniform_mode = 0; + } + } + + result->schedule.push_back(reinterpret_cast(asched)); + } + + //all we've done so far is leak memory if anything goes wrong + //modify state + (*df::global::squad_next_id)++; + fort->squads.push_back(result->id); + df::global::world->squads.all.push_back(result); + found_assignment->squad_id = result->id; + + //todo: find and modify old squad + + return result; +} + +void Units::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags) +{ + df::squad* squad = df::squad::find(squad_id); + df::building* bzone = df::building::find(civzone_id); + + df::building_civzonest* zone = strict_virtual_cast(bzone); + + if (squad == nullptr || zone == nullptr) + return; + + df::squad::T_rooms* room_from_squad = nullptr; + df::building_civzonest::T_squad_room_info* room_from_building = nullptr; + + for (auto room : squad->rooms) + { + if (room->building_id == civzone_id) + { + room_from_squad = room; + break; + } + } + + for (auto room : zone->squad_room_info) + { + if (room->squad_id == squad_id) + { + room_from_building = room; + break; + } + } + + if (flags.whole == 0 && room_from_squad == nullptr && room_from_building == nullptr) + return; + + //if we're setting 0 flags, and there's no room already, don't set a room + bool avoiding_squad_roundtrip = flags.whole == 0 && room_from_squad == nullptr; + + if (!avoiding_squad_roundtrip && room_from_squad == nullptr) + { + room_from_squad = new df::squad::T_rooms(); + room_from_squad->building_id = civzone_id; + squad->rooms.push_back(room_from_squad); + + std::sort(squad->rooms.begin(), squad->rooms.end(), [](df::squad::T_rooms* a, df::squad::T_rooms* b){return a->building_id < b->building_id;}); + } + + if (room_from_building == nullptr) + { + room_from_building = new df::building_civzonest::T_squad_room_info(); + room_from_building->squad_id = squad_id; + zone->squad_room_info.push_back(room_from_building); + + std::sort(zone->squad_room_info.begin(), zone->squad_room_info.end(), [](df::building_civzonest::T_squad_room_info* a, df::building_civzonest::T_squad_room_info* b){return a->squad_id < b->squad_id;}); + } + + if (room_from_squad) + room_from_squad->mode = flags; + + room_from_building->mode = flags; + + if (flags.whole == 0 && !avoiding_squad_roundtrip) + { + for (int i=0; i < (int)squad->rooms.size(); i++) + { + if (squad->rooms[i]->building_id == civzone_id) + { + delete squad->rooms[i]; + squad->rooms.erase(squad->rooms.begin() + i); + i--; + } + } + } +} + df::activity_entry *Units::getMainSocialActivity(df::unit *unit) { CHECK_NULL_POINTER(unit); From 3912c6290f632d3cb1f41620aeeae1a0e050eaf3 Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 30 Jan 2023 06:28:23 +0000 Subject: [PATCH 093/126] Military module start --- docs/dev/Lua API.rst | 9 +- library/CMakeLists.txt | 2 + library/LuaApi.cpp | 14 +- library/include/modules/Military.h | 19 ++ library/include/modules/Units.h | 5 - library/modules/Military.cpp | 307 +++++++++++++++++++++++++++++ library/modules/Units.cpp | 284 -------------------------- 7 files changed, 345 insertions(+), 295 deletions(-) create mode 100644 library/include/modules/Military.h create mode 100644 library/modules/Military.cpp diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index b089fb8ea..9125ca902 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1589,16 +1589,19 @@ Units module Returns a table of the cutoffs used by the above stress level functions. -* ``dfhack.units.makeSquad(assignment_id)`` +Military Module API +~~~~~~~~~~~~~~~~~~~ + +* ``dfhack.military.makeSquad(assignment_id)`` Creates a new squad associated with the assignment. Fails if one already exists Note: This function does not name the squad, but they are otherwise complete -* ``dfhack.units.updateRoomAssignments(squad_id, assignment_id, squad_use_flags)`` +* ``dfhack.military.updateRoomAssignments(squad_id, assignment_id, squad_use_flags)`` Sets the sleep, train, indiv_eq, and squad_eq flags when training at a barracks -* ``dfhack.units.getSquadName(squad)`` +* ``dfhack.military.getSquadName(squad)`` Returns the name of a squad diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 87f869493..92db43563 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -137,6 +137,7 @@ set(MODULE_HEADERS include/modules/MapCache.h include/modules/Maps.h include/modules/Materials.h + include/modules/Military.h include/modules/Once.h include/modules/Persistence.h include/modules/Random.h @@ -164,6 +165,7 @@ set(MODULE_SOURCES modules/MapCache.cpp modules/Maps.cpp modules/Materials.cpp + modules/Military.cpp modules/Once.cpp modules/Persistence.cpp modules/Random.cpp diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index af56bb4c3..13ea1a7fe 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -55,6 +55,7 @@ distribution. #include "modules/MapCache.h" #include "modules/Maps.h" #include "modules/Materials.h" +#include "modules/Military.h" #include "modules/Random.h" #include "modules/Screen.h" #include "modules/Textures.h" @@ -1812,9 +1813,6 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getGoalType), WRAPM(Units, getGoalName), WRAPM(Units, isGoalAchieved), - WRAPM(Units, makeSquad), - WRAPM(Units, updateRoomAssignments), - WRAPM(Units, getSquadName), WRAPM(Units, getPhysicalDescription), WRAPM(Units, getRaceName), WRAPM(Units, getRaceNamePlural), @@ -1939,6 +1937,15 @@ static const luaL_Reg dfhack_units_funcs[] = { { NULL, NULL } }; +/***** Military Module *****/ + +static const LuaWrapper::FunctionReg dfhack_military_module[] = { + WRAPM(Military, makeSquad), + WRAPM(Military, updateRoomAssignments), + WRAPM(Military, getSquadName), + { NULL, NULL } +}; + /***** Items module *****/ static bool items_moveToGround(df::item *item, df::coord pos) @@ -3449,6 +3456,7 @@ void OpenDFHackApi(lua_State *state) OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); OpenModule(state, "textures", dfhack_textures_module); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); + OpenModule(state, "military", dfhack_military_module); OpenModule(state, "items", dfhack_items_module, dfhack_items_funcs); OpenModule(state, "maps", dfhack_maps_module, dfhack_maps_funcs); OpenModule(state, "world", dfhack_world_module, dfhack_world_funcs); diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h new file mode 100644 index 000000000..19ed47ee2 --- /dev/null +++ b/library/include/modules/Military.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Export.h" +#include "DataDefs.h" + +#include "df/squad.h" +#include "df/unit.h" + +namespace DFHack +{ +namespace Military +{ + +DFHACK_EXPORT std::string getSquadName(df::unit *unit); +DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); +DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); + +} +} \ No newline at end of file diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index f3c7baf6a..4fd9246aa 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -37,7 +37,6 @@ distribution. #include "df/mental_attribute_type.h" #include "df/misc_trait_type.h" #include "df/physical_attribute_type.h" -#include "df/squad.h" #include "df/unit.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" @@ -223,10 +222,6 @@ DFHACK_EXPORT df::goal_type getGoalType(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT std::string getGoalName(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT bool isGoalAchieved(df::unit *unit, size_t goalIndex = 0); -DFHACK_EXPORT std::string getSquadName(df::unit *unit); -DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); -DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); - DFHACK_EXPORT df::activity_entry *getMainSocialActivity(df::unit *unit); DFHACK_EXPORT df::activity_event *getMainSocialEvent(df::unit *unit); diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp new file mode 100644 index 000000000..b67b3a129 --- /dev/null +++ b/library/modules/Military.cpp @@ -0,0 +1,307 @@ +#include +#include +#include +#include "modules/Military.h" +#include "modules/Translation.h" +#include "df/building.h" +#include "df/building_civzonest.h" +#include "df/historical_figure.h" +#include "df/historical_entity.h" +#include "df/entity_position.h" +#include "df/entity_position_assignment.h" +#include "df/plotinfost.h" +#include "df/squad.h" +#include "df/squad_position.h" +#include "df/squad_schedule_order.h" +#include "df/squad_order.h" +#include "df/squad_order_trainst.h" +#include "df/world.h" + +using namespace DFHack; +using namespace df::enums; +using df::global::world; +using df::global::plotinfo; + +std::string Military::getSquadName(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->military.squad_id == -1) + return ""; + df::squad *squad = df::squad::find(unit->military.squad_id); + if (!squad) + return ""; + if (squad->alias.size() > 0) + return squad->alias; + return Translation::TranslateName(&squad->name, true); +} + +//only works for making squads for fort mode player controlled dwarf squads +//could be extended straightforwardly by passing in entity +df::squad* Military::makeSquad(int32_t assignment_id) +{ + if (df::global::squad_next_id == nullptr || df::global::plotinfo == nullptr) + return nullptr; + + df::language_name name; + name.type = df::language_name_type::Squad; + + for (int i=0; i < 7; i++) + { + name.words[i] = -1; + name.parts_of_speech[i] = df::part_of_speech::Noun; + } + + df::historical_entity* fort = df::historical_entity::find(df::global::plotinfo->group_id); + + df::entity_position_assignment* found_assignment = nullptr; + + for (auto* assignment : fort->positions.assignments) + { + if (assignment->id == assignment_id) + { + found_assignment = assignment; + break; + } + } + + if (found_assignment == nullptr) + return nullptr; + + //this function does not attempt to delete or replace squads for assignments + if (found_assignment->squad_id != -1) + return nullptr; + + df::entity_position* corresponding_position = nullptr; + + for (auto* position : fort->positions.own) + { + if (position->id == found_assignment->position_id) + { + corresponding_position = position; + break; + } + } + + if (corresponding_position == nullptr) + return nullptr; + + df::squad* result = new df::squad(); + result->id = *df::global::squad_next_id; + result->cur_routine_idx = 0; + result->uniform_priority = result->id + 1; //no idea why, but seems to hold + result->activity = -1; //?? + result->carry_food = 2; + result->carry_water = 1; + result->entity_id = df::global::plotinfo->group_id; + result->leader_position = corresponding_position->id; + result->leader_assignment = found_assignment->id; + result->unk_1 = -1; + result->name = name; + result->ammo.unk_v50_1 = 0; + + int16_t squad_size = corresponding_position->squad_size; + + for (int i=0; i < squad_size; i++) + { + //construct for squad_position seems to set all the attributes correctly + //except I've observed unk_2 is -1 generally + df::squad_position* pos = new df::squad_position(); + pos->unk_2 = -1; + pos->flags.whole = 0; + + result->positions.push_back(pos); + } + + const auto& routines = df::global::plotinfo->alerts.routines; + + for (const auto& routine : routines) + { + df::squad_schedule_entry* asched = (df::squad_schedule_entry*)malloc(sizeof(df::squad_schedule_entry) * 12); + + for(int kk=0; kk < 12; kk++) + { + new (&asched[kk]) df::squad_schedule_entry; + + for(int jj=0; jj < squad_size; jj++) + { + int32_t* order_assignments = new int32_t(); + *order_assignments = -1; + + asched[kk].order_assignments.push_back(order_assignments); + } + } + + auto insert_training_order = [asched, squad_size](int month) + { + df::squad_schedule_order* order = new df::squad_schedule_order(); + order->min_count = squad_size; + //assumed + order->positions.resize(squad_size); + + df::squad_order* s_order = df::allocate(); + + s_order->unk_v40_1 = -1; + s_order->unk_v40_2 = -1; + s_order->year = *df::global::cur_year; + s_order->year_tick = *df::global::cur_year_tick; + s_order->unk_v40_3 = -1; + s_order->unk_1 = 0; + + order->order = s_order; + + asched[month].orders.push_back(order); + //wear uniform while training + asched[month].uniform_mode = 0; + }; + + //I thought this was a terrible hack, but its literally how dwarf fortress does it 1:1 + //Off duty: No orders, Sleep/room at will. Equip/orders only + if (routine->name == "Off duty") + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 0; + asched[i].uniform_mode = 1; + } + } + //Staggered Training: Training orders at 3 4 5, 9 10 11, sleep/room at will. Equip/orders only, except train months which are equip/always + //always seen the training indices 0 1 2 6 7 8, so its unclear. Check if squad id matters + else if (routine->name == "Staggered training") + { + //this is semi randomised for different squads + //appears to be something like squad.id & 1, it isn't smart + //if you alternate squad creation, its 'correctly' staggered + //but it'll also happily not stagger them if you eg delete a squad and make another + std::array indices; + + if ((*df::global::squad_next_id) & 1) + { + indices = {3, 4, 5, 9, 10, 11}; + } + else + { + indices = {0, 1, 2, 6, 7, 8}; + } + + for (int index : indices) + { + insert_training_order(index); + //still sleep in room at will even when training + asched[index].sleep_mode = 0; + } + } + //see above, but with all indices + else if (routine->name == "Constant training") + { + for (int i=0; i < 12; i++) + { + insert_training_order(i); + //still sleep in room at will even when training + asched[i].sleep_mode = 0; + } + } + else if (routine->name == "Ready") + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 2; + asched[i].uniform_mode = 0; + } + } + else + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 0; + asched[i].uniform_mode = 0; + } + } + + result->schedule.push_back(reinterpret_cast(asched)); + } + + //all we've done so far is leak memory if anything goes wrong + //modify state + (*df::global::squad_next_id)++; + fort->squads.push_back(result->id); + df::global::world->squads.all.push_back(result); + found_assignment->squad_id = result->id; + + //todo: find and modify old squad + + return result; +} + +void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags) +{ + df::squad* squad = df::squad::find(squad_id); + df::building* bzone = df::building::find(civzone_id); + + df::building_civzonest* zone = strict_virtual_cast(bzone); + + if (squad == nullptr || zone == nullptr) + return; + + df::squad::T_rooms* room_from_squad = nullptr; + df::building_civzonest::T_squad_room_info* room_from_building = nullptr; + + for (auto room : squad->rooms) + { + if (room->building_id == civzone_id) + { + room_from_squad = room; + break; + } + } + + for (auto room : zone->squad_room_info) + { + if (room->squad_id == squad_id) + { + room_from_building = room; + break; + } + } + + if (flags.whole == 0 && room_from_squad == nullptr && room_from_building == nullptr) + return; + + //if we're setting 0 flags, and there's no room already, don't set a room + bool avoiding_squad_roundtrip = flags.whole == 0 && room_from_squad == nullptr; + + if (!avoiding_squad_roundtrip && room_from_squad == nullptr) + { + room_from_squad = new df::squad::T_rooms(); + room_from_squad->building_id = civzone_id; + squad->rooms.push_back(room_from_squad); + + std::sort(squad->rooms.begin(), squad->rooms.end(), [](df::squad::T_rooms* a, df::squad::T_rooms* b){return a->building_id < b->building_id;}); + } + + if (room_from_building == nullptr) + { + room_from_building = new df::building_civzonest::T_squad_room_info(); + room_from_building->squad_id = squad_id; + zone->squad_room_info.push_back(room_from_building); + + std::sort(zone->squad_room_info.begin(), zone->squad_room_info.end(), [](df::building_civzonest::T_squad_room_info* a, df::building_civzonest::T_squad_room_info* b){return a->squad_id < b->squad_id;}); + } + + if (room_from_squad) + room_from_squad->mode = flags; + + room_from_building->mode = flags; + + if (flags.whole == 0 && !avoiding_squad_roundtrip) + { + for (int i=0; i < (int)squad->rooms.size(); i++) + { + if (squad->rooms[i]->building_id == civzone_id) + { + delete squad->rooms[i]; + squad->rooms.erase(squad->rooms.begin() + i); + i--; + } + } + } +} \ No newline at end of file diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 0ab507517..137a07da6 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1966,290 +1966,6 @@ bool Units::isGoalAchieved(df::unit *unit, size_t goalIndex) && unit->status.current_soul->personality.dreams[goalIndex]->flags.whole != 0; } -std::string Units::getSquadName(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - if (unit->military.squad_id == -1) - return ""; - df::squad *squad = df::squad::find(unit->military.squad_id); - if (!squad) - return ""; - if (squad->alias.size() > 0) - return squad->alias; - return Translation::TranslateName(&squad->name, true); -} - -//only works for making squads for fort mode player controlled dwarf squads -//could be extended straightforwardly by passing in entity -df::squad* Units::makeSquad(int32_t assignment_id) -{ - if (df::global::squad_next_id == nullptr || df::global::plotinfo == nullptr) - return nullptr; - - df::language_name name; - name.type = df::language_name_type::Squad; - - for (int i=0; i < 7; i++) - { - name.words[i] = -1; - name.parts_of_speech[i] = df::part_of_speech::Noun; - } - - df::historical_entity* fort = df::historical_entity::find(df::global::plotinfo->group_id); - - df::entity_position_assignment* found_assignment = nullptr; - - for (auto* assignment : fort->positions.assignments) - { - if (assignment->id == assignment_id) - { - found_assignment = assignment; - break; - } - } - - if (found_assignment == nullptr) - return nullptr; - - //this function does not attempt to delete or replace squads for assignments - if (found_assignment->squad_id != -1) - return nullptr; - - df::entity_position* corresponding_position = nullptr; - - for (auto* position : fort->positions.own) - { - if (position->id == found_assignment->position_id) - { - corresponding_position = position; - break; - } - } - - if (corresponding_position == nullptr) - return nullptr; - - df::squad* result = new df::squad(); - result->id = *df::global::squad_next_id; - result->cur_routine_idx = 0; - result->uniform_priority = result->id + 1; //no idea why, but seems to hold - result->activity = -1; //?? - result->carry_food = 2; - result->carry_water = 1; - result->entity_id = df::global::plotinfo->group_id; - result->leader_position = corresponding_position->id; - result->leader_assignment = found_assignment->id; - result->unk_1 = -1; - result->name = name; - result->ammo.unk_v50_1 = 0; - - int16_t squad_size = corresponding_position->squad_size; - - for (int i=0; i < squad_size; i++) - { - //construct for squad_position seems to set all the attributes correctly - //except I've observed unk_2 is -1 generally - df::squad_position* pos = new df::squad_position(); - pos->unk_2 = -1; - pos->flags.whole = 0; - - result->positions.push_back(pos); - } - - const auto& routines = df::global::plotinfo->alerts.routines; - - for (const auto& routine : routines) - { - df::squad_schedule_entry* asched = (df::squad_schedule_entry*)malloc(sizeof(df::squad_schedule_entry) * 12); - - for(int kk=0; kk < 12; kk++) - { - new (&asched[kk]) df::squad_schedule_entry; - - for(int jj=0; jj < squad_size; jj++) - { - int32_t* order_assignments = new int32_t(); - *order_assignments = -1; - - asched[kk].order_assignments.push_back(order_assignments); - } - } - - auto insert_training_order = [asched, squad_size](int month) - { - df::squad_schedule_order* order = new df::squad_schedule_order(); - order->min_count = squad_size; - //assumed - order->positions.resize(squad_size); - - df::squad_order* s_order = df::allocate(); - - s_order->unk_v40_1 = -1; - s_order->unk_v40_2 = -1; - s_order->year = *df::global::cur_year; - s_order->year_tick = *df::global::cur_year_tick; - s_order->unk_v40_3 = -1; - s_order->unk_1 = 0; - - order->order = s_order; - - asched[month].orders.push_back(order); - //wear uniform while training - asched[month].uniform_mode = 0; - }; - - //I thought this was a terrible hack, but its literally how dwarf fortress does it 1:1 - //Off duty: No orders, Sleep/room at will. Equip/orders only - if (routine->name == "Off duty") - { - for (int i=0; i < 12; i++) - { - asched[i].sleep_mode = 0; - asched[i].uniform_mode = 1; - } - } - //Staggered Training: Training orders at 3 4 5, 9 10 11, sleep/room at will. Equip/orders only, except train months which are equip/always - //always seen the training indices 0 1 2 6 7 8, so its unclear. Check if squad id matters - else if (routine->name == "Staggered training") - { - //this is semi randomised for different squads - //appears to be something like squad.id & 1, it isn't smart - //if you alternate squad creation, its 'correctly' staggered - //but it'll also happily not stagger them if you eg delete a squad and make another - std::array indices; - - if ((*df::global::squad_next_id) & 1) - { - indices = {3, 4, 5, 9, 10, 11}; - } - else - { - indices = {0, 1, 2, 6, 7, 8}; - } - - for (int index : indices) - { - insert_training_order(index); - //still sleep in room at will even when training - asched[index].sleep_mode = 0; - } - } - //see above, but with all indices - else if (routine->name == "Constant training") - { - for (int i=0; i < 12; i++) - { - insert_training_order(i); - //still sleep in room at will even when training - asched[i].sleep_mode = 0; - } - } - else if (routine->name == "Ready") - { - for (int i=0; i < 12; i++) - { - asched[i].sleep_mode = 2; - asched[i].uniform_mode = 0; - } - } - else - { - for (int i=0; i < 12; i++) - { - asched[i].sleep_mode = 0; - asched[i].uniform_mode = 0; - } - } - - result->schedule.push_back(reinterpret_cast(asched)); - } - - //all we've done so far is leak memory if anything goes wrong - //modify state - (*df::global::squad_next_id)++; - fort->squads.push_back(result->id); - df::global::world->squads.all.push_back(result); - found_assignment->squad_id = result->id; - - //todo: find and modify old squad - - return result; -} - -void Units::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags) -{ - df::squad* squad = df::squad::find(squad_id); - df::building* bzone = df::building::find(civzone_id); - - df::building_civzonest* zone = strict_virtual_cast(bzone); - - if (squad == nullptr || zone == nullptr) - return; - - df::squad::T_rooms* room_from_squad = nullptr; - df::building_civzonest::T_squad_room_info* room_from_building = nullptr; - - for (auto room : squad->rooms) - { - if (room->building_id == civzone_id) - { - room_from_squad = room; - break; - } - } - - for (auto room : zone->squad_room_info) - { - if (room->squad_id == squad_id) - { - room_from_building = room; - break; - } - } - - if (flags.whole == 0 && room_from_squad == nullptr && room_from_building == nullptr) - return; - - //if we're setting 0 flags, and there's no room already, don't set a room - bool avoiding_squad_roundtrip = flags.whole == 0 && room_from_squad == nullptr; - - if (!avoiding_squad_roundtrip && room_from_squad == nullptr) - { - room_from_squad = new df::squad::T_rooms(); - room_from_squad->building_id = civzone_id; - squad->rooms.push_back(room_from_squad); - - std::sort(squad->rooms.begin(), squad->rooms.end(), [](df::squad::T_rooms* a, df::squad::T_rooms* b){return a->building_id < b->building_id;}); - } - - if (room_from_building == nullptr) - { - room_from_building = new df::building_civzonest::T_squad_room_info(); - room_from_building->squad_id = squad_id; - zone->squad_room_info.push_back(room_from_building); - - std::sort(zone->squad_room_info.begin(), zone->squad_room_info.end(), [](df::building_civzonest::T_squad_room_info* a, df::building_civzonest::T_squad_room_info* b){return a->squad_id < b->squad_id;}); - } - - if (room_from_squad) - room_from_squad->mode = flags; - - room_from_building->mode = flags; - - if (flags.whole == 0 && !avoiding_squad_roundtrip) - { - for (int i=0; i < (int)squad->rooms.size(); i++) - { - if (squad->rooms[i]->building_id == civzone_id) - { - delete squad->rooms[i]; - squad->rooms.erase(squad->rooms.begin() + i); - i--; - } - } - } -} - df::activity_entry *Units::getMainSocialActivity(df::unit *unit) { CHECK_NULL_POINTER(unit); From d84b1187678ce65a1106a3cec54a73fa3c5d9fbe Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 30 Jan 2023 07:11:42 +0000 Subject: [PATCH 094/126] docs, rework, rename --- docs/changelog.txt | 7 +++++++ docs/dev/Lua API.rst | 10 +++++----- library/LuaApi.cpp | 2 +- library/include/modules/Military.h | 6 +++--- library/modules/Military.cpp | 9 +++------ plugins/manipulator.cpp | 3 ++- 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index dd109a218..2da870ca7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -171,6 +171,12 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``Screen::Pen``: now accepts ``top_of_text`` and ``bottom_of_text`` properties to support offset text in graphics mode - `overlay`: overlay widgets can now specify a default enabled state if they are not already set in the player's overlay config file - ``Lua::Push``: now supports ``std::unordered_map`` +- `Military`: New module for military functionality +- `Military`: new ``makeSquad`` to create a squad +- `Military`: changed ``getSquadName`` to take a squad identifier +- `Military`: new ``updateRoomAssignments`` for assigning a squad to a barracks and archery range +- ``Maps::GetBiomeType`` renamed to ``Maps::getBiomeType`` for consistency +- ``Maps::GetBiomeTypeRef`` renamed to ``Maps::getBiomeTypeRef`` for consistency ## Lua - `helpdb`: new function: ``helpdb.refresh()`` to force a refresh of the database. Call if you are a developer adding new scripts, loading new plugins, or changing help text during play @@ -181,6 +187,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: -@ ``gui.ZScreen``: new attribute: ``defocusable`` for controlling whether a window loses keyboard focus when the map is clicked - ``widgets.Label``: token ``tile`` properties can now be either pens or numeric texture ids - `tiletypes`: now has a Lua API! ``tiletypes_setTile`` +- ``maps.getBiomeType``: exposed preexisting function to Lua ## Removed - `autohauler`: no plans to port to v50, as it just doesn't make sense with the new work detail system diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 9125ca902..304ce6651 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1594,16 +1594,16 @@ Military Module API * ``dfhack.military.makeSquad(assignment_id)`` - Creates a new squad associated with the assignment. Fails if one already exists - Note: This function does not name the squad, but they are otherwise complete + Creates a new squad associated with the assignment. Fails if one already exists. + Note: This function does not name the squad, but they are otherwise complete. * ``dfhack.military.updateRoomAssignments(squad_id, assignment_id, squad_use_flags)`` - Sets the sleep, train, indiv_eq, and squad_eq flags when training at a barracks + Sets the sleep, train, indiv_eq, and squad_eq flags when training at a barracks. -* ``dfhack.military.getSquadName(squad)`` +* ``dfhack.military.getSquadName(squad_id)`` - Returns the name of a squad + Returns the name of a squad/ Action Timer API ~~~~~~~~~~~~~~~~ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 13ea1a7fe..c9bdc3021 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1940,7 +1940,7 @@ static const luaL_Reg dfhack_units_funcs[] = { /***** Military Module *****/ static const LuaWrapper::FunctionReg dfhack_military_module[] = { - WRAPM(Military, makeSquad), + WRAPM(Military, makeSquad), WRAPM(Military, updateRoomAssignments), WRAPM(Military, getSquadName), { NULL, NULL } diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 19ed47ee2..3a9710687 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -10,10 +10,10 @@ namespace DFHack { namespace Military { - -DFHACK_EXPORT std::string getSquadName(df::unit *unit); + +DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); } -} \ No newline at end of file +} diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index b67b3a129..1dd71f11d 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -22,12 +22,9 @@ using namespace df::enums; using df::global::world; using df::global::plotinfo; -std::string Military::getSquadName(df::unit *unit) +std::string Military::getSquadName(int32_t squad_id) { - CHECK_NULL_POINTER(unit); - if (unit->military.squad_id == -1) - return ""; - df::squad *squad = df::squad::find(unit->military.squad_id); + df::squad *squad = df::squad::find(squad_id); if (!squad) return ""; if (squad->alias.size() > 0) @@ -304,4 +301,4 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s } } } -} \ No newline at end of file +} diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp index 6731aa513..b8f6ce706 100644 --- a/plugins/manipulator.cpp +++ b/plugins/manipulator.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -1305,7 +1306,7 @@ void viewscreen_unitlaborsst::refreshNames() cur->job_mode = UnitInfo::JOB; } if (unit->military.squad_id > -1) { - cur->squad_effective_name = Units::getSquadName(unit); + cur->squad_effective_name = Military::getSquadName(unit->military.squad_id); cur->squad_info = stl_sprintf("%i", unit->military.squad_position + 1) + "." + cur->squad_effective_name; } else { cur->squad_effective_name = ""; From 2bd48f1f90ec2ac5d8b07dd7fd3c216041c11f10 Mon Sep 17 00:00:00 2001 From: 20k Date: Tue, 31 Jan 2023 05:31:58 +0000 Subject: [PATCH 095/126] address some review comments --- docs/dev/Lua API.rst | 4 ++-- library/include/modules/Military.h | 1 - library/modules/Units.cpp | 8 -------- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 304ce6651..ec7ff57df 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1589,7 +1589,7 @@ Units module Returns a table of the cutoffs used by the above stress level functions. -Military Module API +Military module ~~~~~~~~~~~~~~~~~~~ * ``dfhack.military.makeSquad(assignment_id)`` @@ -1603,7 +1603,7 @@ Military Module API * ``dfhack.military.getSquadName(squad_id)`` - Returns the name of a squad/ + Returns the name of a squad. Action Timer API ~~~~~~~~~~~~~~~~ diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 3a9710687..8ceb987b5 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -4,7 +4,6 @@ #include "DataDefs.h" #include "df/squad.h" -#include "df/unit.h" namespace DFHack { diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 137a07da6..a636310e1 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -33,7 +33,6 @@ distribution. #include #include #include -#include using namespace std; #include "VersionInfo.h" @@ -52,7 +51,6 @@ using namespace std; #include "MiscUtils.h" #include "df/activity_entry.h" -#include "df/building_civzonest.h" #include "df/burrow.h" #include "df/caste_raw.h" #include "df/creature_raw.h" @@ -63,7 +61,6 @@ using namespace std; #include "df/entity_raw_flags.h" #include "df/identity_type.h" #include "df/game_mode.h" -#include "df/global_objects.h" #include "df/histfig_entity_link_positionst.h" #include "df/histfig_relationship_type.h" #include "df/historical_entity.h" @@ -74,11 +71,6 @@ using namespace std; #include "df/identity.h" #include "df/job.h" #include "df/nemesis_record.h" -#include "df/squad.h" -#include "df/squad_position.h" -#include "df/squad_schedule_order.h" -#include "df/squad_order.h" -#include "df/squad_order_trainst.h" #include "df/tile_occupancy.h" #include "df/plotinfost.h" #include "df/unit_inventory_item.h" From 1eeefdd598007d24940ba95fd2b3febdb33710c4 Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 20 Feb 2023 07:45:08 +0000 Subject: [PATCH 096/126] clean up a variety of unks --- library/modules/Military.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 1dd71f11d..a0d3cea2a 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -92,18 +92,15 @@ df::squad* Military::makeSquad(int32_t assignment_id) result->entity_id = df::global::plotinfo->group_id; result->leader_position = corresponding_position->id; result->leader_assignment = found_assignment->id; - result->unk_1 = -1; result->name = name; - result->ammo.unk_v50_1 = 0; + result->ammo.update = 0; int16_t squad_size = corresponding_position->squad_size; for (int i=0; i < squad_size; i++) { //construct for squad_position seems to set all the attributes correctly - //except I've observed unk_2 is -1 generally df::squad_position* pos = new df::squad_position(); - pos->unk_2 = -1; pos->flags.whole = 0; result->positions.push_back(pos); @@ -137,12 +134,9 @@ df::squad* Military::makeSquad(int32_t assignment_id) df::squad_order* s_order = df::allocate(); - s_order->unk_v40_1 = -1; - s_order->unk_v40_2 = -1; s_order->year = *df::global::cur_year; s_order->year_tick = *df::global::cur_year_tick; s_order->unk_v40_3 = -1; - s_order->unk_1 = 0; order->order = s_order; From 19616f7e32fe6329c5ea363d07cd6faac401bb01 Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 20 Feb 2023 07:48:41 +0000 Subject: [PATCH 097/126] fix changelog issues # Conflicts: # docs/changelog.txt --- docs/changelog.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2da870ca7..6f24ee642 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -79,6 +79,12 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API - ``Gui::any_civzone_hotkey``, ``Gui::getAnyCivZone``, ``Gui::getSelectedCivZone``: new functions to operate on the new zone system - Units module: added new predicates for ``isGeldable()``, ``isMarkedForGelding()``, and ``isPet()`` +- `Military`: New module for military functionality +- `Military`: new ``makeSquad`` to create a squad +- `Military`: changed ``getSquadName`` to take a squad identifier +- `Military`: new ``updateRoomAssignments`` for assigning a squad to a barracks and archery range- ``Maps::GetBiomeType`` renamed to ``Maps::getBiomeType`` for consistency +- ``Maps::GetBiomeType`` renamed to ``Maps::getBiomeType`` for consistency +- ``Maps::GetBiomeTypeRef`` renamed to ``Maps::getBiomeTypeRef`` for consistency ## Lua - ``dfhack.gui.getSelectedCivZone``: returns the Zone that the user has selected currently @@ -171,12 +177,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``Screen::Pen``: now accepts ``top_of_text`` and ``bottom_of_text`` properties to support offset text in graphics mode - `overlay`: overlay widgets can now specify a default enabled state if they are not already set in the player's overlay config file - ``Lua::Push``: now supports ``std::unordered_map`` -- `Military`: New module for military functionality -- `Military`: new ``makeSquad`` to create a squad -- `Military`: changed ``getSquadName`` to take a squad identifier -- `Military`: new ``updateRoomAssignments`` for assigning a squad to a barracks and archery range -- ``Maps::GetBiomeType`` renamed to ``Maps::getBiomeType`` for consistency -- ``Maps::GetBiomeTypeRef`` renamed to ``Maps::getBiomeTypeRef`` for consistency ## Lua - `helpdb`: new function: ``helpdb.refresh()`` to force a refresh of the database. Call if you are a developer adding new scripts, loading new plugins, or changing help text during play From 837f32fdee04bb50fef7b2d2b338b7bba4c855ad Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 20 Feb 2023 07:51:16 +0000 Subject: [PATCH 098/126] more changelog fixes --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6f24ee642..4753371e0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -90,6 +90,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``dfhack.gui.getSelectedCivZone``: returns the Zone that the user has selected currently - ``widgets.FilteredList``: Added ``edit_on_change`` optional parameter to allow a custom callback on filter edit change. - ``widgets.TabBar``: new library widget (migrated from control-panel.lua) +- ``maps.getBiomeType``: exposed preexisting function to Lua # 50.07-alpha1 @@ -187,7 +188,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: -@ ``gui.ZScreen``: new attribute: ``defocusable`` for controlling whether a window loses keyboard focus when the map is clicked - ``widgets.Label``: token ``tile`` properties can now be either pens or numeric texture ids - `tiletypes`: now has a Lua API! ``tiletypes_setTile`` -- ``maps.getBiomeType``: exposed preexisting function to Lua ## Removed - `autohauler`: no plans to port to v50, as it just doesn't make sense with the new work detail system From e50f3dbb64cb6c9b18611b7566e937159fbc2f86 Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 20 Feb 2023 18:02:47 +0000 Subject: [PATCH 099/126] remove unnecessary init --- library/modules/Military.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index a0d3cea2a..e30ea0ae4 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -101,7 +101,6 @@ df::squad* Military::makeSquad(int32_t assignment_id) { //construct for squad_position seems to set all the attributes correctly df::squad_position* pos = new df::squad_position(); - pos->flags.whole = 0; result->positions.push_back(pos); } From 0c9a9c8b9e4fa2bae92fafdb97964ab687d278f7 Mon Sep 17 00:00:00 2001 From: 20k Date: Tue, 21 Feb 2023 20:27:32 +0000 Subject: [PATCH 100/126] cleanup remaining unk --- library/modules/Military.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index e30ea0ae4..9a1fed614 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -135,7 +135,6 @@ df::squad* Military::makeSquad(int32_t assignment_id) s_order->year = *df::global::cur_year; s_order->year_tick = *df::global::cur_year_tick; - s_order->unk_v40_3 = -1; order->order = s_order; From 63d752b3f836d0dd4d72344fade8b60756404e85 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 10:53:30 -0800 Subject: [PATCH 101/126] update docs --- docs/guides/quickfort-user-guide.rst | 2 +- docs/plugins/buildingplan.rst | 97 +++++++++++++++++----------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 6f606c43a..ecee46918 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -1308,7 +1308,7 @@ legacy Python Quickfort. This setting has no effect on DFHack Quickfort, which will use buildingplan to manage everything designated in a ``#build`` blueprint regardless of the buildingplan UI settings. -However, quickfort *does* use `buildingplan's filters ` +However, quickfort *does* use `buildingplan's filters ` for each building type. For example, you can use the buildingplan UI to set the type of stone you want your walls made out of. Or you can specify that all buildingplan-managed chairs and tables must be of Masterful quality. The current diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index a331e9eb8..f77e5b590 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -2,32 +2,73 @@ buildingplan ============ .. dfhack-tool:: - :summary: Plan building construction before you have materials. + :summary: Plan building layouts with or without materials. :tags: fort design buildings -This plugin adds a planning mode for building placement. You can then place -furniture, constructions, and other buildings before the required materials are -available, and they will be created in a suspended state. Buildingplan will -periodically scan for appropriate items, and the jobs will be unsuspended when -the items are available. - -This is very powerful when used with tools like `quickfort`, which allow you to -set a building plan according to a blueprint, and the buildings will simply be -built when you can build them. - -You can use manager work orders or `workflow` to ensure you always have one or -two doors/beds/tables/chairs/etc. available, and place as many as you like. -Materials are used to build the planned buildings as they are produced, with -minimal space dedicated to stockpiles. +Buildingplan allows you to place furniture, constructions, and other buildings, +regardless of whether the required materials are available. This allows you to +focus purely on design elements when you are laying out your fort, and defers +item production concerns to a more convenient time. + +Buildingplan is as an alternative to the vanilla building placement UI. It +appears after you have selected the type of building, furniture, or construction +that you want to place in the vanilla build menu. Buildingplan then takes over +for the actual placement step. If any building materials are not available yet +for the placed building, it will be created in a suspended state. Buildingplan +will periodically scan for appropriate items and attach them. Once all items are +attached, the construction job will be unsuspended and a dwarf will come and +build the building. If you have the `unsuspend` overlay enabled (it is enabled +by default), then buildingplan-suspended buildings will appear with a ``P`` marker +on the main map, as opposed to the usual ``x`` marker for "regular" suspended +buildings. + +If you want to impose restrictions on which items are chosen for the buildings, +buildingplan has full support for quality and material filters. Before you place +a building, you can select a component item in the list and hit ``f`` or click on +the ``filter`` button next to the item description. This will let you choose your +desired item quality range, whether the item must be decorated, and even which +specific materials the item must be made out of. This lets you create layouts +with a consistent color, if that is part of your design. + +If you just care about the heat sensitivity of the building, you can set the +building to be fire- or magma-proof in the placement UI screen or in any item +filter screen, and the restriction will apply to all building items. This makes it +very easy to create magma-safe pump stacks, for example. + +Buildingplan works very well in conjuction with other design tools like +`gui/quickfort`, which allow you to apply a building layout from a blueprint. You +can apply very large, complicated layouts, and the buildings will simply be built +when your dwarves get around to producing the needed materials. If you set filters +in the buildingplan UI before applying the blueprint, the filters will be applied +to the blueprint buildings, just as if you had planned them from the buildingplan +placement UI. + +One way to integrate buildingplan into your gameplay is to create manager +workorders to ensure you always have a few blocks/doors/beds/etc. available. You +can then place as many of each building as you like. Produced items will be used +to build the planned buildings as they are produced, with minimal space dedicated +to stockpiles. The DFHack `orders` library can help with setting up these manager +workorders for you. + +If you do not wish to use the ``buildingplan`` interface, you can turn off the +``buildingplan.planner`` overlay in `gui/overlay`. You should not disable the +``buildingplan`` service entirely in `gui/control-panel` since then existing +planned buildings in loaded forts will stop functioning. Usage ----- :: - enable buildingplan buildingplan [status] - buildingplan set true|false + buildingplan set (true|false) + +Examples +-------- + +``buildingplan`` + Print a report of current settings, which kinds of buildings are planned, + and what kinds of materials the buildings are waiting for. .. _buildingplan-settings: @@ -49,23 +90,5 @@ want to add this line to your ``dfhack-config/init/onMapLoad.init`` file to always configure `buildingplan` to just use blocks for buildings and constructions:: - on-new-fortress buildingplan set boulders false; buildingplan set logs false - -.. _buildingplan-filters: - -Item filtering --------------- - -While placing a building, you can set filters for what materials you want the -building made out of, what quality you want the component items to be, and -whether you want the items to be decorated. - -If a building type takes more than one item to construct, use -:kbd:`Ctrl`:kbd:`Left` and :kbd:`Ctrl`:kbd:`Right` to select the item that you -want to set filters for. Any filters that you set will be used for all buildings -of the selected type placed from that point onward (until you set a new filter -or clear the current one). Buildings placed before the filters were changed will -keep the filter values that were set when the building was placed. - -For example, you can be sure that all your constructed walls are the same color -by setting a filter to accept only certain types of stone. + on-new-fortress buildingplan set boulders false + on-new-fortress buildingplan set logs false From 97ee1022c7200b1c47b7c5e36c3260dcba50582c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 10:55:50 -0800 Subject: [PATCH 102/126] note that filter page is a mock --- plugins/lua/buildingplan.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 54e8c9d8d..0e1341a3b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -498,7 +498,7 @@ local FOOTER_HEIGHT = 4 FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ - frame_title='Choose filters', + frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]', frame={w=80, h=53, l=30, t=8}, resizable=true, index=DEFAULT_NIL, From 3c1d3ce21c57f8fa3bbd06f90fb6ee09f30b86e1 Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 27 Feb 2023 01:45:10 +0000 Subject: [PATCH 103/126] rework docs, comments, clean up unnecessary init --- docs/changelog.txt | 2 +- docs/dev/Lua API.rst | 8 +++++--- library/modules/Military.cpp | 13 +++---------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4753371e0..412ee07a0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -82,7 +82,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `Military`: New module for military functionality - `Military`: new ``makeSquad`` to create a squad - `Military`: changed ``getSquadName`` to take a squad identifier -- `Military`: new ``updateRoomAssignments`` for assigning a squad to a barracks and archery range- ``Maps::GetBiomeType`` renamed to ``Maps::getBiomeType`` for consistency +- `Military`: new ``updateRoomAssignments`` for assigning a squad to a barracks and archery range - ``Maps::GetBiomeType`` renamed to ``Maps::getBiomeType`` for consistency - ``Maps::GetBiomeTypeRef`` renamed to ``Maps::getBiomeTypeRef`` for consistency diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index ec7ff57df..54ac4d48e 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1594,8 +1594,10 @@ Military module * ``dfhack.military.makeSquad(assignment_id)`` - Creates a new squad associated with the assignment. Fails if one already exists. - Note: This function does not name the squad, but they are otherwise complete. + Creates a new squad associated with the assignment (ie df::entity_position_assignment, via `id``) and returns it. + Fails if a squad already exists that is associated with that assignment, or if the assignment is not a fort mode player controlled squad. + Note: This function does not name the squad: consider setting a nickname (under result.name.nickname), and/or filling out the language_name object at result.name. + The returned squad is otherwise complete and requires no more setup to work correctly. * ``dfhack.military.updateRoomAssignments(squad_id, assignment_id, squad_use_flags)`` @@ -1603,7 +1605,7 @@ Military module * ``dfhack.military.getSquadName(squad_id)`` - Returns the name of a squad. + Returns the name of a squad as a string. Action Timer API ~~~~~~~~~~~~~~~~ diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 9a1fed614..07925e8e4 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -84,16 +84,13 @@ df::squad* Military::makeSquad(int32_t assignment_id) df::squad* result = new df::squad(); result->id = *df::global::squad_next_id; - result->cur_routine_idx = 0; result->uniform_priority = result->id + 1; //no idea why, but seems to hold - result->activity = -1; //?? result->carry_food = 2; result->carry_water = 1; result->entity_id = df::global::plotinfo->group_id; result->leader_position = corresponding_position->id; result->leader_assignment = found_assignment->id; result->name = name; - result->ammo.update = 0; int16_t squad_size = corresponding_position->squad_size; @@ -143,7 +140,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) asched[month].uniform_mode = 0; }; - //I thought this was a terrible hack, but its literally how dwarf fortress does it 1:1 + //Dwarf fortress does do this via a series of string comparisons //Off duty: No orders, Sleep/room at will. Equip/orders only if (routine->name == "Off duty") { @@ -153,8 +150,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) asched[i].uniform_mode = 1; } } - //Staggered Training: Training orders at 3 4 5, 9 10 11, sleep/room at will. Equip/orders only, except train months which are equip/always - //always seen the training indices 0 1 2 6 7 8, so its unclear. Check if squad id matters + //Staggered Training: Training orders at months 3 4 5 9 10 11, *or* 0 1 2 6 7 8, sleep/room at will. Equip/orders only, except train months which are equip/always else if (routine->name == "Staggered training") { //this is semi randomised for different squads @@ -209,15 +205,12 @@ df::squad* Military::makeSquad(int32_t assignment_id) result->schedule.push_back(reinterpret_cast(asched)); } - //all we've done so far is leak memory if anything goes wrong - //modify state + //Modify necessary world state (*df::global::squad_next_id)++; fort->squads.push_back(result->id); df::global::world->squads.all.push_back(result); found_assignment->squad_id = result->id; - //todo: find and modify old squad - return result; } From c0bd452c86577aa49d159dba8858078d2179f23a Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 27 Feb 2023 02:06:36 +0000 Subject: [PATCH 104/126] add a failure case check just in case --- library/modules/Military.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 07925e8e4..3b7ce7bec 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -50,6 +50,9 @@ df::squad* Military::makeSquad(int32_t assignment_id) df::historical_entity* fort = df::historical_entity::find(df::global::plotinfo->group_id); + if (fort == nullptr) + return nullptr; + df::entity_position_assignment* found_assignment = nullptr; for (auto* assignment : fort->positions.assignments) From c38a288eee9775706decba2b0f69b43c4550d4c3 Mon Sep 17 00:00:00 2001 From: 20k Date: Mon, 27 Feb 2023 02:15:26 +0000 Subject: [PATCH 105/126] use insert_into_vector, tweak docs again --- docs/dev/Lua API.rst | 4 ++-- library/modules/Military.cpp | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 54ac4d48e..63803a66a 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1594,9 +1594,9 @@ Military module * ``dfhack.military.makeSquad(assignment_id)`` - Creates a new squad associated with the assignment (ie df::entity_position_assignment, via `id``) and returns it. + Creates a new squad associated with the assignment (ie ``df::entity_position_assignment``, via ``id``) and returns it. Fails if a squad already exists that is associated with that assignment, or if the assignment is not a fort mode player controlled squad. - Note: This function does not name the squad: consider setting a nickname (under result.name.nickname), and/or filling out the language_name object at result.name. + Note: This function does not name the squad: consider setting a nickname (under ``squad.name.nickname``), and/or filling out the ``language_name`` object at ``squad.name``. The returned squad is otherwise complete and requires no more setup to work correctly. * ``dfhack.military.updateRoomAssignments(squad_id, assignment_id, squad_use_flags)`` diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 3b7ce7bec..b402cc0fa 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -1,6 +1,7 @@ #include #include #include +#include "MiscUtils.h" #include "modules/Military.h" #include "modules/Translation.h" #include "df/building.h" @@ -258,18 +259,16 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s { room_from_squad = new df::squad::T_rooms(); room_from_squad->building_id = civzone_id; - squad->rooms.push_back(room_from_squad); - std::sort(squad->rooms.begin(), squad->rooms.end(), [](df::squad::T_rooms* a, df::squad::T_rooms* b){return a->building_id < b->building_id;}); + insert_into_vector(squad->rooms, &df::squad::T_rooms::building_id, room_from_squad); } if (room_from_building == nullptr) { room_from_building = new df::building_civzonest::T_squad_room_info(); room_from_building->squad_id = squad_id; - zone->squad_room_info.push_back(room_from_building); - std::sort(zone->squad_room_info.begin(), zone->squad_room_info.end(), [](df::building_civzonest::T_squad_room_info* a, df::building_civzonest::T_squad_room_info* b){return a->squad_id < b->squad_id;}); + insert_into_vector(zone->squad_room_info, &df::building_civzonest::T_squad_room_info::squad_id, room_from_building); } if (room_from_squad) From 851bb50dc8e58f05f2ad4f231a1dbd8755204c61 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 15:37:54 -0800 Subject: [PATCH 106/126] add SDL_PushEvent shim for RemoteFortressReader --- library/include/modules/DFSDL.h | 2 ++ library/modules/DFSDL.cpp | 6 ++++++ plugins/remotefortressreader/remotefortressreader.cpp | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index b5fd119f7..226b9f881 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -8,6 +8,7 @@ namespace DFHack // SDL stand-in type definitions typedef signed short SINT16; typedef void DFSDL_sem; + typedef void DFSDL_Event; typedef struct { @@ -86,6 +87,7 @@ DFHACK_EXPORT DFSDL_Surface * DFSDL_ConvertSurface(DFSDL_Surface *src, const DFS DFHACK_EXPORT void DFSDL_FreeSurface(DFSDL_Surface *surface); DFHACK_EXPORT int DFSDL_SemWait(DFSDL_sem *sem); DFHACK_EXPORT int DFSDL_SemPost(DFSDL_sem *sem); +DFHACK_EXPORT int DFSDL_PushEvent(DFSDL_Event *event); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index a7847fdb5..08e213b94 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -34,6 +34,7 @@ DFSDL_Surface * (*g_SDL_ConvertSurface)(DFSDL_Surface *, const DFSDL_PixelFormat void (*g_SDL_FreeSurface)(DFSDL_Surface *); int (*g_SDL_SemWait)(DFSDL_sem *); int (*g_SDL_SemPost)(DFSDL_sem *); +int (*g_SDL_PushEvent)(DFSDL_Event *); bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -69,6 +70,7 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_FreeSurface); bind(g_sdl_handle, SDL_SemWait); bind(g_sdl_handle, SDL_SemPost); + bind(g_sdl_handle, SDL_PushEvent); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -118,3 +120,7 @@ int DFSDL::DFSDL_SemWait(DFSDL_sem *sem) { int DFSDL::DFSDL_SemPost(DFSDL_sem *sem) { return g_SDL_SemPost(sem); } + +int DFSDL::DFSDL_PushEvent(DFSDL_Event *event) { + return g_SDL_PushEvent(event); +} diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index d1a269d52..f33044877 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -27,6 +27,7 @@ #include "modules/MapCache.h" #include "modules/Maps.h" #include "modules/Materials.h" +#include "modules/DFSDL.h" #include "modules/Translation.h" #include "modules/Units.h" #include "modules/World.h" @@ -2897,7 +2898,7 @@ static command_result PassKeyboardEvent(color_ostream &stream, const KeyboardEve e.key.ksym.scancode = in->scancode(); e.key.ksym.sym = (SDL::Key)in->sym(); e.key.ksym.unicode = in->unicode(); - SDL_PushEvent(&e); + DFHack::DFSDL::DFSDL_PushEvent(&e); #endif return CR_OK; } From c513c246a5be456b9c452c3dd080ec206e21dc0d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 21:17:25 -0800 Subject: [PATCH 107/126] more SDL wrapping for stonesense --- library/include/modules/DFSDL.h | 2 ++ library/modules/DFSDL.cpp | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 226b9f881..9f07ea3db 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -81,7 +81,9 @@ void cleanup(); DFHACK_EXPORT DFSDL_Surface * DFIMG_Load(const char *file); DFHACK_EXPORT int DFSDL_SetAlpha(DFSDL_Surface *surface, uint32_t flag, uint8_t alpha); +DFHACK_EXPORT DFSDL_Surface * DFSDL_GetVideoSurface(void); DFHACK_EXPORT DFSDL_Surface * DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); +DFHACK_EXPORT DFSDL_Surface * DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); DFHACK_EXPORT int DFSDL_UpperBlit(DFSDL_Surface *src, const DFSDL_Rect *srcrect, DFSDL_Surface *dst, DFSDL_Rect *dstrect); DFHACK_EXPORT DFSDL_Surface * DFSDL_ConvertSurface(DFSDL_Surface *src, const DFSDL_PixelFormat *fmt, uint32_t flags); DFHACK_EXPORT void DFSDL_FreeSurface(DFSDL_Surface *surface); diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 08e213b94..6a3e6af2f 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -28,13 +28,15 @@ static const std::vector SDL_IMAGE_LIBS { DFSDL_Surface * (*g_IMG_Load)(const char *) = nullptr; int (*g_SDL_SetAlpha)(DFSDL_Surface *, uint32_t, uint8_t) = nullptr; -DFSDL_Surface * (*g_SDL_CreateRGBSurface)(uint32_t, int, int, int, uint32_t, uint32_t, uint32_t, uint32_t); -int (*g_SDL_UpperBlit)(DFSDL_Surface *, const DFSDL_Rect *, DFSDL_Surface *, DFSDL_Rect *); -DFSDL_Surface * (*g_SDL_ConvertSurface)(DFSDL_Surface *, const DFSDL_PixelFormat *, uint32_t); -void (*g_SDL_FreeSurface)(DFSDL_Surface *); -int (*g_SDL_SemWait)(DFSDL_sem *); -int (*g_SDL_SemPost)(DFSDL_sem *); -int (*g_SDL_PushEvent)(DFSDL_Event *); +DFSDL_Surface * (*g_SDL_GetVideoSurface)(void) = nullptr; +DFSDL_Surface * (*g_SDL_CreateRGBSurface)(uint32_t, int, int, int, uint32_t, uint32_t, uint32_t, uint32_t) = nullptr; +DFSDL_Surface * (*g_SDL_CreateRGBSurfaceFrom)(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) = nullptr; +int (*g_SDL_UpperBlit)(DFSDL_Surface *, const DFSDL_Rect *, DFSDL_Surface *, DFSDL_Rect *) = nullptr; +DFSDL_Surface * (*g_SDL_ConvertSurface)(DFSDL_Surface *, const DFSDL_PixelFormat *, uint32_t) = nullptr; +void (*g_SDL_FreeSurface)(DFSDL_Surface *) = nullptr; +int (*g_SDL_SemWait)(DFSDL_sem *) = nullptr; +int (*g_SDL_SemPost)(DFSDL_sem *) = nullptr; +int (*g_SDL_PushEvent)(DFSDL_Event *) = nullptr; bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -64,7 +66,9 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_image_handle, IMG_Load); bind(g_sdl_handle, SDL_SetAlpha); + bind(g_sdl_handle, SDL_GetVideoSurface); bind(g_sdl_handle, SDL_CreateRGBSurface); + bind(g_sdl_handle, SDL_CreateRGBSurfaceFrom); bind(g_sdl_handle, SDL_UpperBlit); bind(g_sdl_handle, SDL_ConvertSurface); bind(g_sdl_handle, SDL_FreeSurface); @@ -97,10 +101,18 @@ int DFSDL::DFSDL_SetAlpha(DFSDL_Surface *surface, uint32_t flag, uint8_t alpha) return g_SDL_SetAlpha(surface, flag, alpha); } +DFSDL_Surface * DFSDL::DFSDL_GetVideoSurface(void) { + return g_SDL_GetVideoSurface(); +} + DFSDL_Surface * DFSDL::DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) { return g_SDL_CreateRGBSurface(flags, width, height, depth, Rmask, Gmask, Bmask, Amask); } +DFSDL_Surface * DFSDL::DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) { + return g_SDL_CreateRGBSurfaceFrom(pixels, width, height, depth, pitch, Rmask, Gmask, Bmask, Amask); +} + int DFSDL::DFSDL_UpperBlit(DFSDL_Surface *src, const DFSDL_Rect *srcrect, DFSDL_Surface *dst, DFSDL_Rect *dstrect) { return g_SDL_UpperBlit(src, srcrect, dst, dstrect); } From 8f99b03a534dbcc8c56221aec445b8c99263cd26 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 21:25:05 -0800 Subject: [PATCH 108/126] update stonesense ref --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 6570fe010..3e494d9d9 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 6570fe01081f7e402495bc5339b4ff7a1aabf305 +Subproject commit 3e494d9d968add443ebd63cc167933cc813f0eee From d2da06acc637bc95c3031efaaa0d1d9c250f74c7 Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 26 Feb 2023 21:37:02 -0800 Subject: [PATCH 109/126] Update docs/changelog.txt --- docs/changelog.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 412ee07a0..0d29f5f3c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -79,10 +79,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API - ``Gui::any_civzone_hotkey``, ``Gui::getAnyCivZone``, ``Gui::getSelectedCivZone``: new functions to operate on the new zone system - Units module: added new predicates for ``isGeldable()``, ``isMarkedForGelding()``, and ``isPet()`` -- `Military`: New module for military functionality -- `Military`: new ``makeSquad`` to create a squad -- `Military`: changed ``getSquadName`` to take a squad identifier -- `Military`: new ``updateRoomAssignments`` for assigning a squad to a barracks and archery range +- ``Military``: New module for military functionality +- ``Military``: new ``makeSquad`` to create a squad +- ``Military``: changed ``getSquadName`` to take a squad identifier +- ``Military``: new ``updateRoomAssignments`` for assigning a squad to a barracks and archery range - ``Maps::GetBiomeType`` renamed to ``Maps::getBiomeType`` for consistency - ``Maps::GetBiomeTypeRef`` renamed to ``Maps::getBiomeTypeRef`` for consistency From 87ba0d270c9b0036236666ca451b889ef714b0c1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 27 Feb 2023 05:49:22 +0000 Subject: [PATCH 110/126] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index d4170eacf..8ae81f8d8 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d4170eacfc0f82fcb7364e558d0e782dc497f7d5 +Subproject commit 8ae81f8d8f1f96d82b9074b205073bb8e8d29f96 diff --git a/scripts b/scripts index c7345f6fe..1bf1cec84 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit c7345f6fe096bc6ce1700b70b4f7d4c65b2a3e57 +Subproject commit 1bf1cec84ce34cdf56eabe5140d8f6e8cd028169 From df0c7c27cb8020722b6ffd31fa6ca6269c0f4841 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 22:04:22 -0800 Subject: [PATCH 111/126] adjust to structures change --- library/modules/Units.cpp | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index a636310e1..533b40ca8 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -588,19 +588,7 @@ bool Units::isMischievous(df::unit *unit) bool Units::isAvailableForAdoption(df::unit* unit) { CHECK_NULL_POINTER(unit); - auto refs = unit->specific_refs; - for(size_t i=0; itype; - if( reftype == df::specific_ref_type::PETINFO_PET ) - { - //df::pet_info* pet = ref->pet; - return true; - } - } - - return false; + return unit->flags3.bits.available_for_adoption; } bool Units::isPet(df::unit* unit) From 18189510b533eb9d19120edd85093b18e3319439 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 27 Feb 2023 07:15:47 +0000 Subject: [PATCH 112/126] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 1bf1cec84..81183a380 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1bf1cec84ce34cdf56eabe5140d8f6e8cd028169 +Subproject commit 81183a380b11f4c3045a7888c35afe215d2185ad From 9b8400ab40df4006d869f7dc6c9ba00bcf55933c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 22:54:15 -0800 Subject: [PATCH 113/126] prevent planned buildings from being resumed note this only prevents unsuspending from the building sheet panel, not the tasks screen --- plugins/lua/buildingplan.lua | 18 +++++++++++++++++- plugins/lua/overlay.lua | 3 +++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 0e1341a3b..25e9bc46e 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -1625,11 +1625,27 @@ function InspectorOverlay:make_top_priority() self:reset() end +local RESUME_BUTTON_FRAME = {t=15, h=3, r=73, w=25} + +local function mouse_is_over_resume_button(rect) + local x,y = dfhack.screen.getMousePos() + if not x then return false end + if y < RESUME_BUTTON_FRAME.t or y > RESUME_BUTTON_FRAME.t + RESUME_BUTTON_FRAME.h - 1 then + return false + end + if x > rect.x2 - RESUME_BUTTON_FRAME.r + 1 or x < rect.x2 - RESUME_BUTTON_FRAME.r - RESUME_BUTTON_FRAME.w + 2 then + return false + end + return true +end + function InspectorOverlay:onInput(keys) if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then return false end - if keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + if keys._MOUSE_L_DOWN and mouse_is_over_resume_button(self.frame_parent_rect) then + return true + elseif keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then self:reset() end return InspectorOverlay.super.onInput(self, keys) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 56ad72372..3d476bf0d 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -493,6 +493,9 @@ function feed_viewscreen_widgets(vs_name, keys) return false end gui.markMouseClicksHandled(keys) + if keys._MOUSE_L_DOWN then + df.global.enabler.mouse_lbut = 0 + end return true end From 4f933e0a368a14743408331b3a02d26e2517ec02 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 23:06:25 -0800 Subject: [PATCH 114/126] ensure reachability for selected items --- plugins/buildingplan/buildingplan_cycle.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 655dc8c1a..329ee3b99 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -5,6 +5,7 @@ #include "modules/Items.h" #include "modules/Job.h" +#include "modules/Maps.h" #include "modules/Materials.h" #include "df/building_design.h" @@ -151,6 +152,10 @@ static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, return NULL; } +static bool isAccessibleFrom(df::item *item, df::job *job) { + return Maps::canWalkBetween(Items::getPosition(item), job->pos); +} + static void doVector(color_ostream &out, df::job_item_vector_id vector_id, map &buckets, unordered_map &planned_buildings) { @@ -182,9 +187,10 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto job = bld->jobs[0]; auto filter_idx = task.second; auto &pb = planned_buildings.at(id); - if (matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, + if (isAccessibleFrom(item, job) + && matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, pb.item_filters[filter_idx]) - && Job::attachJobItem(job, item, + && Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { MaterialInfo material; From c1ea43244b6cb9649db4179f273ef276b971718b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 23:38:00 -0800 Subject: [PATCH 115/126] order buckets by pickiness --- plugins/buildingplan/buildingplan.cpp | 35 ++++++++++++++++++++++++--- plugins/buildingplan/itemfilter.cpp | 31 ------------------------ plugins/buildingplan/itemfilter.h | 10 ++++---- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 4fa119ebc..53ab4cdff 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -323,9 +323,34 @@ static command_result do_command(color_ostream &out, vector ¶meters) // core will already be suspended when coming in through here // -static string getBucket(const df::job_item & ji) { +static string getBucket(const df::job_item & ji, const PlannedBuilding & pb, int idx) { + if (idx < 0 || (size_t)idx < pb.item_filters.size()) + return "INVALID"; + std::ostringstream ser; + // put elements in front that significantly affect the difficulty of matching + // the filter. ensure the lexicographically "less" value is the pickier value. + const ItemFilter & item_filter = pb.item_filters[idx]; + + if (item_filter.getDecoratedOnly()) + ser << "Da"; + else + ser << "Db"; + + if (ji.flags2.bits.magma_safe || pb.heat_safety == HEAT_SAFETY_MAGMA) + ser << "Ha"; + else if (ji.flags2.bits.fire_safe || pb.heat_safety == HEAT_SAFETY_FIRE) + ser << "Hb"; + else + ser << "Hc"; + + size_t num_materials = item_filter.getMaterials().size(); + if (num_materials == 0 || num_materials >= 9 || item_filter.getMaterialMask().whole) + ser << "M9"; + else + ser << "M" << num_materials; + // pull out and serialize only known relevant fields. if we miss a few, then // the filter bucket will be slighly less specific than it could be, but // that's probably ok. we'll just end up bucketing slightly different items @@ -337,6 +362,8 @@ static string getBucket(const df::job_item & ji) { << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' << ji.metal_ore << ':' << ji.has_tool_use; + ser << ':' << item_filter.serialize(); + return ser.str(); } @@ -394,7 +421,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { int32_t id = bld->id; for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { auto job_item = job_items[job_item_idx]; - auto bucket = getBucket(*job_item); + auto bucket = getBucket(*job_item, pb, job_item_idx); // if there are multiple vector_ids, schedule duplicate tasks. after // the correct number of items are matched, the extras will get popped @@ -682,7 +709,7 @@ static int getQueuePosition(color_ostream &out, df::building *bld, int index) { if (!tasks.count(vec_id)) continue; auto &buckets = tasks.at(vec_id); - string bucket_id = getBucket(*job_item); + string bucket_id = getBucket(*job_item, pb, index); if (!buckets.count(bucket_id)) continue; int bucket_pos = -1; @@ -711,7 +738,7 @@ static void makeTopPriority(color_ostream &out, df::building *bld) { if (!tasks.count(vec_id)) continue; auto &buckets = tasks.at(vec_id); - string bucket_id = getBucket(*job_items[index]); + string bucket_id = getBucket(*job_items[index], pb, index); if (!buckets.count(bucket_id)) continue; auto &bucket = buckets.at(bucket_id); diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index a714b62d4..86c9c1378 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -131,37 +131,6 @@ void ItemFilter::setMaterials(const vector &materials) { this->materials = materials; } -string ItemFilter::getMinQuality() const { - return ENUM_KEY_STR(item_quality, min_quality); -} - -string ItemFilter::getMaxQuality() const { - return ENUM_KEY_STR(item_quality, max_quality); -} - -bool ItemFilter::getDecoratedOnly() const { - return decorated_only; -} - -uint32_t ItemFilter::getMaterialMask() const { - return mat_mask.whole; -} - -static string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } - -vector ItemFilter::getMaterials() const { - vector descriptions; - transform_(materials, descriptions, material_to_string_fn); - - if (descriptions.size() == 0) - bitfield_to_string(&descriptions, mat_mask); - - if (descriptions.size() == 0) - descriptions.push_back("any"); - - return descriptions; -} - static bool matchesMask(DFHack::MaterialInfo &mat, df::dfhack_material_category mat_mask) { return mat_mask.whole ? mat.matches(mat_mask) : true; } diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 6eb7551b4..29eb7226c 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -20,11 +20,11 @@ public: void setMaterialMask(uint32_t mask); void setMaterials(const std::vector &materials); - std::string getMinQuality() const; - std::string getMaxQuality() const; - bool getDecoratedOnly() const; - uint32_t getMaterialMask() const; - std::vector getMaterials() const; + df::item_quality getMinQuality() const { return min_quality; } + df::item_quality getMaxQuality() const {return max_quality; } + bool getDecoratedOnly() const { return decorated_only; } + df::dfhack_material_category getMaterialMask() const { return mat_mask; } + std::vector getMaterials() const { return materials; } bool matches(df::dfhack_material_category mask) const; bool matches(DFHack::MaterialInfo &material) const; From 0f2c88265e0f1f2a8f55572afd31916fb48b0e11 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 23:41:28 -0800 Subject: [PATCH 116/126] scan IN_PLAY last so more specific vectors are scanned first --- plugins/buildingplan/buildingplan_cycle.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 329ee3b99..375e26212 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -241,6 +241,7 @@ struct VectorsToScanLast { vectors.push_back(df::job_item_vector_id::BOULDER); vectors.push_back(df::job_item_vector_id::WOOD); vectors.push_back(df::job_item_vector_id::BAR); + vectors.push_back(df::job_item_vector_id::IN_PLAY); } }; @@ -254,7 +255,7 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, for (auto it = tasks.begin(); it != tasks.end(); ) { auto vector_id = it->first; - // we could make this a set, but it's only three elements + // we could make this a set, but it's only a few elements if (std::find(vectors_to_scan_last.vectors.begin(), vectors_to_scan_last.vectors.end(), vector_id) != vectors_to_scan_last.vectors.end()) { From a5d22705e862d0a0e9d34c3b37c2b40b19c21ade Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Feb 2023 04:13:05 -0800 Subject: [PATCH 117/126] add label_below attribute --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 2 ++ library/lua/gui/widgets.lua | 12 +++++++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7010bdd31..4a3428031 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -50,6 +50,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``dfhack.job.attachJobItem()``: allows you to attach specific items to a job - ``dfhack.screen.paintTile()``: you can now explicitly clear the interface cursor from a map tile by passing ``0`` as the tile value - ``widgets.Label``: token ``tile`` properties can now be functions that return a value +- ``widgets.CycleHotkeyLabel``: add ``label_below`` attribute for compact 2-line output -@ ``widgets.FilteredList``: search key matching is now case insensitive by default -@ ``gui.INTERIOR_FRAME``: a panel frame style for use in highlighting off interior areas of a UI diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index f9aafe4e0..9f2386660 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4896,6 +4896,8 @@ It has the following attributes: hotkey. :label_width: The number of spaces to allocate to the ``label`` (for use in aligning a column of ``CycleHotkeyLabel`` labels). +:label_below: If ``true``, then the option value will apear below the label + instead of to the right of it. Defaults to ``false``. :options: A list of strings or tables of ``{label=string, value=string[, pen=pen]}``. String options use the same string for the label and value and the default pen. The optional ``pen`` diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index ab018a50a..b86e31710 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1490,6 +1490,7 @@ CycleHotkeyLabel.ATTRS{ key_back=DEFAULT_NIL, label=DEFAULT_NIL, label_width=DEFAULT_NIL, + label_below=false, options=DEFAULT_NIL, initial_option=1, on_change=DEFAULT_NIL, @@ -1498,12 +1499,17 @@ CycleHotkeyLabel.ATTRS{ function CycleHotkeyLabel:init() self:setOption(self.initial_option) + local val_gap = 1 + if self.label_below then + val_gap = 0 + (self.key_back and 1 or 0) + (self.key and 3 or 0) + end + self:setText{ self.key_back ~= nil and {key=self.key_back, key_sep='', width=0, on_activate=self:callback('cycle', true)} or {}, {key=self.key, key_sep=': ', text=self.label, width=self.label_width, on_activate=self:callback('cycle')}, - ' ', - {text=self:callback('getOptionLabel'), + self.label_below and NEWLINE or '', + {gap=val_gap, text=self:callback('getOptionLabel'), pen=self:callback('getOptionPen')}, } end @@ -1580,7 +1586,7 @@ end function CycleHotkeyLabel:onInput(keys) if CycleHotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L_DOWN and self:getMousePos() then + elseif keys._MOUSE_L_DOWN and self:getMousePos() and not is_disabled(self) then self:cycle() return true end From 9f794a0710bedaaf0f5c9313990999127506f00b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Feb 2023 04:13:29 -0800 Subject: [PATCH 118/126] filter dialog mock, draft 2; implement Slider --- plugins/lua/buildingplan.lua | 614 +++++++++++++++++++++++------------ 1 file changed, 405 insertions(+), 209 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 25e9bc46e..58bb257b9 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -475,9 +475,157 @@ function ItemSelectionScreen:init() end -------------------------------- --- FilterSelection +-- Slider -- +Slider = defclass(Slider, widgets.Widget) +Slider.ATTRS{ + num_stops=DEFAULT_NIL, + get_left_idx_fn=DEFAULT_NIL, + get_right_idx_fn=DEFAULT_NIL, + on_left_change=DEFAULT_NIL, + on_right_change=DEFAULT_NIL, +} + +function Slider:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function Slider:init() + if self.num_stops < 2 then error('too few Slider stops') end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +local function slider_get_width_per_idx(self) + return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L_DOWN then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + local width_per_idx = slider_get_width_per_idx(self) + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(right_idx-1) + 4 + if x < left_pos then + self.on_left_change(self.get_left_idx_fn() - 1) + elseif x < left_pos+3 then + self.is_dragging_target = 'left' + self.is_dragging_idx = x - left_pos + elseif x < right_pos then + self.is_dragging_target = 'both' + self.is_dragging_idx = x - left_pos + elseif x < right_pos+3 then + self.is_dragging_target = 'right' + self.is_dragging_idx = x - right_pos + else + self.on_right_change(self.get_right_idx_fn() + 1) + end + return true +end + +local function slider_do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = self.is_dragging_target == 'right' and -2 or 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + local new_left_idx, new_right_idx + if self.is_dragging_target == 'right' then + new_right_idx = new_idx + else + new_left_idx = new_idx + if self.is_dragging_target == 'both' then + new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() + if new_right_idx > self.num_stops then + return + end + end + end + if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then + self.on_left_change(new_left_idx) + end + if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then + self.on_right_change(new_right_idx) + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function Slider:onRenderBody(dc, rect) + local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + local width_per_idx = slider_get_width_per_idx(self) + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx > stop_idx or right_idx < stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif right_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if right_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + -- draw tabs + dc:seek(width_per_idx*(left_idx-1)) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + dc:seek(width_per_idx*(right_idx-1)+4) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + -- manage dragging + if self.is_dragging_target then + slider_do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + +-------------------------------- +-- QualityAndMaterialsPage +-- + +QualityAndMaterialsPage = defclass(QualityAndMaterialsPage, widgets.Panel) +QualityAndMaterialsPage.ATTRS{ + frame={t=0, l=0}, + index=DEFAULT_NIL, +} + +local TYPE_COL_WIDTH = 20 +local HEADER_HEIGHT = 8 +local QUALITY_HEIGHT = 9 +local FOOTER_HEIGHT = 4 + -- returns whether the items matched by the specified filter can have a quality -- rating. This also conveniently indicates whether an item can be decorated. local function can_be_improved(idx) @@ -491,236 +639,172 @@ local function can_be_improved(idx) filter.item_type ~= df.item_type.BOULDER end -local OPTIONS_COL_WIDTH = 28 -local TYPE_COL_WIDTH = 20 -local HEADER_HEIGHT = 5 -local FOOTER_HEIGHT = 4 - -FilterSelection = defclass(FilterSelection, widgets.Window) -FilterSelection.ATTRS{ - frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]', - frame={w=80, h=53, l=30, t=8}, - resizable=true, - index=DEFAULT_NIL, -} - -local STANDIN_PEN = to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '} +function QualityAndMaterialsPage:init() + self.lowest_other_item_heat_safety = 2 -function FilterSelection:init() self:addviews{ widgets.Panel{ - view_id='options_panel', - frame={l=0, t=0, b=FOOTER_HEIGHT, w=OPTIONS_COL_WIDTH}, - autoarrange_subviews=true, + view_id='header', + frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, + frame_inset={l=1}, subviews={ - widgets.Panel{ - view_id='quality_panel', - frame={l=0, r=0, h=24}, - frame_inset={t=1}, - frame_style=gui.INTERIOR_FRAME, - frame_title='Item quality', - subviews={ - widgets.HotkeyLabel{ - frame={l=0, t=0}, - key='CUSTOM_SHIFT_Q', - }, - widgets.HotkeyLabel{ - frame={l=1, t=0}, - key='CUSTOM_SHIFT_W', - label='Set max quality', - }, - widgets.Panel{ - view_id='quality_slider', - frame={l=0, t=2, w=3, h=15}, - frame_background=STANDIN_PEN, - }, - widgets.Label{ - frame={l=3, t=3}, - text='- Artifact (1)', - }, - widgets.Label{ - frame={l=3, t=5}, - text='- Masterful (3)', - }, - widgets.Label{ - frame={l=3, t=7}, - text='- Exceptional (34)', - }, - widgets.Label{ - frame={l=3, t=9}, - text='- Superior (50)', - }, - widgets.Label{ - frame={l=3, t=11}, - text='- FinelyCrafted (67)', - }, - widgets.Label{ - frame={l=3, t=13}, - text='- WellCrafted (79)', - }, - widgets.Label{ - frame={l=3, t=15}, - text='- Ordinary (206)', - }, - widgets.HotkeyLabel{ - frame={l=0, t=18}, - key='CUSTOM_SHIFT_Z', - }, - widgets.HotkeyLabel{ - frame={l=1, t=18}, - key='CUSTOM_SHIFT_X', - label='Set min quality', - }, - widgets.CycleHotkeyLabel{ - frame={l=0, t=20}, - key='CUSTOM_SHIFT_D', - label='Decorated only:', - options={'No', 'Yes'}, - }, + widgets.Label{ + frame={l=0, t=0, h=1, r=0}, + text={ + 'Current filter:', + {gap=1, pen=COLOR_LIGHTCYAN, text=self:callback('get_summary')} + }, + }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={t=2, l=0, w=35}, + key='CUSTOM_SHIFT_G', + label='Building heat safety:', + options={ + {label='Fire Magma', value=0, pen=COLOR_GREY}, + {label='Fire Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, }, - widgets.ResizingPanel{ - view_id='building_panel', - frame={l=0, r=0}, - frame_inset={t=1}, - frame_style=gui.INTERIOR_FRAME, - frame_title='Building options', - autoarrange_subviews=true, - autoarrange_gap=1, - subviews={ - widgets.WrappedLabel{ - frame={l=0}, - text_to_wrap='These options will affect all items for the current building type.', - }, - widgets.CycleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_G', - label='Building safety:', - options={ - {label='Any', value=0}, - {label='Magma', value=2, pen=COLOR_RED}, - {label='Fire', value=1, pen=COLOR_LIGHTRED}, - }, - }, + widgets.Label{ + frame={t=2, l=30}, + text='Magma', + auto_width=true, + text_pen=COLOR_GREY, + visible=function() return self.subviews.safety:getOptionValue() == 1 end, + }, + widgets.Label{ + frame={t=3, l=3}, + text='Other items for this building may not be able to use all of their selected materials.', + visible=function() return self.subviews.safety:getOptionValue() > self.lowest_other_item_heat_safety end, + }, + widgets.EditField{ + frame={l=0, t=4, w=23}, + label_text='Search: ', + on_char=function(ch) return ch:match('%l') end, + }, + widgets.CycleHotkeyLabel{ + frame={l=24, t=4, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_R', + options={'name', 'available'}, + }, + widgets.ToggleHotkeyLabel{ + frame={l=24, t=5, w=24}, + label='Hide unavailable:', + key='CUSTOM_SHIFT_H', + initial_option=false, + }, + widgets.Label{ + frame={l=1, b=0}, + text='Type', + text_pen=COLOR_LIGHTRED, + }, + widgets.Label{ + frame={l=TYPE_COL_WIDTH, b=0}, + text='Material', + text_pen=COLOR_LIGHTRED, + }, + }, + }, + widgets.Panel{ + view_id='materials_lists', + frame={l=0, t=HEADER_HEIGHT, r=0, b=FOOTER_HEIGHT+QUALITY_HEIGHT}, + frame_style=gui.INTERIOR_FRAME, + subviews={ + widgets.List{ + view_id='materials_categories', + frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3}, + scroll_keys={}, + choices={ + {text='Stone', key='CUSTOM_SHIFT_S'}, + {text='Wood', key='CUSTOM_SHIFT_O'}, + {text='Metal', key='CUSTOM_SHIFT_M'}, + {text='Other', key='CUSTOM_SHIFT_T'}, }, }, - widgets.Panel{ - view_id='global_panel', - frame={l=0, r=0, b=0}, - frame_inset={t=1}, - frame_style=gui.INTERIOR_FRAME, - frame_title='Global options', - autoarrange_subviews=true, - subviews={ - widgets.WrappedLabel{ - frame={l=0}, - text_to_wrap='These options will affect the selection of "Generic Materials" for future buildings.', - }, - widgets.Panel{ - frame={h=1}, - }, - widgets.ToggleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_B', - label='Blocks', - label_width=8, - }, - widgets.ToggleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_L', - label='Logs', - label_width=8, - }, - widgets.ToggleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_O', - label='Boulders', - label_width=8, - }, - widgets.ToggleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_P', - label='Bars', - label_width=8, - }, + widgets.List{ + view_id='materials_mats', + frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, + choices={ + {text='9 - granite'}, + {text='0 - graphite'}, }, }, }, }, widgets.Panel{ - view_id='materials_panel', - frame={l=OPTIONS_COL_WIDTH, t=0, b=FOOTER_HEIGHT, r=0}, + view_id='divider', + frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=FOOTER_HEIGHT+QUALITY_HEIGHT, w=1}, + on_render=self:callback('draw_divider'), + }, + widgets.Panel{ + view_id='quality_panel', + frame={l=0, r=0, h=QUALITY_HEIGHT, b=FOOTER_HEIGHT}, + frame_style=gui.INTERIOR_FRAME, + frame_title='Item quality', subviews={ - widgets.Panel{ - view_id='header', - frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, - subviews={ - widgets.EditField{ - frame={l=1, t=0}, - label_text='Search: ', - on_char=function(ch) return ch:match('%l') end, - }, - widgets.CycleHotkeyLabel{ - frame={l=1, t=2, w=21}, - label='Sort by:', - key='CUSTOM_SHIFT_R', - options={'name', 'available'}, - }, - widgets.ToggleHotkeyLabel{ - frame={l=24, t=2, w=24}, - label='Hide unavailable:', - key='CUSTOM_SHIFT_H', - initial_option=false, - }, - widgets.Label{ - frame={l=1, b=0}, - text='Type', - text_pen=COLOR_LIGHTRED, - }, - widgets.Label{ - frame={l=TYPE_COL_WIDTH, b=0}, - text='Material', - text_pen=COLOR_LIGHTRED, - }, + widgets.CycleHotkeyLabel{ + frame={l=0, t=1, w=23}, + key='CUSTOM_SHIFT_D', + label='Decorated only:', + options={'No', 'Yes'}, + enabled=function() return can_be_improved(self.index) end, + }, + widgets.CycleHotkeyLabel{ + view_id='min_quality', + frame={l=0, t=3, 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}, }, + on_change=function(val) self:set_min_quality(val+1) end, }, - widgets.Panel{ - view_id='materials_lists', - frame={l=0, t=HEADER_HEIGHT, r=0, b=0}, - frame_style=gui.INTERIOR_FRAME, - subviews={ - widgets.List{ - view_id='materials_categories', - frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3}, - scroll_keys={}, - choices={ - {text='Stone', key='CUSTOM_SHIFT_S'}, - {text='Wood', key='CUSTOM_SHIFT_W'}, - {text='Metal', key='CUSTOM_SHIFT_M'}, - {text='Other', key='CUSTOM_SHIFT_O'}, - }, - }, - widgets.List{ - view_id='materials_mats', - frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, - choices={ - {text='9 - granite'}, - {text='0 - graphite'}, - }, - }, + widgets.CycleHotkeyLabel{ + view_id='max_quality', + frame={r=1, t=3, 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}, }, + on_change=function(val) self:set_max_quality(val+1) end, + }, + Slider{ + frame={l=0, t=6}, + 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=self:callback('set_min_quality'), + on_right_change=self:callback('set_max_quality'), }, - widgets.Panel{ - view_id='divider', - frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=0, w=1}, - on_render=self:callback('draw_divider'), - } }, }, widgets.Panel{ view_id='footer', frame={l=0, r=0, b=0, h=FOOTER_HEIGHT}, - frame_inset={l=20, t=1}, + frame_inset={t=1, l=1}, subviews={ widgets.HotkeyLabel{ frame={l=0, t=0}, @@ -757,6 +841,22 @@ function FilterSelection:init() } end +function QualityAndMaterialsPage:set_min_quality(idx) + idx = math.min(6, math.max(0, idx-1)) + self.subviews.min_quality:setOption(idx) + if self.subviews.max_quality:getOptionValue() < idx then + self.subviews.max_quality:setOption(idx) + end +end + +function QualityAndMaterialsPage:set_max_quality(idx) + idx = math.min(6, math.max(0, idx-1)) + self.subviews.max_quality:setOption(idx) + if self.subviews.min_quality:getOptionValue() > idx then + self.subviews.min_quality:setOption(idx) + end +end + local texpos = dfhack.textures.getThinBordersTexposStart() local tp = function(offset) if texpos == -1 then return nil end @@ -767,7 +867,7 @@ local TOP_PEN = to_pen{tile=tp(10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK} local MID_PEN = to_pen{tile=tp(4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK} local BOT_PEN = to_pen{tile=tp(11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} -function FilterSelection:draw_divider(dc) +function QualityAndMaterialsPage:draw_divider(dc) local y2 = dc.height - 1 for y=0,y2 do dc:seek(0, y) @@ -781,6 +881,100 @@ function FilterSelection:draw_divider(dc) end end +function QualityAndMaterialsPage:get_summary() + return 'filter summary' +end + +-------------------------------- +-- GlobalSettingsPage +-- + +GlobalSettingsPage = defclass(GlobalSettingsPage, widgets.ResizingPanel) +GlobalSettingsPage.ATTRS{ + autoarrange_subviews=true, + frame={t=0, l=0}, + frame_inset={l=1, r=1}, +} + +function GlobalSettingsPage:init() + self:addviews{ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap='These options will affect the selection of "Generic Materials" for all future buildings.', + }, + widgets.Panel{ + frame={h=1}, + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_B', + label='Blocks', + label_width=8, + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_L', + label='Logs', + label_width=8, + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_O', + label='Boulders', + label_width=8, + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_R', + label='Bars', + label_width=8, + }, + } +end + +-------------------------------- +-- FilterSelection +-- + +FilterSelection = defclass(FilterSelection, widgets.Window) +FilterSelection.ATTRS{ + frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]', + frame={w=53, h=53, l=30, t=8}, + frame_inset={t=1}, + resizable=true, + index=DEFAULT_NIL, + autoarrange_subviews=true, +} + +function FilterSelection:init() + self:addviews{ + widgets.TabBar{ + frame={t=0}, + labels={ + 'Quality and materials', + 'Global settings', + }, + on_select=function(idx) + self.subviews.pages:setSelected(idx) + self:updateLayout() + end, + get_cur_page=function() return self.subviews.pages:getSelected() end, + key='CUSTOM_CTRL_T', + }, + widgets.Widget{ + frame={h=1}, + }, + widgets.Pages{ + view_id='pages', + frame={t=5, l=0, b=0, r=0}, + subviews={ + QualityAndMaterialsPage{index=self.index}, + GlobalSettingsPage{}, + }, + }, + } +end + FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) FilterSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', @@ -794,10 +988,12 @@ function FilterSelectionScreen:init() end function FilterSelectionScreen:onShow() + -- don't let the building "shadow" follow the mouse cursor while this screen is open df.global.game.main_interface.bottom_mode_selected = -1 end function FilterSelectionScreen:onDismiss() + -- re-enable building shadow df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT end From 1d855014c26d1247cfdafafea94db791a01639e4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Feb 2023 10:16:58 -0800 Subject: [PATCH 119/126] implement global settings page --- plugins/buildingplan/buildingplan.cpp | 18 ++++++++++++++++-- plugins/lua/buildingplan.lua | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 53ab4cdff..01eeda120 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -603,6 +603,20 @@ static int getAvailableItems(lua_State *L) { return 1; } +static int getGlobalSettings(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering getGlobalSettings\n"); + map settings; + settings.emplace("blocks", get_config_bool(config, CONFIG_BLOCKS)); + settings.emplace("logs", get_config_bool(config, CONFIG_LOGS)); + settings.emplace("boulders", get_config_bool(config, CONFIG_BOULDERS)); + settings.emplace("bars", get_config_bool(config, CONFIG_BARS)); + Lua::Push(L, settings); + return 1; +} + static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(status,out).print( "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", @@ -762,8 +776,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(isPlannedBuilding), DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(doCycle), - DFHACK_LUA_FUNCTION(scheduleCycle), - DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), DFHACK_LUA_FUNCTION(hasFilter), DFHACK_LUA_FUNCTION(setMaterialFilter), DFHACK_LUA_FUNCTION(setHeatSafetyFilter), @@ -774,6 +787,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { }; DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getGlobalSettings), DFHACK_LUA_COMMAND(getAvailableItems), DFHACK_LUA_COMMAND(getMaterialFilter), DFHACK_LUA_COMMAND(getHeatSafetyFilter), diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 58bb257b9..95ba9b09c 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -906,30 +906,54 @@ function GlobalSettingsPage:init() frame={h=1}, }, widgets.ToggleHotkeyLabel{ + view_id='blocks', frame={l=0}, key='CUSTOM_B', label='Blocks', label_width=8, + on_change=self:callback('update_setting', 'blocks'), }, widgets.ToggleHotkeyLabel{ + view_id='logs', frame={l=0}, key='CUSTOM_L', label='Logs', label_width=8, + on_change=self:callback('update_setting', 'logs'), }, widgets.ToggleHotkeyLabel{ + view_id='boulders', frame={l=0}, key='CUSTOM_O', label='Boulders', label_width=8, + on_change=self:callback('update_setting', 'boulders'), }, widgets.ToggleHotkeyLabel{ + view_id='bars', frame={l=0}, key='CUSTOM_R', label='Bars', label_width=8, + on_change=self:callback('update_setting', 'bars'), }, } + + self:init_settings() +end + +function GlobalSettingsPage:init_settings() + local settings = getGlobalSettings() + local subviews = self.subviews + subviews.blocks:setOption(settings.blocks) + subviews.logs:setOption(settings.logs) + subviews.boulders:setOption(settings.boulders) + subviews.bars:setOption(settings.bars) +end + +function GlobalSettingsPage:update_setting(setting, val) + dfhack.run_command('buildingplan', 'set', setting, tostring(val)) + self:init_settings() end -------------------------------- From 97e5fdb78eb76fbd83165a6529d924b86e5ddc93 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Feb 2023 12:27:21 -0800 Subject: [PATCH 120/126] implement saving and retrieving item quality filters --- plugins/buildingplan/buildingplan.cpp | 54 ++++++++++++++++- plugins/lua/buildingplan.lua | 83 +++++++++++++++++++++++---- 2 files changed, 124 insertions(+), 13 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 01eeda120..43f15ff5d 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -629,12 +629,22 @@ static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtyp BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key); for (auto &filter : filters.getItemFilters()) { - if (filter.isEmpty()) + if (!filter.isEmpty()) return true; } return false; } +static void clearFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + TRACE(status,out).print("entering clearFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key); + if (filters.getItemFilters().size() <= index) + return; + filters.setItemFilter(out, ItemFilter(), index); + call_buildingplan_lua(&out, "signal_reset"); +} + static void setMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, string filter) { DEBUG(status,out).print("entering setMaterialFilter\n"); call_buildingplan_lua(&out, "signal_reset"); @@ -682,6 +692,45 @@ static int getHeatSafetyFilter(lua_State *L) { return 1; } +static void setQualityFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, + int decorated, int min_quality, int max_quality) { + DEBUG(status,out).print("entering setQualityFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key).getItemFilters(); + if (filters.size() <= index) + return; + ItemFilter filter = filters[index]; + filter.setDecoratedOnly(decorated != 0); + filter.setMinQuality(min_quality); + filter.setMaxQuality(max_quality); + get_item_filters(out, key).setItemFilter(out, filter, index); + call_buildingplan_lua(&out, "signal_reset"); +} + +static int getQualityFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getQualityFilter building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(*out, key).getItemFilters(); + if (filters.size() <= index) + return 0; + auto &filter = filters[index]; + map ret; + ret.emplace("decorated", filter.getDecoratedOnly()); + ret.emplace("min_quality", filter.getMinQuality()); + ret.emplace("max_quality", filter.getMaxQuality()); + Lua::Push(L, ret); + return 1; +} + static bool validate_pb(color_ostream &out, df::building *bld, int index) { if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) return false; @@ -778,8 +827,10 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), DFHACK_LUA_FUNCTION(hasFilter), + DFHACK_LUA_FUNCTION(clearFilter), DFHACK_LUA_FUNCTION(setMaterialFilter), DFHACK_LUA_FUNCTION(setHeatSafetyFilter), + DFHACK_LUA_FUNCTION(setQualityFilter), DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_FUNCTION(makeTopPriority), @@ -791,5 +842,6 @@ DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(getAvailableItems), DFHACK_LUA_COMMAND(getMaterialFilter), DFHACK_LUA_COMMAND(getHeatSafetyFilter), + DFHACK_LUA_COMMAND(getQualityFilter), DFHACK_LUA_END }; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 95ba9b09c..2aaa34bbb 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -310,7 +310,7 @@ end function ItemSelection:get_choices(sort_fn) local item_ids = getAvailableItems(uibs.building_type, - uibs.building_subtype, uibs.custom_type, self.index - 1) + uibs.building_subtype, uibs.custom_type, self.index-1) local buckets = {} for _,item_id in ipairs(item_ids) do local item = df.item.find(item_id) @@ -641,6 +641,9 @@ end function QualityAndMaterialsPage:init() self.lowest_other_item_heat_safety = 2 + self.dirty = true + + local enable_item_quality = can_be_improved(self.index) self:addviews{ widgets.Panel{ @@ -665,6 +668,7 @@ function QualityAndMaterialsPage:init() {label='Fire Magma', value=2, pen=COLOR_RED}, {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, + on_change=self:callback('set_heat_safety'), }, widgets.Label{ frame={t=2, l=30}, @@ -745,11 +749,16 @@ function QualityAndMaterialsPage:init() frame_title='Item quality', subviews={ widgets.CycleHotkeyLabel{ + view_id='decorated', frame={l=0, t=1, w=23}, key='CUSTOM_SHIFT_D', label='Decorated only:', - options={'No', 'Yes'}, - enabled=function() return can_be_improved(self.index) end, + options={ + {label='No', value=false}, + {label='Yes', value=true}, + }, + enabled=enable_item_quality, + on_change=self:callback('set_decorated'), }, widgets.CycleHotkeyLabel{ view_id='min_quality', @@ -767,6 +776,7 @@ function QualityAndMaterialsPage:init() {label='Masterful', value=5}, {label='Artifact', value=6}, }, + enabled=enable_item_quality, on_change=function(val) self:set_min_quality(val+1) end, }, widgets.CycleHotkeyLabel{ @@ -785,6 +795,7 @@ function QualityAndMaterialsPage:init() {label='Masterful', value=5}, {label='Artifact', value=6}, }, + enabled=enable_item_quality, on_change=function(val) self:set_max_quality(val+1) end, }, Slider{ @@ -798,6 +809,7 @@ function QualityAndMaterialsPage:init() end, on_left_change=self:callback('set_min_quality'), on_right_change=self:callback('set_max_quality'), + active=enable_item_quality, }, }, }, @@ -841,20 +853,64 @@ function QualityAndMaterialsPage:init() } end +function QualityAndMaterialsPage:refresh() + local summary = '' + local subviews = self.subviews + + local heat = getHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type) + subviews.safety:setOption(heat) + if heat >= 2 then summary = summary .. 'Magma safe ' + elseif heat == 1 then summary = summary .. 'Fire safe ' + end + + local quality = getQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) + subviews.decorated:setOption(quality.decorated ~= 0) + subviews.min_quality:setOption(quality.min_quality) + subviews.max_quality:setOption(quality.max_quality) + + self.summary = summary + self.dirty = false +end + +function QualityAndMaterialsPage:get_summary() + -- TODO: summarize materials + return self.summary +end + +function QualityAndMaterialsPage:set_heat_safety(heat) + setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) + self.dirty = true +end + +function QualityAndMaterialsPage:set_decorated(decorated) + local subviews = self.subviews + setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, + decorated and 1 or 0, subviews.min_quality:getOptionValue(), subviews.max_quality:getOptionValue()) + self.dirty = true +end + function QualityAndMaterialsPage:set_min_quality(idx) idx = math.min(6, math.max(0, idx-1)) - self.subviews.min_quality:setOption(idx) - if self.subviews.max_quality:getOptionValue() < idx then - self.subviews.max_quality:setOption(idx) + local subviews = self.subviews + subviews.min_quality:setOption(idx) + if subviews.max_quality:getOptionValue() < idx then + subviews.max_quality:setOption(idx) end + setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, + subviews.decorated:getOptionValue() and 1 or 0, idx, subviews.max_quality:getOptionValue()) + self.dirty = true end function QualityAndMaterialsPage:set_max_quality(idx) idx = math.min(6, math.max(0, idx-1)) - self.subviews.max_quality:setOption(idx) - if self.subviews.min_quality:getOptionValue() > idx then - self.subviews.min_quality:setOption(idx) + local subviews = self.subviews + subviews.max_quality:setOption(idx) + if subviews.min_quality:getOptionValue() > idx then + subviews.min_quality:setOption(idx) end + setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, + subviews.decorated:getOptionValue() and 1 or 0, subviews.min_quality:getOptionValue(), idx) + self.dirty = true end local texpos = dfhack.textures.getThinBordersTexposStart() @@ -881,8 +937,11 @@ function QualityAndMaterialsPage:draw_divider(dc) end end -function QualityAndMaterialsPage:get_summary() - return 'filter summary' +function QualityAndMaterialsPage:onRenderFrame(dc, rect) + QualityAndMaterialsPage.super.onRenderFrame(self, dc, rect) + if self.dirty then + self:refresh() + end end -------------------------------- @@ -1444,7 +1503,7 @@ function PlannerOverlay:set_filter(idx) end function PlannerOverlay:clear_filter(idx) - setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1, "") + clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end local function get_placement_data() From 926bc8b7d4060a034058e82986faf8929e91ce7e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Mar 2023 04:38:40 -0800 Subject: [PATCH 121/126] cache valid materials on world load --- plugins/buildingplan/buildingplan.cpp | 58 ++++++++++++++++++++++++--- plugins/lua/buildingplan.lua | 2 + 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 43f15ff5d..476d1863d 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -52,6 +52,7 @@ void set_config_bool(PersistentDataItem &c, int index, bool value) { static PersistentDataItem config; // for use in counting available materials for the UI +static vector mat_cache; static unordered_map, BuildingTypeKeyHash> job_item_cache; static unordered_map cur_heat_safety; static unordered_map cur_item_filters; @@ -142,6 +143,47 @@ static const vector & get_job_items(color_ostream &out, Bu return jitems; } +static void cache_matched(int16_t type, int32_t index) { + static const df::dfhack_material_category building_material_categories( + df::dfhack_material_category::mask_glass | + df::dfhack_material_category::mask_metal | + df::dfhack_material_category::mask_soap | + df::dfhack_material_category::mask_stone | + df::dfhack_material_category::mask_wood + ); + + MaterialInfo mi; + mi.decode(type, index); + if (mi.matches(building_material_categories)) { + DEBUG(status).print("cached material: %s\n", mi.toString().c_str()); + mat_cache.emplace_back(mi); + } + else + TRACE(status).print("not matched: %s\n", mi.toString().c_str()); +} + +static void load_material_cache() { + df::world_raws &raws = world->raws; + for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) + if (raws.mat_table.builtin[i]) + cache_matched(i, -1); + + for (size_t i = 0; i < raws.inorganics.size(); i++) + cache_matched(0, i); + + for (size_t i = 0; i < raws.plants.all.size(); i++) { + df::plant_raw *p = raws.plants.all[i]; + if (p->material.size() <= 1) + continue; + for (size_t j = 0; j < p->material.size(); j++) { + if (p->material[j]->id == "WOOD") { + cache_matched(DFHack::MaterialInfo::PLANT_BASE+j, i); + break; + } + } + } +} + static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { if (cur_heat_safety.count(key)) return cur_heat_safety.at(key); @@ -221,6 +263,7 @@ static void clear_state(color_ostream &out) { } } job_item_cache.clear(); + mat_cache.clear(); } DFhackCExport command_result plugin_load_data (color_ostream &out) { @@ -236,6 +279,8 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DEBUG(status,out).print("loading persisted state\n"); clear_state(out); + load_material_cache(); + vector filter_configs; World::GetPersistentData(&filter_configs, FILTER_CONFIG_KEY); for (auto &cfg : filter_configs) { @@ -250,7 +295,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { PlannedBuilding pb(out, building_configs[idx]); df::building *bld = df::building::find(pb.id); if (!bld) { - INFO(status).print("building %d no longer exists; skipping\n", pb.id); + INFO(status,out).print("building %d no longer exists; skipping\n", pb.id); pb.remove(out); continue; } @@ -639,7 +684,7 @@ static void clearFilter(color_ostream &out, df::building_type type, int16_t subt TRACE(status,out).print("entering clearFilter\n"); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key); - if (filters.getItemFilters().size() <= index) + if (index < 0 || filters.getItemFilters().size() <= (size_t)index) return; filters.setItemFilter(out, ItemFilter(), index); call_buildingplan_lua(&out, "signal_reset"); @@ -661,8 +706,8 @@ static int getMaterialFilter(lua_State *L) { DEBUG(status,*out).print( "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); - vector filter; - Lua::PushVector(L, filter); + map counts_per_material; + Lua::Push(L, counts_per_material); return 1; } @@ -697,7 +742,7 @@ static void setQualityFilter(color_ostream &out, df::building_type type, int16_t DEBUG(status,out).print("entering setQualityFilter\n"); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key).getItemFilters(); - if (filters.size() <= index) + if (index < 0 || filters.size() <= (size_t)index) return; ItemFilter filter = filters[index]; filter.setDecoratedOnly(decorated != 0); @@ -825,7 +870,8 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(isPlannedBuilding), DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(doCycle), - DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(scheduleCycle), + DFHACK_LUA_FUNCTION(countAvailableItems), DFHACK_LUA_FUNCTION(hasFilter), DFHACK_LUA_FUNCTION(clearFilter), DFHACK_LUA_FUNCTION(setMaterialFilter), diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 2aaa34bbb..413dd545a 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -868,6 +868,8 @@ function QualityAndMaterialsPage:refresh() subviews.min_quality:setOption(quality.min_quality) subviews.max_quality:setOption(quality.max_quality) + local materials = getMaterialFilter(ibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) + self.summary = summary self.dirty = false end From 4f3cdeaf054c2ccbd89f0da3edd3674184e6a9d2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Mar 2023 04:59:41 -0800 Subject: [PATCH 122/126] stub out reachability check for now it's more complicated than we thought --- plugins/buildingplan/buildingplan_cycle.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 375e26212..c8cd018e4 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -152,8 +152,19 @@ static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, return NULL; } +// This is tricky. we want to choose an item that can be brought to the job site, but that's not +// necessarily the same as job->pos. it could be many tiles off in any direction (e.g. for bridges), or +// up or down (e.g. for stairs). static bool isAccessibleFrom(df::item *item, df::job *job) { - return Maps::canWalkBetween(Items::getPosition(item), job->pos); + // stub this out for now until we have a good algorithm. + // df::coord item_pos = Items::getPosition(item); + // const df::coord &job_pos = job->pos; + // return Maps::canWalkBetween(item_pos, job_pos) || + // Maps::canWalkBetween(item_pos, df::coord{job_pos.x-1, job_pos.y, job_pos.z}) || + // Maps::canWalkBetween(item_pos, df::coord{job_pos.x+1, job_pos.y, job_pos.z}) || + // Maps::canWalkBetween(item_pos, df::coord{job_pos.x, job_pos.y-1, job_pos.z}) || + // Maps::canWalkBetween(item_pos, df::coord{job_pos.x, job_pos.y+1, job_pos.z}); + return true; } static void doVector(color_ostream &out, df::job_item_vector_id vector_id, From 28599eb2bb37547052d60ed0718b8317e0fd3ae2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Mar 2023 05:28:12 -0800 Subject: [PATCH 123/126] ensure item is on walkable tile --- plugins/buildingplan/buildingplan_cycle.cpp | 26 +++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index c8cd018e4..f401c90a8 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -11,6 +11,7 @@ #include "df/building_design.h" #include "df/item.h" #include "df/job.h" +#include "df/map_block.h" #include "df/world.h" #include @@ -154,17 +155,18 @@ static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, // This is tricky. we want to choose an item that can be brought to the job site, but that's not // necessarily the same as job->pos. it could be many tiles off in any direction (e.g. for bridges), or -// up or down (e.g. for stairs). -static bool isAccessibleFrom(df::item *item, df::job *job) { - // stub this out for now until we have a good algorithm. - // df::coord item_pos = Items::getPosition(item); - // const df::coord &job_pos = job->pos; - // return Maps::canWalkBetween(item_pos, job_pos) || - // Maps::canWalkBetween(item_pos, df::coord{job_pos.x-1, job_pos.y, job_pos.z}) || - // Maps::canWalkBetween(item_pos, df::coord{job_pos.x+1, job_pos.y, job_pos.z}) || - // Maps::canWalkBetween(item_pos, df::coord{job_pos.x, job_pos.y-1, job_pos.z}) || - // Maps::canWalkBetween(item_pos, df::coord{job_pos.x, job_pos.y+1, job_pos.z}); - return true; +// up or down (e.g. for stairs). For now, just return if the item is on a walkable tile. +static bool isAccessibleFrom(color_ostream &out, df::item *item, df::job *job) { + df::coord item_pos = Items::getPosition(item); + df::map_block *block = Maps::getTileBlock(item_pos); + bool is_walkable = false; + if (block) { + uint16_t walkability_group = index_tile(block->walkable, item_pos); + is_walkable = walkability_group != 0; + TRACE(cycle,out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", + item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "" : "not "); + } + return is_walkable; } static void doVector(color_ostream &out, df::job_item_vector_id vector_id, @@ -198,7 +200,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto job = bld->jobs[0]; auto filter_idx = task.second; auto &pb = planned_buildings.at(id); - if (isAccessibleFrom(item, job) + if (isAccessibleFrom(out, item, job) && matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, pb.item_filters[filter_idx]) && Job::attachJobItem(job, item, From 982d6a995a827b3b2c1729eb697b592f446fcf87 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Mar 2023 05:29:33 -0800 Subject: [PATCH 124/126] fix signed/unsigned error --- plugins/buildingplan/buildingplan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 476d1863d..7479d348d 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -765,7 +765,7 @@ static int getQualityFilter(lua_State *L) { type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); - if (filters.size() <= index) + if (index < 0 || filters.size() <= (size_t)index) return 0; auto &filter = filters[index]; map ret; From 80da035186ffb779e098087420f05179abf31616 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Mar 2023 06:00:21 -0800 Subject: [PATCH 125/126] always allow constructions to be placed even if some tiles are invalid. the first selected tile must still be valid --- plugins/lua/buildingplan.lua | 45 +++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 413dd545a..e82b03cb9 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -1274,10 +1274,11 @@ function ItemLine:get_item_line_text() return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end -function ItemLine:reduce_quantity() +function ItemLine:reduce_quantity(used_quantity) if not self.available then return end local filter = get_cur_filters()[self.idx] - self.available = math.max(0, self.available - get_quantity(filter, self.is_hollow_fn())) + used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn()) + self.available = math.max(0, self.available - used_quantity) end local function get_placement_errors() @@ -1602,7 +1603,7 @@ function PlannerOverlay:onInput(keys) or self.subviews.errors:getMousePos() then return true end - if #uibs.errors > 0 then return true end + if not is_construction() and #uibs.errors > 0 then return true end if dfhack.gui.getMousePos() then if is_choosing_area() or cur_building_has_no_area() then local filters = get_cur_filters() @@ -1671,6 +1672,8 @@ function reload_cursors() end reload_cursors() +local ONE_BY_ONE = xy2pos(1, 1) + function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) @@ -1685,20 +1688,26 @@ function PlannerOverlay:onRenderFrame(dc, rect) local pos = self.saved_pos or uibs.pos local bounds = { - x1 = math.min(selection_pos.x, pos.x), - x2 = math.max(selection_pos.x, pos.x), - y1 = math.min(selection_pos.y, pos.y), - y2 = math.max(selection_pos.y, pos.y), + x1 = math.max(0, math.min(selection_pos.x, pos.x)), + x2 = math.min(df.global.world.map.x_count-1, math.max(selection_pos.x, pos.x)), + y1 = math.max(0, math.min(selection_pos.y, pos.y)), + y2 = math.min(df.global.world.map.y_count-1, math.max(selection_pos.y, pos.y)), } local hollow = self.subviews.hollow:getOptionValue() - local pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN + local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN + + local get_pen_fn = is_construction() and function(pos) + return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and GOOD_PEN or BAD_PEN + end or function() + return default_pen + end local function get_overlay_pen(pos) - if not hollow then return pen end + if not hollow then return get_pen_fn(pos) end if pos.x == bounds.x1 or pos.x == bounds.x2 or pos.y == bounds.y1 or pos.y == bounds.y2 then - return pen + return get_pen_fn(pos) end return gui.TRANSPARENT_PEN end @@ -1752,11 +1761,8 @@ function PlannerOverlay:place_building(placement_data, chosen_items) width=placement_data.width, height=placement_data.height, direction=uibs.direction} if err then - for _,b in ipairs(blds) do - dfhack.buildings.deconstruct(b) - end - dfhack.printerr(err .. (' (%d, %d, %d)'):format(pos.x, pos.y, pos.z)) - return + -- it's ok if some buildings fail to build + goto continue end -- assign fields for the types that need them. we can't pass them all in -- to the call to constructBuilding since attempting to assign unrelated @@ -1771,10 +1777,11 @@ function PlannerOverlay:place_building(placement_data, chosen_items) table.insert(blds, bld) ::continue:: end end end - self.subviews.item1:reduce_quantity() - self.subviews.item2:reduce_quantity() - self.subviews.item3:reduce_quantity() - self.subviews.item4:reduce_quantity() + local used_quantity = is_construction() and #blds or false + self.subviews.item1:reduce_quantity(used_quantity) + self.subviews.item2:reduce_quantity(used_quantity) + self.subviews.item3:reduce_quantity(used_quantity) + self.subviews.item4:reduce_quantity(used_quantity) for _,bld in ipairs(blds) do -- attach chosen items and reduce job_item quantity if chosen_items then From c8c1572bc4d80b28d2287023fbdd5a715f733520 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Mar 2023 06:08:51 -0800 Subject: [PATCH 126/126] fix typo --- plugins/lua/buildingplan.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index e82b03cb9..a0da8d838 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -868,8 +868,6 @@ function QualityAndMaterialsPage:refresh() subviews.min_quality:setOption(quality.min_quality) subviews.max_quality:setOption(quality.max_quality) - local materials = getMaterialFilter(ibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) - self.summary = summary self.dirty = false end