From 9c331bdda12aedb111fdc0dc0327817274049935 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sun, 10 Sep 2023 19:28:46 -0500 Subject: [PATCH 01/32] Use pathability groups (thanks @myk002) to detect stranded citizens --- warn-stranded.lua | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 warn-stranded.lua diff --git a/warn-stranded.lua b/warn-stranded.lua new file mode 100644 index 0000000000..74e181224e --- /dev/null +++ b/warn-stranded.lua @@ -0,0 +1,46 @@ +-- Detects and alerts when a citizen is stranded +-- by Azrazalea + +-- Taken from warn-starving +local function getSexString(sex) + local sym = df.pronoun_type.attrs[sex].symbol + if not sym then + return "" + end + return "("..sym..")" +end + +function doCheck() + local grouped = {} + local citizens = dfhack.units.getCitizens() + + -- Pathability group calculation is from gui/pathable + for _, unit in pairs(citizens) do + local target = unit.pos + local block = dfhack.maps.getTileBlock(target) + local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 + local groupTable = grouped[walkGroup] + + if groupTable == nil then + grouped[walkGroup] = { unit } + else + table.insert(groupTable, unit) + end + end + + local strandedUnits = {} + + for _, units in pairs(grouped) do + if #units == 1 then + table.insert(strandedUnits, units[1]) + end + end + + print("Number of stranded: ") + print(#strandedUnits) + for _, unit in pairs(strandedUnits) do + print('['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit)) + end +end + +doCheck() From 09a3431efaffd109bcdb1ef2f0bfcf27a608679c Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sun, 10 Sep 2023 20:13:51 -0500 Subject: [PATCH 02/32] Add documentation and enable warn-stranded in control panel --- changelog.txt | 3 ++ docs/warn-stranded.rst | 27 +++++++++++++++++ gui/control-panel.lua | 3 ++ warn-stranded.lua | 68 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 docs/warn-stranded.rst diff --git a/changelog.txt b/changelog.txt index 4722c3b630..dde33631bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,9 @@ Template for new versions: ## New Features +## New Scripts +- `warn-stranded`: new repeatable maintenance script to check for stranded units, based off warn-starving + ## Fixes - `suspendmanager`: fix errors when constructing near the map edge - `gui/sandbox`: fix scrollbar moving double distance on click diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst new file mode 100644 index 0000000000..b5db2dd496 --- /dev/null +++ b/docs/warn-stranded.rst @@ -0,0 +1,27 @@ +warn-stranded +============= + +.. dfhack-tool:: + :summary: Reports citizens that are stranded and can't reach any other unit + :tags: fort units + +If any (live) units are stranded the game will pause and you'll get a warning dialog telling you +which units are isolated. This gives you a chance to rescue them before +they get overly stressed or start starving. + +You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. + +Usage +----- + +:: + + warn-stranded + +Examples +-------- + +``warn-stranded`` + +Options +------- diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 1ca6ed3b21..228dd875a2 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -134,6 +134,9 @@ local REPEATS = { ['warn-starving']={ desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, + ['warn-stranded']={ + desc='Show a warning dialog when units are stranded from all others.' + command={'--time', '300', '--timeUnits', 'ticks', '--command', '[', 'warn-stranded', ']'}}, ['empty-wheelbarrows']={ desc='Empties wheelbarrows which have rocks stuck in them.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, diff --git a/warn-stranded.lua b/warn-stranded.lua index 74e181224e..b08b0be080 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -1,7 +1,41 @@ -- Detects and alerts when a citizen is stranded -- by Azrazalea +-- Heavily based off of warn-starving +-- Thanks myk002 for telling me about pathability groups! +--@ module = true + +local gui = require 'gui' +local utils = require 'utils' +local widgets = require 'gui.widgets' + +warning = defclass(warning, gui.ZScreen) +warning.ATTRS = { + focus_path='warn-stranded', + force_pause=true, + pass_mouse_clicks=false, +} + +function warning:init(info) + local main = widgets.Window{ + frame={w=80, h=18}, + frame_title='Stranded Citizen Warning', + resizable=true, + autoarrange_subviews=true + } + + main:addviews{ + widgets.WrappedLabel{ + text_to_wrap=table.concat(info.messages, NEWLINE), + } + } + + self:addviews{main} +end + +function warning:onDismiss() + view = nil +end --- Taken from warn-starving local function getSexString(sex) local sym = df.pronoun_type.attrs[sex].symbol if not sym then @@ -30,17 +64,37 @@ function doCheck() local strandedUnits = {} + for _, units in pairs(grouped) do if #units == 1 then table.insert(strandedUnits, units[1]) end end - print("Number of stranded: ") - print(#strandedUnits) - for _, unit in pairs(strandedUnits) do - print('['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit)) - end + if #strandedUnits > 0 then + dfhack.color(COLOR_LIGHTMAGENTA) + + local messages = {} + local preface = "Number of stranded: "..#strandedUnits + print(dfhack.df2console(preface)) + table.insert(messages, preface) + for _, unit in pairs(strandedUnits) do + local unitString = '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) + print(dfhack.df2console(unitString)) + table.insert(messages, unitString) + end + + dfhack.color() + return warning{messages=messages}:show() + end +end + +if dfhack_flags.module then + return +end + +if not dfhack.isMapLoaded() then + qerror('warn-stranded requires a map to be loaded') end -doCheck() +view = view and view:raise() or doCheck() From 97910b8a920630b30ccbc41afb60fd73fbae29e8 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sun, 10 Sep 2023 23:17:05 -0500 Subject: [PATCH 03/32] warn-stranded: Add GUI to allow persistently ignoring units in --- docs/warn-stranded.rst | 11 ++- warn-stranded.lua | 181 +++++++++++++++++++++++++++++++++-------- 2 files changed, 158 insertions(+), 34 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index b5db2dd496..6242068f87 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -11,17 +11,24 @@ they get overly stressed or start starving. You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. +If you ignore a unit, either call ``warn-stranded clear`` in the dfhack console or if you have multiple +stranded you can toggle/clear all units in the warning dialog. + Usage ----- :: - warn-stranded + warn-stranded [clear] Examples -------- -``warn-stranded`` +``warn-stranded clear`` + Clear all ignored units and then check for ones that are stranded. Options ------- + +``clear`` + Will clear all ignored units so that warnings will be displayed again. diff --git a/warn-stranded.lua b/warn-stranded.lua index b08b0be080..5b23d1ac50 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -1,6 +1,7 @@ -- Detects and alerts when a citizen is stranded -- by Azrazalea --- Heavily based off of warn-starving +-- Logic heavily based off of warn-starving +-- GUI heavily based off of autobutcher -- Thanks myk002 for telling me about pathability groups! --@ module = true @@ -8,6 +9,16 @@ local gui = require 'gui' local utils = require 'utils' local widgets = require 'gui.widgets' +local function clear() + dfhack.persistent.delete('warnStrandedIgnore') +end + +local args = utils.invert({...}) +if args.clear then + clear() +end + + warning = defclass(warning, gui.ZScreen) warning.ATTRS = { focus_path='warn-stranded', @@ -16,24 +27,36 @@ warning.ATTRS = { } function warning:init(info) - local main = widgets.Window{ - frame={w=80, h=18}, - frame_title='Stranded Citizen Warning', - resizable=true, - autoarrange_subviews=true - } - - main:addviews{ - widgets.WrappedLabel{ - text_to_wrap=table.concat(info.messages, NEWLINE), - } + self:addviews{ + widgets.Window{ + view_id = 'main', + frame={w=80, h=18}, + frame_title='Stranded Citizen Warning', + resizable=true, + subviews = { + widgets.Label{ + frame = { l = 0, t = 0}, + text_pen = COLOR_CYAN, + text = 'Number Stranded: '..#info.units, + }, + widgets.List{ + view_id = 'list', + frame = { t = 3, b = 5 }, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + }, + widgets.Label{ + view_id = 'bottom_ui', + frame = { b = 0, h = 1 }, + text = 'filled by updateBottom()' + } + } + } } - self:addviews{main} -end - -function warning:onDismiss() - view = nil + self.units = info.units + self:initListChoices() + self:updateBottom() end local function getSexString(sex) @@ -44,6 +67,113 @@ local function getSexString(sex) return "("..sym..")" end +local function getUnitDescription(unit) + return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. + ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) +end + + +local function unitIgnored(unit) + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + if currentIgnore == nil then return false end + + local tbl = string.gmatch(currentIgnore['value'], '%d+') + local index = 1 + for id in tbl do + if tonumber(id) == unit.id then + return true, index + end + index = index + 1 + end + + return false +end + +local function toggleUnitIgnore(unit) + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + local tbl = {} + + if currentIgnore == nil then + currentIgnore = { key = 'warnStrandedIgnore' } + else + local index = 1 + for v in string.gmatch(currentIgnore['value'], '%d+') do + tbl[index] = v + index = index + 1 + end + end + + local ignored, index = unitIgnored(unit) + + if ignored then + table.remove(tbl, index) + else + table.insert(tbl, unit.id) + end + + dfhack.persistent.delete('warnStrandedIgnore') + currentIgnore.value = table.concat(tbl, ' ') + dfhack.persistent.save(currentIgnore) +end + +function warning:initListChoices() + local choices = {} + for _, unit in pairs(self.units) do + local text = '' + + dfhack.printerr('Ignored: ', unitIgnored(unit)) + + if unitIgnored(unit) then + text = '[IGNORED] ' + end + + text = text..getUnitDescription(unit) + table.insert(choices, { text = text, unit = unit }) + end + local list = self.subviews.list + list:setChoices(choices, 1) +end + +function warning:updateBottom() + self.subviews.bottom_ui:setText( + { + { key = 'SELECT', text = ': Toggle ignore unit', on_activate = self:callback('onIgnore') }, ' ', + { key = 'CUSTOM_SHIFT_I', text = ': Ignore all', on_activate = self:callback('onIgnoreAll') }, ' ', + { key = 'CUSTOM_SHIFT_C', text = ': Clear all ignored', on_activate = self:callback('onClear') }, + } + ) +end + +function warning:onIgnore() + local index, choice = self.subviews.list:getSelected() + local unit = choice.unit + + toggleUnitIgnore(unit) + self:initListChoices() +end + +function warning:onIgnoreAll() + local choices = self.subviews.list:getChoices() + + for _, choice in pairs(choices) do + if not unitIgnored(choice.unit) then + toggleUnitIgnore(choice.unit) + end + end + + self:dismiss() +end + +function warning:onClear() + clear() + self:initListChoices() + self:updateBottom() +end + +function warning:onDismiss() + view = nil +end + function doCheck() local grouped = {} local citizens = dfhack.units.getCitizens() @@ -66,26 +196,13 @@ function doCheck() for _, units in pairs(grouped) do - if #units == 1 then + if #units == 1 and not unitIgnored(units[1]) then table.insert(strandedUnits, units[1]) end end if #strandedUnits > 0 then - dfhack.color(COLOR_LIGHTMAGENTA) - - local messages = {} - local preface = "Number of stranded: "..#strandedUnits - print(dfhack.df2console(preface)) - table.insert(messages, preface) - for _, unit in pairs(strandedUnits) do - local unitString = '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) - print(dfhack.df2console(unitString)) - table.insert(messages, unitString) - end - - dfhack.color() - return warning{messages=messages}:show() + return warning{units=strandedUnits}:show() end end From 91ac082fb1486c8eaa325b63c915ee65a1e9f32c Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 00:26:37 -0500 Subject: [PATCH 04/32] Apply suggestions from code review Co-authored-by: Myk --- changelog.txt | 2 +- docs/warn-stranded.rst | 7 +------ warn-stranded.lua | 10 ++-------- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/changelog.txt b/changelog.txt index dde33631bb..d247ec6801 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,7 +31,7 @@ Template for new versions: ## New Features ## New Scripts -- `warn-stranded`: new repeatable maintenance script to check for stranded units, based off warn-starving +- `warn-stranded`: new repeatable maintenance script to check for stranded units, based off `warn-starving` ## Fixes - `suspendmanager`: fix errors when constructing near the map edge diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 6242068f87..db9fd82a1e 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -5,7 +5,7 @@ warn-stranded :summary: Reports citizens that are stranded and can't reach any other unit :tags: fort units -If any (live) units are stranded the game will pause and you'll get a warning dialog telling you +If any (live) units are stranded, the game will pause and you'll get a warning dialog telling you which units are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. @@ -27,8 +27,3 @@ Examples ``warn-stranded clear`` Clear all ignored units and then check for ones that are stranded. -Options -------- - -``clear`` - Will clear all ignored units so that warnings will be displayed again. diff --git a/warn-stranded.lua b/warn-stranded.lua index 5b23d1ac50..51834721ac 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -179,17 +179,11 @@ function doCheck() local citizens = dfhack.units.getCitizens() -- Pathability group calculation is from gui/pathable - for _, unit in pairs(citizens) do + for _, unit in ipairs(citizens) do local target = unit.pos local block = dfhack.maps.getTileBlock(target) local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - local groupTable = grouped[walkGroup] - - if groupTable == nil then - grouped[walkGroup] = { unit } - else - table.insert(groupTable, unit) - end + table.insert(ensure_key(grouped, walkGroup), unit) end local strandedUnits = {} From fa394505e3b3db5a98198a83acc299443a80f9f5 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 01:02:57 -0500 Subject: [PATCH 05/32] Manual fixes from review --- docs/warn-stranded.rst | 3 +- warn-stranded.lua | 162 ++++++++++++++++++++--------------------- 2 files changed, 78 insertions(+), 87 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index db9fd82a1e..4af7ee7b1a 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -2,7 +2,7 @@ warn-stranded ============= .. dfhack-tool:: - :summary: Reports citizens that are stranded and can't reach any other unit + :summary: Reports citizens that are stranded and can't reach any other unit. :tags: fort units If any (live) units are stranded, the game will pause and you'll get a warning dialog telling you @@ -26,4 +26,3 @@ Examples ``warn-stranded clear`` Clear all ignored units and then check for ones that are stranded. - diff --git a/warn-stranded.lua b/warn-stranded.lua index 51834721ac..ab12f2a880 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -13,58 +13,58 @@ local function clear() dfhack.persistent.delete('warnStrandedIgnore') end -local args = utils.invert({...}) -if args.clear then - clear() -end - - -warning = defclass(warning, gui.ZScreen) -warning.ATTRS = { - focus_path='warn-stranded', - force_pause=true, - pass_mouse_clicks=false, -} +warning = defclass(warning, gui.ZScreenModal) function warning:init(info) - self:addviews{ - widgets.Window{ - view_id = 'main', - frame={w=80, h=18}, - frame_title='Stranded Citizen Warning', - resizable=true, - subviews = { - widgets.Label{ - frame = { l = 0, t = 0}, - text_pen = COLOR_CYAN, - text = 'Number Stranded: '..#info.units, - }, - widgets.List{ - view_id = 'list', - frame = { t = 3, b = 5 }, - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, - }, - widgets.Label{ - view_id = 'bottom_ui', - frame = { b = 0, h = 1 }, - text = 'filled by updateBottom()' - } - } - } - } - - self.units = info.units - self:initListChoices() - self:updateBottom() + self:addviews{ + widgets.Window{ + view_id = 'main', + frame={w=80, h=18}, + frame_title='Stranded Citizen Warning', + resizable=true, + subviews = { + widgets.Label{ + frame = { l=0, t=0}, + text_pen = COLOR_CYAN, + text = 'Number Stranded: '..#info.units, + }, + widgets.List{ + view_id = 'list', + frame = { t = 3, l=0 }, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + }, + widgets.HotkeyLabel{ + frame = { b=3, l=0}, + key='SELECT', + label='Toggle Ignore', + on_activate=self:callback('onIgnore'), + }, + widgets.HotkeyLabel{ + frame = { b=2, l=0 }, + key = 'CUSTOM_SHIFT_I', + label = 'Ignore All', + on_activate = self:callback('onIgnoreAll') }, + widgets.HotkeyLabel{ + frame = { b=1, l=0 }, + key = 'CUSTOM_SHIFT_C', + label = 'Clear All Ignored', + on_activate = self:callback('onClear'), + }, + } + } + } + + self.units = info.units + self:initListChoices() end local function getSexString(sex) - local sym = df.pronoun_type.attrs[sex].symbol - if not sym then - return "" - end - return "("..sym..")" + local sym = df.pronoun_type.attrs[sex].symbol + if not sym then + return "" + end + return "("..sym..")" end local function getUnitDescription(unit) @@ -98,8 +98,8 @@ local function toggleUnitIgnore(unit) else local index = 1 for v in string.gmatch(currentIgnore['value'], '%d+') do - tbl[index] = v - index = index + 1 + tbl[index] = v + index = index + 1 end end @@ -118,11 +118,9 @@ end function warning:initListChoices() local choices = {} - for _, unit in pairs(self.units) do + for _, unit in ipairs(self.units) do local text = '' - dfhack.printerr('Ignored: ', unitIgnored(unit)) - if unitIgnored(unit) then text = '[IGNORED] ' end @@ -134,16 +132,6 @@ function warning:initListChoices() list:setChoices(choices, 1) end -function warning:updateBottom() - self.subviews.bottom_ui:setText( - { - { key = 'SELECT', text = ': Toggle ignore unit', on_activate = self:callback('onIgnore') }, ' ', - { key = 'CUSTOM_SHIFT_I', text = ': Ignore all', on_activate = self:callback('onIgnoreAll') }, ' ', - { key = 'CUSTOM_SHIFT_C', text = ': Clear all ignored', on_activate = self:callback('onClear') }, - } - ) -end - function warning:onIgnore() local index, choice = self.subviews.list:getSelected() local unit = choice.unit @@ -155,7 +143,7 @@ end function warning:onIgnoreAll() local choices = self.subviews.list:getChoices() - for _, choice in pairs(choices) do + for _, choice in ipairs(choices) do if not unitIgnored(choice.unit) then toggleUnitIgnore(choice.unit) end @@ -167,45 +155,49 @@ end function warning:onClear() clear() self:initListChoices() - self:updateBottom() end function warning:onDismiss() - view = nil + view = nil end function doCheck() - local grouped = {} - local citizens = dfhack.units.getCitizens() - - -- Pathability group calculation is from gui/pathable - for _, unit in ipairs(citizens) do - local target = unit.pos - local block = dfhack.maps.getTileBlock(target) - local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - table.insert(ensure_key(grouped, walkGroup), unit) - end + local grouped = {} + local citizens = dfhack.units.getCitizens() + + -- Pathability group calculation is from gui/pathable + for _, unit in ipairs(citizens) do + local target = xyz2pos(dfhack.units.getPosition(unit)) + local block = dfhack.maps.getTileBlock(target) + local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 + table.insert(ensure_key(grouped, walkGroup), unit) + end - local strandedUnits = {} + local strandedUnits = {} - for _, units in pairs(grouped) do - if #units == 1 and not unitIgnored(units[1]) then - table.insert(strandedUnits, units[1]) - end - end + for _, units in pairs(grouped) do + if #units == 1 and not unitIgnored(units[1]) then + table.insert(strandedUnits, units[1]) + end + end - if #strandedUnits > 0 then - return warning{units=strandedUnits}:show() + if #strandedUnits > 0 then + return warning{units=strandedUnits}:show() end end if dfhack_flags.module then - return + return end if not dfhack.isMapLoaded() then - qerror('warn-stranded requires a map to be loaded') + qerror('warn-stranded requires a map to be loaded') +end + +local args = utils.invert({...}) +if args.clear then + clear() end view = view and view:raise() or doCheck() From 3850f5849eeaf0f88cb99cc9fbd3cb4b03cae112 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 18:21:11 -0500 Subject: [PATCH 06/32] Second round review fixes --- gui/control-panel.lua | 2 +- warn-stranded.lua | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 228dd875a2..c4cccc281d 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -135,7 +135,7 @@ local REPEATS = { desc='Show a warning dialog when units are starving or dehydrated.', command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, ['warn-stranded']={ - desc='Show a warning dialog when units are stranded from all others.' + desc='Show a warning dialog when units are stranded from all others.', command={'--time', '300', '--timeUnits', 'ticks', '--command', '[', 'warn-stranded', ']'}}, ['empty-wheelbarrows']={ desc='Empties wheelbarrows which have rocks stuck in them.', diff --git a/warn-stranded.lua b/warn-stranded.lua index ab12f2a880..ee513dc559 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -1,8 +1,6 @@ -- Detects and alerts when a citizen is stranded --- by Azrazalea -- Logic heavily based off of warn-starving -- GUI heavily based off of autobutcher --- Thanks myk002 for telling me about pathability groups! --@ module = true local gui = require 'gui' From 1e38a3a7131ea96edbc003b160668a55200faee2 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 18:21:50 -0500 Subject: [PATCH 07/32] Fix indentation --- warn-stranded.lua | 268 +++++++++++++++++++++++----------------------- 1 file changed, 134 insertions(+), 134 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index ee513dc559..6288fad5e2 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,194 +8,194 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local function clear() - dfhack.persistent.delete('warnStrandedIgnore') + dfhack.persistent.delete('warnStrandedIgnore') end warning = defclass(warning, gui.ZScreenModal) function warning:init(info) - self:addviews{ - widgets.Window{ - view_id = 'main', - frame={w=80, h=18}, - frame_title='Stranded Citizen Warning', - resizable=true, - subviews = { - widgets.Label{ - frame = { l=0, t=0}, - text_pen = COLOR_CYAN, - text = 'Number Stranded: '..#info.units, - }, - widgets.List{ - view_id = 'list', - frame = { t = 3, l=0 }, - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, - }, - widgets.HotkeyLabel{ - frame = { b=3, l=0}, - key='SELECT', - label='Toggle Ignore', - on_activate=self:callback('onIgnore'), - }, - widgets.HotkeyLabel{ - frame = { b=2, l=0 }, - key = 'CUSTOM_SHIFT_I', - label = 'Ignore All', - on_activate = self:callback('onIgnoreAll') }, - widgets.HotkeyLabel{ - frame = { b=1, l=0 }, - key = 'CUSTOM_SHIFT_C', - label = 'Clear All Ignored', - on_activate = self:callback('onClear'), - }, - } - } - } - - self.units = info.units - self:initListChoices() + self:addviews{ + widgets.Window{ + view_id = 'main', + frame={w=80, h=18}, + frame_title='Stranded Citizen Warning', + resizable=true, + subviews = { + widgets.Label{ + frame = { l=0, t=0}, + text_pen = COLOR_CYAN, + text = 'Number Stranded: '..#info.units, + }, + widgets.List{ + view_id = 'list', + frame = { t = 3, l=0 }, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + }, + widgets.HotkeyLabel{ + frame = { b=3, l=0}, + key='SELECT', + label='Toggle Ignore', + on_activate=self:callback('onIgnore'), + }, + widgets.HotkeyLabel{ + frame = { b=2, l=0 }, + key = 'CUSTOM_SHIFT_I', + label = 'Ignore All', + on_activate = self:callback('onIgnoreAll') }, + widgets.HotkeyLabel{ + frame = { b=1, l=0 }, + key = 'CUSTOM_SHIFT_C', + label = 'Clear All Ignored', + on_activate = self:callback('onClear'), + }, + } + } + } + + self.units = info.units + self:initListChoices() end local function getSexString(sex) - local sym = df.pronoun_type.attrs[sex].symbol - if not sym then - return "" - end - return "("..sym..")" + local sym = df.pronoun_type.attrs[sex].symbol + if not sym then + return "" + end + return "("..sym..")" end local function getUnitDescription(unit) - return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. - ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) + return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. + ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) end local function unitIgnored(unit) - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - if currentIgnore == nil then return false end - - local tbl = string.gmatch(currentIgnore['value'], '%d+') - local index = 1 - for id in tbl do - if tonumber(id) == unit.id then - return true, index - end - index = index + 1 - end - - return false + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + if currentIgnore == nil then return false end + + local tbl = string.gmatch(currentIgnore['value'], '%d+') + local index = 1 + for id in tbl do + if tonumber(id) == unit.id then + return true, index + end + index = index + 1 + end + + return false end local function toggleUnitIgnore(unit) - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - local tbl = {} - - if currentIgnore == nil then - currentIgnore = { key = 'warnStrandedIgnore' } - else - local index = 1 - for v in string.gmatch(currentIgnore['value'], '%d+') do - tbl[index] = v - index = index + 1 - end - end - - local ignored, index = unitIgnored(unit) - - if ignored then - table.remove(tbl, index) - else - table.insert(tbl, unit.id) - end - - dfhack.persistent.delete('warnStrandedIgnore') - currentIgnore.value = table.concat(tbl, ' ') - dfhack.persistent.save(currentIgnore) + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + local tbl = {} + + if currentIgnore == nil then + currentIgnore = { key = 'warnStrandedIgnore' } + else + local index = 1 + for v in string.gmatch(currentIgnore['value'], '%d+') do + tbl[index] = v + index = index + 1 + end + end + + local ignored, index = unitIgnored(unit) + + if ignored then + table.remove(tbl, index) + else + table.insert(tbl, unit.id) + end + + dfhack.persistent.delete('warnStrandedIgnore') + currentIgnore.value = table.concat(tbl, ' ') + dfhack.persistent.save(currentIgnore) end function warning:initListChoices() - local choices = {} - for _, unit in ipairs(self.units) do - local text = '' - - if unitIgnored(unit) then - text = '[IGNORED] ' - end - - text = text..getUnitDescription(unit) - table.insert(choices, { text = text, unit = unit }) - end - local list = self.subviews.list - list:setChoices(choices, 1) + local choices = {} + for _, unit in ipairs(self.units) do + local text = '' + + if unitIgnored(unit) then + text = '[IGNORED] ' + end + + text = text..getUnitDescription(unit) + table.insert(choices, { text = text, unit = unit }) + end + local list = self.subviews.list + list:setChoices(choices, 1) end function warning:onIgnore() - local index, choice = self.subviews.list:getSelected() - local unit = choice.unit + local index, choice = self.subviews.list:getSelected() + local unit = choice.unit - toggleUnitIgnore(unit) - self:initListChoices() + toggleUnitIgnore(unit) + self:initListChoices() end function warning:onIgnoreAll() - local choices = self.subviews.list:getChoices() + local choices = self.subviews.list:getChoices() - for _, choice in ipairs(choices) do - if not unitIgnored(choice.unit) then - toggleUnitIgnore(choice.unit) - end - end + for _, choice in ipairs(choices) do + if not unitIgnored(choice.unit) then + toggleUnitIgnore(choice.unit) + end + end - self:dismiss() + self:dismiss() end function warning:onClear() - clear() - self:initListChoices() + clear() + self:initListChoices() end function warning:onDismiss() - view = nil + view = nil end function doCheck() - local grouped = {} - local citizens = dfhack.units.getCitizens() + local grouped = {} + local citizens = dfhack.units.getCitizens() - -- Pathability group calculation is from gui/pathable - for _, unit in ipairs(citizens) do - local target = xyz2pos(dfhack.units.getPosition(unit)) - local block = dfhack.maps.getTileBlock(target) - local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - table.insert(ensure_key(grouped, walkGroup), unit) - end + -- Pathability group calculation is from gui/pathable + for _, unit in ipairs(citizens) do + local target = xyz2pos(dfhack.units.getPosition(unit)) + local block = dfhack.maps.getTileBlock(target) + local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 + table.insert(ensure_key(grouped, walkGroup), unit) + end - local strandedUnits = {} + local strandedUnits = {} - for _, units in pairs(grouped) do - if #units == 1 and not unitIgnored(units[1]) then - table.insert(strandedUnits, units[1]) - end - end + for _, units in pairs(grouped) do + if #units == 1 and not unitIgnored(units[1]) then + table.insert(strandedUnits, units[1]) + end + end - if #strandedUnits > 0 then - return warning{units=strandedUnits}:show() - end + if #strandedUnits > 0 then + return warning{units=strandedUnits}:show() + end end if dfhack_flags.module then - return + return end if not dfhack.isMapLoaded() then - qerror('warn-stranded requires a map to be loaded') + qerror('warn-stranded requires a map to be loaded') end local args = utils.invert({...}) if args.clear then - clear() + clear() end view = view and view:raise() or doCheck() From d3b7828036782f9b38e2e1c23d311535f6960bd6 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 21:06:49 -0500 Subject: [PATCH 08/32] Refactor: Main group is biggest group, lists all stranded groups --- warn-stranded.lua | 142 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 114 insertions(+), 28 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 6288fad5e2..5e331def13 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -21,39 +21,40 @@ function warning:init(info) frame_title='Stranded Citizen Warning', resizable=true, subviews = { - widgets.Label{ - frame = { l=0, t=0}, - text_pen = COLOR_CYAN, - text = 'Number Stranded: '..#info.units, - }, widgets.List{ view_id = 'list', - frame = { t = 3, l=0 }, + frame = { t = 1, l=0 }, text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, }, widgets.HotkeyLabel{ - frame = { b=3, l=0}, + frame = { b=4, l=0}, key='SELECT', label='Toggle Ignore', on_activate=self:callback('onIgnore'), }, widgets.HotkeyLabel{ - frame = { b=2, l=0 }, + frame = { b=3, l=0 }, key = 'CUSTOM_SHIFT_I', label = 'Ignore All', on_activate = self:callback('onIgnoreAll') }, widgets.HotkeyLabel{ - frame = { b=1, l=0 }, + frame = { b=2, l=0 }, key = 'CUSTOM_SHIFT_C', label = 'Clear All Ignored', on_activate = self:callback('onClear'), }, + widgets.HotkeyLabel{ + frame = { b=1, l=0}, + key = 'CUSTOM_Z', + label = 'Zoom to unit', + on_activate = self:callback('onZoom'), + } } } } - self.units = info.units + self.groups = info.groups self:initListChoices() end @@ -116,23 +117,35 @@ end function warning:initListChoices() local choices = {} - for _, unit in ipairs(self.units) do - local text = '' - if unitIgnored(unit) then - text = '[IGNORED] ' + for groupIndex, group in ipairs(self.groups) do + local groupDesignation = nil + + if group['mainGroup'] then + groupDesignation = ' (Main Group)' + else + groupDesignation = ' (Group '..groupIndex..')' end - text = text..getUnitDescription(unit) - table.insert(choices, { text = text, unit = unit }) + for _, unit in ipairs(group['units']) do + local text = '' + + if unitIgnored(unit) then + text = '[IGNORED] ' + end + + text = text..getUnitDescription(unit)..groupDesignation + table.insert(choices, { text = text, data = {unit = unit, group = index} }) + end end + local list = self.subviews.list list:setChoices(choices, 1) end function warning:onIgnore() local index, choice = self.subviews.list:getSelected() - local unit = choice.unit + local unit = choice.data['unit'] toggleUnitIgnore(unit) self:initListChoices() @@ -142,8 +155,8 @@ function warning:onIgnoreAll() local choices = self.subviews.list:getChoices() for _, choice in ipairs(choices) do - if not unitIgnored(choice.unit) then - toggleUnitIgnore(choice.unit) + if not unitIgnored(choice.data['unit']) then + toggleUnitIgnore(choice.data['unit']) end end @@ -155,33 +168,106 @@ function warning:onClear() self:initListChoices() end +function warning:onZoom() + local index, choice = self.subviews.list:getSelected() + local unit = choice.data['unit'] + + local target = xyz2pos(dfhack.units.getPosition(unit)) + dfhack.gui.revealInDwarfmodeMap(target, true) +end + function warning:onDismiss() view = nil end -function doCheck() - local grouped = {} +local function compareGroups(group_one, group_two) + return #group_one['units'] > #group_two['units'] +end + +local function getStrandedUnits() + local grouped = { n = 0 } local citizens = dfhack.units.getCitizens() + -- Don't use ignored units to determine if there are any stranded units + -- but keep them to display later + local ignoredGroup = {} + -- Pathability group calculation is from gui/pathable for _, unit in ipairs(citizens) do local target = xyz2pos(dfhack.units.getPosition(unit)) local block = dfhack.maps.getTileBlock(target) local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - table.insert(ensure_key(grouped, walkGroup), unit) + + if unitIgnored(unit) then + table.insert(ensure_key(ignoredGroup, walkGroup), unit) + else + table.insert(ensure_key(grouped, walkGroup), unit) + grouped['n'] = grouped['n'] + 1 + end + end + + -- No one is stranded, so stop here + if grouped['n'] <= 1 then + return false, {} + end + + -- We needed the table for easy grouping + -- Now let us get an array so we can sort easily + local rawGroups = {} + for index, units in pairs(grouped) do + if not (index == 'n') then + table.insert(rawGroups, { units = units, walkGroup = index }) + end end - local strandedUnits = {} + -- This data structure is super easy to sort from biggest to smallest + -- Our group number is just the array index and is sorted for us + table.sort(rawGroups, compareGroups) + + -- The biggest group is not stranded + mainGroup = rawGroups[1]['walkGroup'] + table.remove(rawGroups, 1) + -- Merge ignoredGroup with grouped + for index, units in pairs(ignoredGroup) do + local groupIndex = nil + + -- Handle ignored units in mainGroup by shifting other groups down + -- We need to list them so they can be toggled + if index == mainGroup then + table.insert(rawGroups, 1, { units = {}, walkGroup = mainGroup, mainGroup = true }) + groupIndex = 1 + end - for _, units in pairs(grouped) do - if #units == 1 and not unitIgnored(units[1]) then - table.insert(strandedUnits, units[1]) + -- Find matching group + for i, group in ipairs(rawGroups) do + if group[walkGroup] == index then + groupIndex = i + end + end + + -- No matching group + if groupIndex == nil then + table.insert(rawGroups, { units = {}, walkGroup = index }) + end + + -- Put all the units in the appropriate group + for _, unit in ipairs(units) do + table.insert(rawGroups[groupIndex]['units'], unit) end end - if #strandedUnits > 0 then - return warning{units=strandedUnits}:show() + -- Key = group number (not pathability group number) + -- Value = { units = , walkGroup = , mainGroup = } + return true, rawGroups +end + + +function doCheck() + local result, strandedGroups = getStrandedUnits() + + if result then + return warning{groups=strandedGroups}:show() end end From 07a2bfb4d94536f6196b407cc7e8ac0373bbd651 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 11 Sep 2023 22:37:13 -0500 Subject: [PATCH 09/32] Bugfix: Use coherent method to determine if no stranded units --- warn-stranded.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 5e331def13..cca630e1a9 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -185,7 +185,8 @@ local function compareGroups(group_one, group_two) end local function getStrandedUnits() - local grouped = { n = 0 } + local groupCount = 0 + local grouped = {} local citizens = dfhack.units.getCitizens() -- Don't use ignored units to determine if there are any stranded units @@ -202,12 +203,14 @@ local function getStrandedUnits() table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) - grouped['n'] = grouped['n'] + 1 + if #grouped[walkGroup] == 1 then + groupCount = groupCount + 1 + end end end -- No one is stranded, so stop here - if grouped['n'] <= 1 then + if groupCount <= 1 then return false, {} end From f08f09e04e4577ccb0325fb650e5d409879ac2d2 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 15 Sep 2023 01:23:44 -0500 Subject: [PATCH 10/32] Add command-line status command with ids and walkGroup options This duplicates too much code and needs a documentation update but it works --- warn-stranded.lua | 71 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index cca630e1a9..ad426d8c73 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -211,16 +211,14 @@ local function getStrandedUnits() -- No one is stranded, so stop here if groupCount <= 1 then - return false, {} + return false, ignoredGroup end -- We needed the table for easy grouping -- Now let us get an array so we can sort easily local rawGroups = {} for index, units in pairs(grouped) do - if not (index == 'n') then - table.insert(rawGroups, { units = units, walkGroup = index }) - end + table.insert(rawGroups, { units = units, walkGroup = index }) end -- This data structure is super easy to sort from biggest to smallest @@ -244,7 +242,7 @@ local function getStrandedUnits() -- Find matching group for i, group in ipairs(rawGroups) do - if group[walkGroup] == index then + if group.walkGroup == index then groupIndex = i end end @@ -252,6 +250,7 @@ local function getStrandedUnits() -- No matching group if groupIndex == nil then table.insert(rawGroups, { units = {}, walkGroup = index }) + groupIndex = #rawGroups end -- Put all the units in the appropriate group @@ -283,8 +282,68 @@ if not dfhack.isMapLoaded() then end local args = utils.invert({...}) -if args.clear then + +if args.clear or args.all then clear() end +if args.status then + local result, strandedGroups = getStrandedUnits() + + if not result then + print('No citizens are currently stranded.') + + -- We have some ignored citizens + if not (next(strandedGroups) == nil) then + print('\nIgnored citizens:') + + for walkGroup, units in pairs(strandedGroups) do + for _, unit in ipairs(units) do + local text = '' + + if args.ids then + text = text..'|'..unit.id..'| ' + end + + text = text..getUnitDescription(unit)..' {'..walkGroup..'}' + print(text) + end + end + end + + return false + end + + for groupIndex, group in ipairs(strandedGroups) do + local groupDesignation = nil + + if group['mainGroup'] then + groupDesignation = ' (Main Group)' + else + groupDesignation = ' (Group '..groupIndex..')' + end + + if args.walk_groups then + groupDesignation = groupDesignation..' {'..group.walkGroup..'}' + end + + for _, unit in ipairs(group['units']) do + local text = '' + + if unitIgnored(unit) then + text = '[IGNORED] ' + end + + if args.ids then + text = text..'|'..unit.id..'| ' + end + + text = text..getUnitDescription(unit)..groupDesignation + print(text) + end + end + + return true +end + view = view and view:raise() or doCheck() From 15807043f43c89deda7190e0de7350e0f9251418 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 15 Sep 2023 01:31:13 -0500 Subject: [PATCH 11/32] Flip order to be ascending by group size This means the ones that are "most stranded" will be near the top --- warn-stranded.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index ad426d8c73..9283c2cd0b 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -181,7 +181,7 @@ function warning:onDismiss() end local function compareGroups(group_one, group_two) - return #group_one['units'] > #group_two['units'] + return #group_one['units'] < #group_two['units'] end local function getStrandedUnits() @@ -227,7 +227,7 @@ local function getStrandedUnits() -- The biggest group is not stranded mainGroup = rawGroups[1]['walkGroup'] - table.remove(rawGroups, 1) + table.remove(rawGroups, #rawGroups) -- Merge ignoredGroup with grouped for index, units in pairs(ignoredGroup) do From ceb7413a615f2f8e3378641df14153ec796ff78b Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 15 Sep 2023 03:00:38 -0500 Subject: [PATCH 12/32] Refactor to better reuse code and organize --- warn-stranded.lua | 266 +++++++++++++++++++++++++++------------------- 1 file changed, 158 insertions(+), 108 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 9283c2cd0b..74412222ce 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -6,11 +6,125 @@ local gui = require 'gui' local utils = require 'utils' local widgets = require 'gui.widgets' +local args = nil +-- =============================================== +-- Utility Functions +-- =============================================== + +-- Clear the ignore list local function clear() dfhack.persistent.delete('warnStrandedIgnore') end +-- Taken from warn-starving +local function getSexString(sex) + local sym = df.pronoun_type.attrs[sex].symbol + + if sym then + return "("..sym..")" + else + return "" + end +end + +-- Partially taken from warn-starving +local function getUnitDescription(unit) + return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. + ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) +end + +-- Use group data, index, and command arguments to generate a group +-- designation string. +local function getGroupDesignation(group, groupIndex) + local groupDesignation = '' + + if group['mainGroup'] then + groupDesignation = ' (Main Group)' + else + groupDesignation = ' (Group '..groupIndex..')' + end + + if args.walk_groups then + groupDesignation = groupDesignation..' {'..group.walkGroup..'}' + end + + return groupDesignation +end + +-- Check for and potentially add unit.id to text. Controlled by command args. +local function addId(text, unit) + if args.ids then + return text..'|'..unit.id..'| ' + else + return text + end +end + +-- Uses persistent API. Low-level, deserializes 'warnStrandedIgnored' key and +-- will return an initialized empty warnStrandedIgnored table if needed. +-- Performance characterstics unknown of persistent API +local function deserializeIgnoredUnits() + local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') + if currentIgnore == nil then return {} end + + local tbl = {} + + for v in string.gmatch(currentIgnore['value'], '%d+') do + table.insert(tbl, v) + end + + return tbl +end + +-- Uses persistent API. Deserializes 'warnStrandedIgnore' key to determine if unit is ignored +-- deserializedIgnores is optional but allows us to only call deserialize once like an explicit cache. +local function unitIgnored(unit, deserializedIgnores) + local ignores = deserializedIgnores or deserializeIgnoredUnits() + + for index, id in ipairs(ignores) do + if tonumber(id) == unit.id then + return true, index + end + end + + return false +end + +-- Check for and potentially add [IGNORED] to text. Controlled by command args. +-- Optional deserializedIgnores allows us to call deserialize once for a group of operations +local function addIgnored(text, unit, deserializedIgnores) + if unitIgnored(unit, deserializedIgnores) then + return text..'[IGNORED] ' + end + + return text +end + +-- Uses persistent API. Toggles a unit's ignored status by deserializing 'warnStrandedIgnore' key +-- then serializing the resulting table after the toggle. +-- Optional cache parameter could affect data integrity. Make sure you don't need data reloaded +-- before using it. Calling several times in a row can use the return result of the function +-- as input to the next call. +local function toggleUnitIgnore(unit, deserializedIgnores) + local ignores = deserializedIgnores or deserializeIgnoredUnits() + local is_ignored, index = unitIgnored(unit, ignores) + + if is_ignored then + table.remove(ignores, index) + else + table.insert(ignores, unit.id) + end + + dfhack.persistent.delete('warnStrandedIgnore') + dfhack.persistent.save({key = 'warnStrandedIgnore', value = table.concat(ignores, ' ')}) + + return ignores +end + +-- =============================================================== +-- Graphical Interface +-- =============================================================== warning = defclass(warning, gui.ZScreenModal) function warning:init(info) @@ -58,83 +172,21 @@ function warning:init(info) self:initListChoices() end -local function getSexString(sex) - local sym = df.pronoun_type.attrs[sex].symbol - if not sym then - return "" - end - return "("..sym..")" -end - -local function getUnitDescription(unit) - return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. - ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) -end - - -local function unitIgnored(unit) - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - if currentIgnore == nil then return false end - - local tbl = string.gmatch(currentIgnore['value'], '%d+') - local index = 1 - for id in tbl do - if tonumber(id) == unit.id then - return true, index - end - index = index + 1 - end - - return false -end - -local function toggleUnitIgnore(unit) - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - local tbl = {} - - if currentIgnore == nil then - currentIgnore = { key = 'warnStrandedIgnore' } - else - local index = 1 - for v in string.gmatch(currentIgnore['value'], '%d+') do - tbl[index] = v - index = index + 1 - end - end - - local ignored, index = unitIgnored(unit) - - if ignored then - table.remove(tbl, index) - else - table.insert(tbl, unit.id) - end - - dfhack.persistent.delete('warnStrandedIgnore') - currentIgnore.value = table.concat(tbl, ' ') - dfhack.persistent.save(currentIgnore) -end function warning:initListChoices() local choices = {} for groupIndex, group in ipairs(self.groups) do - local groupDesignation = nil - - if group['mainGroup'] then - groupDesignation = ' (Main Group)' - else - groupDesignation = ' (Group '..groupIndex..')' - end + local groupDesignation = getGroupDesignation(group, groupIndex) + local ignoresCache = deserializeIgnoredUnits() for _, unit in ipairs(group['units']) do local text = '' - if unitIgnored(unit) then - text = '[IGNORED] ' - end - + text = addIgnored(text, unit, ignoresCache) + text = addId(text, unit) text = text..getUnitDescription(unit)..groupDesignation + table.insert(choices, { text = text, data = {unit = unit, group = index} }) end end @@ -153,10 +205,12 @@ end function warning:onIgnoreAll() local choices = self.subviews.list:getChoices() + local ignoresCache = deserializeIgnoredUnits() for _, choice in ipairs(choices) do - if not unitIgnored(choice.data['unit']) then - toggleUnitIgnore(choice.data['unit']) + -- We don't want to flip ignored units to unignored + if not unitIgnored(choice.data['unit'], ignoresCache) then + ignoresCache = toggleUnitIgnore(choice.data['unit'], ignoresCache) end end @@ -180,6 +234,10 @@ function warning:onDismiss() view = nil end +-- ====================================================================== +-- Core Logic +-- ====================================================================== + local function compareGroups(group_one, group_two) return #group_one['units'] < #group_two['units'] end @@ -192,6 +250,7 @@ local function getStrandedUnits() -- Don't use ignored units to determine if there are any stranded units -- but keep them to display later local ignoredGroup = {} + local ignoresCache = deserializeIgnoredUnits() -- Pathability group calculation is from gui/pathable for _, unit in ipairs(citizens) do @@ -199,10 +258,12 @@ local function getStrandedUnits() local block = dfhack.maps.getTileBlock(target) local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - if unitIgnored(unit) then + if unitIgnored(unit, ignoresCache) then table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) + + -- Count each new group if #grouped[walkGroup] == 1 then groupCount = groupCount + 1 end @@ -281,69 +342,58 @@ if not dfhack.isMapLoaded() then qerror('warn-stranded requires a map to be loaded') end -local args = utils.invert({...}) +-- ========================================================================= +-- Command Line Interface +-- ========================================================================= -if args.clear or args.all then +args = utils.invert({...}) + +if args.clear then clear() end if args.status then local result, strandedGroups = getStrandedUnits() - if not result then - print('No citizens are currently stranded.') + if result then + local ignoresCache = deserializeIgnoredUnits() - -- We have some ignored citizens - if not (next(strandedGroups) == nil) then - print('\nIgnored citizens:') + for groupIndex, group in ipairs(strandedGroups) do + local groupDesignation = getGroupDesignation(group, groupIndex) - for walkGroup, units in pairs(strandedGroups) do - for _, unit in ipairs(units) do - local text = '' + for _, unit in ipairs(group['units']) do + local text = '' - if args.ids then - text = text..'|'..unit.id..'| ' - end + text = addIgnored(text, unit, ignoresCache) + text = addId(text, unit) - text = text..getUnitDescription(unit)..' {'..walkGroup..'}' - print(text) - end + print(text..getUnitDescription(unit)..groupDesignation) end end - return false + return true end - for groupIndex, group in ipairs(strandedGroups) do - local groupDesignation = nil - if group['mainGroup'] then - groupDesignation = ' (Main Group)' - else - groupDesignation = ' (Group '..groupIndex..')' - end + print('No citizens are currently stranded.') - if args.walk_groups then - groupDesignation = groupDesignation..' {'..group.walkGroup..'}' - end + -- We have some ignored citizens + if not (next(strandedGroups) == nil) then + print('\nIgnored citizens:') - for _, unit in ipairs(group['units']) do - local text = '' + for walkGroup, units in pairs(strandedGroups) do + for _, unit in ipairs(units) do + local text = '' - if unitIgnored(unit) then - text = '[IGNORED] ' - end + text = addId(text, unit) - if args.ids then - text = text..'|'..unit.id..'| ' + text = text..getUnitDescription(unit)..' {'..walkGroup..'}' + print(text) end - - text = text..getUnitDescription(unit)..groupDesignation - print(text) end end - return true + return false end view = view and view:raise() or doCheck() From e2cf30f9ae6d3d95c168b374c7d4be4a0add2eb5 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 18 Sep 2023 00:35:28 -0500 Subject: [PATCH 13/32] Add ignore and unignore command line commands --- warn-stranded.lua | 140 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 134 insertions(+), 6 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 74412222ce..a4ac34792f 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -6,7 +6,9 @@ local gui = require 'gui' local utils = require 'utils' local widgets = require 'gui.widgets' -local args = nil +local argparse = require 'argparse' +local args = {...} +local args_walk_groups, args_ids, args_clear, args_group = false, false, false, false -- =============================================== -- Utility Functions @@ -45,7 +47,7 @@ local function getGroupDesignation(group, groupIndex) groupDesignation = ' (Group '..groupIndex..')' end - if args.walk_groups then + if args_walk_groups then groupDesignation = groupDesignation..' {'..group.walkGroup..'}' end @@ -54,7 +56,7 @@ end -- Check for and potentially add unit.id to text. Controlled by command args. local function addId(text, unit) - if args.ids then + if args_ids then return text..'|'..unit.id..'| ' else return text @@ -325,6 +327,64 @@ local function getStrandedUnits() return true, rawGroups end +local function findCitizen(unitId) + local citizens = dfhack.units.getCitizens() + + for _, citizen in ipairs(citizens) do + if citizen.id == unitId then return citizen end + end + + return nil +end + +local function ignoreGroup(groups, groupNumber) + local ignored = deserializeIgnoredUnits() + + if groupNumber > #groups then + print('Group '..groupNumber..' does not exist') + return false + end + + if groups[groupNumber]['mainGroup'] then + print('Group '..groupNumber..' is the main group of dwarves. Not ignoring.') + return false + end + + for _, unit in ipairs(groups[groupNumber]['units']) do + if unitIgnored(unit, ignored) then + print('Unit '..unit.id..' already ignored, doing nothing to them.') + else + print('Ignoring unit '..unit.id) + toggleUnitIgnore(unit, ignored) + end + end + + return true +end + +local function unignoreGroup(groups, groupNumber) + local ignored = deserializeIgnoredUnits() + + if groupNumber > #groups then + print('Group '..groupNumber..' does not exist') + return false + end + + if group[groupNumber]['mainGroup'] then + print('Group '..groupNumber..' is the main group of dwarves. Unignoring.') + end + + for _, unit in ipairs(groups[groupNumber]['units']) do + if unitIgnored(unit, ignored) then + print('Unignoring unit '..unit.id) + toggleUnitIgnore(unit, ignored) + else + print('Unit '..unit.id..' not already ignored, doing nothing to them.') + end + end + + return true +end function doCheck() local result, strandedGroups = getStrandedUnits() @@ -346,13 +406,19 @@ end -- Command Line Interface -- ========================================================================= -args = utils.invert({...}) +local positionals = argparse.processArgsGetopt(args, { + {'w', 'walkgroups', handler=function() args_walk_groups = true end}, + {'i', 'ids', handler=function() args_ids = true end}, + {'c', 'clear', handler=function() args_clear = true end}, + {'g', 'group', handler=function() args_group = true end}, +}) -if args.clear then +if args_clear then + print('Clearing unit ignore list.') clear() end -if args.status then +if positionals[1] == 'status' then local result, strandedGroups = getStrandedUnits() if result then @@ -396,4 +462,66 @@ if args.status then return false end +if positionals[1] == 'ignore' then + local parameter = tonumber(positionals[2]) + + if parameter and not args_group then + local citizen = findCitizen(parameter) + + if citizen == nil then + print('No citizen with unit id '..parameter..' found in the fortress') + return false + + end + + if unitIgnored(citizen) then + print('Unit '..parameter..' is already ignored. You may want to use the unignore command.') + return false + end + + print('Ignoring unit '..parameter) + toggleUnitIgnore(citizen) + return true + elseif parameter and args_group then + print('Ignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() + return ignoreGroup(strandedCitizens, parameter) + else + print('Must provide unit or group id to the ignore command.') + end + + return false +end + +if positionals[1] == 'unignore' then + local parameter = tonumber(positionals[2]) + + if parameter and not args_group then + local citizen = findCitizen(parameter) + + if citizen == nil then + print('No citizen with unit id '..parameter..' found in the fortress') + return false + + end + + if unitIgnored(citizen) == false then + print('Unit '..parameter..' is not ignored. You may want to use the ignore command.') + return false + end + + print('Unignoring unit '..parameter) + toggleUnitIgnore(citizen) + return true + elseif parameter and args_group then + print('Unignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() + return unignoreGroup(strandedCitizens, parameter) + else + print('Must provide unit id to ignore command.') + end + + return false +end + view = view and view:raise() or doCheck() From 453a842f3c07bd67f1be6ffe3f28b65ad38d9265 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 19 Sep 2023 00:16:06 -0500 Subject: [PATCH 14/32] Improve GUI, now decently happy with it --- warn-stranded.lua | 57 ++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index a4ac34792f..edf928fd63 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -133,39 +133,45 @@ function warning:init(info) self:addviews{ widgets.Window{ view_id = 'main', - frame={w=80, h=18}, + frame={w=80, h=25}, + min_size={w=60, h=25}, frame_title='Stranded Citizen Warning', resizable=true, + autoarrange_subviews=true, subviews = { widgets.List{ + frame={h=15}, view_id = 'list', - frame = { t = 1, l=0 }, text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + on_submit=self:callback('onIgnore'), }, - widgets.HotkeyLabel{ - frame = { b=4, l=0}, - key='SELECT', - label='Toggle Ignore', - on_activate=self:callback('onIgnore'), + widgets.Panel{ + frame={h=5}, + autoarrange_subviews=true, + subviews = { + widgets.HotkeyLabel{ + key='SELECT', + label='Toggle Ignore', + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_SHIFT_I', + label = 'Ignore All', + on_activate = self:callback('onIgnoreAll'), + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_SHIFT_C', + label = 'Clear All Ignored', + on_activate = self:callback('onClear'), + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_Z', + label = 'Zoom to unit', + on_activate = self:callback('onZoom'), + } + } }, - widgets.HotkeyLabel{ - frame = { b=3, l=0 }, - key = 'CUSTOM_SHIFT_I', - label = 'Ignore All', - on_activate = self:callback('onIgnoreAll') }, - widgets.HotkeyLabel{ - frame = { b=2, l=0 }, - key = 'CUSTOM_SHIFT_C', - label = 'Clear All Ignored', - on_activate = self:callback('onClear'), - }, - widgets.HotkeyLabel{ - frame = { b=1, l=0}, - key = 'CUSTOM_Z', - label = 'Zoom to unit', - on_activate = self:callback('onZoom'), - } + } } } @@ -197,8 +203,7 @@ function warning:initListChoices() list:setChoices(choices, 1) end -function warning:onIgnore() - local index, choice = self.subviews.list:getSelected() +function warning:onIgnore(_, choice) local unit = choice.data['unit'] toggleUnitIgnore(unit) From f96cdfa8ce9a56083f8363456ebc9ac4ee2e3d05 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 19 Sep 2023 00:55:45 -0500 Subject: [PATCH 15/32] Add proper toggle group option to GUI only This could be added to command line but we already have ignore group and unignore group which seems to handle the use cases. --- warn-stranded.lua | 66 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index edf928fd63..bdbe7dd017 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -124,6 +124,44 @@ local function toggleUnitIgnore(unit, deserializedIgnores) return ignores end +-- Does the usual GUI pattern when groups can be in a partial state +-- Will ignore everything, unless all units in group are already ignored +-- If all units in the group are ignored, then it will unignore all of them +local function toggleGroup(groups, groupNumber) + local ignored = deserializeIgnoredUnits() + + if groupNumber > #groups then + print('Group '..groupNumber..' does not exist') + return false + end + + if groups[groupNumber]['mainGroup'] then + print('Group '..groupNumber..' is the main group of dwarves. Cannot toggle.') + return false + end + + local group = groups[groupNumber] + + local allIgnored = true + for _, unit in ipairs(group['units']) do + if not unitIgnored(unit, ignored) then + allIgnored = false + goto process + end + end + ::process:: + + for _, unit in ipairs(group['units']) do + local isIgnored = unitIgnored(unit, ignored) + + if allIgnored == isIgnored then + toggleUnitIgnore(unit, ignored) + end + end + + return true +end + -- =============================================================== -- Graphical Interface -- =============================================================== @@ -154,6 +192,11 @@ function warning:init(info) key='SELECT', label='Toggle Ignore', }, + widgets.HotkeyLabel{ + key='CUSTOM_G', + label='Toggle Group', + on_activate = self:callback('onToggleGroup'), + }, widgets.HotkeyLabel{ key = 'CUSTOM_SHIFT_I', label = 'Ignore All', @@ -195,7 +238,7 @@ function warning:initListChoices() text = addId(text, unit) text = text..getUnitDescription(unit)..groupDesignation - table.insert(choices, { text = text, data = {unit = unit, group = index} }) + table.insert(choices, { text = text, data = {unit = unit, group = groupIndex} }) end end @@ -237,6 +280,14 @@ function warning:onZoom() dfhack.gui.revealInDwarfmodeMap(target, true) end +function warning:onToggleGroup() + local index, choice = self.subviews.list:getSelected() + local group = choice.data['group'] + + toggleGroup(self.groups, group) + self:initListChoices() +end + function warning:onDismiss() view = nil end @@ -411,12 +462,13 @@ end -- Command Line Interface -- ========================================================================= -local positionals = argparse.processArgsGetopt(args, { - {'w', 'walkgroups', handler=function() args_walk_groups = true end}, - {'i', 'ids', handler=function() args_ids = true end}, - {'c', 'clear', handler=function() args_clear = true end}, - {'g', 'group', handler=function() args_group = true end}, -}) +local options = { + {'w', 'walkgroups', handler=function() args_walk_groups = true end}, + {'i', 'ids', handler=function() args_ids = true end}, + {'c', 'clear', handler=function() args_clear = true end}, + {'g', 'group', handler=function() args_group = true end}, +} +local positionals = argparse.processArgsGetopt(args, options) if args_clear then print('Clearing unit ignore list.') From 2f1a75164265919a649b32625a7c12a349d10bf7 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 19 Sep 2023 01:13:48 -0500 Subject: [PATCH 16/32] Update help --- docs/warn-stranded.rst | 43 ++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 4af7ee7b1a..0519da5458 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -5,24 +5,47 @@ warn-stranded :summary: Reports citizens that are stranded and can't reach any other unit. :tags: fort units -If any (live) units are stranded, the game will pause and you'll get a warning dialog telling you -which units are isolated. This gives you a chance to rescue them before -they get overly stressed or start starving. +If any (live) units are stranded from the main group, the game will pause and you'll get a warning dialog telling you +which units are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. -You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. +Each unit will be put into a group with the other units stranded together. + +There is a command line interface that can print status of units without pausing or bringing up a window. + +The GUI and command-line both also have the ability to ignore units so they don't trigger a pause and window. -If you ignore a unit, either call ``warn-stranded clear`` in the dfhack console or if you have multiple -stranded you can toggle/clear all units in the warning dialog. +You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. Usage ----- -:: +``warn-stranded -[wicg] [status|ignore|unignore] `` + + -w, --walkgroups: List the raw pathability walkgroup number of each unit in all views. + + -i, --ids: List the id of each unit in all views. - warn-stranded [clear] + -g, --group: Only affects ignore/unignore. Interpret positional argument as group ID and perform operation to the entire group. + + -c, --clear: Clear the entire ignore list first before doing anything else. Examples -------- -``warn-stranded clear`` - Clear all ignored units and then check for ones that are stranded. +``warn-stranded -c`` + Clear all ignored units and then check for ones that are stranded. + +``warn-stranded -wi`` + Standard GUI invocation, but list walkgroups and ids in the table. + +``warn-stranded -wic status`` + Clear all ignored units. Then list all stranded units and all ignored units. Include walkgroups and ids in the output. + +``warn-stranded ignore 1`` + Ignore unit with id 1. + +``warn-stranded ignore -g 2`` + Ignore stranded unit group 2. + +``warn-stranded unignore [-g] 1`` + Unignore unit or stranded group 1. From 6b78003e814917cadea4620450d78f3290f2cd4c Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Tue, 19 Sep 2023 01:28:06 -0500 Subject: [PATCH 17/32] Fix minor display bug from self-review --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index bdbe7dd017..63bede9c39 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -345,7 +345,7 @@ local function getStrandedUnits() table.sort(rawGroups, compareGroups) -- The biggest group is not stranded - mainGroup = rawGroups[1]['walkGroup'] + mainGroup = rawGroups[#rawGroups]['walkGroup'] table.remove(rawGroups, #rawGroups) -- Merge ignoredGroup with grouped From 4b86576c1f0b37f32c2706e2fa0e8335ff37631c Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 21:48:16 -0500 Subject: [PATCH 18/32] Update help to match new usage/commands after review --- docs/warn-stranded.rst | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 0519da5458..3232d0662e 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -5,8 +5,9 @@ warn-stranded :summary: Reports citizens that are stranded and can't reach any other unit. :tags: fort units -If any (live) units are stranded from the main group, the game will pause and you'll get a warning dialog telling you -which units are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. +If any (live) groups of units are stranded from the main (largest) group, +the game will pause and you'll get a warning dialog telling you which units are isolated. +This gives you a chance to rescue them before they get overly stressed or start starving. Each unit will be put into a group with the other units stranded together. @@ -19,33 +20,30 @@ You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Ma Usage ----- -``warn-stranded -[wicg] [status|ignore|unignore] `` +:: - -w, --walkgroups: List the raw pathability walkgroup number of each unit in all views. - - -i, --ids: List the id of each unit in all views. - - -g, --group: Only affects ignore/unignore. Interpret positional argument as group ID and perform operation to the entire group. - - -c, --clear: Clear the entire ignore list first before doing anything else. + warn-stranded + warn-stranded status + warn-stranded clear + warn-stranded (ignore|ignoregroup|unignore|unignoregroup) Examples -------- -``warn-stranded -c`` - Clear all ignored units and then check for ones that are stranded. +``warn-stranded status`` + List all stranded units and all ignored units. Includes unit ids in the output. -``warn-stranded -wi`` - Standard GUI invocation, but list walkgroups and ids in the table. - -``warn-stranded -wic status`` - Clear all ignored units. Then list all stranded units and all ignored units. Include walkgroups and ids in the output. +``warn-stranded clear`` + Clear(unignore) all ignored units. ``warn-stranded ignore 1`` Ignore unit with id 1. -``warn-stranded ignore -g 2`` +``warn-stranded ignoregroup 2`` Ignore stranded unit group 2. -``warn-stranded unignore [-g] 1`` - Unignore unit or stranded group 1. +``warn-stranded unignore 1`` + Unignore unit with id 1. + +``warn-stranded unignoregroup 3`` + Unignore stranded unit group 3. From 15d42a361cc57f23abd33d04f2d06adcacc3c305 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 22:15:09 -0500 Subject: [PATCH 19/32] Implement remaining review changes --- warn-stranded.lua | 145 +++++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 74 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 63bede9c39..d1585acb2c 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,7 +8,6 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -local args_walk_groups, args_ids, args_clear, args_group = false, false, false, false -- =============================================== -- Utility Functions @@ -38,7 +37,7 @@ end -- Use group data, index, and command arguments to generate a group -- designation string. -local function getGroupDesignation(group, groupIndex) +local function getGroupDesignation(group, groupIndex, walkGroup) local groupDesignation = '' if group['mainGroup'] then @@ -47,20 +46,16 @@ local function getGroupDesignation(group, groupIndex) groupDesignation = ' (Group '..groupIndex..')' end - if args_walk_groups then + if walkGroup then groupDesignation = groupDesignation..' {'..group.walkGroup..'}' end return groupDesignation end --- Check for and potentially add unit.id to text. Controlled by command args. +-- Add unit.id to text local function addId(text, unit) - if args_ids then - return text..'|'..unit.id..'| ' - else - return text - end + return text..'|'..unit.id..'| ' end -- Uses persistent API. Low-level, deserializes 'warnStrandedIgnored' key and @@ -93,7 +88,7 @@ local function unitIgnored(unit, deserializedIgnores) return false end --- Check for and potentially add [IGNORED] to text. Controlled by command args. +-- Check for and potentially add [IGNORED] to text. -- Optional deserializedIgnores allows us to call deserialize once for a group of operations local function addIgnored(text, unit, deserializedIgnores) if unitIgnored(unit, deserializedIgnores) then @@ -155,7 +150,7 @@ local function toggleGroup(groups, groupNumber) local isIgnored = unitIgnored(unit, ignored) if allIgnored == isIgnored then - toggleUnitIgnore(unit, ignored) + ignored = toggleUnitIgnore(unit, ignored) end end @@ -183,6 +178,9 @@ function warning:init(info) text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, on_submit=self:callback('onIgnore'), + on_select=self:callback('onZoom'), + on_double_click=self:callback('onIgnore'), + on_double_click2=self:callback('onToggleGroup'), }, widgets.Panel{ frame={h=5}, @@ -190,28 +188,27 @@ function warning:init(info) subviews = { widgets.HotkeyLabel{ key='SELECT', - label='Toggle Ignore', + label='Toggle ignore', }, widgets.HotkeyLabel{ key='CUSTOM_G', - label='Toggle Group', + label='Toggle group', on_activate = self:callback('onToggleGroup'), }, widgets.HotkeyLabel{ key = 'CUSTOM_SHIFT_I', - label = 'Ignore All', + label = 'Ignore all', on_activate = self:callback('onIgnoreAll'), }, widgets.HotkeyLabel{ key = 'CUSTOM_SHIFT_C', - label = 'Clear All Ignored', + label = 'Clear all ignored', on_activate = self:callback('onClear'), }, - widgets.HotkeyLabel{ - key = 'CUSTOM_Z', - label = 'Zoom to unit', - on_activate = self:callback('onZoom'), - } + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap='Click to ignore/unignore unit. Shift doubleclick to ignore/unignore a group of units.', + }, } }, @@ -235,7 +232,6 @@ function warning:initListChoices() local text = '' text = addIgnored(text, unit, ignoresCache) - text = addId(text, unit) text = text..getUnitDescription(unit)..groupDesignation table.insert(choices, { text = text, data = {unit = unit, group = groupIndex} }) @@ -433,7 +429,7 @@ local function unignoreGroup(groups, groupNumber) for _, unit in ipairs(groups[groupNumber]['units']) do if unitIgnored(unit, ignored) then print('Unignoring unit '..unit.id) - toggleUnitIgnore(unit, ignored) + ignored = toggleUnitIgnore(unit, ignored) else print('Unit '..unit.id..' not already ignored, doing nothing to them.') end @@ -462,19 +458,15 @@ end -- Command Line Interface -- ========================================================================= -local options = { - {'w', 'walkgroups', handler=function() args_walk_groups = true end}, - {'i', 'ids', handler=function() args_ids = true end}, - {'c', 'clear', handler=function() args_clear = true end}, - {'g', 'group', handler=function() args_group = true end}, -} local positionals = argparse.processArgsGetopt(args, options) -if args_clear then +if positionals[1] == 'clear' then print('Clearing unit ignore list.') - clear() + return clear() end +local parameter = tonumber(positionals[2]) + if positionals[1] == 'status' then local result, strandedGroups = getStrandedUnits() @@ -482,7 +474,7 @@ if positionals[1] == 'status' then local ignoresCache = deserializeIgnoredUnits() for groupIndex, group in ipairs(strandedGroups) do - local groupDesignation = getGroupDesignation(group, groupIndex) + local groupDesignation = getGroupDesignation(group, groupIndex, true) for _, unit in ipairs(group['units']) do local text = '' @@ -509,8 +501,8 @@ if positionals[1] == 'status' then local text = '' text = addId(text, unit) - text = text..getUnitDescription(unit)..' {'..walkGroup..'}' + print(text) end end @@ -520,65 +512,70 @@ if positionals[1] == 'status' then end if positionals[1] == 'ignore' then - local parameter = tonumber(positionals[2]) + if not parameter then + print('Must provide unit id to the ignore command.') + return false + end - if parameter and not args_group then - local citizen = findCitizen(parameter) + local citizen = findCitizen(parameter) - if citizen == nil then - print('No citizen with unit id '..parameter..' found in the fortress') - return false + if citizen == nil then + print('No citizen with unit id '..parameter..' found in the fortress') + return false + end - end + if unitIgnored(citizen) then + print('Unit '..parameter..' is already ignored. You may want to use the unignore command.') + return false + end - if unitIgnored(citizen) then - print('Unit '..parameter..' is already ignored. You may want to use the unignore command.') - return false - end + print('Ignoring unit '..parameter) + toggleUnitIgnore(citizen) + return true +end - print('Ignoring unit '..parameter) - toggleUnitIgnore(citizen) - return true - elseif parameter and args_group then - print('Ignoring group '..parameter) - local _, strandedCitizens = getStrandedUnits() - return ignoreGroup(strandedCitizens, parameter) - else - print('Must provide unit or group id to the ignore command.') +if positionals[1] == 'ignoregroup' then + if not parameter then + print('Must provide group id to the ignoregroup command.') end - return false + print('Ignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() + return ignoreGroup(strandedCitizens, parameter) end if positionals[1] == 'unignore' then - local parameter = tonumber(positionals[2]) + if not parameter then + print('Must provide unit id to unignore command.') + return false + end - if parameter and not args_group then - local citizen = findCitizen(parameter) + local citizen = findCitizen(parameter) - if citizen == nil then - print('No citizen with unit id '..parameter..' found in the fortress') - return false + if citizen == nil then + print('No citizen with unit id '..parameter..' found in the fortress') + return false + end - end + if unitIgnored(citizen) == false then + print('Unit '..parameter..' is not ignored. You may want to use the ignore command.') + return false + end - if unitIgnored(citizen) == false then - print('Unit '..parameter..' is not ignored. You may want to use the ignore command.') - return false - end + print('Unignoring unit '..parameter) + toggleUnitIgnore(citizen) + return true +end - print('Unignoring unit '..parameter) - toggleUnitIgnore(citizen) - return true - elseif parameter and args_group then - print('Unignoring group '..parameter) - local _, strandedCitizens = getStrandedUnits() - return unignoreGroup(strandedCitizens, parameter) - else - print('Must provide unit id to ignore command.') +if positionals[1] == 'unignoregroup' then + if not parameter then + print('Must provide group id to unignoregroup command.') + return false end - return false + print('Unignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() + return unignoreGroup(strandedCitizens, parameter) end view = view and view:raise() or doCheck() From b7ab4115c7f6d9403f1fcf17b34417787da8741e Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 22:34:23 -0500 Subject: [PATCH 20/32] Fix first round of testing bugs post review changes --- warn-stranded.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index d1585acb2c..1f1f8a40b1 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -207,7 +207,7 @@ function warning:init(info) }, widgets.WrappedLabel{ frame={b=0, l=0, r=0}, - text_to_wrap='Click to ignore/unignore unit. Shift doubleclick to ignore/unignore a group of units.', + text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', }, } }, @@ -458,7 +458,7 @@ end -- Command Line Interface -- ========================================================================= -local positionals = argparse.processArgsGetopt(args, options) +local positionals = argparse.processArgsGetopt(args, {}) if positionals[1] == 'clear' then print('Clearing unit ignore list.') From 3609a8d636c6305bfbd516dc184c8d061336310f Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 22:41:30 -0500 Subject: [PATCH 21/32] Fix typo in unignore group --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 1f1f8a40b1..1b80385ddc 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -422,7 +422,7 @@ local function unignoreGroup(groups, groupNumber) return false end - if group[groupNumber]['mainGroup'] then + if groups[groupNumber]['mainGroup'] then print('Group '..groupNumber..' is the main group of dwarves. Unignoring.') end From 81a24a850b25a195dec3fe780aaa22e2aca01bc8 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Thu, 5 Oct 2023 22:56:09 -0500 Subject: [PATCH 22/32] Remove jarring selection reset on every action --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 1b80385ddc..bdb56747aa 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -239,7 +239,7 @@ function warning:initListChoices() end local list = self.subviews.list - list:setChoices(choices, 1) + list:setChoices(choices) end function warning:onIgnore(_, choice) From 3255e5755026cc728fe300e1bd7254fbb205e398 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 6 Oct 2023 16:52:59 -0500 Subject: [PATCH 23/32] Update docs/warn-stranded.rst wording from review Co-authored-by: Myk --- docs/warn-stranded.rst | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 3232d0662e..50bbeead40 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -2,18 +2,18 @@ warn-stranded ============= .. dfhack-tool:: - :summary: Reports citizens that are stranded and can't reach any other unit. + :summary: Reports citizens that are stranded and can't reach any other citizens. :tags: fort units -If any (live) groups of units are stranded from the main (largest) group, -the game will pause and you'll get a warning dialog telling you which units are isolated. +If any (live) groups of fort citizens are stranded from the main (largest) group, +the game will pause and you'll get a warning dialog telling you which citizens are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. -Each unit will be put into a group with the other units stranded together. +Each ciitizen will be put into a group with the other citizens stranded together. -There is a command line interface that can print status of units without pausing or bringing up a window. +There is a command line interface that can print status of citizens without pausing or bringing up a window. -The GUI and command-line both also have the ability to ignore units so they don't trigger a pause and window. +The GUI and command-line both also have the ability to ignore citizens so they don't trigger a pause and window. You can enable ``warn-stranded`` notifications in `gui/control-panel` on the "Maintenance" tab. @@ -30,20 +30,24 @@ Usage Examples -------- +``warn-stranded`` + Standard command that checks for standed citizens and causes a window to pop up with a warning if any are stranded. + Does nothing when there are no unignored stranded citizens. + ``warn-stranded status`` - List all stranded units and all ignored units. Includes unit ids in the output. + List all stranded citizens and all ignored citizens. Includes citizen unit ids. ``warn-stranded clear`` - Clear(unignore) all ignored units. + Clear(unignore) all ignored citizens. ``warn-stranded ignore 1`` - Ignore unit with id 1. + Ignore citizen with unit id 1. ``warn-stranded ignoregroup 2`` - Ignore stranded unit group 2. + Ignore stranded citizen group 2. ``warn-stranded unignore 1`` - Unignore unit with id 1. + Unignore citizen with unit id 1. ``warn-stranded unignoregroup 3`` - Unignore stranded unit group 3. + Unignore stranded citizen group 3. From 748bff59f9a84c593b15e49567fb012f351a7f33 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 6 Oct 2023 23:37:58 -0500 Subject: [PATCH 24/32] Attempt to implement review refactors --- warn-stranded.lua | 274 ++++++++++++++++++++++------------------------ 1 file changed, 132 insertions(+), 142 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index bdb56747aa..670b6b41c2 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,6 +8,8 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} +local scriptPrefix = 'warn-stranded' +local ignoresCache = {} -- =============================================== -- Utility Functions @@ -15,7 +17,7 @@ local args = {...} -- Clear the ignore list local function clear() - dfhack.persistent.delete('warnStrandedIgnore') + dfhack.persistent.delete(scriptPrefix) end -- Taken from warn-starving @@ -31,8 +33,9 @@ end -- Partially taken from warn-starving local function getUnitDescription(unit) - return '['..dfhack.units.getProfessionName(unit)..'] '..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).. - ' '..getSexString(unit.sex)..' Stress category: '..dfhack.units.getStressCategory(unit) + return ('[%s] %s %s'):format(dfhack.units.getProfessionName(unit), + dfhack.TranslateName(dfhack.units.getVisibleName(unit)), + getSexString(unit.sex)) end -- Use group data, index, and command arguments to generate a group @@ -58,73 +61,67 @@ local function addId(text, unit) return text..'|'..unit.id..'| ' end --- Uses persistent API. Low-level, deserializes 'warnStrandedIgnored' key and --- will return an initialized empty warnStrandedIgnored table if needed. --- Performance characterstics unknown of persistent API -local function deserializeIgnoredUnits() - local currentIgnore = dfhack.persistent.get('warnStrandedIgnore') - if currentIgnore == nil then return {} end +-- =============================================== +-- Persistence API +-- =============================================== +-- Optional refresh parameter forces us to load from API instead of using cache + +-- Uses persistent API. Low-level, gets all entries currently in our persistent table +-- will return an empty array if needed. Clears and adds entries to our cache. +-- Returns the new global ignoresCache value +local function getIgnoredUnits() + local ignores = dfhack.persistent.get_all(scriptPrefix) + if ignores == nil then return {} end - local tbl = {} + ignoresCache = {} - for v in string.gmatch(currentIgnore['value'], '%d+') do - table.insert(tbl, v) + for _, entry in ipairs(ignores) do + unit_id = entry.ints[1] + ignoresCache[unit_id] = entry end - return tbl + return ignoresCache end --- Uses persistent API. Deserializes 'warnStrandedIgnore' key to determine if unit is ignored --- deserializedIgnores is optional but allows us to only call deserialize once like an explicit cache. -local function unitIgnored(unit, deserializedIgnores) - local ignores = deserializedIgnores or deserializeIgnoredUnits() - - for index, id in ipairs(ignores) do - if tonumber(id) == unit.id then - return true, index - end - end +-- Uses persistent API. Optional refresh parameter forces us to load from API, +-- instead of using our cache. +-- Returns the persistent entry or nil +local function unitIgnored(unit, refresh) + if refresh then getIgnoredUnits() end - return false + return ignoresCache[unit.id] end -- Check for and potentially add [IGNORED] to text. --- Optional deserializedIgnores allows us to call deserialize once for a group of operations -local function addIgnored(text, unit, deserializedIgnores) - if unitIgnored(unit, deserializedIgnores) then +local function addIgnored(text, unit, refresh) + if unitIgnored(unit, refresh) then return text..'[IGNORED] ' end return text end --- Uses persistent API. Toggles a unit's ignored status by deserializing 'warnStrandedIgnore' key --- then serializing the resulting table after the toggle. --- Optional cache parameter could affect data integrity. Make sure you don't need data reloaded --- before using it. Calling several times in a row can use the return result of the function --- as input to the next call. -local function toggleUnitIgnore(unit, deserializedIgnores) - local ignores = deserializedIgnores or deserializeIgnoredUnits() - local is_ignored, index = unitIgnored(unit, ignores) - - if is_ignored then - table.remove(ignores, index) +-- Uses persistent API. Toggles a unit's ignored status by deleting the entry from the persistence API +-- and from the ignoresCache table. +-- Returns true if the unit was already ignored, false if it wasn't. +local function toggleUnitIgnore(unit, refresh) + local entry = unitIgnored(unit, refresh) + + if entry then + entry:delete() + table.remove(ignoresCache, unit.id) + return true else - table.insert(ignores, unit.id) + entry = dfhack.persistent.save({key = scriptPrefix, ints = {unit.id}}) + ignoresCache[unit.id] = entry + return false end - - dfhack.persistent.delete('warnStrandedIgnore') - dfhack.persistent.save({key = 'warnStrandedIgnore', value = table.concat(ignores, ' ')}) - - return ignores end -- Does the usual GUI pattern when groups can be in a partial state -- Will ignore everything, unless all units in group are already ignored -- If all units in the group are ignored, then it will unignore all of them local function toggleGroup(groups, groupNumber) - local ignored = deserializeIgnoredUnits() - if groupNumber > #groups then print('Group '..groupNumber..' does not exist') return false @@ -139,7 +136,7 @@ local function toggleGroup(groups, groupNumber) local allIgnored = true for _, unit in ipairs(group['units']) do - if not unitIgnored(unit, ignored) then + if not unitIgnored(unit) then allIgnored = false goto process end @@ -147,10 +144,10 @@ local function toggleGroup(groups, groupNumber) ::process:: for _, unit in ipairs(group['units']) do - local isIgnored = unitIgnored(unit, ignored) + local isIgnored = unitIgnored(unit) if allIgnored == isIgnored then - ignored = toggleUnitIgnore(unit, ignored) + ignored = toggleUnitIgnore(unit) end end @@ -160,68 +157,63 @@ end -- =============================================================== -- Graphical Interface -- =============================================================== -warning = defclass(warning, gui.ZScreenModal) - -function warning:init(info) +WarningWindow = defclass(WarningWindow, widgets.Window) +WarningWindow.ATTRS{ + frame={w=80, h=25}, + min_size={w=60, h=25}, + frame_title='Stranded Citizen Warning', + resizable=true, + autoarrange_subviews=true, +} + +function WarningWindow:init() self:addviews{ - widgets.Window{ - view_id = 'main', - frame={w=80, h=25}, - min_size={w=60, h=25}, - frame_title='Stranded Citizen Warning', - resizable=true, + widgets.List{ + frame={h=15}, + view_id = 'list', + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + on_submit=self:callback('onIgnore'), + on_select=self:callback('onZoom'), + on_double_click=self:callback('onIgnore'), + on_double_click2=self:callback('onToggleGroup'), + }, + widgets.Panel{ + frame={h=5}, autoarrange_subviews=true, subviews = { - widgets.List{ - frame={h=15}, - view_id = 'list', - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, - on_submit=self:callback('onIgnore'), - on_select=self:callback('onZoom'), - on_double_click=self:callback('onIgnore'), - on_double_click2=self:callback('onToggleGroup'), + widgets.HotkeyLabel{ + key='SELECT', + label='Toggle ignore', }, - widgets.Panel{ - frame={h=5}, - autoarrange_subviews=true, - subviews = { - widgets.HotkeyLabel{ - key='SELECT', - label='Toggle ignore', - }, - widgets.HotkeyLabel{ - key='CUSTOM_G', - label='Toggle group', - on_activate = self:callback('onToggleGroup'), - }, - widgets.HotkeyLabel{ - key = 'CUSTOM_SHIFT_I', - label = 'Ignore all', - on_activate = self:callback('onIgnoreAll'), - }, - widgets.HotkeyLabel{ - key = 'CUSTOM_SHIFT_C', - label = 'Clear all ignored', - on_activate = self:callback('onClear'), - }, - widgets.WrappedLabel{ - frame={b=0, l=0, r=0}, - text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', - }, - } + widgets.HotkeyLabel{ + key='CUSTOM_G', + label='Toggle group', + on_activate = self:callback('onToggleGroup'), + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_SHIFT_I', + label = 'Ignore all', + on_activate = self:callback('onIgnoreAll'), + }, + widgets.HotkeyLabel{ + key = 'CUSTOM_SHIFT_C', + label = 'Clear all ignored', + on_activate = self:callback('onClear'), + }, + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', }, - } - } + }, } self.groups = info.groups self:initListChoices() end - -function warning:initListChoices() +function WarningWindow:initListChoices() local choices = {} for groupIndex, group in ipairs(self.groups) do @@ -242,33 +234,32 @@ function warning:initListChoices() list:setChoices(choices) end -function warning:onIgnore(_, choice) +function WarningWindow:onIgnore(_, choice) local unit = choice.data['unit'] toggleUnitIgnore(unit) self:initListChoices() end -function warning:onIgnoreAll() +function WarningWindow:onIgnoreAll() local choices = self.subviews.list:getChoices() - local ignoresCache = deserializeIgnoredUnits() for _, choice in ipairs(choices) do -- We don't want to flip ignored units to unignored - if not unitIgnored(choice.data['unit'], ignoresCache) then - ignoresCache = toggleUnitIgnore(choice.data['unit'], ignoresCache) + if not unitIgnored(choice.data['unit']) then + toggleUnitIgnore(choice.data['unit']) end end self:dismiss() end -function warning:onClear() +function WarningWindow:onClear() clear() self:initListChoices() end -function warning:onZoom() +function WarningWindow:onZoom() local index, choice = self.subviews.list:getSelected() local unit = choice.data['unit'] @@ -276,7 +267,7 @@ function warning:onZoom() dfhack.gui.revealInDwarfmodeMap(target, true) end -function warning:onToggleGroup() +function WarningWindow:onToggleGroup() local index, choice = self.subviews.list:getSelected() local group = choice.data['group'] @@ -284,7 +275,13 @@ function warning:onToggleGroup() self:initListChoices() end -function warning:onDismiss() +WarningScreen = defclass(WarningScreen, gui.ZScreenModal) + +function WarningScreen:init(info) + self:addviews{WarningWindow{info}} +end + +function WarningScreen:onDismiss() view = nil end @@ -304,7 +301,6 @@ local function getStrandedUnits() -- Don't use ignored units to determine if there are any stranded units -- but keep them to display later local ignoredGroup = {} - local ignoresCache = deserializeIgnoredUnits() -- Pathability group calculation is from gui/pathable for _, unit in ipairs(citizens) do @@ -312,7 +308,7 @@ local function getStrandedUnits() local block = dfhack.maps.getTileBlock(target) local walkGroup = block and block.walkable[target.x % 16][target.y % 16] or 0 - if unitIgnored(unit, ignoresCache) then + if unitIgnored(unit) then table.insert(ensure_key(ignoredGroup, walkGroup), unit) else table.insert(ensure_key(grouped, walkGroup), unit) @@ -390,8 +386,6 @@ local function findCitizen(unitId) end local function ignoreGroup(groups, groupNumber) - local ignored = deserializeIgnoredUnits() - if groupNumber > #groups then print('Group '..groupNumber..' does not exist') return false @@ -403,11 +397,11 @@ local function ignoreGroup(groups, groupNumber) end for _, unit in ipairs(groups[groupNumber]['units']) do - if unitIgnored(unit, ignored) then + if unitIgnored(unit) then print('Unit '..unit.id..' already ignored, doing nothing to them.') else print('Ignoring unit '..unit.id) - toggleUnitIgnore(unit, ignored) + toggleUnitIgnore(unit) end end @@ -415,8 +409,6 @@ local function ignoreGroup(groups, groupNumber) end local function unignoreGroup(groups, groupNumber) - local ignored = deserializeIgnoredUnits() - if groupNumber > #groups then print('Group '..groupNumber..' does not exist') return false @@ -427,9 +419,9 @@ local function unignoreGroup(groups, groupNumber) end for _, unit in ipairs(groups[groupNumber]['units']) do - if unitIgnored(unit, ignored) then + if unitIgnored(unit) then print('Unignoring unit '..unit.id) - ignored = toggleUnitIgnore(unit, ignored) + ignored = toggleUnitIgnore(unit) else print('Unit '..unit.id..' not already ignored, doing nothing to them.') end @@ -442,7 +434,7 @@ function doCheck() local result, strandedGroups = getStrandedUnits() if result then - return warning{groups=strandedGroups}:show() + return WarningScreen{groups=strandedGroups}:show() end end @@ -459,27 +451,23 @@ end -- ========================================================================= local positionals = argparse.processArgsGetopt(args, {}) +local parameter = tonumber(positionals[2]) if positionals[1] == 'clear' then print('Clearing unit ignore list.') - return clear() -end - -local parameter = tonumber(positionals[2]) + clear() -if positionals[1] == 'status' then +elseif positionals[1] == 'status' then local result, strandedGroups = getStrandedUnits() if result then - local ignoresCache = deserializeIgnoredUnits() - for groupIndex, group in ipairs(strandedGroups) do local groupDesignation = getGroupDesignation(group, groupIndex, true) for _, unit in ipairs(group['units']) do local text = '' - text = addIgnored(text, unit, ignoresCache) + text = addIgnored(text, unit) text = addId(text, unit) print(text..getUnitDescription(unit)..groupDesignation) @@ -508,10 +496,7 @@ if positionals[1] == 'status' then end end - return false -end - -if positionals[1] == 'ignore' then +elseif positionals[1] == 'ignore' then if not parameter then print('Must provide unit id to the ignore command.') return false @@ -531,20 +516,17 @@ if positionals[1] == 'ignore' then print('Ignoring unit '..parameter) toggleUnitIgnore(citizen) - return true -end -if positionals[1] == 'ignoregroup' then +elseif positionals[1] == 'ignoregroup' then if not parameter then print('Must provide group id to the ignoregroup command.') end print('Ignoring group '..parameter) local _, strandedCitizens = getStrandedUnits() - return ignoreGroup(strandedCitizens, parameter) -end + ignoreGroup(strandedCitizens, parameter) -if positionals[1] == 'unignore' then +elseif positionals[1] == 'unignore' then if not parameter then print('Must provide unit id to unignore command.') return false @@ -564,18 +546,26 @@ if positionals[1] == 'unignore' then print('Unignoring unit '..parameter) toggleUnitIgnore(citizen) - return true -end -if positionals[1] == 'unignoregroup' then +elseif positionals[1] == 'unignoregroup' then if not parameter then print('Must provide group id to unignoregroup command.') return false end print('Unignoring group '..parameter) + local _, strandedCitizens = getStrandedUnits() - return unignoreGroup(strandedCitizens, parameter) + unignoreGroup(strandedCitizens, parameter) +else + view = view and view:raise() or doCheck() end -view = view and view:raise() or doCheck() +-- Load ignores list on save game load +dfhack.onStateChange[scriptPrefix] = function(state_change) + if state_change ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + getIgnoredUnits() +end From d1c182916da25ddc2d2f5feff5b15094a321c129 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Fri, 6 Oct 2023 23:40:15 -0500 Subject: [PATCH 25/32] Remove attempt at using const --- warn-stranded.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 670b6b41c2..eaff6f45b8 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,7 +8,7 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -local scriptPrefix = 'warn-stranded' +local scriptPrefix = 'warn-stranded' local ignoresCache = {} -- =============================================== From f3befea75a1b6215b25af0df5c75fd398b1d38af Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sat, 7 Oct 2023 00:39:02 -0500 Subject: [PATCH 26/32] Fix post-review-change bugs --- warn-stranded.lua | 52 +++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index eaff6f45b8..7546d89846 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -1,15 +1,15 @@ -- Detects and alerts when a citizen is stranded -- Logic heavily based off of warn-starving -- GUI heavily based off of autobutcher ---@ module = true +--@module = true local gui = require 'gui' local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -local scriptPrefix = 'warn-stranded' -local ignoresCache = {} +scriptPrefix = 'warn-stranded' +ignoresCache = ignoresCache or {} -- =============================================== -- Utility Functions @@ -17,7 +17,10 @@ local ignoresCache = {} -- Clear the ignore list local function clear() - dfhack.persistent.delete(scriptPrefix) + for index, entry in pairs(ignoresCache) do + entry:delete() + ignoresCache[index] = nil + end end -- Taken from warn-starving @@ -76,6 +79,8 @@ local function getIgnoredUnits() ignoresCache = {} for _, entry in ipairs(ignores) do + print(entry) + printall(ignoresCache) unit_id = entry.ints[1] ignoresCache[unit_id] = entry end @@ -109,10 +114,10 @@ local function toggleUnitIgnore(unit, refresh) if entry then entry:delete() - table.remove(ignoresCache, unit.id) + ignoresCache[unit.id] = nil return true else - entry = dfhack.persistent.save({key = scriptPrefix, ints = {unit.id}}) + entry = dfhack.persistent.save({key = scriptPrefix, ints = {unit.id}}, true) ignoresCache[unit.id] = entry return false end @@ -145,9 +150,10 @@ local function toggleGroup(groups, groupNumber) for _, unit in ipairs(group['units']) do local isIgnored = unitIgnored(unit) + if isIgnored then isIgnored = true else isIgnored = false end if allIgnored == isIgnored then - ignored = toggleUnitIgnore(unit) + toggleUnitIgnore(unit) end end @@ -166,7 +172,7 @@ WarningWindow.ATTRS{ autoarrange_subviews=true, } -function WarningWindow:init() +function WarningWindow:init(info) self:addviews{ widgets.List{ frame={h=15}, @@ -218,12 +224,11 @@ function WarningWindow:initListChoices() for groupIndex, group in ipairs(self.groups) do local groupDesignation = getGroupDesignation(group, groupIndex) - local ignoresCache = deserializeIgnoredUnits() for _, unit in ipairs(group['units']) do local text = '' - text = addIgnored(text, unit, ignoresCache) + text = addIgnored(text, unit) text = text..getUnitDescription(unit)..groupDesignation table.insert(choices, { text = text, data = {unit = unit, group = groupIndex} }) @@ -251,7 +256,7 @@ function WarningWindow:onIgnoreAll() end end - self:dismiss() + self:initListChoices() end function WarningWindow:onClear() @@ -278,7 +283,7 @@ end WarningScreen = defclass(WarningScreen, gui.ZScreenModal) function WarningScreen:init(info) - self:addviews{WarningWindow{info}} + self:addviews{WarningWindow{groups=info.groups}} end function WarningScreen:onDismiss() @@ -438,6 +443,16 @@ function doCheck() end end +-- Load ignores list on save game load +-- WARNING: This has to be above `dfhack_flags.module` or it will not work as intended on first game load +dfhack.onStateChange[scriptPrefix] = function(state_change) + if state_change ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + getIgnoredUnits() +end + if dfhack_flags.module then return end @@ -474,6 +489,8 @@ elseif positionals[1] == 'status' then end end + printall(dfhack.persistent.get_all(scriptPrefix)) + print(dfhack.persistent.get_all(scriptPrefix)) return true end @@ -539,7 +556,7 @@ elseif positionals[1] == 'unignore' then return false end - if unitIgnored(citizen) == false then + if not unitIgnored(citizen) then print('Unit '..parameter..' is not ignored. You may want to use the ignore command.') return false end @@ -560,12 +577,3 @@ elseif positionals[1] == 'unignoregroup' then else view = view and view:raise() or doCheck() end - --- Load ignores list on save game load -dfhack.onStateChange[scriptPrefix] = function(state_change) - if state_change ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return - end - - getIgnoredUnits() -end From 5d3de5c219bd017f1328ac18fa24328da7d1c6cb Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sat, 7 Oct 2023 00:40:37 -0500 Subject: [PATCH 27/32] Remove debug prints --- warn-stranded.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 7546d89846..80f5bff0d5 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -79,8 +79,6 @@ local function getIgnoredUnits() ignoresCache = {} for _, entry in ipairs(ignores) do - print(entry) - printall(ignoresCache) unit_id = entry.ints[1] ignoresCache[unit_id] = entry end @@ -489,8 +487,6 @@ elseif positionals[1] == 'status' then end end - printall(dfhack.persistent.get_all(scriptPrefix)) - print(dfhack.persistent.get_all(scriptPrefix)) return true end From 60a9a3e033e1a3da16e4216f50b443b0b3492e73 Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Sat, 7 Oct 2023 10:45:16 -0500 Subject: [PATCH 28/32] Apply easy fixes from code review Co-authored-by: Myk --- changelog.txt | 2 +- docs/warn-stranded.rst | 6 +++--- gui/control-panel.lua | 2 +- warn-stranded.lua | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index d247ec6801..07a731bb5a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,7 +31,7 @@ Template for new versions: ## New Features ## New Scripts -- `warn-stranded`: new repeatable maintenance script to check for stranded units, based off `warn-starving` +- `warn-stranded`: new repeatable maintenance script to check for stranded units, similar to `warn-starving` ## Fixes - `suspendmanager`: fix errors when constructing near the map edge diff --git a/docs/warn-stranded.rst b/docs/warn-stranded.rst index 50bbeead40..10a92a8f2a 100644 --- a/docs/warn-stranded.rst +++ b/docs/warn-stranded.rst @@ -9,7 +9,7 @@ If any (live) groups of fort citizens are stranded from the main (largest) group the game will pause and you'll get a warning dialog telling you which citizens are isolated. This gives you a chance to rescue them before they get overly stressed or start starving. -Each ciitizen will be put into a group with the other citizens stranded together. +Each citizen will be put into a group with the other citizens stranded together. There is a command line interface that can print status of citizens without pausing or bringing up a window. @@ -31,14 +31,14 @@ Examples -------- ``warn-stranded`` - Standard command that checks for standed citizens and causes a window to pop up with a warning if any are stranded. + Standard command that checks citizens and pops up a warning if any are stranded. Does nothing when there are no unignored stranded citizens. ``warn-stranded status`` List all stranded citizens and all ignored citizens. Includes citizen unit ids. ``warn-stranded clear`` - Clear(unignore) all ignored citizens. + Clear (unignore) all ignored citizens. ``warn-stranded ignore 1`` Ignore citizen with unit id 1. diff --git a/gui/control-panel.lua b/gui/control-panel.lua index c4cccc281d..4836fdc877 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -136,7 +136,7 @@ local REPEATS = { command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, ['warn-stranded']={ desc='Show a warning dialog when units are stranded from all others.', - command={'--time', '300', '--timeUnits', 'ticks', '--command', '[', 'warn-stranded', ']'}}, + command={'--time', '0.25', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, ['empty-wheelbarrows']={ desc='Empties wheelbarrows which have rocks stuck in them.', command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, diff --git a/warn-stranded.lua b/warn-stranded.lua index 80f5bff0d5..ee116910da 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -8,7 +8,7 @@ local utils = require 'utils' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -scriptPrefix = 'warn-stranded' +local scriptPrefix = 'warn-stranded' ignoresCache = ignoresCache or {} -- =============================================== From 5d11aa55a3bd656d693c86626708c0df9f959dae Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 9 Oct 2023 18:35:22 -0500 Subject: [PATCH 29/32] Update getIgnoredUnits and rename to loadIgnoredUnits --- warn-stranded.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index ee116910da..b1a65be863 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -72,12 +72,12 @@ end -- Uses persistent API. Low-level, gets all entries currently in our persistent table -- will return an empty array if needed. Clears and adds entries to our cache. -- Returns the new global ignoresCache value -local function getIgnoredUnits() +local function loadIgnoredUnits() local ignores = dfhack.persistent.get_all(scriptPrefix) - if ignores == nil then return {} end - ignoresCache = {} + if ignores == nil then return ignoresCache end + for _, entry in ipairs(ignores) do unit_id = entry.ints[1] ignoresCache[unit_id] = entry @@ -90,7 +90,7 @@ end -- instead of using our cache. -- Returns the persistent entry or nil local function unitIgnored(unit, refresh) - if refresh then getIgnoredUnits() end + if refresh then loadIgnoredUnits() end return ignoresCache[unit.id] end @@ -448,7 +448,7 @@ dfhack.onStateChange[scriptPrefix] = function(state_change) return end - getIgnoredUnits() + loadIgnoredUnits() end if dfhack_flags.module then From da145162c6286f76202b1592d7afa7239bdfaf5f Mon Sep 17 00:00:00 2001 From: Lily Carpenter Date: Mon, 9 Oct 2023 23:54:42 -0500 Subject: [PATCH 30/32] Attempt at making a GUI layout that better uses space --- warn-stranded.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index b1a65be863..70a63b8b45 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -175,8 +175,6 @@ function WarningWindow:init(info) widgets.List{ frame={h=15}, view_id = 'list', - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, on_submit=self:callback('onIgnore'), on_select=self:callback('onZoom'), on_double_click=self:callback('onIgnore'), @@ -184,29 +182,39 @@ function WarningWindow:init(info) }, widgets.Panel{ frame={h=5}, - autoarrange_subviews=true, subviews = { widgets.HotkeyLabel{ + frame={b=3, l=0}, key='SELECT', label='Toggle ignore', + auto_width=true, }, widgets.HotkeyLabel{ + frame={b=3, l=21}, key='CUSTOM_G', label='Toggle group', on_activate = self:callback('onToggleGroup'), + auto_width=true, + }, widgets.HotkeyLabel{ + frame={b=3, l=37}, key = 'CUSTOM_SHIFT_I', label = 'Ignore all', on_activate = self:callback('onIgnoreAll'), + auto_width=true, + }, widgets.HotkeyLabel{ + frame={b=3, l=52}, key = 'CUSTOM_SHIFT_C', label = 'Clear all ignored', on_activate = self:callback('onClear'), + auto_width=true, + }, widgets.WrappedLabel{ - frame={b=0, l=0, r=0}, + frame={b=1, l=0}, text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', }, } From 5dbbcf5d57daec4e869cb842c606c09aa51afd7f Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 16 Oct 2023 12:30:08 -0700 Subject: [PATCH 31/32] Apply suggestions from code review --- warn-stranded.lua | 84 ++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/warn-stranded.lua b/warn-stranded.lua index 70a63b8b45..cd190e3c1f 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -163,62 +163,53 @@ end -- =============================================================== WarningWindow = defclass(WarningWindow, widgets.Window) WarningWindow.ATTRS{ - frame={w=80, h=25}, - min_size={w=60, h=25}, - frame_title='Stranded Citizen Warning', + frame={w=60, h=25, r=2, t=18}, + resize_min={w=50, h=15}, + frame_title='Stranded citizen warning', resizable=true, - autoarrange_subviews=true, } function WarningWindow:init(info) self:addviews{ widgets.List{ - frame={h=15}, + frame={l=0, r=0, t=0, b=6}, view_id = 'list', - on_submit=self:callback('onIgnore'), on_select=self:callback('onZoom'), on_double_click=self:callback('onIgnore'), on_double_click2=self:callback('onToggleGroup'), }, - widgets.Panel{ - frame={h=5}, - subviews = { - widgets.HotkeyLabel{ - frame={b=3, l=0}, - key='SELECT', - label='Toggle ignore', - auto_width=true, - }, - widgets.HotkeyLabel{ - frame={b=3, l=21}, - key='CUSTOM_G', - label='Toggle group', - on_activate = self:callback('onToggleGroup'), - auto_width=true, - - }, - widgets.HotkeyLabel{ - frame={b=3, l=37}, - key = 'CUSTOM_SHIFT_I', - label = 'Ignore all', - on_activate = self:callback('onIgnoreAll'), - auto_width=true, - - }, - widgets.HotkeyLabel{ - frame={b=3, l=52}, - key = 'CUSTOM_SHIFT_C', - label = 'Clear all ignored', - on_activate = self:callback('onClear'), - auto_width=true, - - }, - widgets.WrappedLabel{ - frame={b=1, l=0}, - text_to_wrap='Click to toggle unit ignore. Shift doubleclick to toggle a group.', - }, - } + widgets.WrappedLabel{ + frame={b=3, l=0}, + text_to_wrap='Double click to toggle unit ignore. Shift double click to toggle a group.', }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='SELECT', + label='Toggle ignore', + on_activate=self:callback('onIgnore'), + auto_width=true, + }, + widgets.HotkeyLabel{ + frame={b=1, l=23}, + key='CUSTOM_G', + label='Toggle group', + on_activate = self:callback('onToggleGroup'), + auto_width=true, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key = 'CUSTOM_SHIFT_I', + label = 'Ignore all', + on_activate = self:callback('onIgnoreAll'), + auto_width=true, + + }, + widgets.HotkeyLabel{ + frame={b=0, l=23}, + key = 'CUSTOM_SHIFT_C', + label = 'Clear all ignored', + on_activate = self:callback('onClear'), + auto_width=true, } self.groups = info.groups @@ -246,6 +237,9 @@ function WarningWindow:initListChoices() end function WarningWindow:onIgnore(_, choice) + if not choice then + _, choice = self.subviews.list:getSelected() + end local unit = choice.data['unit'] toggleUnitIgnore(unit) @@ -307,7 +301,7 @@ end local function getStrandedUnits() local groupCount = 0 local grouped = {} - local citizens = dfhack.units.getCitizens() + local citizens = dfhack.units.getCitizens(true) -- Don't use ignored units to determine if there are any stranded units -- but keep them to display later From d141a512f470d12f62d994fa455810874ebb28e4 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 16 Oct 2023 12:33:22 -0700 Subject: [PATCH 32/32] Update warn-stranded.lua --- warn-stranded.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/warn-stranded.lua b/warn-stranded.lua index cd190e3c1f..81bdef5de4 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -210,6 +210,7 @@ function WarningWindow:init(info) label = 'Clear all ignored', on_activate = self:callback('onClear'), auto_width=true, + }, } self.groups = info.groups